Create exchange2; restore ICS sources to exchange
Change-Id: I076ad6c1e24ed071fe2d2867ca0fe3fbaff5a95f
diff --git a/Android.mk b/Android.mk
index 0a07ab3..aa7cbc7 100644
--- a/Android.mk
+++ b/Android.mk
@@ -22,7 +22,6 @@
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_SRC_FILES += $(call all-java-files-under, exchange1_src)
LOCAL_STATIC_JAVA_LIBRARIES := android-common com.android.emailcommon
LOCAL_STATIC_JAVA_LIBRARIES += calendar-common
@@ -36,25 +35,9 @@
include $(BUILD_PACKAGE)
include $(CLEAR_VARS)
-#
-# Exchange2
-#
-LOCAL_MODULE_TAGS := optional
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_SRC_FILES += $(call all-java-files-under, exchange2_src)
-LOCAL_STATIC_JAVA_LIBRARIES := android-common com.android.emailcommon2
-LOCAL_STATIC_JAVA_LIBRARIES += calendar-common
+##################################################
+# Build all sub-directories
-LOCAL_PACKAGE_NAME := Exchange2
-LOCAL_OVERRIDES_PACKAGES := Exchange
-
-LOCAL_PROGUARD_FLAG_FILES := proguard.flags
-
-LOCAL_EMMA_COVERAGE_FILTER += +com.android.exchange.*
-
-include $(BUILD_PACKAGE)
-
-# additionally, build unit tests in a separate .apk
- include $(call all-makefiles-under,$(LOCAL_PATH))
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/exchange2/Android.mk b/exchange2/Android.mk
new file mode 100644
index 0000000..24aba9a
--- /dev/null
+++ b/exchange2/Android.mk
@@ -0,0 +1,38 @@
+# Copyright 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.
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+#
+# Exchange2
+#
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := android-common com.android.emailcommon2
+LOCAL_STATIC_JAVA_LIBRARIES += calendar-common
+
+LOCAL_PACKAGE_NAME := Exchange2
+LOCAL_OVERRIDES_PACKAGES := Exchange
+
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+LOCAL_EMMA_COVERAGE_FILTER += +com.android.exchange.*
+
+include $(BUILD_PACKAGE)
+
+# additionally, build unit tests in a separate .apk
+ include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/exchange2/AndroidManifest.xml b/exchange2/AndroidManifest.xml
new file mode 100644
index 0000000..7537b73
--- /dev/null
+++ b/exchange2/AndroidManifest.xml
@@ -0,0 +1,170 @@
+<?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.
+-->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.exchange"
+ android:versionCode="500000"
+ android:versionName="5.0"
+ >
+
+ <uses-permission
+ android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+ <uses-permission
+ android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <uses-permission
+ android:name="android.permission.INTERNET"/>
+ <uses-permission
+ android:name="android.permission.VIBRATE"/>
+ <uses-permission
+ android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission
+ android:name="android.permission.GET_ACCOUNTS" />
+ <uses-permission
+ android:name="android.permission.MANAGE_ACCOUNTS" />
+ <uses-permission
+ android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
+ <uses-permission
+ android:name="android.permission.READ_SYNC_SETTINGS" />
+ <uses-permission
+ android:name="android.permission.WRITE_SYNC_SETTINGS" />
+
+ <uses-permission
+ android:name="android.permission.READ_CONTACTS"/>
+ <uses-permission
+ android:name="android.permission.WRITE_CONTACTS"/>
+ <uses-permission
+ android:name="android.permission.READ_CALENDAR"/>
+ <uses-permission
+ android:name="android.permission.WRITE_CALENDAR"/>
+ <uses-permission
+ android:name="android.permission.USE_CREDENTIALS"/>
+
+ <!-- Only required if a store implements push mail and needs to keep network open -->
+ <uses-permission
+ android:name="android.permission.WAKE_LOCK"/>
+ <uses-permission
+ android:name="android.permission.READ_PHONE_STATE"/>
+
+ <uses-permission
+ android:name="com.android.email.permission.READ_ATTACHMENT"/>
+ <uses-permission
+ android:name="com.android.email.permission.ACCESS_PROVIDER"/>
+
+ <application
+ android:icon="@mipmap/icon"
+ android:label="@string/app_name"
+ android:name="Exchange"
+ android:theme="@android:style/Theme.Holo.Light"
+ >
+
+ <receiver
+ android:name="com.android.exchange.EmailSyncAlarmReceiver"/>
+ <receiver
+ android:name="com.android.exchange.MailboxAlarmReceiver"/>
+
+ <receiver
+ android:name=".service.ExchangeBroadcastReceiver"
+ android:enabled="true">
+ <intent-filter>
+ <action
+ android:name="android.intent.action.BOOT_COMPLETED" />
+ <action
+ android:name="android.intent.action.DEVICE_STORAGE_LOW" />
+ <action
+ android:name="android.intent.action.DEVICE_STORAGE_OK" />
+ <action
+ android:name="android.accounts.LOGIN_ACCOUNTS_CHANGED" />
+ </intent-filter>
+ </receiver>
+
+ <service
+ android:name=".service.ExchangeBroadcastProcessorService" />
+
+ <!--Required stanza to register the EAS EmailSyncAdapterService with SyncManager -->
+ <service
+ android:name="com.android.exchange.EmailSyncAdapterService"
+ android:exported="true">
+ <intent-filter>
+ <action
+ android:name="android.content.SyncAdapter" />
+ </intent-filter>
+ <meta-data android:name="android.content.SyncAdapter"
+ android:resource="@xml/syncadapter_email" />
+ </service>
+
+ <!--Required stanza to register the EAS ContactsSyncAdapterService with SyncManager -->
+ <service
+ android:name="com.android.exchange.ContactsSyncAdapterService"
+ android:exported="true">
+ <intent-filter>
+ <action
+ android:name="android.content.SyncAdapter" />
+ </intent-filter>
+ <meta-data android:name="android.content.SyncAdapter"
+ android:resource="@xml/syncadapter_contacts" />
+ </service>
+
+ <!--Required stanza to register the EAS 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 ExchangeService as a separate process -->
+ <service
+ android:name="com.android.exchange.ExchangeService"
+ android:enabled="true"
+ android:permission="com.android.email.permission.ACCESS_PROVIDER"
+ >
+ <intent-filter>
+ <action
+ android:name="com.android.email.EXCHANGE_INTENT" />
+ </intent-filter>
+ </service>
+
+ <provider
+ android:name="com.android.exchange.provider.ExchangeDirectoryProvider"
+ android:authorities="com.android.exchange.directory.provider"
+ android:readPermission="android.permission.READ_CONTACTS"
+ android:multiprocess="false"
+ >
+ <meta-data
+ android:name="android.content.ContactDirectory"
+ android:value="true"/>
+ </provider>
+
+ <activity android:name=".EasCertificateRequestor">
+ <intent-filter>
+ <action android:name="com.android.emailcommon.REQUEST_CERT" />
+ <data android:scheme="eas" android:path="/certrequest" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <activity android:name=".SettingsRedirector">
+ <intent-filter>
+ <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/exchange2/proguard.flags b/exchange2/proguard.flags
new file mode 100644
index 0000000..0b7da4b
--- /dev/null
+++ b/exchange2/proguard.flags
@@ -0,0 +1,23 @@
+# keep names that are used by reflection.
+
+# Keep names that are used only by unit tests
+-keep class ** {
+ *** *ForTest(...);
+}
+
+-keepclasseswithmembers class com.android.exchange.adapter.Parser {
+ *** resetInput(java.io.InputStream);
+}
+
+-keepclasseswithmembers class com.android.exchange.provider.GalResult {
+ *** addGalData(com.android.exchange.provider.GalResult$GalData);
+ *** addGalData(long, java.lang.String, java.lang.String);
+}
+
+-keepclasseswithmembers class com.android.exchange.CalendarSyncEnabler {
+ public <init>(android.content.Context);
+}
+
+-keepclasseswithmembers class com.android.exchange.provider.MailboxUtilities {
+ *** setFlagsAndChildrensParentKey(android.content.Context, java.lang.String, java.lang.String);
+}
diff --git a/exchange2/res/drawable-hdpi/stat_notify_calendar.png b/exchange2/res/drawable-hdpi/stat_notify_calendar.png
new file mode 100644
index 0000000..5c8329d
--- /dev/null
+++ b/exchange2/res/drawable-hdpi/stat_notify_calendar.png
Binary files differ
diff --git a/exchange2/res/drawable-mdpi/stat_notify_calendar.png b/exchange2/res/drawable-mdpi/stat_notify_calendar.png
new file mode 100644
index 0000000..5758d04
--- /dev/null
+++ b/exchange2/res/drawable-mdpi/stat_notify_calendar.png
Binary files differ
diff --git a/exchange2/res/drawable-xhdpi/stat_notify_calendar.png b/exchange2/res/drawable-xhdpi/stat_notify_calendar.png
new file mode 100644
index 0000000..34fc22b
--- /dev/null
+++ b/exchange2/res/drawable-xhdpi/stat_notify_calendar.png
Binary files differ
diff --git a/exchange2/res/mipmap-hdpi/icon.png b/exchange2/res/mipmap-hdpi/icon.png
new file mode 100644
index 0000000..6fd0a98
--- /dev/null
+++ b/exchange2/res/mipmap-hdpi/icon.png
Binary files differ
diff --git a/exchange2/res/mipmap-mdpi/icon.png b/exchange2/res/mipmap-mdpi/icon.png
new file mode 100644
index 0000000..0369413
--- /dev/null
+++ b/exchange2/res/mipmap-mdpi/icon.png
Binary files differ
diff --git a/exchange2/res/mipmap-xhdpi/icon.png b/exchange2/res/mipmap-xhdpi/icon.png
new file mode 100644
index 0000000..777ef30
--- /dev/null
+++ b/exchange2/res/mipmap-xhdpi/icon.png
Binary files differ
diff --git a/exchange2/res/values-af/strings.xml b/exchange2/res/values-af/strings.xml
new file mode 100644
index 0000000..f2c0748
--- /dev/null
+++ b/exchange2/res/values-af/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Aanvaar: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Afgewys: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Voorlopig: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Gekanselleer: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Opgedateer: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Wanneer: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Waar: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Wanneer: <xliff:g id="EVENTDATE">%s</xliff:g> (herhalend)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Wanneer: <xliff:g id="EVENTDATE">%s</xliff:g> (hele dag)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Wanneer: <xliff:g id="EVENTDATE">%s</xliff:g> (die hele dag, herhalend)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Exchange-kalender bygevoeg"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange-dienste"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Hierdie gebeurtenis is gekanselleer vir: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Die details van hierdie gebeurtenis het verander vir: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Moenie geheuekaarte toelaat nie"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Moenie ongetekende programme toelaat nie"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Moenie ongetekende programinstalleerders toelaat nie"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Moenie Wi-Fi toelaat nie"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Moenie teksboodskappe toelaat nie"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Moenie POP3- of IMAP-rekeninge toelaat nie"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Moenie infrarooi kommunikasie toelaat nie"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Moenie HTML-e-pos toelaat nie"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Moenie blaaiers toelaat nie"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Moenie verbruiker-e-pos toelaat nie"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Moenie internetdeling toelaat nie"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Vereis SMIME-boodskappe"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Beperk Bluetooth-gebruik"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Weier gespesifiseerde programme"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Laat net gespesifiseerde programme toe"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Beperk grootte van teks-e-pos"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Beperk grootte van HTML-e-pos"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Vereis SD-kaart-enkripsie"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Moenie aanhegsels toelaat nie"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Beperk aanhegselgrootte"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Laat net handmatige sinkronisasie toe tydens swerwing"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Vereis toestelenkripsie"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Outomaties"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Een dag"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Drie dae"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Een week"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Twee weke"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Een maand"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Alle"</string>
+</resources>
diff --git a/exchange2/res/values-am/strings.xml b/exchange2/res/values-am/strings.xml
new file mode 100644
index 0000000..f9712e3
--- /dev/null
+++ b/exchange2/res/values-am/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"የማይክሮሶፍት Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"ተቀብሏል፡ <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"ሳይቀበል ቀረ: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"ጊዜያዊ፡<xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"ቀርቷል: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"የዘመነ፡<xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"መቼ:<xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"የት: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"መቼ፡ <xliff:g id="EVENTDATE">%s</xliff:g>(ድግግሞሽ)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"የሚከተለው ሲሆን፤ <xliff:g id="EVENTDATE">%s</xliff:g> (ሁሉም ቀን)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"የሚከተለው ሲሆን፤ <xliff:g id="EVENTDATE">%s</xliff:g> (ሁሉም ቀን፣ ተደጋጋሚ)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"የቀን መቁጠሪያ ልውውጥ ታክሏል"</string>
+ <string name="app_name" msgid="5316597712787122829">"አገልግሎቶች ለዋውጥ"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"ይህ ከስተት ለ<xliff:g id="DATE">%s</xliff:g> ተሰርዟል።"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"የዚህ ክስተት ዝርዝሮች ለ፡<xliff:g id="DATE">%s</xliff:g> ተለውጠዋል"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"የማከማቻ ካርዶችን አትፍቀድ"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"ያልተረጋገጡ የትግበራ አትፍቀድ"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"ያልተረጋገጡ የትግበራ ጫኞችን አትፍቀድ"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Wi-Fi አትፍቀድ"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"የፅሁፍ መልዕክት አትፍቀድ"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"POP3 ወይም IMAP መለያዎች አትፍቀድ"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"የታህተቀይ ግኑኙነት አትፍቀድ"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"የHTML ኢሜይል አትፍቀድ"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"አሳሾች አትፍቀድ"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"የሸማች ኢሜይል አትፍቀድ"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"የበይነ መረብ ማጋራት አትፍቀድ"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"የSMIME መልዕክቶች ጠይቅ"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"የብሉቱዝ አገልግሎትን ገድብ"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"የተወሰኑ ትግበራዎችን አትፍቀድ"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"የተወሰኑ ትግበራዎችን ብቻ ፍቀድ"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"የፅሁፍ ኢሜይል መጠንን ገድብ"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"የHTML ኢሜይል መጠን ገድብ"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"የsd ካርድ ምስጠራ ጠይቅ"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"አባሪዎች አትፍቀድ"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"የአባሪ መጠንን ገድብ"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"በእንቅስቃሴ ላይ በእጅ አመሳስል ብቻ ፍቀድ"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"የመሳሪያ ምስጠራ ጠይቅ"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"ራስ ሰር"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"አንድ ቀን"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"ሦስት ቀን"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"አንድ ሳምንት"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"ሁለት ሳምንቶች"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"አንድ ወር"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"ሁሉም"</string>
+</resources>
diff --git a/exchange2/res/values-ar/strings.xml b/exchange2/res/values-ar/strings.xml
new file mode 100644
index 0000000..395fa1f
--- /dev/null
+++ b/exchange2/res/values-ar/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"مقبول: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"مرفوض: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"موافقة مبدئية: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"تم الإلغاء: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"تم التحديث: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"متى: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"المكان: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"الموعد: <xliff:g id="EVENTDATE">%s</xliff:g> (متكرر)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"الموعد: <xliff:g id="EVENTDATE">%s</xliff:g> (اليوم كله)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"الموعد: <xliff:g id="EVENTDATE">%s</xliff:g> (اليوم كله، بشكل متكرر)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"تمت إضافة تقويم Exchange"</string>
+ <string name="app_name" msgid="5316597712787122829">"خدمات Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"تم إلغاء هذا الحدث لـ: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"تم تغيير التفاصيل لهذا الحدث لـ: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"عدم السماح ببطاقات التخزين"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"عدم السماح بالتطبيقات غير الموقعة"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"عدم السماح بمثبتات التطبيقات غير الموقعة"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"عدم السماح باتصالات Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"عدم السماح بالرسائل النصية"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"عدم السماح بحسابات POP3 أو IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"عدم السماح باتصالات الأشعة تحت الحمراء"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"عدم السماح برسائل HTML الإلكترونية"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"عدم السماح للمتصفحات"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"عدم السماح برسائل المستهلكين الإلكترونية"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"عدم السماح بالمشاركة عبر الإنترنت"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"يلزم توفر الرسائل بتنسيق SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"تقييد استخدام البلوتوث"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"عدم السماح بتطبيقات محددة"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"السماح بتطبيقات محددة فقط"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"تقييد حجم الرسالة الإلكترونية النصية"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"تقييد حجم رسائل HTML الإلكترونية"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"يلزم تشفير بطاقة SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"عدم السماح بالمرفقات"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"تقييد حجم المرفق"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"السماح بالمزامنة اليدوية فقط أثناء التجوال"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"يلزم تشفير الجهاز"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"تلقائي"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"يوم واحد"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"ثلاثة أيام"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"أسبوع"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"أسبوعان"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"شهر واحد"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"الكل"</string>
+</resources>
diff --git a/exchange2/res/values-be/strings.xml b/exchange2/res/values-be/strings.xml
new file mode 100644
index 0000000..72a5396
--- /dev/null
+++ b/exchange2/res/values-be/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Прынята: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Адхілена: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Папярэдне: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Адменена: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Абноўлена: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Калі: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Дзе: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Калі: <xliff:g id="EVENTDATE">%s</xliff:g> (перыядычна)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Калі: <xliff:g id="EVENTDATE">%s</xliff:g> (на цэлы дзень)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Калі: <xliff:g id="EVENTDATE">%s</xliff:g> (на цэлы дзень, перыядычная)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Каляндар Exchange дададзены"</string>
+ <string name="app_name" msgid="5316597712787122829">"Службы Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Гэтае мерапрыемства было адмененае: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Параметры гэтай падзеі былі былі змененыя: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Забараніць карты памяці"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Забараніць непадпісаныя прыкладанні"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Забараніць усталяванне непадпісаных прыкладанняў"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Забараніць выкарыстанне Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Забараніць абмен тэкстав. паведамленнямі"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Забаранiць улiковыя запiсы з выкарыстаннем POP3 і IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Забараніць ІЧ-сувязь"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Забаранiць фармат HTML для электр. пошты"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Забаранiць для браўзэраў"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Забаранiць электр. пошту карыстальнiкаў"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Забараніць агульны доступ у Інтэрнэт"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Патрабаваць паведамленні SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Абмежаваць выкарыстанне Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Забараніць пэўныя прыкладанні"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Дазваляць толькі пэўныя прыкладанні"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Абмежаваць памер тэкста электр. лістоў"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Абмежаваць памер электронных лістоў HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Патрабаваць шыфраванне SD-карты"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Забараніць далучэннi"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Абмежаваць памер далучэнняў"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Дазваляць ручную сiнхранiзацыю ў роўмiнгу"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Патрабаваць шыфраванне прылады"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Аўтаматычна"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"1 дзень"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Тры дні"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"1 тыдзень"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Два тыдні"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Адзін месяц"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Усе"</string>
+</resources>
diff --git a/exchange2/res/values-bg/strings.xml b/exchange2/res/values-bg/strings.xml
new file mode 100644
index 0000000..d8d218d
--- /dev/null
+++ b/exchange2/res/values-bg/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Прието: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Отклонено: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Колебливо: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Анулирано: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Актуализация:<xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Кога: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Къде: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Кога: <xliff:g id="EVENTDATE">%s</xliff:g> (с повторения)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Кога: <xliff:g id="EVENTDATE">%s</xliff:g> (цял ден)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Кога: <xliff:g id="EVENTDATE">%s</xliff:g> (цял ден, периодично)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Добавен е календар от Exchange"</string>
+ <string name="app_name" msgid="5316597712787122829">"Услуги на Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Това събитие от <xliff:g id="DATE">%s</xliff:g> е анулирано"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Подробностите за това събитие са променени за: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Забраняване на карти за съхранение"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Без неподписани приложения"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Без неподп. инсталатори на приложения"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Без разрешаване на WiFi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Забраняване на текстови съобщения"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Забраняване на POP3 или IMAP профили"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Забраняване на инфрачервени комуникации"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Забраняване на HTML имейли"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Забраняване на браузъри"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Забраняване на маркетингови имейли"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Без споделяне на интернет"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Изискване на SMIME съобщения"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Ограничаване на ползването на Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Забраняване на посочените приложения"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Разрешаване само на посочени приложения"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Ограничаване на размера на текст. имейли"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Ограничаване на размера на HTML имейлите"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Изискване за шифроване на SD картата"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Забраняване на прикачени файлове"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Огранич. на размера на прикач. файлове"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Само ръчно синхронизиране при роуминг"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Изискване за шифроване на устройството"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Автоматично"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Един ден"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Три дни"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Една седмица"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Две седмици"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Един месец"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Всички"</string>
+</resources>
diff --git a/exchange2/res/values-ca/strings.xml b/exchange2/res/values-ca/strings.xml
new file mode 100644
index 0000000..9763ac6
--- /dev/null
+++ b/exchange2/res/values-ca/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Acceptada: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Rebutjada: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Provisional: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Cancel·lada: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Actualitzada: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Quan: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"On: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Quan: <xliff:g id="EVENTDATE">%s</xliff:g> (periòdica)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Quan: <xliff:g id="EVENTDATE">%s</xliff:g> (tot el dia)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Quan: <xliff:g id="EVENTDATE">%s</xliff:g> (tot el dia, periòdic)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"S\'ha afegit el calendari d\'Exchange"</string>
+ <string name="app_name" msgid="5316597712787122829">"Serveis d\'Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Aquest esdeveniment s\'ha cancel·lat per a: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Els detalls d\'aquest esdeveniment han canviat per a: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"No permetis targetes d\'emmagatzematge"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"No permetis aplicacions no signades"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"No permetis instal. d\'aplic. no sign."</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"No permetis la Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"No permetis els missatges de text"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"No permetis els comptes POP3 ni IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"No permetis comunicacions per infraroigs"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"No permetis el correu electrònic HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"No permetis els navegadors"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"No permetis correus electr. de consum."</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"No permetis la compartició per Internet"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Requereix missatges SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Restringeix l\'ús de Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"No permetis aplicacions especificades"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Permet només les aplicacions especif."</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Restring. mida de correu electr. de text"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Restringeix mida del correu electr. HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Requereix l\'encriptació de targetes SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"No permetis fitxers adjunts"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Restringeix la mida del fitxer adjunt"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Permet només sincr. manual en itineràn."</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Requereix l\'encriptació del dispositiu"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automàtic"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Un dia"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Tres dies"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Una setmana"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Dues setmanes"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Un mes"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Tots"</string>
+</resources>
diff --git a/exchange2/res/values-cs/strings.xml b/exchange2/res/values-cs/strings.xml
new file mode 100644
index 0000000..93b712f
--- /dev/null
+++ b/exchange2/res/values-cs/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Potvrzeno: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Odmítnuto: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Možná: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Zrušeno: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Aktualizováno: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Kdy: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Kde: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Kdy: <xliff:g id="EVENTDATE">%s</xliff:g> (pravidelně)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Kdy: <xliff:g id="EVENTDATE">%s</xliff:g> (celý den)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Kdy: <xliff:g id="EVENTDATE">%s</xliff:g> (celý den, opakující se)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Kalendář Exchange byl přidán"</string>
+ <string name="app_name" msgid="5316597712787122829">"Služby Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Tato událost byla zrušena pro datum: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Podrobnosti této události byly změněny pro datum: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Nepovolit paměťové karty"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Nepovolit nepodepsané aplikace"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Nepovolit nepodepsané instalátory"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Nepovolit připojení Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Nepovolit textové zprávy"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Nepovolovat účty POP3 a IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Nepovolit komunikaci IR"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Nepovolit e-maily ve formátu HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Nepovolit prohlížeče"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Nepovolit marketingové e-maily"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Nepovolit sdílení internetu"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Požadovat zprávy ve formátu SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Omezit použití připojení Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Blokovat zadané aplikace"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Povolit pouze zadané aplikace"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Omezit velikost textových e-mailů"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Omezit velikost e-mailů ve formátu HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Požadovat šifrování karty SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Nepovolit přílohy"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Omezit velikost příloh"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Při roamingu povolit jen ruční synch."</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Požadovat šifrování zařízení"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automatické"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Jeden den"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Tři dny"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Jeden týden"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Dva týdny"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Jeden měsíc"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Vše"</string>
+</resources>
diff --git a/exchange2/res/values-da/strings.xml b/exchange2/res/values-da/strings.xml
new file mode 100644
index 0000000..7a4425b
--- /dev/null
+++ b/exchange2/res/values-da/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Accepteret: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Afvist: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Foreløbig: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Annulleret: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Opdateret: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Hvornår: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Hvor: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Hvornår: <xliff:g id="EVENTDATE">%s</xliff:g> (flere gange)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Hvornår: <xliff:g id="EVENTDATE">%s</xliff:g> (hele dagen)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Hvornår: <xliff:g id="EVENTDATE">%s</xliff:g> (hele dagen, tilbagevendende)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Exchange-kalender er tilføjet"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange-tjenester"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Denne begivenhed er annulleret for: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Oplysningerne om denne begivenhed er ændret for: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Tillad ikke hukommelseskort"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Tillad ikke usignerede apps"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Tillad ikke usignerede app-installationsprogrammer"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Tillad ikke Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Tillad ikke sms-beskeder"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Tillad ikke POP3- eller IMAP-konti"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Tillad ikke infrarød kommunikation"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Tillad ikke e-mail i HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Tillad ikke browsere"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Tillad ikke e-mail fra tredjepart"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Tillad ikke internetdeling"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Kræv SMIME-beskeder"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Begræns brugen af bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Tillad ikke angivne apps"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Tillad kun angivne apps"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Begræns tekststørrelsen for e-mail"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Begræns størrelsen på e-mail i HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Kræv kryptering af SD-kort"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Tillad ikke vedhæftede filer"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Begræns størrelsen på vedhæftede filer"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Tillad kun manuel synkron. under roaming"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Kræv kryptering af enhed"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automatisk"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"En dag"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Tre dage"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"En uge"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"To uger"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"En måned"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Alle"</string>
+</resources>
diff --git a/exchange2/res/values-de/strings.xml b/exchange2/res/values-de/strings.xml
new file mode 100644
index 0000000..43e9c02
--- /dev/null
+++ b/exchange2/res/values-de/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Akzeptiert: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Abgelehnt: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Mit Vorbehalt: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Abgebrochen: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Aktualisiert: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Wann: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Wo: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Wann: <xliff:g id="EVENTDATE">%s</xliff:g> (wiederkehrend)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Wann: <xliff:g id="EVENTDATE">%s</xliff:g> (ganztägig)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Wann: <xliff:g id="EVENTDATE">%s</xliff:g> (ganztägig, wiederkehrend)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Exchange-Kalender hinzugefügt"</string>
+ <string name="app_name" msgid="5316597712787122829">"Austauschdienste"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Dieser Termin wurde storniert für: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Die Details dieses Termins wurden geändert für: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Speicherkarten nicht zulassen"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Keine unsignierten Apps"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Keine unsignierten App-Installer"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"WLAN nicht zulassen"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"SMS/MMS nicht zulassen"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"POP3- oder IMAP-Konten nicht zulassen"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Infrarotkommunikation nicht zulassen"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"HTML-E-Mails nicht zulassen"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Browser nicht zulassen"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Verbraucher-E-Mails nicht zulassen"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Teilen über das Internet nicht zulassen"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"SMIME-Nachrichten erforderlich"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Bluetooth-Nutzung beschränken"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Angegebene Apps nicht zulassen"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Nur angegebene Apps zulassen"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"E-Mail-Text beschränken"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Größe von HTML-E-Mails beschränken"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"SD-Kartenverschlüsselung erforderlich"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Anhänge nicht zulassen"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Größe von Anhängen beschränken"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Bei Roaming nur manuelle Synchronis."</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Geräteverschlüsselung erforderlich"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automatisch"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Ein Tag"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Drei Tage"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Eine Woche"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Zwei Wochen"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Ein Monat"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Alle"</string>
+</resources>
diff --git a/exchange2/res/values-el/strings.xml b/exchange2/res/values-el/strings.xml
new file mode 100644
index 0000000..79b27cf
--- /dev/null
+++ b/exchange2/res/values-el/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Έγινε αποδοχή: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Απορρίφθηκε: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Αβέβαιη παρουσία: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Ακυρώθηκε: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Ενημερώθηκε: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Πότε: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Που: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Πότε: <xliff:g id="EVENTDATE">%s</xliff:g> (επαναλαμβανόμενο)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Πότε: <xliff:g id="EVENTDATE">%s</xliff:g> (όλη την ημέρα)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Πότε: <xliff:g id="EVENTDATE">%s</xliff:g> (όλη την ημέρα, επαναλαμβανόμενα)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Προστέθηκε ημερολόγιο Exchange"</string>
+ <string name="app_name" msgid="5316597712787122829">"Υπηρεσίες Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Αυτό το συμβάν ακυρώθηκε για: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Οι λεπτομέρειες αυτού του συμβάντος άλλαξαν για: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Δεν επιτρέπονται κάρτες αποθήκευσης"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Απαγόρευση μη υπογεγραμμένων εφαρμογών"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Απαγόρ. μη υπογεγρ. προγ. εγκατ. εφαρμ."</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Απαγόρευση σύνδεσης Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Δεν επιτρέπ. ανταλλαγή μηνυμ. κειμένου"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Δεν επιτρέπονται λογαρ. POP3 ή IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Δεν επιτρέπoνται υπέρυθρες επικοινωνίες"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Δεν επιτρέπεται ηλ. ταχ. HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Δεν επιτρέπονται προγρ. περιήγησης"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Δεν επιτρέπεται ηλ. ταχ. καταναλωτών"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Απαγόρευση κοινής χρήσης διαδικτύου"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Απαιτούνται μηνύματα SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Περιορισμός χρήσης bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Απαγόρευση συγκεκριμένων εφαρμογών"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Επιτρέπονται μόνο συγκεκριμ. εφαρμογές"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Περιορισμός μεγέθους κειμένου ηλ. ταχ."</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Περιορισμός μεγέθους ηλ. ταχ. HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Απαιτείται κρυπτογράφηση κάρτας SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Δεν επιτρέπονται συνημμένα"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Περιορισμός μεγέθους συνημμένων"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Σε περιαγωγή μόνο μη αυτ. συγχρονισμός"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Απαιτείται κρυπτογράφηση συσκευής"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Αυτόματη"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Μία ημέρα"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Τρεις ημέρες"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Μία εβδομάδα"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Δύο εβδομάδες"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Ένας μήνας"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Όλες"</string>
+</resources>
diff --git a/exchange2/res/values-en-rGB/strings.xml b/exchange2/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..069e0be
--- /dev/null
+++ b/exchange2/res/values-en-rGB/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Accepted: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Declined: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Tentative: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Cancelled: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Updated: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"When: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Where: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"When: <xliff:g id="EVENTDATE">%s</xliff:g> (recurring)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"When: <xliff:g id="EVENTDATE">%s</xliff:g> (all day)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"When: <xliff:g id="EVENTDATE">%s</xliff:g> (all day, recurring)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Exchange calendar added"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange Services"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"This event has been cancelled for: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"The details of this event have been changed for: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Don\'t allow storage cards"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Don\'t allow unsigned apps"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Don\'t allow unsigned app installers"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Don\'t allow Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Don\'t allow text messaging"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Don\'t allow POP3 or IMAP accounts"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Don\'t allow infrared communications"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Don\'t allow HTML email"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Don\'t allow browsers"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Don\'t allow consumer email"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Don\'t allow Internet sharing"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Require SMIME messages"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Restrict Bluetooth usage"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Disallow specified apps"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Allow only specified apps"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Restrict text email size"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Restrict HTML email size"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Require SD card encryption"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Don\'t allow attachments"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Restrict attachment size"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Only allow manual sync while roaming"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Require device encryption"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automatic"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"One day"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Three days"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"one week"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Two weeks"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"One month"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"All"</string>
+</resources>
diff --git a/exchange2/res/values-es-rUS/strings.xml b/exchange2/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..5753aba
--- /dev/null
+++ b/exchange2/res/values-es-rUS/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Aceptado: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Rechazada: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Tentativo: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Cancelado: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Actualizado: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Cuándo: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Dónde: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Cuando: <xliff:g id="EVENTDATE">%s</xliff:g> (recurrente)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Cuándo: <xliff:g id="EVENTDATE">%s</xliff:g> (todo el día)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Cuándo: <xliff:g id="EVENTDATE">%s</xliff:g> (todo el día, se repite)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Calendario de Exchange agregado"</string>
+ <string name="app_name" msgid="5316597712787122829">"Servicios de Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Este evento ha sido cancelado para: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Los detalles de este evento se han modificado para: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"No permitir tarjetas de memoria"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"No instalar aplicaciones sin firmar"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"No permitir instaladores sin firmar"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"No permitir Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"No permitir envío de mensajes de texto"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"No permitir cuentas POP3 ni IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"No permitir comunicación por infrarrojos"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"No recibir correos electrónicos HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"No permitir el uso de navegadores"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"No recibir correos comerciales"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"No permitir uso compartido de Internet"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Recibir solo mensajes SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Restringir el uso de Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Bloquear determinadas aplicaciones"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Permitir determinadas aplicaciones"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Restringir el tamaño de correos de texto"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Restringir el tamaño de los correos HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Requerir la encriptación de tarjetas SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"No descargar archivos adjuntos"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Restringir tamaño de archivos adjuntos"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Sincro. manual solo en itinerancia"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Requerir la encriptación del dispositivo"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automático"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Un día"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Tres días"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Una semana"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Dos semanas"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Un mes"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Todos"</string>
+</resources>
diff --git a/exchange2/res/values-es/strings.xml b/exchange2/res/values-es/strings.xml
new file mode 100644
index 0000000..1e7f3fe
--- /dev/null
+++ b/exchange2/res/values-es/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Aceptada: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Rechazada: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Provisional: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Cancelada: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Actualizado: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Cuándo: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Dónde: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Cuándo: <xliff:g id="EVENTDATE">%s</xliff:g> (periódico)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Cuando: <xliff:g id="EVENTDATE">%s</xliff:g> (todo el día)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Cuando: <xliff:g id="EVENTDATE">%s</xliff:g> (todo el día, periódico)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Se ha añadido un calendario de Exchange."</string>
+ <string name="app_name" msgid="5316597712787122829">"Servicios de Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Este evento se ha cancelado el día: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Los detalles de este evento se han cambiado al día: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"No permitir tarjetas de almacenamiento"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"No permitir aplicaciones sin firmar"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"No permitir instaladores sin firmar"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"No permitir Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"No permitir mensajes de texto"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"No permitir cuentas POP3 ni IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"No permitir comunicación por infrarrojos"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"No permitir correos HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"No permitir navegadores"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"No permitir correos comerciales"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"No permitir compartir conexión a Internet"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Solicitar mensajes SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Restringir uso de Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"No permitir aplicaciones especificadas"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Permitir solo aplicaciones especificadas"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Restringir tamaño de letra del correo"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Restringir el tamaño del correo HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Solicitar encriptación de tarjeta SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"No permitir archivos adjuntos"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Restringir tamaño de archivos adjuntos"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Sincronización manual solo en itinerancia"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Solicitar encriptación de dispositivo"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automática"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Un día"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Tres días"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Una semana"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Dos semanas"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Un mes"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Todo"</string>
+</resources>
diff --git a/exchange2/res/values-et/strings.xml b/exchange2/res/values-et/strings.xml
new file mode 100644
index 0000000..139842b
--- /dev/null
+++ b/exchange2/res/values-et/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Vastu võetud: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Keeldutud: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Katseline: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Tühistatud: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Värskendatud: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Millal: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Kus: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Millal: <xliff:g id="EVENTDATE">%s</xliff:g> (korduv)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Toimumisaeg: <xliff:g id="EVENTDATE">%s</xliff:g> (terve päev)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Toimumisaeg: <xliff:g id="EVENTDATE">%s</xliff:g> (terve päev, korduv)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Exchange\'i kalender lisatud"</string>
+ <string name="app_name" msgid="5316597712787122829">"Vahetusteenused"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"See sündmus on tühistatud: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Selle sündmuse andmed on muutunud: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Keela mälukaardid"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Keela allkirjastamata rakendused"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Keela allkirjata rakenduste installerid"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Keela WiFi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Keela tekstsõnumside"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Keela POP3- või IMAP-kontod"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Keela infrapunaside"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Keela HTML-meilid"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Keela brauserid"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Keela tarbijate meilid"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Keela Interneti jagamine"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Nõua SMIME-sõnumeid"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Piira Bluetoothi kasutamist"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Keela määratud rakendused"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Luba ainult määratud rakendused"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Piira tekstmeilide suurust"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Piira HTML-meili suurust"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Nõua SD-kaardi krüpteerimist"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Keela manused"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Piira manuse suurust"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Luba käsitsi sünkr. ainult rändlusel"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Nõua seadme krüpteerimist"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automaatne"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Üks päev"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Kolm päeva"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Üks nädal"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Kaks nädalat"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Üks kuu"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Kõik"</string>
+</resources>
diff --git a/exchange2/res/values-fa/strings.xml b/exchange2/res/values-fa/strings.xml
new file mode 100644
index 0000000..13559bb
--- /dev/null
+++ b/exchange2/res/values-fa/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"پذیرفته شده: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"پذیرفته نشد: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"احتمالی: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"لغو شد: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"به روزرسانی شد: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"زمان: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"مکان: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"زمان: <xliff:g id="EVENTDATE">%s</xliff:g> (تکرار)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"زمان: <xliff:g id="EVENTDATE">%s</xliff:g> (در تمام طول روز)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"زمان: <xliff:g id="EVENTDATE">%s</xliff:g> (در تمام طول روز، بصورت تکراری)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"تقویم Exchange اضافه شد"</string>
+ <string name="app_name" msgid="5316597712787122829">"سرویس های Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"این رویداد لغو شده است به مدت: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"جزئیات این رویداد تغییر کرده اند به مدت: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"کارتهای حافظه مجاز نیست"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"برنامههای امضا نشده مجاز نیستند"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"نصب کنندههای برنامه امضا نشده مجاز نیستند"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Wi-Fi مجاز نیست"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"پیامرسانی نوشتاری مجاز نیست"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"حسابهای POP3 یا IMAP مجاز نیست"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"ارتباطات مادون قرمز مجاز نیست"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"ایمیل HTML مجاز نیست"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"مرورگرها مجاز نیستند"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"ایمیل مصرفکننده مجاز نیست"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"اشتراکگذاری اینترنتی مجاز نیست"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"پیامهای SMIME لازم است"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"استفاده از بلوتوث محدود است"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"برنامههای خاص مجاز نیستند"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"فقط برنامههای خاص مجاز هستند"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"اندازه نوشتار ایمیل محدود است"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"اندازه ایمیل HTML محدود است"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"کارت sd باید رمزگذاری شود"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"پیوستها مجاز نیستند"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"اندازه پیوست محدود است"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"در حالت رومینگ فقط همگامسازی دستی مجاز است"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"دستگاه باید رمزگذاری شود"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"خودکار"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"یک روز"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"سه روز"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"یک هفته"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"دو هفته"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"یک ماه"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"همه"</string>
+</resources>
diff --git a/exchange2/res/values-fi/strings.xml b/exchange2/res/values-fi/strings.xml
new file mode 100644
index 0000000..3fbc7eb
--- /dev/null
+++ b/exchange2/res/values-fi/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Hyväksytty: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Hylätty: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Alustava: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Peruutettu: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Päivitetty: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Milloin: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Missä: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Milloin: <xliff:g id="EVENTDATE">%s</xliff:g> (toistuva)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Milloin: <xliff:g id="EVENTDATE">%s</xliff:g> (koko päivän)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Milloin: <xliff:g id="EVENTDATE">%s</xliff:g> (koko päivän, toistuva)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Exchange-kalenteri lisätty"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange Services"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Poikkeus toistuvassa tapahtumassa, peruutettu <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Poikkeus toistuvassa tapahtumassa, muutettu <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Älä salli tallennuskortteja"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Älä salli allekirjoittamatt. sovelluksia"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Älä salli allekirjoittamatt. sov. asent."</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Älä salli wifi-yhteyttä"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Älä salli tekstiviestejä"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Älä salli POP3- tai IMAP-tilejä"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Älä salli infrapunaviestintää"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Älä salli HTML-sähköpostia"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Älä salli selaimia"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Älä salli kuluttajasähköpostia"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Älä salli internet-jakamista"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Vaadi SMIME-viestit"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Rajoita bluetoothin käyttöä"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Älä salli määritettyjä sovelluksia"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Salli vain määritetyt sovellukset"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Rajoita tekstimuot. sähköpostien kokoa"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Rajoita HTML-muot. sähköpostien kokoa"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Vaatii SD-kortin salauksen"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Älä salli liitetiedostoja"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Rajoita liitetiedostojen kokoa"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Salli vain man. synkr. roaming-tilassa"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Vaatii laitteen salauksen"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automaattinen"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Yksi päivä"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Kolme päivää"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Yksi viikko"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Kaksi viikkoa"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Yksi kuukausi"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Kaikki"</string>
+</resources>
diff --git a/exchange2/res/values-fr/strings.xml b/exchange2/res/values-fr/strings.xml
new file mode 100644
index 0000000..6f89dc7
--- /dev/null
+++ b/exchange2/res/values-fr/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Réunion acceptée : <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Réunion refusée : <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Projet de réunion : <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Réunion annulée : <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Mise à jour : <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Date : <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Lieu : <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Date : <xliff:g id="EVENTDATE">%s</xliff:g> (périodique)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Quand : <xliff:g id="EVENTDATE">%s</xliff:g> (toute la journée)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Quand : <xliff:g id="EVENTDATE">%s</xliff:g> (toute la journée, récurrent)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Agenda Exchange ajouté"</string>
+ <string name="app_name" msgid="5316597712787122829">"Services Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Cet événement a été annulé pour : <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Les détails de cet événement ont été modifiés pour : <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Ne pas autoriser les cartes de stockage"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Ne pas autoriser programmes d\'installation non signés"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Ne pas autoriser programmes d\'installation non signés"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Ne pas autoriser les communications Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Ne pas autoriser les SMS"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Ne pas autoriser les comptes POP3 ni IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Ne pas autoriser communication infrarouge"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Ne pas autoriser e-mails au format HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Ne pas autoriser les navigateurs"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Ne pas autoriser e-mails publicitaires"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Ne pas autoriser partage connexion Internet"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Exiger le format SMIME pour les messages"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Limiter l\'utilisation de Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Interdire les applications spécifiées"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Autoriser applications spécifiées uniq."</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Autoriser e-mails texte selon taille"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Autoriser e-mails HTML selon taille"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Exiger le chiffrement de la carte SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Ne pas autoriser les pièces jointes"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Autoriser pièces jointes selon taille"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Interdire synchro auto en itinérance"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Exiger le chiffrement de l\'appareil"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automatique"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Un jour"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Trois jours"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Une semaine"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Deux semaines"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Un mois"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Tous"</string>
+</resources>
diff --git a/exchange2/res/values-hi/strings.xml b/exchange2/res/values-hi/strings.xml
new file mode 100644
index 0000000..15c7b80
--- /dev/null
+++ b/exchange2/res/values-hi/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"स्वीकृत: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"अस्वीकृत: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"अस्थायी: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"रद्द हो गई: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"अपडेट किया गया: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"कब: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"कहां: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"कब: <xliff:g id="EVENTDATE">%s</xliff:g> (पुनरावर्ती)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"कब: <xliff:g id="EVENTDATE">%s</xliff:g> (पूरा दिन)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"कब: <xliff:g id="EVENTDATE">%s</xliff:g> (पूरा दिन, पुनरावृत्ति)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Exchange कैलेंडर जोड़ा गया"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange Services"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"यह ईवेंट इसके लिए रद्द कर दिया गया है: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"इस ईवेंट का विवरण इसके लिए बदल दिया गया है: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"संग्रहण कार्ड को अनुमति न दें"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"अहस्ताक्षरित एप्लिकेशन की अनुमति न दें"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"अहस्ता. एप्लि. इंस्टॉलर को अनुमति न दें"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Wi-Fi की अनुमति न दें"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"पाठ संदेश सेवा की अनुमति न दें"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"POP3 या IMAP खातों को अनुमति न दें"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"अवरक्त संचारों को अनुमति न दें"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"HTML ईमेल को अनुमति न दें"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"ब्राउज़र को अनुमति न दें"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"उपभोक्ता ईमेल की अनुमति न दें"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"इंटरनेट साझाकरण की अनुमति न दें"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"SMIME संदेश आवश्यक"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Bluetooth का उपयोग प्रतिबंधित करें"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"निर्दिष्ट एप्लिकेशन की अनुमति न दें"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"केवल निर्दिष्ट एप्लिकेशन की अनुमति दें"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"पाठ ईमेल का आकार प्रतिबंधित करें"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"HTML ईमेल का आकार प्रतिबंधित करें"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"SD कार्ड एन्क्रिप्शन आवश्यक"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"अनुलग्नकों की अनुमति न दें"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"अनुलग्नक का आकार प्रतिबंधित करें"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"रोमिंग पर मैन्युअल समन्वयन ही करने दें"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"उपकरण एन्क्रिप्शन आवश्यक"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"स्वचालित"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"एक दिन"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"तीन दिन"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"एक सप्ताह"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"दो सप्ताह"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"एक माह"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"सभी"</string>
+</resources>
diff --git a/exchange2/res/values-hr/strings.xml b/exchange2/res/values-hr/strings.xml
new file mode 100644
index 0000000..ad41a0d
--- /dev/null
+++ b/exchange2/res/values-hr/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Prihvaćeno: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Odbijeno: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Probno: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Otkazano: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Ažurirano: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Kad: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Gdje: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Kada: <xliff:g id="EVENTDATE">%s</xliff:g> (ponavljanje)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Kada: <xliff:g id="EVENTDATE">%s</xliff:g> (cijeli dan)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Kada: <xliff:g id="EVENTDATE">%s</xliff:g> (cijeli dan, ponavlja se)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Dodan je Exchange kalendar"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange Services"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Ovaj je događaj otkazan za: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Pojedinosti ovog događaja promijenjene su za: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Ne dopusti memorijske kartice"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Ne dopusti nepotpisane aplikacije"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Ne dopusti instal. prog. nepotpis. apl."</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Ne dopusti WiFi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Ne dopusti slanje SMS poruka"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Ne dopusti POP3 ili IMAP račune"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Ne dopusti infracrvenu komunikaciju"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Ne dopusti e-poštu u HTML-u"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Ne dopusti preglednike"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Ne dopusti potrošačku e-poštu"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Ne dopusti dijeljenje na internetu"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Zahtijevaj SMIME poruke"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Ograniči upotrebu Bluetootha"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Odbaci navedene aplikacije"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Dopusti samo navedene aplikacije"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Ograniči veličinu tekstualne e-pošte"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Ograniči veličinu e-pošte u HTML-u"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Zahtijevaj enkripciju SD kartice"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Ne dopusti privitke"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Ograniči veličinu privitka"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Dopusti samo ručnu sinkr. u roamingu"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Zahtijevaj enkripciju uređaja"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automatski"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Jedan dan"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Tri dana"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Jedan tjedan"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Dva tjedna"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Jedan mjesec"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Sve"</string>
+</resources>
diff --git a/exchange2/res/values-hu/strings.xml b/exchange2/res/values-hu/strings.xml
new file mode 100644
index 0000000..0f20934
--- /dev/null
+++ b/exchange2/res/values-hu/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Elfogadva: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Elutasítva: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Feltételes: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Lemondva: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Frissítve: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Időpont: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Helyszín: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Időpont: <xliff:g id="EVENTDATE">%s</xliff:g> (ismétlődő)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Időpont: <xliff:g id="EVENTDATE">%s</xliff:g> (egész nap)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Időpont: <xliff:g id="EVENTDATE">%s</xliff:g> (egész nap, ismétlődő)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Exchange-naptár hozzáadva"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange szolgáltatások"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Ezt az eseményt törölték: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Az esemény részletei a következőre módosultak: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Memóriakártyák nem engedélyezettek"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Csak hiteles alkalmazások engedélyezése"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Csak hiteles alkalmazásokat telepítsen"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Wi-Fi tiltása"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Szöveges üzenetküldés nem engedélyezett"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"POP3/IMAP-fiókok nem engedélyezettek"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Infra kommunikáció nem engedélyezett"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"HTML e-mailek nem engedélyezettek"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"A böngészők nem engedélyezettek"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Fogyasztói e-mailek nem engedélyezettek"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Internetmegosztás tiltása"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"SMIME-üzenetek szükségesek"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Bluetooth használatának korlátozása"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Meghatározott alkalmazások tiltása"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Csak megadott alkalmazások engedélyezése"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Szöveges e-mailek méretének korlátozása"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"HTML e-mailek méretének korlátozása"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"SD-kártya titkosítása szükséges"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Mellékletek nem engedélyezettek"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Mellékletek méretének korlátozása"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Csak kézi szinkronizálás barangoláskor"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Eszköztitkosítás szükséges"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automatikus"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Egy nap"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Három nap"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Egy hét"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Két hét"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Egy hónap"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Összes"</string>
+</resources>
diff --git a/exchange2/res/values-in/strings.xml b/exchange2/res/values-in/strings.xml
new file mode 100644
index 0000000..be67dd0
--- /dev/null
+++ b/exchange2/res/values-in/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Diterima: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Ditolak: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Tentatif: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Dibatalkan: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Diperbarui: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Kapan: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Di mana: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Waktu: <xliff:g id="EVENTDATE">%s</xliff:g> (terjadi berulang)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Kapan: <xliff:g id="EVENTDATE">%s</xliff:g> (sepanjang hari)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Kapan: <xliff:g id="EVENTDATE">%s</xliff:g> (sepanjang hari, berulang)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Tukar kalender yang ditambahkan"</string>
+ <string name="app_name" msgid="5316597712787122829">"Layanan Pertukaran"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Acara ini telah dibatalkan untuk: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Detail acara ini telah diubah untuk: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Jangan izinkan kartu penyimpanan"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Jangan izinkan apl tak bertanda tangan"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Jgn izinkan pmasang apl tak bertanda tgn"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Jangan izinkan Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Jangan izinkan pesan teks"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Jangan izinkan akun POP3 atau IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Jangan izinkan komunikasi inframerah"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Jangan izinkan email HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Jangan izinkan browser"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Jangan izinkan email pelanggan"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Jangan izinkan berbagi internet"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Memerlukan pesan SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Batasi penggunaan Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Tolak apl yang disebutkan"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Izinkan apl yang disebutkan saja"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Batasi ukuran email teks"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Batasi ukuran email HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Membutuhkan enkripsi kartu SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Jangan izinkan lampiran"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Batasi ukuran lampiran"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Hanya izinkan sinkr. manual saat roaming"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Memerlukan enkripsi perangkat"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Otomatis"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Satu hari"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Tiga hari"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Satu minggu"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Dua minggu"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Satu bulan"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Semua"</string>
+</resources>
diff --git a/exchange2/res/values-it/strings.xml b/exchange2/res/values-it/strings.xml
new file mode 100644
index 0000000..406d1a0
--- /dev/null
+++ b/exchange2/res/values-it/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Accettato: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Rifiutato: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Tentativo: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Annullato: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Aggiornamento riuscito: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Quando: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Dove: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Quando: <xliff:g id="EVENTDATE">%s</xliff:g> (ricorrente)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Quando: <xliff:g id="EVENTDATE">%s</xliff:g> (tutto il giorno)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Quando: <xliff:g id="EVENTDATE">%s</xliff:g> (tutto il giorno, ricorrente)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Calendario Exchange aggiunto"</string>
+ <string name="app_name" msgid="5316597712787122829">"Servizi Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Questo evento è stato annullato per la data: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"I dettagli di questo evento sono cambiati per la data: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Non consentire schede di memoria"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Non consentire applicazioni non firmate"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Non consentire installer app. non firmate"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Non consentire Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Non consentire messaggi di testo"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Non consentire account POP3 o IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Non consentire comunicazione a infrarossi"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Non consentire email HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Non consentire browser"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Non consentire email di livello consumer"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Non consentire condivisione Internet"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Richiedi messaggi SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Limita utilizzo Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Non consentire applicazioni specificate"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Consenti solo applicazioni specificate"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Limita dimensioni testo email"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Limita dimensioni email HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Richiedi crittografia scheda SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Non consentire allegati"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Limita dimensioni allegati"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Consenti solo sincr. manuale in roaming"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Richiedi crittografia dispositivo"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automatico"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Un giorno"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Tre giorni"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Una settimana"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Due settimane"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Un mese"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Tutto"</string>
+</resources>
diff --git a/exchange2/res/values-iw/strings.xml b/exchange2/res/values-iw/strings.xml
new file mode 100644
index 0000000..f32b378
--- /dev/null
+++ b/exchange2/res/values-iw/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"התקבלה: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"נדחה: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"לא סופי: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"בוטל: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"עודכן: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"מתי: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"היכן: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"מתי: <xliff:g id="EVENTDATE">%s</xliff:g> (אירוע חוזר)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"מתי: <xliff:g id="EVENTDATE">%s</xliff:g> (כל יום)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"מתי: <xliff:g id="EVENTDATE">%s</xliff:g> (כל היום, חוזר)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"נוסף יומן של Exchange"</string>
+ <string name="app_name" msgid="5316597712787122829">"שירותי Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"אירוע זה בוטל לתאריך: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"הפרטים של אירוע זה השתנו בתאריך: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"אל תאפשר כרטיסי אחסון"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"אל תאפשר יישומים לא חתומים"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"אל תאפשר תוכנות התקנה לא חתומות של יישום."</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"אל תאפשר WiFi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"אל תאפשר העברת הודעות טקסט"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"אל תאפשר חשבונות מסוג POP3 או IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"אל תאפשר תקשורת אינפרא-אדום"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"אל תאפשר דוא\"ל בפורמט HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"אל תאפשר דפדפנים"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"אל תאפשר דוא\"ל צרכן"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"אל תאפשר שיתוף באינטרנט"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"נדרשת העברת הודעות SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"הגבל את השימוש ב-Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"אל תאפשר יישומים שצוינו"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"אפשר רק יישומים שצוינו"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"הגבל את גודל הטקסט בדוא\"ל"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"הגבל גודל דוא\"ל בפורמט HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"נדרשת הצפנת כרטיס SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"אל תאפשר קבצים מצורפים"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"הגבל את גודל הקובץ המצורף"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"אפשר רק סינכרון ידני בעת נדידה"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"נדרשת הצפנת מכשיר"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"אוטומטי"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"יום אחד"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"שלושה ימים"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"שבוע אחד"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"שבועיים"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"חודש אחד"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"הכל"</string>
+</resources>
diff --git a/exchange2/res/values-ja/strings.xml b/exchange2/res/values-ja/strings.xml
new file mode 100644
index 0000000..7cdfafe
--- /dev/null
+++ b/exchange2/res/values-ja/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"承諾しました: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"辞退しました: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"仮承諾: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"辞退しました: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"更新されました: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"日時: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"場所: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"日時: <xliff:g id="EVENTDATE">%s</xliff:g>(定期的)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"日付: <xliff:g id="EVENTDATE">%s</xliff:g>(終日)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"日付: <xliff:g id="EVENTDATE">%s</xliff:g>(終日、定期的)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Exchangeカレンダーを追加しました"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchangeサービス"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"この予定はキャンセルされました: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"この予定の詳細が変更されました: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"SDカードを許可しない"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"無署名のアプリを許可しない"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"無署名のアプリインストーラを許可しない"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Wi-Fiを許可しない"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"テキストメッセージを許可しない"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"POP3/IMAPアカウントを許可しない"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"赤外線通信を許可しない"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"HTMLメールを許可しない"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"ブラウザを許可しない"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"ダイレクトメールを許可しない"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"インターネット共有を許可しない"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"SMIME形式のメッセージを必須とする"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Bluetoothの使用を制限する"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"指定したアプリを許可しない"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"指定したアプリのみ許可する"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"テキストメールのサイズを制限する"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"HTMLメールのサイズを制限する"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"SDカードの暗号化を必須とする"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"添付ファイルを許可しない"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"添付ファイルのサイズを制限する"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"ローミング中は手動での同期のみを許可する"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"端末の暗号化を必須とする"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"自動"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"1日"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"3日間"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"1週間"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"2週間"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"1か月"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"すべて"</string>
+</resources>
diff --git a/exchange2/res/values-ko/strings.xml b/exchange2/res/values-ko/strings.xml
new file mode 100644
index 0000000..516705f
--- /dev/null
+++ b/exchange2/res/values-ko/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"수락: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"거부: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"미정: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"취소: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"업데이트: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"일시: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"장소: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"일시: <xliff:g id="EVENTDATE">%s</xliff:g>(반복 일정)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"일시: <xliff:g id="EVENTDATE">%s</xliff:g>(종일)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"일시: <xliff:g id="EVENTDATE">%s</xliff:g>(종일, 반복)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Exchange 캘린더 추가됨"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange 서비스"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"<xliff:g id="DATE">%s</xliff:g>에 있을 이 일정이 취소되었습니다."</string>
+ <string name="exception_updated" msgid="3397583105901142050">"<xliff:g id="DATE">%s</xliff:g>에 있을 이 일정의 세부사항이 변경되었습니다."</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"저장 카드 허용 안함"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"서명되지 않은 앱 허용 안함"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"서명되지 않은 앱 설치 프로그램 허용 안함"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Wi-Fi 허용 안함"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"문자 메시지 허용 안함"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"POP3 또는 IMAP 계정 허용 안함"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"적외선 통신 허용 안함"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"HTML 이메일 허용 안함"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"브라우저 허용 안함"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"소비자 이메일 허용 안함"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"인터넷 공유 허용 안함"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"SMIME 메시지 필요"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"블루투스 사용 제한"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"지정된 앱 허용 안함"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"지정한 앱만 허용"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"텍스트 이메일 크기 제한"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"HTML 이메일 크기 제한"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"SD 카드 암호화 필요"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"첨부 파일 허용 안함"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"첨부 파일 크기 제한"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"로밍 중에 수동 동기화만 허용"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"기기 암호화 필요"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"자동"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"하루"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"3일"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"1주"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"2주"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"1달"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"전체"</string>
+</resources>
diff --git a/exchange2/res/values-lt/strings.xml b/exchange2/res/values-lt/strings.xml
new file mode 100644
index 0000000..1174644
--- /dev/null
+++ b/exchange2/res/values-lt/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Priimta: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Atmesta: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Neapsispręsta: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Atšaukta: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Atnaujinta: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Kada: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Kur: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Kada: <xliff:g id="EVENTDATE">%s</xliff:g> (pasikartojantis)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Kada: <xliff:g id="EVENTDATE">%s</xliff:g> (visą dieną)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Kada: <xliff:g id="EVENTDATE">%s</xliff:g> (visą dieną, pasikartojantis)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Pridėtas „Exchange“ kalendorius"</string>
+ <string name="app_name" msgid="5316597712787122829">"Keitimo paslaugos"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Šis įvykis atšauktas: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Išsami šio įvykio informacija buvo pakeista: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Neleisti atminties kortelių"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Neleisti anoniminių programų"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Neleisti anon. programų diegimo progr."</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Neleisti „Wi-Fi“"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Neleisti teksto pranešimų"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Neleisti POP3 ar IMAP prieigos paskyrų"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Neleisti infraraudonųjų spindulių ryšių"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Neleisti HTML el. laiškų"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Neleisti naršyklių"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Neleisti vartotojų el. laiškų"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Neleisti bendrinimo internetu"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Reikalauti SMIME pranešimų"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Apriboti „Bluetooth“ naudojimą"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Neleisti nurodytų programų"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Leisti tik nurodytas programas"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Riboti teksto el. laiškų dydį"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Riboti HTML el. laiškų dydį"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Reikalauti SD kortelės šifravimo"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Neleisti priedų"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Riboti priedo dydį"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Veikiant tarptink. r. sinch. tik pačiam"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Reikalauti įrenginių šifravimo"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automatinis"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Vieną dieną"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Tris dienas"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Viena savaitė"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Dvi savaitės"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Vienas mėnuo"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Visas"</string>
+</resources>
diff --git a/exchange2/res/values-lv/strings.xml b/exchange2/res/values-lv/strings.xml
new file mode 100644
index 0000000..b615f28
--- /dev/null
+++ b/exchange2/res/values-lv/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Pieņemts: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Noraidīts: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Pagaidu: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Atcelts: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Atjaunināts: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Kad: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Kur: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Kad: <xliff:g id="EVENTDATE">%s</xliff:g> (tiek atkārtots)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Kad: <xliff:g id="EVENTDATE">%s</xliff:g> (visu dienu)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Kad: <xliff:g id="EVENTDATE">%s</xliff:g> (visu dienu, atkārtoti)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Exchange kalendārs ir pievienots"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange pakalpojumi"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Pasākums šajā datumā ir atcelts: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Šī pasākuma dati šādā datumā ir mainīti: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Neatļaut atmiņas kartes"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Neatļaut neparakstītas lietotnes"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Neatļaut neparakst. lietotņu instal. p."</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Neatļaut Wi-Fi savienojumu"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Neatļaut īsziņas"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Neatļaut POP3 vai IMAP kontus"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Neatļaut infrasarkanos sakarus"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Neatļaut HTML e-pasta ziņojumus"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Neatļaut pārlūkprogrammas"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Neatļaut patērētāju e-pasta ziņojumus"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Neatļaut kopīgošanu internetā"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Pieprasīt SMIME formāta ziņojumus"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Ierobežot Bluetooth lietošanu"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Neatļaut noteiktas lietotnes"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Atļaut tikai norādītās lietotnes"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Ierobežot teksta e-pasta ziņojumu izmēru"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Ierobežot HTML e-pasta ziņojumu izmēru"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Pieprasīt SD kartes šifrēšanu"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Neatļaut pielikumus"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Ierobežot pielikumu izmēru"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Atļaut tikai man. sinhr., izm. viesabon."</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Pieprasīt ierīces šifrēšanu"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automātiski"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Viena diena"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Trīs dienas"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Viena nedēļa"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Divas nedēļas"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Viens mēnesis"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Visi"</string>
+</resources>
diff --git a/exchange2/res/values-ms/strings.xml b/exchange2/res/values-ms/strings.xml
new file mode 100644
index 0000000..d838e16
--- /dev/null
+++ b/exchange2/res/values-ms/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Diterima: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Ditolak: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Sementaraan: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Dibatalkan: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Dikemas kini: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Bila: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Di mana: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Bila: <xliff:g id="EVENTDATE">%s</xliff:g> (berulangan)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Bila: <xliff:g id="EVENTDATE">%s</xliff:g> (sepanjang hari)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Bila: <xliff:g id="EVENTDATE">%s</xliff:g> (sepanjang hari, berulang-ulang)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Kalendar Exchange ditambah"</string>
+ <string name="app_name" msgid="5316597712787122829">"Perkhidmatan Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Acara ini sudah dibatalkan untuk: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Butiran acara ini telah ditukar kepada: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Jgn benarkan kad storan"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Jgn bnrkan apl tanpa tandatangan"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Jgn bnrkan pemsng apl tanpa tandatangan"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Jgn benarkan Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Jgn benarkan pemesejan teks"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Jgn benarkan akaun POP3 atau IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Jgn benarkan komunikasi inframerah"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Jgn benarkan e-mel HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Jgn benarkan penyemak imbas"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Jgn benarkan e-mel pengguna"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Jgn benarkan perkongsian internet"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Perlukan mesej SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Hadkan penggunaan Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Jgn benarkan apl tertentu"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Benarnya hanya apl dinyatakan"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Hadkan saiz e-mel teks"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Hadkan saiz e-mel HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Memerlukan penyulitan kad SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Jgn benarkan lampiran"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Hadkan saiz lampiran"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Bnr sgrk manual sj semasa perayauan"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Perlukan penyulitan peranti"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automatik"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Satu hari"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Tiga hari"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Satu minggu"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Dua minggu"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Satu bulan"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Semua"</string>
+</resources>
diff --git a/exchange2/res/values-nb/strings.xml b/exchange2/res/values-nb/strings.xml
new file mode 100644
index 0000000..2557782
--- /dev/null
+++ b/exchange2/res/values-nb/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Godtatt: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Avslått: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Foreslått: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Kansellert: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Oppdatert: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Når: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Hvor: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Når: <xliff:g id="EVENTDATE">%s</xliff:g> (gjentakende)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Når: <xliff:g id="EVENTDATE">%s</xliff:g> (hele dagen)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Når: <xliff:g id="EVENTDATE">%s</xliff:g> (hele dagen, gjentakende)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Exchange-kalender er lagt til"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange-tjenester"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Denne aktiviteten er kansellert for: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Detaljene for denne aktiviteten er endret for: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Ikke tillat minnekort"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Ikke tillat usignerte apper"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Ikke tillat usign. appinstallasjonsprog."</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Ikke tillat Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Ikke tillat tekstmeldinger"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Ikke tillat POP3- eller IMAP-kontoer"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Ikke tillat infrarød kommunikasjon"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Ikke tillat e-post i HTML-format"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Ikke tillat nettlesere"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Ikke tillat forbruker-e-post"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Ikke tillat Internett-deling"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Krev SMIME-meldinger"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Begrens bruk av Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Forby angitte apper"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Tillat bare spesifikke apper"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Begrens størr. på e-post i tekstformat"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Begrens størr. på e-post i HTML-format"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Krev kryptering av SD-kort"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Ikke tillat vedlegg"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Begrens vedleggsstørrelse"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Bare tillat manuell synk. ved streifing"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Krev enhetskryptering"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automatisk"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Én dag"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Tre dager"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Én uke"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"To uker"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Én måned"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Alle"</string>
+</resources>
diff --git a/exchange2/res/values-nl/strings.xml b/exchange2/res/values-nl/strings.xml
new file mode 100644
index 0000000..c211304
--- /dev/null
+++ b/exchange2/res/values-nl/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Geaccepteerd: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Afgewezen: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Voorlopig: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Geannuleerd: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Bijgewerkt: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Wanneer: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Waar: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Wanneer: <xliff:g id="EVENTDATE">%s</xliff:g> (herhalen)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Wanneer: <xliff:g id="EVENTDATE">%s</xliff:g> (hele dag)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Wanneer: <xliff:g id="EVENTDATE">%s</xliff:g> (hele dag, herhalen)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Exchange-agenda toegevoegd"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange Services"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Deze afspraak is geannuleerd voor: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"De details van deze afspraak zijn gewijzigd voor: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Geheugenkaarten niet toestaan"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Niet-ondertekende apps niet toestaan"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Niet-ondertekende appinstallers weigeren"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Wifi niet toestaan"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Sms\'en niet toestaan"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"POP3- of IMAP-accounts niet toestaan"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Infraroodcommunicatie niet toestaan"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"HTML-e-mail niet toestaan"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Browsers niet toestaan"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Consumenten-e-mail niet toestaan"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Internet delen niet toestaan"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"SMIME-berichten vereisen"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Het gebruik van Bluetooth beperken"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Opgegeven apps niet toestaan"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Alleen opgegeven apps toestaan"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Tekstgrootte van e-mail beperken"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Grootte van HTML-e-mails beperken"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Codering van SD-kaart vereisen"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Bijlagen niet toestaan"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Grootte van bijlagen beperken"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Alleen handm. synchr. toestn bij roaming"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Apparaatencryptie vereisen"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automatisch"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Eén dag"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Drie dagen"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Eén week"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Twee weken"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Eén maand"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Alles"</string>
+</resources>
diff --git a/exchange2/res/values-pl/strings.xml b/exchange2/res/values-pl/strings.xml
new file mode 100644
index 0000000..af7db82
--- /dev/null
+++ b/exchange2/res/values-pl/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Zaakceptowano: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Odrzucono: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Wahanie: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Anulowano: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Zaktualizowano: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Kiedy: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Gdzie: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Kiedy: <xliff:g id="EVENTDATE">%s</xliff:g> (powtarzane)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Data: <xliff:g id="EVENTDATE">%s</xliff:g> (cały dzień)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Data: <xliff:g id="EVENTDATE">%s</xliff:g> (cały dzień, cykliczne)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Dodano kalendarz Exchange"</string>
+ <string name="app_name" msgid="5316597712787122829">"Usługi wymiany"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"To zdarzenie zostało anulowane dla: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Szczegóły tego zdarzenia zostały zmienione dla: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Nie zezwalaj na karty pamięci"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Nie zezwalaj na niepodpisane aplikacje"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Nie zezwalaj na niepodpisane instalatory"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Nie zezwalaj na używanie sieci Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Nie zezwalaj na wiadomości SMS"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Nie zezwalaj na konta POP3 ani IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Nie zezwalaj na łączność w podczerwieni"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Nie zezwalaj na e-maile w formacie HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Nie zezwalaj na przeglądarki"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Nie zezwalaj na e-maile komercyjne"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Nie zezwalaj na udostępnianie internetu"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Wymagaj wiadomości w formacie SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Ogranicz użycie Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Nie zezwalaj na określone aplikacje"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Zezwalaj tylko na określone aplikacje"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Ograniczaj rozmiar e-maili tekstowych"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Ogranicz rozmiar e-maili w formacie HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Wymagaj szyfrowania na karcie SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Nie zezwalaj na załączniki"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Ograniczaj rozmiar załączników"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"W roamingu tylko synchronizacja ręczna"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Wymagaj szyfrowania w urządzeniu"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automatycznie"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Jeden dzień"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Trzy dni"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Jeden tydzień"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Dwa tygodnie"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Jeden miesiąc"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Wszystkie"</string>
+</resources>
diff --git a/exchange2/res/values-pt-rPT/strings.xml b/exchange2/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..f9ad5e9
--- /dev/null
+++ b/exchange2/res/values-pt-rPT/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Aceite: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Recusado: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Provisório: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Cancelado: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Actualizado: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Quando: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Onde: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Quando: <xliff:g id="EVENTDATE">%s</xliff:g> (periódica)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Quando: <xliff:g id="EVENTDATE">%s</xliff:g> (todo o dia)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Quando: <xliff:g id="EVENTDATE">%s</xliff:g> (todo o dia, recorrente)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Calendário do Exchange adicionado"</string>
+ <string name="app_name" msgid="5316597712787122829">"Serviços do Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Este evento foi cancelado para: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Os detalhes deste evento foram alterados para: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Não permitir cartões de armazenamento"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Não permitir aplicações não assinadas"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Não permitir prog. de inst. não assinados"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Não permitir Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Não permitir mensagens de texto"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Não permitir contas POP3 ou IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Não permitir comun. por infravermelhos"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Não permitir e-mails em HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Não permitir navegadores"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Não permitir e-mails ao consumidor"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Não permitir partilha de internet"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Solicitar mensagens SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Limitar utilização de Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Não permitir aplicações especificadas"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Permitir apenas aplicações especificadas"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Limitar tamanho de e-mails de texto"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Limitar tamanho de e-mails em HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Solicitar encriptação do cartão SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Não permitir anexos"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Limitar tamanho dos anexos"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Permitir apenas sinc. manual em roaming"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Solicitar encriptação do aparelho"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automático"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Um dia"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Três dias"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Uma semana"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Duas semanas"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Um mês"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Todas"</string>
+</resources>
diff --git a/exchange2/res/values-pt/strings.xml b/exchange2/res/values-pt/strings.xml
new file mode 100644
index 0000000..1944e59
--- /dev/null
+++ b/exchange2/res/values-pt/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Aceito: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Recusado: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Tentativa: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Cancelado: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Atualizado: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Quando:<xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Onde: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Quando: <xliff:g id="EVENTDATE">%s</xliff:g> (recorrente)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Quando: <xliff:g id="EVENTDATE">%s</xliff:g> (o dia todo)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Quando: <xliff:g id="EVENTDATE">%s</xliff:g> (o dia todo, recorrente)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Agenda do Exchange adicionada"</string>
+ <string name="app_name" msgid="5316597712787122829">"Trocar serviços"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Este evento foi cancelado para: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Os detalhes deste evento foram alterados para: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Não permitir cartões de armazenamento"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Não permitir aplicativos não assinados"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Não permitir instal. aplic. não assinado"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Não permitir Wi-FI"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Não permitir mensagens de texto"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Não permitir contas POP3 ou IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Não permitir comunic. via infravermelho"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Não permitir e-mail em HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Não permitir navegadores"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Não permitir e-mail do consumidor"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Não permitir compart. pela Internet"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Exigir mensagens em SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Restringir o uso de Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Desautorizar aplicativos especificados"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Permitir somente aplic. especificados"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Restringir o tamanho do e-mail em texto"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Restringir tamanho do e-mail em HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Exigir criptografia do cartão SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Não permitir anexos"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Restringir tamanho do anexo"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Permitir sinc. manual somente em roaming"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Exigir criptografia do dispositivo"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automático"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Um dia"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Três dias"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Uma semana"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Duas semanas"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Um mês"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Todos"</string>
+</resources>
diff --git a/exchange2/res/values-rm/strings.xml b/exchange2/res/values-rm/strings.xml
new file mode 100644
index 0000000..13b3491
--- /dev/null
+++ b/exchange2/res/values-rm/strings.xml
@@ -0,0 +1,90 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Acceptà: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Refusà: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Project dad inscunter: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Interrut: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Actualisà: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Cura: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Nua: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Cura: <xliff:g id="EVENTDATE">%s</xliff:g> (periodic)"</string>
+ <!-- no translation found for meeting_allday (6696389765484663513) -->
+ <skip />
+ <!-- no translation found for meeting_allday_recurring (2320264182781062684) -->
+ <skip />
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Agiuntà in chalender Exchange"</string>
+ <!-- no translation found for app_name (5316597712787122829) -->
+ <skip />
+ <string name="exception_cancel" msgid="6160117429428313805">"Questa occurrenza è vegnida annullada: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Ils detagls da questa occurrenza èn vegnids modifitgads: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <!-- no translation found for policy_dont_allow_storage_cards (2765447013574188473) -->
+ <skip />
+ <!-- no translation found for policy_dont_allow_unsigned_apps (4896164334956001479) -->
+ <skip />
+ <!-- no translation found for policy_dont_allow_unsigned_installers (1326544905185523540) -->
+ <skip />
+ <!-- no translation found for policy_dont_allow_wifi (3109487776704143995) -->
+ <skip />
+ <!-- no translation found for policy_dont_allow_text_messaging (7846141657345860427) -->
+ <skip />
+ <!-- no translation found for policy_dont_allow_pop_imap (4702932192358698651) -->
+ <skip />
+ <!-- no translation found for policy_dont_allow_irda (1848561629495912430) -->
+ <skip />
+ <!-- no translation found for policy_dont_allow_html (5888652525907651489) -->
+ <skip />
+ <!-- no translation found for policy_dont_allow_browser (1018764395507493616) -->
+ <skip />
+ <!-- no translation found for policy_dont_allow_consumer_email (6958427300686692292) -->
+ <skip />
+ <!-- no translation found for policy_dont_allow_internet_sharing (2370083814654927695) -->
+ <skip />
+ <!-- no translation found for policy_require_smime (673557150920820590) -->
+ <skip />
+ <!-- no translation found for policy_bluetooth_restricted (5248824127186039567) -->
+ <skip />
+ <!-- no translation found for policy_app_blacklist (8169194058285873461) -->
+ <skip />
+ <!-- no translation found for policy_app_whitelist (3670572644342165306) -->
+ <skip />
+ <!-- no translation found for policy_text_truncation (1783448050735715818) -->
+ <skip />
+ <!-- no translation found for policy_html_truncation (102158408055486343) -->
+ <skip />
+ <!-- no translation found for policy_require_sd_encryption (366468398301273342) -->
+ <skip />
+ <!-- no translation found for policy_dont_allow_attachments (6250520458670348907) -->
+ <skip />
+ <!-- no translation found for policy_max_attachment_size (4020279603050888661) -->
+ <skip />
+ <!-- no translation found for policy_require_manual_sync_roaming (6637416341015662148) -->
+ <skip />
+ <!-- no translation found for policy_require_encryption (7984702283392885348) -->
+ <skip />
+ <!-- no translation found for account_setup_options_mail_window_auto (4188895354366183790) -->
+ <skip />
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"In di"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Trais dis"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Ina emna"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Duas emnas"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"In mais"</string>
+ <!-- no translation found for account_setup_options_mail_window_all (5372861827683632364) -->
+ <skip />
+</resources>
diff --git a/exchange2/res/values-ro/strings.xml b/exchange2/res/values-ro/strings.xml
new file mode 100644
index 0000000..c5d8825
--- /dev/null
+++ b/exchange2/res/values-ro/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Acceptat: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Refuzat: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Tentativă: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Anulat: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Actualizat: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Când: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Unde: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Când: <xliff:g id="EVENTDATE">%s</xliff:g> (recurent)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Când: <xliff:g id="EVENTDATE">%s</xliff:g> (toată ziua)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Când: <xliff:g id="EVENTDATE">%s</xliff:g> (toată ziua, repetat)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Calendar Exchange adăugat"</string>
+ <string name="app_name" msgid="5316597712787122829">"Servicii Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Acest eveniment a fost anulat pentru: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Detaliile acestui eveniment au fost modificate pentru: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Nu permiteţi carduri de stocare"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Nu permiteţi aplicaţii nesemnate"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Nu permiteţi instal. aplic. nesemnate"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Nu permiteţi Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Nu permiteţi mesagerie text"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Nu permiteţi conturi POP3 sau IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Nu permiteţi comunicaţiile în infraroşu"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Nu permiteţi e-mailuri HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Nu permiteţi browserele"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Nu permiteţi e-mailuri client"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Nu permiteţi distribuirea prin internet"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Solicitaţi mesaje SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Limitaţi util. funcţiilor Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Nu permiteţi anumite aplicaţii"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Permiteţi numai aplicaţii indicate"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Limitaţi dimensiunea e-mailurilor text"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Limitaţi dimensiunea e-mailurilor HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Necesită criptarea cardului SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Nu permiteţi ataşamente"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Limitaţi dimensiunea ataşamentului"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"În roaming permiteţi doar sincr. manuală"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Solicitaţi criptarea dispozitivului"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automat"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"O zi"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Trei zile"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"O săptămână"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Două săptămâni"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"O lună"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Toate"</string>
+</resources>
diff --git a/exchange2/res/values-ru/strings.xml b/exchange2/res/values-ru/strings.xml
new file mode 100644
index 0000000..7bb86c2
--- /dev/null
+++ b/exchange2/res/values-ru/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Принято: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Отклонено: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Может быть: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Отменено: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Обновлено: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Время: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Где: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Время: <xliff:g id="EVENTDATE">%s</xliff:g> (повторяющееся)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Дата: <xliff:g id="EVENTDATE">%s</xliff:g> (весь день)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Дата: <xliff:g id="EVENTDATE">%s</xliff:g> (весь день, повторяется)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Календарь Exchange добавлен"</string>
+ <string name="app_name" msgid="5316597712787122829">"Службы Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Это мероприятие отменено: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Сведения об этом мероприятии были изменены: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Не использовать карты памяти"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Запрещать неподписанные приложения"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Запрещать неподписанные установщики"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Не использовать Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Запрещать SMS"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Не использовать аккаунты POP3 и IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Не использовать инфракрасный порт"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Не принимать письма в формате HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Не использовать браузеры"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Не принимать пользовательскую почту"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Не использовать режим модема"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Принимать только письма в формате SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Ограничить использование Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Запрещать указанные приложения"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Разрешать только указанные приложения"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Ограничивать размер текстовых писем"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Ограничивать размер писем в формате HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Всегда шифровать SD-карты"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Не загружать прикрепленные файлы"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Ограничивать размер прикрепленных файлов"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"В роуминге только ручная синхронизация"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Всегда шифровать устройство"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Автоматически"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Один день"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Три дня"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Одна неделя"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Две недели"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Один месяц"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Все"</string>
+</resources>
diff --git a/exchange2/res/values-sk/strings.xml b/exchange2/res/values-sk/strings.xml
new file mode 100644
index 0000000..0a907ea
--- /dev/null
+++ b/exchange2/res/values-sk/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Potvrdené: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Odmietnuté: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Možno: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Zrušené: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Aktualizované: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Kedy: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Kde: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Kedy: <xliff:g id="EVENTDATE">%s</xliff:g> (pravidelne)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Kedy: <xliff:g id="EVENTDATE">%s</xliff:g> (celý deň)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Kedy: <xliff:g id="EVENTDATE">%s</xliff:g> (celý deň, opakujúce sa)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Kalendár Exchange bol pridaný"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange Services"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Táto udalosť bola zrušená pre termín: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Došlo k zmene podrobností tejto udalosti pre termín: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Nepovoliť karty s ukladacím priestorom"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Nepovoliť nepodpísané aplikácie"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Nepovoliť nepodpísané inštalátory aplikácií"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Nepovoliť siete Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Nepovoliť správy SMS a MMS"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Nepovoliť účty POP3 ani IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Nepovoliť infračervenú komunikáciu"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Nepovoliť e-mail vo formáte HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Nepovoliť prehliadače"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Nepovoliť marketingové e-maily"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Nepovoliť zdieľanie internetu"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Vyžadovať správy vo formáte SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Obmedziť použitie rozhrania Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Nepovoliť určené aplikácie"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Povoliť iba určené aplikácie"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Obmedziť veľkosť textového e-mailu"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Obmedziť veľkosť e-mailu vo formáte HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Vyžadovať šifrovanie kariet SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Nepovoliť prílohy"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Obmedziť veľkosť prílohy"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Povoliť ručnú synchr. len pri roamingu"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Vyžadovať šifrovanie zariadenia"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automaticky"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Jeden deň"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Tri dni"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Týždeň"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Dva týdny"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Jeden mesiac"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Všetky"</string>
+</resources>
diff --git a/exchange2/res/values-sl/strings.xml b/exchange2/res/values-sl/strings.xml
new file mode 100644
index 0000000..61bfbf9
--- /dev/null
+++ b/exchange2/res/values-sl/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Sprejeto: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Zavrnjeno: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Pogojno sprejeto: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Preklicano: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Posodobljeno: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Kdaj: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Kje: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Kdaj: <xliff:g id="EVENTDATE">%s</xliff:g> (se ponavlja)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Kdaj: <xliff:g id="EVENTDATE">%s</xliff:g> (ves dan)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Kdaj: <xliff:g id="EVENTDATE">%s</xliff:g> (ves dan, ponavljajoče)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Dodan koledar računa Exchange"</string>
+ <string name="app_name" msgid="5316597712787122829">"Storitve Exchange Services"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Ta dogodek je bil preklican za: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Podrobnosti tega dogodka so bile spremenjene: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Ne dovoli pomnilniških kartic"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Ne dovoli nepodpisanih programov"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Ne dovoli nepodpisanih namest. programov"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Ne dovoli povezave Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Ne dovoli sporočil SMS"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Ne dovoli računov POP3 ali IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Ne dovoli komunik. z infrardečo povezavo"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Ne dovoli e-poštnih sporočil HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Ne dovoli brskalnikov"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Ne dovoli potrošnišk. e-poštnih sporočil"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Ne dovoli skupne rabe interneta"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Zahtevaj sporočila v obliki zapisa SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Omeji uporabo Bluetootha"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Ne dovoli navedenih programov"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Dovoli samo navedene programe"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Omeji besedilo v e-poštnem sporočilu"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Omeji velikost e-poštnih sporočil HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Zahtevaj šifriranje kartice SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Ne dovoli prilog"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Omeji velikost prilog"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Med gostovanjem dovoli samo ročno sinhr."</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Zahtevaj šifriranje naprave"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Samodejno"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"En dan"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Tri dni"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"En teden"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Dva tedna"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"En mesec"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Vse"</string>
+</resources>
diff --git a/exchange2/res/values-sr/strings.xml b/exchange2/res/values-sr/strings.xml
new file mode 100644
index 0000000..6abf45c
--- /dev/null
+++ b/exchange2/res/values-sr/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Прихваћено: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Одбијен: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Проба: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Отказано: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Ажурирано: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Када: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Где: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Када: <xliff:g id="EVENTDATE">%s</xliff:g> (редовно)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Када: <xliff:g id="EVENTDATE">%s</xliff:g> (цео дан)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Када: <xliff:g id="EVENTDATE">%s</xliff:g> (цео дан, понавља се)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Додат је Exchange календар"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange услуге"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Овај догађај је отказан за: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Детаљи овог догађаја су промењени за: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Не дозволи меморијске картице"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Не дозволи непотписане апликације"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Не дозволи непотписане инст. програме апликација"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Не дозволи Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Не дозволи размену текстуалних порука"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Не дозволи POP3 или IMAP налоге"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Не дозволи инфрацрвене комуникације"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Не дозволи HTML е-пошту"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Не дозволи прегледаче"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Не дозволи корисничку е-пошту"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Не дозволи дељење интернет везе"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Захтевај SMIME поруке"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Ограничи коришћење Bluetooth-а"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Онемогући наведене апликације"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Дозволи само наведене апликације"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Ограничи величину текста порука е-поште"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Ограничи величину HTML порука е-поште"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Захтевај шифровање SD картице"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Не дозволи прилоге"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Ограничи величину прилога"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Дозволи само ручну синхрониз. у ромингу"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Захтевај шифровање уређаја"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Аутоматски"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Један дан"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Три дана"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Недељу дана"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Две недеље"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Месец дана"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Све"</string>
+</resources>
diff --git a/exchange2/res/values-sv/strings.xml b/exchange2/res/values-sv/strings.xml
new file mode 100644
index 0000000..45d9454
--- /dev/null
+++ b/exchange2/res/values-sv/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Accepterat: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Avvisat: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Preliminärt: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Inställt: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Uppdaterat: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"När: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Var: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"När: <xliff:g id="EVENTDATE">%s</xliff:g> (återkommande)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"När: <xliff:g id="EVENTDATE">%s</xliff:g> (hela dagen)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"När: <xliff:g id="EVENTDATE">%s</xliff:g> (hela dagen, återkommande)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Exchange-kalendern har lagts till"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange-tjänster"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Det här händelsen har ställts in för: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Den här händelsens information har ändrats för: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Tillåt inte minneskort"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Tillåt inte osignerade appar"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Tillåt inte installationsprogram för osignerade appar"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Tillåt inte Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Tillåt inte SMS"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Tillåt inte POP3- eller IMAP-konton"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Tillåt inte infraröd kommunikation"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Tillåt inte HTML-e-post"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Tillåt inte webbläsare"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Tillåt inte e-post från konsument"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Tillåt inte Internetdelning"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Kräv SMIME-meddelanden"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Begränsa Bluetooth-användning"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Tillåt inte specificerade appar"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Tillåt endast specificerade appar"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Begränsa textstorleken i e-post"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Begränsa storleken på HTML-e-post"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Kräv kryptering av SD-kort"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Tillåt inte bilagor"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Begränsa bilagans storlek"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Tillåt endast manuell synk vid roaming"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Kräv enhetskryptering"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Automatisk"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"En dag"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Tre dagar"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"En vecka"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Två veckor"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"En månad"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Alla"</string>
+</resources>
diff --git a/exchange2/res/values-sw/strings.xml b/exchange2/res/values-sw/strings.xml
new file mode 100644
index 0000000..e424e5e
--- /dev/null
+++ b/exchange2/res/values-sw/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Amekubali: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Amekataa: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Kwa sasa: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Zilizoghairiwa: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Imesasishwa: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Lini: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Wapi: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Wakati: <xliff:g id="EVENTDATE">%s</xliff:g> (itarudiwa)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Lini: <xliff:g id="EVENTDATE">%s</xliff:g> (siku nzima)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Lini: <xliff:g id="EVENTDATE">%s</xliff:g> (siku nzima, inajirudia)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Kalenda ya Exchange imeongezwa"</string>
+ <string name="app_name" msgid="5316597712787122829">"Huduma za Ubadilishanaji"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Tukio hili limeghairiwa kwa: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Maelezo ya tukio hili yamebadilishwa kwa: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"usiruhusu kadi za kuhifadhi"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Usiruhusu programu ambazo hazina saini"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Usiruhusu visakanishaji vya programu ambavyo havina saini"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Usiruhusu Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Usiruhusu utumaji ujumbe wa maandishi"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"usiruhusu akaunti za POP3 au IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"usiruhusu mawasiliano ya infraredi"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Usiruhusu barua pepe ya HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"usiruhusu vivinjari"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Usiruhusu barua pepe ya watumiaji"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Usiruhusu ushiriki kwenye mtandao"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Inahitaji ujumbe wa SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Zuia matumizi ya Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Usiruhusu programu zilizobainishwa"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Ruhusu programu zilizobainishwa pekee"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"weka mipaka ya ukubwa wa barua pepe ya maandishi"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"weka mipaka ya ukubwa wa barua pepe ya HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Inahitaji usimbaji fiche wa kadi ya SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Usiruhusu viambatisho"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"weka mipaka ya ukubwa wa viambatisho"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Ruhusu usawazishaji wa mkono wakati wa urandaji pekee"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Inahitaji usimbaji fiche wa kifaa"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Kiotomatiki"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Siku moja"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Siku tatu"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Wiki moja"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Wiki mbili"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Mwezi mmoja"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Zote"</string>
+</resources>
diff --git a/exchange2/res/values-th/strings.xml b/exchange2/res/values-th/strings.xml
new file mode 100644
index 0000000..85f6652
--- /dev/null
+++ b/exchange2/res/values-th/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"ตกลงเข้าร่วม: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"ปฏิเสธ: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"ชั่วคราว: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"ยกเลิกแล้ว: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"อัปเดต: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"เวลา: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"สถานที่: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"เวลา: <xliff:g id="EVENTDATE">%s</xliff:g> (เกิดซ้ำ)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"เมื่อ: <xliff:g id="EVENTDATE">%s</xliff:g> (ตลอดวัน)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"เมื่อ: <xliff:g id="EVENTDATE">%s</xliff:g> (ตลอดวัน เกิดซ้ำ)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"เพิ่มปฏิทิน Exchange แล้ว"</string>
+ <string name="app_name" msgid="5316597712787122829">"บริการแลกเปลี่ยน"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"กิจกรรมถูกยกเลิกแล้วในวันที่: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"วันที่ของกิจกรรมนี้ถูกเปลี่ยนเป็น: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"ไม่อนุญาตให้มีการ์ดบันทึกข้อมูล"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"ไม่อนุญาตแอปพลิเคชันที่ไม่ได้ลงชื่อ"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"ไม่อนุญาตตัวติดตั้งแอปฯ ที่ไม่ได้ลงชื่อ"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"ไม่อนุญาตให้ใช้ WiFi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"ไม่อนุญาตการรับส่งข้อความ"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"ไม่อนุญาตให้ใช้บัญชี POP3 หรือ IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"ไม่อนุญาตให้มีการสื่อสารอินฟราเรด"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"ไม่อนุญาตให้รับอีเมลแบบ HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"ไม่อนุญาตให้ใช้เบราว์เซอร์"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"ไม่อนุญาตให้รับอีเมลลูกค้า"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"ไม่อนุญาตการแบ่งปันอินเทอร์เน็ต"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"ต้องมีข้อความรูปแบบ SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"จำกัดการใช้บลูทูธ"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"ไม่อนุญาตให้ใช้แอปพลิเคชันที่ระบุ"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"อนุญาตแอปพลิเคชันที่ระบุเท่านั้น"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"จำกัดขนาดอีเมลแบบข้อความ"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"จำกัดขนาดอีเมลแบบ HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"ต้องมีการเข้ารหัสการ์ด SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"ไม่อนุญาตให้ดาวน์โหลดไฟล์แนบ"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"จำกัดขนาดไฟล์แนบ"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"อนุญาตเฉพาะการซิงค์ด้วยตนเองขณะโรมมิ่ง"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"ต้องมีการเข้ารหัสอุปกรณ์"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"อัตโนมัติ"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"หนึ่งวัน"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"3 วัน"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"1 สัปดาห์"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"สองสัปดาห์"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"1 เดือน"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"ทั้งหมด"</string>
+</resources>
diff --git a/exchange2/res/values-tl/strings.xml b/exchange2/res/values-tl/strings.xml
new file mode 100644
index 0000000..8c4083a
--- /dev/null
+++ b/exchange2/res/values-tl/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Tinanggap: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Tinanggihan: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Pansamantala: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Kinansela: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Na-update: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Kailan: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Saan: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Kailan: <xliff:g id="EVENTDATE">%s</xliff:g> (paulit-ulit)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Kailan: <xliff:g id="EVENTDATE">%s</xliff:g> (buong araw)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Kailan: <xliff:g id="EVENTDATE">%s</xliff:g> (buong araw, umuulit)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Idinagdag ang kalendaryong Exchange"</string>
+ <string name="app_name" msgid="5316597712787122829">"Magpalitan ng Mga Serbisyo"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Kinansela ang kaganapang ito para sa: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Ang mga detalye ng kaganapang ito ay binago para sa: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Huwag payagan ang mga storage card"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Huwag payagan ang mga di-pirmadong app"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Huwag payagan di-pirmadong app installer"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Huwag payagan ang Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Huwag payagan pagpapadala text message"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Huwag payagan mga POP3 o IMAP na account"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Wag payagan mga komunikasyon sa infrared"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Huwag payagan ang HTML na email"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Huwag payagan ang mga browser"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Huwag payagan ang email ng consumer"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Huwag payagan pagbabahagi ng internet"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Hilingin ang mga SMIME na mensahe"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Paghigpitan ang paggamit ng Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Huwag payagan ang mga tinukoy na app"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Payagan lamang ang mga tinukoy na app"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Paghigpitan ang laki ng tekstong email"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Paghigpitan ang laki ng HTML na email"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Hilingin ang pag-encrypt ng SD card"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Huwag payagan ang mga attachment"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Paghigpitan ang laki ng attachment"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Payag lang manu-mano sync kapag roaming"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Hilingin ang pag-encrypt ng device"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Awtomatiko"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Isang araw"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Tatlong araw"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Isang linggo"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Dalawang linggo"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Isang buwan"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Lahat"</string>
+</resources>
diff --git a/exchange2/res/values-tr/strings.xml b/exchange2/res/values-tr/strings.xml
new file mode 100644
index 0000000..a3373db
--- /dev/null
+++ b/exchange2/res/values-tr/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Kabul edildi: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Reddedildi: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Kararsız: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"İptal Edildi: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Güncellendi: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Zaman: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Yer: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Zaman: <xliff:g id="EVENTDATE">%s</xliff:g> (yinelenen)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Tarih: <xliff:g id="EVENTDATE">%s</xliff:g> (tüm gün)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Tarih: <xliff:g id="EVENTDATE">%s</xliff:g> (tüm gün, düzenli)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Exchange takvimi eklendi"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange Hizmetleri"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Bu olay şu tarih için iptal edildi: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Bu olayın ayrıntıları değiştirildi: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Depolama kartlarına izin verme"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"İmzasız uygulamalara izin verme"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"İmzasız uygulama yükleyicilere izin verme"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Kablosuza izin verme"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Kısa mesaja izin verme"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"POP3 veya IMAP hesaplarına izin verme"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Kızılötesi iletişime izin verme"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"HTML e-postaya izin verme"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Tarayıcılara izin verme"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Tüketici e-postasına izin verme"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"İnternet paylaşımına izin verme"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"İletilerin SMIME olmasını gerektir"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Bluetooth kullanımını kısıtla"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Belirtilen uygulamalara izin verme"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Sadece belirtilen uygulamalara izin ver"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Metin e-posta boyutunu kısıtla"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"HTML e-posta boyutunu kısıtla"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"SD kart şifrelemesi gerektir"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Eklere izin verme"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Ek boyutunu kısıtla"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Dolaşımda sadece elle senk. izin ver"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Cihaz şifrelemesi gerektir"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Otomatik"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Bir gün"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Üç gün"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Bir hafta"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"İki hafta"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Bir ay"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Tümü"</string>
+</resources>
diff --git a/exchange2/res/values-uk/strings.xml b/exchange2/res/values-uk/strings.xml
new file mode 100644
index 0000000..9980194
--- /dev/null
+++ b/exchange2/res/values-uk/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Прийнято: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Відхилено: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Попередньо: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Скасовано: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Оновлено: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Коли: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Де: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Коли: <xliff:g id="EVENTDATE">%s</xliff:g> (повтор.)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Коли: <xliff:g id="EVENTDATE">%s</xliff:g> (увесь день)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Коли: <xliff:g id="EVENTDATE">%s</xliff:g> (увесь день, періодично)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Додано календар Exchange"</string>
+ <string name="app_name" msgid="5316597712787122829">"Служби Exchange"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Цю подію було скасовано для: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Деталі цієї події змінено на: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Не дозволяти карти пам’яті"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Не дозволяти непідписані програми"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Не дозвол. встановлюв. непідпис. прогр."</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Не дозволяти Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Не дозволяти текстові повідомлення"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Не дозволяти облік. записи POP3 або IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Не дозволяти інфрачервоний зв’язок"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Не дозволяти листи у форматі HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Не дозволяти веб-переглядачі"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Не дозволяти поштові сервіси"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Не дозвол. надання доступу до Інтернету"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Вимагати повідомлення у форматі SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Обмежувати використання Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Забороняти вказані програми"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Дозволяти лише вказані програми"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Обмежувати розмір тексту листів"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Обмежувати розмір листів у форматі HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Вимагати шифрування карти SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Не дозволяти вкладені файли"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Обмежувати розмір вкладених файлів"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Дозвол. синхрон. вручну лише в роумінгу"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Вимагати шифрування пристрою"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Автоматично"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Один день"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Три дні"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"1 тиждень"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"2 тижні"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Один місяць"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Усі"</string>
+</resources>
diff --git a/exchange2/res/values-vi/strings.xml b/exchange2/res/values-vi/strings.xml
new file mode 100644
index 0000000..fc77541
--- /dev/null
+++ b/exchange2/res/values-vi/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Được chấp nhận: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Đã từ chối: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Dự kiến: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Đã hủy: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Đã cập nhật: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Thời gian: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Địa điểm: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Thời gian: <xliff:g id="EVENTDATE">%s</xliff:g> (lặp lại)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Thời gian: <xliff:g id="EVENTDATE">%s</xliff:g> (cả ngày)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Thời gian: <xliff:g id="EVENTDATE">%s</xliff:g> (cả ngày, lặp lại)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Đã thêm lịch Exchange"</string>
+ <string name="app_name" msgid="5316597712787122829">"Dịch vụ hối đoái"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Sự kiện này đã bị hủy cho: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Chi tiết về sự kiện này đã được thay đổi cho: <xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"Không cho phép thẻ lưu trữ"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Không cho phép ứng dụng chưa kiểm tra"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Ko cho phép tr.cài đặt ứ.dụng chưa k.tra"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Không cho phép Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"Không cho phép nhắn tin văn bản"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Không cho phép tài khoản POP3 hoặc IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"Không cho phép giao tiếp bằng hồng ngoại"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"Không cho phép email HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"Không cho phép trình duyệt"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"Không cho phép email của người dùng"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Không cho phép chia sẻ kết nối Internet"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"Yêu cầu tin nhắn SMIME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Hạn chế sử dụng Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Ko cho phép các ứng dụng được chỉ định"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Chỉ cho phép các ứng dụng được chỉ định"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Hạn chế kích thước email văn bản"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Hạn chế kích thước email HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Yêu cầu mã hóa thẻ SD"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"Không cho phép tệp đính kèm"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Hạn chế kích thước của tệp đính kèm"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"Chỉ được đ.bộ hóa thủ công khi ch.vùng"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"Yêu cầu mã hóa thiết bị"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Tự động"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Một ngày"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Ba ngày"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Một tuần"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Hai tuần"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Một tháng"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Tất cả"</string>
+</resources>
diff --git a/exchange2/res/values-zh-rCN/strings.xml b/exchange2/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..2c004ed
--- /dev/null
+++ b/exchange2/res/values-zh-rCN/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"已接受:<xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"已拒绝:<xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"暂定:<xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"已取消:<xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"已更新:<xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"时间:<xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"地点:<xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"时间:<xliff:g id="EVENTDATE">%s</xliff:g>(重复)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"日期:<xliff:g id="EVENTDATE">%s</xliff:g>(全天)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"日期:<xliff:g id="EVENTDATE">%s</xliff:g>(全天,循环)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"已添加 Exchange 日历"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange 服务"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"定于以下日期的活动已取消:<xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"定于以下日期的活动的详细信息已更改:<xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"不允许使用存储卡"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"不允许安装未签名的应用"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"不允许通过未签名的安装程序来安装应用"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"不允许使用 Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"不允许使用短信功能"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"不允许使用 POP3 或 IMAP 帐户"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"不允许红外通信"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"不允许接收 HTML 电子邮件"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"不允许使用浏览器"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"不允许接收消费者电子邮件"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"不允许共享互联网连接"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"电子邮件必须采用 SMIME 格式"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"限制蓝牙功能"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"不允许使用指定的应用"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"只允许使用指定的应用"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"限制文本电子邮件的大小"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"限制 HTML 电子邮件的大小"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"要求 SD 卡加密"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"不允许下载附件"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"限制附件的大小"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"只允许在漫游时手动同步"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"要求设备加密"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"自动"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"一天"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"三天"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"一周"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"两周"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"一个月"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"全部"</string>
+</resources>
diff --git a/exchange2/res/values-zh-rTW/strings.xml b/exchange2/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..12842a6
--- /dev/null
+++ b/exchange2/res/values-zh-rTW/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Microsoft Exchange ActiveSync"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"已接受:<xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"已拒絕:<xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"未定:<xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"已取消:<xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"已更新:<xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"登入時間:<xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"地點:<xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"時間:<xliff:g id="EVENTDATE">%s</xliff:g> (週期性)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"時間:<xliff:g id="EVENTDATE">%s</xliff:g> (整天)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"時間:<xliff:g id="EVENTDATE">%s</xliff:g> (整天,週期性)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"已新增 Exchange 日曆"</string>
+ <string name="app_name" msgid="5316597712787122829">"Exchange 服務"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"已將 <xliff:g id="DATE">%s</xliff:g> 時的此活動取消"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"已變更此活動於 <xliff:g id="DATE">%s</xliff:g> 時的詳細資料"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"不允許儲存卡"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"不允許未簽署的應用程式"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"不允許未簽署的應用程式安裝程式"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"不允許 WiFi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"不允許傳送簡訊"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"不允許 POP3 或 IMAP 帳戶"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"不允許紅外線通訊"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"不允許接收 HTML 電子郵件"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"不允許瀏覽器"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"不允許接收消費者電子郵件"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"不允許網際網路共用功能"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"需要 SMIME 郵件"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"限制藍牙使用量"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"不允許指定的應用程式"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"只允許指定的應用程式"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"限制文字電子郵件大小"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"限制 HTML 電子郵件大小"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"需要 SD 卡加密"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"不允許附件"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"限制附件大小"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"漫遊時僅允許手動同步處理"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"需要裝置加密"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"自動"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"1 天"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"3 天"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"1 週"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"2 週"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"1 個月"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"全部"</string>
+</resources>
diff --git a/exchange2/res/values-zu/strings.xml b/exchange2/res/values-zu/strings.xml
new file mode 100644
index 0000000..b97af51
--- /dev/null
+++ b/exchange2/res/values-zu/strings.xml
@@ -0,0 +1,63 @@
+<?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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="exchange_name_alternate" msgid="5772529644749041052">"Ukuvumelanisa Okusebenzayo Kokushintshanisa kwe-Microsoft"</string>
+ <string name="meeting_accepted" msgid="8796609373330400268">"Kwamukeliwe: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_declined" msgid="6707617183246608552">"Inqabile: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_tentative" msgid="8250995722130443785">"Okwesikhashana: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_canceled" msgid="3949893881872084244">"Kukhanseliwe: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_updated" msgid="8529675857361702860">"Kubuyekeziwe: <xliff:g id="SUBJECT">%s</xliff:g>"</string>
+ <string name="meeting_when" msgid="2765696159697448656">"Nini: <xliff:g id="WHEN">%s</xliff:g>"</string>
+ <string name="meeting_where" msgid="5992367535856553079">"Kuphi: <xliff:g id="WHERE">%s</xliff:g>"</string>
+ <string name="meeting_recurring" msgid="3134262212606714023">"Nini: <xliff:g id="EVENTDATE">%s</xliff:g> (okuvela njalo)"</string>
+ <string name="meeting_allday" msgid="6696389765484663513">"Nini: <xliff:g id="EVENTDATE">%s</xliff:g> (usuku lonke)"</string>
+ <string name="meeting_allday_recurring" msgid="2320264182781062684">"Nini: <xliff:g id="EVENTDATE">%s</xliff:g> (usuku lonke, okuqhubekayo)"</string>
+ <string name="notification_exchange_calendar_added" msgid="6823659622379350159">"Ukushintsha ikhalenda kufakiwe"</string>
+ <string name="app_name" msgid="5316597712787122829">"Amasevisi Okushintshelana"</string>
+ <string name="exception_cancel" msgid="6160117429428313805">"Lesi senzakalo sikhanselwe i-<xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="exception_updated" msgid="3397583105901142050">"Imininingwane yalesenzakalo ishintshelwe:<xliff:g id="DATE">%s</xliff:g>"</string>
+ <string name="policy_dont_allow_storage_cards" msgid="2765447013574188473">"ungavumeli amakhadi okulonda"</string>
+ <string name="policy_dont_allow_unsigned_apps" msgid="4896164334956001479">"Ungavumeli ama-apps angabhalisiwe"</string>
+ <string name="policy_dont_allow_unsigned_installers" msgid="1326544905185523540">"Ungafumeli izifaki ze-app ezingangenanga"</string>
+ <string name="policy_dont_allow_wifi" msgid="3109487776704143995">"Ungavumeli i-Wi-Fi"</string>
+ <string name="policy_dont_allow_text_messaging" msgid="7846141657345860427">"ungavumeli imilayezo yombhalo"</string>
+ <string name="policy_dont_allow_pop_imap" msgid="4702932192358698651">"Ungavumeli i-POP3 noma ama-akhawunti e-IMAP"</string>
+ <string name="policy_dont_allow_irda" msgid="1848561629495912430">"ungavumeli ukukhulumisana kwe-infrared"</string>
+ <string name="policy_dont_allow_html" msgid="5888652525907651489">"ungavumeli i-imeyli ye-HTML"</string>
+ <string name="policy_dont_allow_browser" msgid="1018764395507493616">"ungavumeli isiphequluli"</string>
+ <string name="policy_dont_allow_consumer_email" msgid="6958427300686692292">"ungavumeli i-imeyli yomthengi"</string>
+ <string name="policy_dont_allow_internet_sharing" msgid="2370083814654927695">"Ungavumeli ukwabelana nge-inthanethi"</string>
+ <string name="policy_require_smime" msgid="673557150920820590">"idinga imiyalezo ye-SMME"</string>
+ <string name="policy_bluetooth_restricted" msgid="5248824127186039567">"Nciphisa ukusetshenziswa kwe-Bluetooth"</string>
+ <string name="policy_app_blacklist" msgid="8169194058285873461">"Ungavumeli ama-apps angacacisiwe"</string>
+ <string name="policy_app_whitelist" msgid="3670572644342165306">"Vumela ama-apps avunyelwe"</string>
+ <string name="policy_text_truncation" msgid="1783448050735715818">"Nciphisa usayizi wombhalo we-imeyli"</string>
+ <string name="policy_html_truncation" msgid="102158408055486343">"Nciphisa usayizi wama-imeyli e-HTML"</string>
+ <string name="policy_require_sd_encryption" msgid="366468398301273342">"Idinga ikhadimfihlo lekhadi le-sd"</string>
+ <string name="policy_dont_allow_attachments" msgid="6250520458670348907">"ungavumeli okunamathiselwa ku-imeyli"</string>
+ <string name="policy_max_attachment_size" msgid="4020279603050888661">"Nciphisa usayizi wokunamathiselwa ku-imeyli"</string>
+ <string name="policy_require_manual_sync_roaming" msgid="6637416341015662148">"vumela ukuvumelanisa ngokwenziwa ngezandla kuphela uma uzulazula"</string>
+ <string name="policy_require_encryption" msgid="7984702283392885348">"idinga amakhodimfihlo edivayisi"</string>
+ <string name="account_setup_options_mail_window_auto" msgid="4188895354366183790">"Okuzenzakalelayo"</string>
+ <string name="account_setup_options_mail_window_1day" msgid="3965715241135811407">"Usuku olulodwa"</string>
+ <string name="account_setup_options_mail_window_3days" msgid="736181102295878114">"Izinsuku ezintathu"</string>
+ <string name="account_setup_options_mail_window_1week" msgid="5639718031108023741">"Iviki elilodwa"</string>
+ <string name="account_setup_options_mail_window_2weeks" msgid="4567049268124213035">"Amaviki amabili"</string>
+ <string name="account_setup_options_mail_window_1month" msgid="5846359669750047081">"Inyanga eyodwa"</string>
+ <string name="account_setup_options_mail_window_all" msgid="5372861827683632364">"Konke"</string>
+</resources>
diff --git a/exchange2/res/values/arrays.xml b/exchange2/res/values/arrays.xml
new file mode 100644
index 0000000..d2f24f6
--- /dev/null
+++ b/exchange2/res/values/arrays.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <!-- Mail sync window sizes for EAS accounts -->
+ <!-- The window length array/strings below MUST remain in sync with com.android.email -->
+ <string-array name="account_settings_mail_window_entries">
+ <item>@string/account_setup_options_mail_window_auto</item>
+ <item>@string/account_setup_options_mail_window_1day</item>
+ <item>@string/account_setup_options_mail_window_3days</item>
+ <item>@string/account_setup_options_mail_window_1week</item>
+ <item>@string/account_setup_options_mail_window_2weeks</item>
+ <item>@string/account_setup_options_mail_window_1month</item>
+ <item>@string/account_setup_options_mail_window_all</item>
+ </string-array>
+
+</resources>
diff --git a/exchange2/res/values/strings.xml b/exchange2/res/values/strings.xml
new file mode 100644
index 0000000..1716e35
--- /dev/null
+++ b/exchange2/res/values/strings.xml
@@ -0,0 +1,198 @@
+<?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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- 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. -->
+
+ <!-- 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 subject for meeting accepted response. This will be followed
+ by a colon and the title of the meeting (i.e. the title of the meeting becomes
+ part of the subject of the message that's sent) -->
+ <string name="meeting_accepted">
+ Accepted:
+ <xliff:g id="subject">%s</xliff:g>
+ </string>
+ <!-- Message subject for meeting declined response. This is followed by
+ a colon and the title of the meeting (i.e. the title of the meeting becomes
+ part of the subject of the message that's sent) -->
+ <string name="meeting_declined">
+ Declined:
+ <xliff:g id="subject">%s</xliff:g>
+ </string>
+ <!-- Message subject for meeting tentative response. This is followed by
+ a colon and the title of the meeting (i.e. the title of the meeting becomes
+ part of the subject of the message that's sent) -->
+ <string name="meeting_tentative">
+ Tentative:
+ <xliff:g id="subject">%s</xliff:g>
+ </string>
+ <!-- Message subject for a canceled meeting email. This is followed by a
+ colon and the title of the meeting (i.e. the title of the meeting becomes
+ part of the subject of the message that's sent) -->
+ <string name="meeting_canceled">
+ Canceled:
+ <xliff:g id="subject">%s</xliff:g>
+ </string>
+ <!-- Message subject for an updated meeting email. This is followed by a
+ colon and the title of the meeting (i.e. the title of the meeting becomes
+ part of the subject of the message that's sent) -->
+ <string name="meeting_updated">
+ Updated:
+ <xliff:g id="subject">%s</xliff:g>
+ </string>
+
+ <!-- Indicate when a meeting takes place. This is presented in in bullet
+ form, as in, "When: xxx" followed by "Where: xxx" -->
+ <string name="meeting_when">
+ When:
+ <xliff:g id="when">%s</xliff:g>
+ </string>
+ <!-- Indicate where a meeting takes place. This is presented in in bullet
+ form, as in, "When: xxx" followed by "Where: xxx" -->
+ <string name="meeting_where">
+ Where:
+ <xliff:g id="where">%s</xliff:g>
+ </string>
+ <!-- Indicate that a meeting is recurring. This would normally be presented
+ after "When: xxx", e.g. "When: Tue, Mar 10, 2010 at 2:30pm (recurring)" -->
+ <string name="meeting_recurring">
+ When:
+ <xliff:g id="eventdate" example="Tue, Mar 10, 2010 at 2:30 pm">%s</xliff:g>
+ (recurring)
+ </string>
+ <!-- Indicate that a meeting lasts all day. This would normally be presented
+ after "When: xxx", e.g. "When: Tue, Mar 10, 2010 (all day)" -->
+ <string name="meeting_allday">
+ When:
+ <xliff:g id="eventdate" example="Tue, Mar 10, 2010 ">%s</xliff:g>
+ (all day)
+ </string>
+ <!-- Indicate that a meeting lasts all day and is recurring. This would normally be presented
+ after "When: xxx", e.g. "When: Tue, Mar 10, 2010 (all day, recurring)" -->
+ <string name="meeting_allday_recurring">
+ When:
+ <xliff:g id="eventdate" example="Tue, Mar 10, 2010 ">%s</xliff:g>
+ (all day, recurring)
+ </string>
+
+ <!-- Notification message in notifications window when calendar sync is
+ automatically enabled for pre-existing Exchange accounts on upgrade -->
+ <string name="notification_exchange_calendar_added">Exchange calendar added</string>
+
+ <!-- The name of this APK as shown in manage applications settings. [CHAR LIMIT=30] -->
+ <string name="app_name" >Exchange Services</string>
+
+ <!-- Used as the body text of a message reporting to an attendee that a
+ specific instance of a recurring meeting has been canceled -->
+ <string name="exception_cancel">
+ This event has been canceled
+ for:
+ <xliff:g id="date">%s</xliff:g>
+ </string>
+ <!-- Used as the body text of a message reporting to an attendee that a
+ specific instance of a recurring meeting has been changed -->
+ <string name="exception_updated">
+ The details of this event have been changed
+ for:
+ <xliff:g id="date">%s</xliff:g>
+ </string>
+
+ <!-- The following are a list of policies that the user's server might require, but that can't
+ be enforced by our device. We will list them separated by commas, as required -->
+ <!-- A policy in which the device may not have a storage card [CHAR LIMIT=50] -->
+ <string name="policy_dont_allow_storage_cards">Don\'t allow storage cards</string>
+ <!-- A policy in which the device may not have unsigned applications installed
+ [CHAR LIMIT=50] -->
+ <string name="policy_dont_allow_unsigned_apps">Don\'t allow unsigned apps</string>
+ <!-- A policy in which the device may not allow application installation via an unsigned
+ installer [CHAR LIMIT=50] -->
+ <string name="policy_dont_allow_unsigned_installers">Don\'t allow unsigned app
+ installers</string>
+ <!-- A policy in which the device may not allow wifi communications [CHAR LIMIT=50] -->
+ <string name="policy_dont_allow_wifi">Don\'t allow Wi-Fi</string>
+ <!-- A policy in which the device may not allow text messaging [CHAR LIMIT=50] -->
+ <string name="policy_dont_allow_text_messaging">Don\'t allow text messaging</string>
+ <!-- A policy in which the device may not allow POP3 or IMAP email accounts [CHAR LIMIT=50] -->
+ <string name="policy_dont_allow_pop_imap">Don\'t allow POP3 or IMAP accounts</string>
+ <!-- A policy in which the device may allow infrared communications [CHAR LIMIT=50] -->
+ <string name="policy_dont_allow_irda">Don\'t allow infrared communications</string>
+ <!-- A policy in which the device may allow HTML email to be received [CHAR LIMIT=50] -->
+ <string name="policy_dont_allow_html">Don\'t allow HTML email</string>
+ <!-- A policy in which the device may not allow the user of web browsers [CHAR LIMIT=50] -->
+ <string name="policy_dont_allow_browser">Don\'t allow browsers</string>
+ <!-- A policy in which the device may not allow the receipt of consumer email
+ [CHAR LIMIT=50] -->
+ <string name="policy_dont_allow_consumer_email">Don\'t allow consumer email</string>
+ <!-- A policy in which the device may not allow internet connection sharing
+ [CHAR LIMIT=50] -->
+ <string name="policy_dont_allow_internet_sharing">Don\'t allow Internet sharing</string>
+ <!-- A policy in which messages must be in SMIME format [CHAR LIMIT=50] -->
+ <string name="policy_require_smime">Require SMIME messages</string>
+ <!-- A policy in which the device's bluetooth capabilities are restricted [CHAR LIMIT=50] -->
+ <string name="policy_bluetooth_restricted">Restrict Bluetooth usage</string>
+ <!-- A policy in which the device blocks specified applications [CHAR LIMIT=50]-->
+ <string name="policy_app_blacklist">Disallow specified apps</string>
+ <!-- A policy in which the device allows only specified applications [CHAR LIMIT=50] -->
+ <string name="policy_app_whitelist">Allow only specified apps</string>
+ <!-- A policy in which the device limits the amount of text that can be displayed for a
+ given message [CHAR LIMIT=50] -->
+ <string name="policy_text_truncation">Restrict text email size</string>
+ <!-- A policy in which the device limits the amount of HTML text that can be displayed for a
+ given message [CHAR LIMIT=50] -->
+ <string name="policy_html_truncation">Restrict HTML email size</string>
+ <!-- A policy in which the device requires device or sd card encryption [CHAR LIMIT=50] -->
+ <string name="policy_require_sd_encryption">Require SD card encryption</string>
+
+ <!-- The following are a list of policies that the user's server requires and that are
+ in force. We will list them separated by commas, as required -->
+ <!-- A policy in which attachments aren't allowed to be downloaded [CHAR LIMIT=50] -->
+ <string name="policy_dont_allow_attachments">Don\'t allow attachments</string>
+ <!-- A policy in which the device restricts the size of attachments that can be downloaded
+ [CHAR LIMIT=50] -->
+ <string name="policy_max_attachment_size">Restrict attachment size</string>
+ <!-- A policy in which the device may only sync manually while roaming [CHAR LIMIT=50] -->
+ <string name="policy_require_manual_sync_roaming">Only allow manual sync while roaming</string>
+
+ <!-- The following is a policy that may or not be supported on a particular device -->
+ <!-- A policy in which the device requires device or sd card encryption [CHAR LIMIT=50] -->
+ <string name="policy_require_encryption">Require device encryption</string>
+
+
+ <!-- The window length strings below MUST remain in sync with those in com.android.email -->
+ <!-- In account setup options & account settings screens (exchange), sync window length; this
+ implies loading a 'reasonable' number of messages [CHAR LIMIT=25] -->
+ <string name="account_setup_options_mail_window_auto">Automatic</string>
+ <!-- A sync window length setting (i.e. load messages this far back) [CHAR LIMIT=25] -->
+ <string name="account_setup_options_mail_window_1day">One day</string>
+ <!-- A sync window length setting (i.e. load messages this far back) [CHAR LIMIT=25] -->
+ <string name="account_setup_options_mail_window_3days">Three days</string>
+ <!-- A sync window length setting (i.e. load messages this far back) [CHAR LIMIT=25] -->
+ <string name="account_setup_options_mail_window_1week">One week</string>
+ <!-- A sync window length setting (i.e. load messages this far back) [CHAR LIMIT=25] -->
+ <string name="account_setup_options_mail_window_2weeks">Two weeks</string>
+ <!-- A sync window length setting (i.e. load messages this far back) [CHAR LIMIT=25] -->
+ <string name="account_setup_options_mail_window_1month">One month</string>
+ <!-- A sync window length setting (i.e. load messages this far back) [CHAR LIMIT=25] -->
+ <string name="account_setup_options_mail_window_all">All</string>
+</resources>
diff --git a/exchange2/res/xml/syncadapter_calendar.xml b/exchange2/res/xml/syncadapter_calendar.xml
new file mode 100644
index 0000000..a6c0fc6
--- /dev/null
+++ b/exchange2/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/exchange2/res/xml/syncadapter_contacts.xml b/exchange2/res/xml/syncadapter_contacts.xml
new file mode 100644
index 0000000..4691aee
--- /dev/null
+++ b/exchange2/res/xml/syncadapter_contacts.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2009, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- The attributes in this XML file provide configuration information -->
+<!-- for the SyncAdapter. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="com.android.contacts"
+ android:accountType="com.android.exchange"
+/>
diff --git a/exchange2/res/xml/syncadapter_email.xml b/exchange2/res/xml/syncadapter_email.xml
new file mode 100644
index 0000000..c7ca850
--- /dev/null
+++ b/exchange2/res/xml/syncadapter_email.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2010, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- The attributes in this XML file provide configuration information -->
+<!-- for the SyncAdapter. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="com.android.email.provider"
+ android:accountType="com.android.exchange"
+ android:supportsUploading="false"
+/>
diff --git a/exchange2/src/com/android/exchange/AbstractSyncService.java b/exchange2/src/com/android/exchange/AbstractSyncService.java
new file mode 100644
index 0000000..6ccc7c3
--- /dev/null
+++ b/exchange2/src/com/android/exchange/AbstractSyncService.java
@@ -0,0 +1,296 @@
+/*
+ * 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.emailcommon.provider.Account;
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.exchange.utility.FileLogger;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Base class for all protocol services SyncManager (extends Service, implements
+ * Runnable) instantiates subclasses to run a sync (either timed, or push, or
+ * mail placed in outbox, etc.) EasSyncService is currently implemented; my goal
+ * would be to move IMAP to this structure when it comes time to introduce push
+ * functionality.
+ */
+public abstract class AbstractSyncService implements Runnable {
+
+ public String TAG = "AbstractSyncService";
+
+ public static final int SECONDS = 1000;
+ public static final int MINUTES = 60*SECONDS;
+ public static final int HOURS = 60*MINUTES;
+ public static final int DAYS = 24*HOURS;
+
+ public static final int CONNECT_TIMEOUT = 30*SECONDS;
+ public static final int NETWORK_WAIT = 15*SECONDS;
+
+ public static final String EAS_PROTOCOL = "eas";
+ public static final int EXIT_DONE = 0;
+ public static final int EXIT_IO_ERROR = 1;
+ public static final int EXIT_LOGIN_FAILURE = 2;
+ public static final int EXIT_EXCEPTION = 3;
+ public static final int EXIT_SECURITY_FAILURE = 4;
+ public static final int EXIT_ACCESS_DENIED = 5;
+
+ public Mailbox mMailbox;
+ protected long mMailboxId;
+ protected int mExitStatus = EXIT_EXCEPTION;
+ protected String mMailboxName;
+ public Account mAccount;
+ public Context mContext;
+ public int mChangeCount = 0;
+ public volatile int mSyncReason = 0;
+ protected volatile boolean mStop = false;
+ protected volatile Thread mThread;
+ protected final Object mSynchronizer = new Object();
+
+ protected volatile long mRequestTime = 0;
+ protected LinkedBlockingQueue<Request> mRequestQueue = new LinkedBlockingQueue<Request>();
+
+ /**
+ * Sent by SyncManager to request that the service stop itself cleanly
+ */
+ public abstract void stop();
+
+ /**
+ * Sent by SyncManager to indicate that an alarm has fired for this service, and that its
+ * pending (network) operation has timed out. The service is NOT automatically stopped,
+ * although the behavior is service dependent.
+ *
+ * @return true if the operation was stopped normally; false if the thread needed to be
+ * interrupted.
+ */
+ public abstract boolean alarm();
+
+ /**
+ * Sent by SyncManager to request that the service reset itself cleanly; the meaning of this
+ * operation is service dependent.
+ */
+ public abstract void reset();
+
+ /**
+ * Called to validate an account; abstract to allow each protocol to do what
+ * is necessary. For consistency with the Email app's original
+ * functionality, success is indicated by a failure to throw an Exception
+ * (ugh). Parameters are self-explanatory
+ *
+ * @param hostAuth
+ * @return a Bundle containing a result code and, depending on the result, a PolicySet or an
+ * error message
+ */
+ public abstract Bundle validateAccount(HostAuth hostAuth, Context context);
+
+ public AbstractSyncService(Context _context, Mailbox _mailbox) {
+ mContext = _context;
+ mMailbox = _mailbox;
+ mMailboxId = _mailbox.mId;
+ mMailboxName = _mailbox.mServerId;
+ mAccount = Account.restoreAccountWithId(_context, _mailbox.mAccountKey);
+ }
+
+ // Will be required when subclasses are instantiated by name
+ public AbstractSyncService(String prefix) {
+ }
+
+ /**
+ * The UI can call this static method to perform account validation. This method wraps each
+ * protocol's validateAccount method. Arguments are self-explanatory, except where noted.
+ *
+ * @param klass the protocol class (EasSyncService.class for example)
+ * @param hostAuth
+ * @param context
+ * @return a Bundle containing a result code and, depending on the result, a PolicySet or an
+ * error message
+ */
+ public static Bundle validate(Class<? extends AbstractSyncService> klass,
+ HostAuth hostAuth, Context context) {
+ AbstractSyncService svc;
+ try {
+ svc = klass.newInstance();
+ return svc.validateAccount(hostAuth, context);
+ } catch (IllegalAccessException e) {
+ } catch (InstantiationException e) {
+ }
+ return null;
+ }
+
+ public static class ValidationResult {
+ static final int NO_FAILURE = 0;
+ static final int CONNECTION_FAILURE = 1;
+ static final int VALIDATION_FAILURE = 2;
+ static final int EXCEPTION = 3;
+
+ static final ValidationResult succeeded = new ValidationResult(true, NO_FAILURE, null);
+ boolean success;
+ int failure = NO_FAILURE;
+ String reason = null;
+ Exception exception = null;
+
+ ValidationResult(boolean _success, int _failure, String _reason) {
+ success = _success;
+ failure = _failure;
+ reason = _reason;
+ }
+
+ ValidationResult(boolean _success) {
+ success = _success;
+ }
+
+ ValidationResult(Exception e) {
+ success = false;
+ failure = EXCEPTION;
+ exception = e;
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+ }
+
+ public boolean isStopped() {
+ return mStop;
+ }
+
+ public Object getSynchronizer() {
+ return mSynchronizer;
+ }
+
+ /**
+ * Convenience methods to do user logging (i.e. connection activity). Saves a bunch of
+ * repetitive code.
+ */
+ public void userLog(String string, int code, String string2) {
+ if (Eas.USER_LOG) {
+ userLog(string + code + string2);
+ }
+ }
+
+ public void userLog(String string, int code) {
+ if (Eas.USER_LOG) {
+ userLog(string + code);
+ }
+ }
+
+ public void userLog(String str, Exception e) {
+ if (Eas.USER_LOG) {
+ Log.e(TAG, str, e);
+ } else {
+ Log.e(TAG, str + e);
+ }
+ if (Eas.FILE_LOG) {
+ FileLogger.log(e);
+ }
+ }
+
+ /**
+ * Standard logging for EAS.
+ * If user logging is active, we concatenate any arguments and log them using Log.d
+ * We also check for file logging, and log appropriately
+ * @param strings strings to concatenate and log
+ */
+ public void userLog(String ...strings) {
+ if (Eas.USER_LOG) {
+ String logText;
+ if (strings.length == 1) {
+ logText = strings[0];
+ } else {
+ StringBuilder sb = new StringBuilder(64);
+ for (String string: strings) {
+ sb.append(string);
+ }
+ logText = sb.toString();
+ }
+ Log.d(TAG, logText);
+ if (Eas.FILE_LOG) {
+ FileLogger.log(TAG, logText);
+ }
+ }
+ }
+
+ /**
+ * Error log is used for serious issues that should always be logged
+ * @param str the string to log
+ */
+ public void errorLog(String str) {
+ Log.e(TAG, str);
+ if (Eas.FILE_LOG) {
+ FileLogger.log(TAG, str);
+ }
+ }
+
+ /**
+ * Waits for up to 10 seconds for network connectivity; returns whether or not there is
+ * network connectivity.
+ *
+ * @return whether there is network connectivity
+ */
+ public boolean hasConnectivity() {
+ ConnectivityManager cm =
+ (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ int tries = 0;
+ while (tries++ < 1) {
+ // Use the same test as in ExchangeService#waitForConnectivity
+ // TODO: Create common code for this test in emailcommon
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ if (info != null) {
+ return true;
+ }
+ try {
+ Thread.sleep(10*SECONDS);
+ } catch (InterruptedException e) {
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Request handling (common functionality)
+ * Can be overridden if desired
+ */
+
+ public void addRequest(Request req) {
+ mRequestQueue.offer(req);
+ }
+
+ public void removeRequest(Request req) {
+ mRequestQueue.remove(req);
+ }
+
+ public boolean hasPendingRequests() {
+ return !mRequestQueue.isEmpty();
+ }
+
+ public void clearRequests() {
+ mRequestQueue.clear();
+ }
+}
diff --git a/exchange2/src/com/android/exchange/CalendarSyncAdapterService.java b/exchange2/src/com/android/exchange/CalendarSyncAdapterService.java
new file mode 100644
index 0000000..5ff03b3
--- /dev/null
+++ b/exchange2/src/com/android/exchange/CalendarSyncAdapterService.java
@@ -0,0 +1,152 @@
+/*
+ * 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 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.CalendarContract.Events;
+import android.util.Log;
+
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.AccountColumns;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.Mailbox;
+
+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;
+ private static final String DIRTY_IN_ACCOUNT =
+ Events.DIRTY + "=1 AND " + Events.ACCOUNT_NAME + "=?";
+ private static final String[] ID_SYNC_KEY_PROJECTION =
+ new String[] {MailboxColumns.ID, MailboxColumns.SYNC_KEY};
+ private static final int ID_SYNC_KEY_MAILBOX_ID = 0;
+ private static final int ID_SYNC_KEY_SYNC_KEY = 1;
+
+ 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 ExchangeService to start a
+ * calendar sync when we get the signal from 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 (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
+ Cursor c = cr.query(Events.CONTENT_URI,
+ new String[] {Events._ID}, DIRTY_IN_ACCOUNT, new String[] {account.name}, null);
+ try {
+ if (!c.moveToFirst()) {
+ if (logging) {
+ Log.d(TAG, "No changes for " + account.name);
+ }
+ return;
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ // Find the (EmailProvider) account associated with this email address
+ Cursor accountCursor =
+ cr.query(com.android.emailcommon.provider.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, ID_SYNC_KEY_PROJECTION,
+ ACCOUNT_AND_TYPE_CALENDAR, new String[] {Long.toString(accountId)}, null);
+ try {
+ if (mailboxCursor.moveToFirst()) {
+ if (logging) {
+ Log.d(TAG, "Upload sync requested for " + account.name);
+ }
+ String syncKey = mailboxCursor.getString(ID_SYNC_KEY_SYNC_KEY);
+ if ((syncKey == null) || (syncKey.equals("0"))) {
+ if (logging) {
+ Log.d(TAG, "Can't sync; mailbox in initial state");
+ }
+ return;
+ }
+ // Ask for a sync from our sync manager
+ ExchangeService.serviceRequest(mailboxCursor.getLong(
+ ID_SYNC_KEY_MAILBOX_ID), ExchangeService.SYNC_UPSYNC);
+ }
+ } finally {
+ mailboxCursor.close();
+ }
+ }
+ } finally {
+ accountCursor.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/exchange2/src/com/android/exchange/CalendarSyncEnabler.java b/exchange2/src/com/android/exchange/CalendarSyncEnabler.java
new file mode 100644
index 0000000..5989614
--- /dev/null
+++ b/exchange2/src/com/android/exchange/CalendarSyncEnabler.java
@@ -0,0 +1,116 @@
+/*
+ * 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.emailcommon.Logging;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.CalendarContract;
+import android.util.Log;
+
+/**
+ * Utility class to enable Exchange calendar sync for all existing Exchange accounts.
+ *
+ * <p>Exchange calendar was first supported on Froyo. It wasn't supported on Eclair, which
+ * was the first version that supported Exchange email.
+ *
+ * <p>This class is used only once when the devices is upgraded to Froyo (or later) from Eclair,
+ * to enable calendar sync for all the existing Exchange accounts.
+ */
+public class CalendarSyncEnabler {
+ private final Context mContext;
+
+ public CalendarSyncEnabler(Context context) {
+ this.mContext = context;
+ }
+
+ /**
+ * Enable calendar sync for all the existing exchange accounts, and post a notification if any.
+ */
+ public final void enableEasCalendarSync() {
+ String emailAddresses = enableEasCalendarSyncInternalForTest();
+ if (emailAddresses.length() > 0) {
+ // Exchange account(s) found.
+ showNotificationForTest(emailAddresses.toString());
+ }
+ }
+
+ /**
+ * Enable calendar sync for all the existing exchange accounts
+ *
+ * @return email addresses of the Exchange accounts joined with spaces as delimiters,
+ * or the empty string if there's no Exchange accounts.
+ */
+ /* package for testing */ final String enableEasCalendarSyncInternalForTest() {
+ StringBuilder emailAddresses = new StringBuilder();
+
+ Account[] exchangeAccounts = AccountManager.get(mContext)
+ .getAccountsByType(Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ for (Account account : exchangeAccounts) {
+ final String emailAddress = account.name;
+ Log.i(Logging.LOG_TAG, "Enabling Exchange calendar sync for " + emailAddress);
+
+ ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1);
+ ContentResolver.setSyncAutomatically(account, CalendarContract.AUTHORITY, true);
+
+ // Accumulate addresses for notification.
+ if (emailAddresses.length() > 0) {
+ emailAddresses.append(' ');
+ }
+ emailAddresses.append(emailAddress);
+ }
+ return emailAddresses.toString();
+ }
+
+ // *** Taken from NotificationController
+ public static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2;
+
+ /**
+ * Show the "Exchange calendar added" notification.
+ *
+ * @param emailAddresses space delimited list of email addresses of Exchange accounts. It'll
+ * be shown on the notification.
+ */
+ /* package for testing */ void showNotificationForTest(String emailAddresses) {
+ // Launch Calendar app when clicked.
+ PendingIntent launchCalendarPendingIntent = PendingIntent.getActivity(mContext, 0,
+ createLaunchCalendarIntent(), 0);
+
+ String tickerText = mContext.getString(R.string.notification_exchange_calendar_added);
+ Notification n = new Notification(R.drawable.stat_notify_calendar,
+ tickerText, System.currentTimeMillis());
+ n.setLatestEventInfo(mContext, tickerText, emailAddresses, launchCalendarPendingIntent);
+ n.flags = Notification.FLAG_AUTO_CANCEL;
+
+ NotificationManager nm =
+ (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.notify(NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED, n);
+ }
+
+ /** @return {@link Intent} to launch the Calendar app. */
+ private Intent createLaunchCalendarIntent() {
+ return new Intent(Intent.ACTION_VIEW, Uri.parse("content://com.android.calendar/time"));
+ }
+}
diff --git a/exchange2/src/com/android/exchange/CommandStatusException.java b/exchange2/src/com/android/exchange/CommandStatusException.java
new file mode 100644
index 0000000..80b405a
--- /dev/null
+++ b/exchange2/src/com/android/exchange/CommandStatusException.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+/**
+ * ActiveSync command error status definitions (EAS 14.0 and later); these are in addition to the
+ * command-specific errors defined for earlier protocol versions
+ */
+public class CommandStatusException extends EasException {
+ private static final long serialVersionUID = 1L;
+
+ // A status response to an EAS account. Responses < 16 correspond to command-specific errors as
+ // reported by EAS versions < 14.0; responses > 100 correspond to generic errors as reported
+ // by EAS versions 14.0 and greater
+ public final int mStatus;
+ // If the error refers to a specific data item, that item's id (as provided by the server) is
+ // stored here
+ public final String mItemId;
+
+ public static class CommandStatus {
+ private static final long serialVersionUID = 1L;
+
+ // Fatal user/provisioning issues (put on security hold)
+ public static final int USER_DISABLED_FOR_SYNC = 126;
+ public static final int USERS_DISABLED_FOR_SYNC = 127;
+ public static final int USER_ON_LEGACY_SERVER_CANT_SYNC = 128;
+ public static final int DEVICE_QUARANTINED = 129;
+ public static final int ACCESS_DENIED = 130;
+ public static final int USER_ACCOUNT_DISABLED = 131;
+ public static final int NOT_PROVISIONABLE_PARTIAL = 139;
+ public static final int NOT_PROVISIONABLE_LEGACY_DEVICE = 141;
+ public static final int TOO_MANY_PARTNERSHIPS = 177;
+
+ // Sync state problems (bad key, multiple client conflict, etc.)
+ public static final int SYNC_STATE_LOCKED = 133;
+ public static final int SYNC_STATE_CORRUPT = 134;
+ public static final int SYNC_STATE_EXISTS = 135;
+ public static final int SYNC_STATE_INVALID = 136;
+
+ // Soft provisioning errors, we need to send Provision command
+ public static final int NEEDS_PROVISIONING_WIPE = 140;
+ public static final int NEEDS_PROVISIONING = 142;
+ public static final int NEEDS_PROVISIONING_REFRESH = 143;
+ public static final int NEEDS_PROVISIONING_INVALID = 144;
+
+ // WTF issues (really shouldn't happen in our implementation)
+ public static final int WTF_INVALID_COMMAND = 137;
+ public static final int WTF_INVALID_PROTOCOL = 138;
+ public static final int WTF_DEVICE_CLAIMS_EXTERNAL_MANAGEMENT = 145;
+ public static final int WTF_UNKNOWN_ITEM_TYPE = 147;
+ public static final int WTF_REQUIRES_PROXY_WITHOUT_SSL = 148;
+
+ // For SmartReply/SmartForward
+ public static final int ITEM_NOT_FOUND = 150;
+
+ // Transient or possibly transient errors
+ public static final int SERVER_ERROR_RETRY = 111;
+ public static final int SYNC_STATE_NOT_FOUND = 132;
+
+ // String version of error status codes (for logging only)
+ private static final int STATUS_TEXT_START = 101;
+ private static final int STATUS_TEXT_END = 150;
+ private static final String[] STATUS_TEXT = {
+ "InvalidContent", "InvalidWBXML", "InvalidXML", "InvalidDateTime", "InvalidIDCombo",
+ "InvalidIDs", "InvalidMIME", "DeviceIdError", "DeviceTypeError", "ServerError",
+ "ServerErrorRetry", "ADAccessDenied", "Quota", "ServerOffline", "SendQuota",
+ "RecipientUnresolved", "ReplyNotAllowed", "SentPreviously", "NoRecipient", "SendFailed",
+ "ReplyFailed", "AttsTooLarge", "NoMailbox", "CantBeAnonymous", "UserNotFound",
+ "UserDisabled", "NewMailbox", "LegacyMailbox", "DeviceBlocked", "AccessDenied",
+ "AcctDisabled", "SyncStateNF", "SyncStateLocked", "SyncStateCorrupt", "SyncStateExists",
+ "SyncStateInvalid", "BadCommand", "BadVersion", "NotFullyProvisionable", "RemoteWipe",
+ "LegacyDevice", "NotProvisioned", "PolicyRefresh", "BadPolicyKey", "ExternallyManaged",
+ "NoRecurrence", "UnexpectedClass", "RemoteHasNoSSL", "InvalidRequest", "ItemNotFound"
+ };
+
+ public static boolean isNeedsProvisioning(int status) {
+ return (status == CommandStatus.NEEDS_PROVISIONING ||
+ status == CommandStatus.NEEDS_PROVISIONING_REFRESH ||
+ status == CommandStatus.NEEDS_PROVISIONING_INVALID ||
+ status == CommandStatus.NEEDS_PROVISIONING_WIPE);
+ }
+
+ public static boolean isBadSyncKey(int status) {
+ return (status == CommandStatus.SYNC_STATE_CORRUPT ||
+ status == CommandStatus.SYNC_STATE_INVALID);
+ }
+
+ public static boolean isDeniedAccess(int status) {
+ return (status == CommandStatus.USER_DISABLED_FOR_SYNC ||
+ status == CommandStatus.USERS_DISABLED_FOR_SYNC ||
+ status == CommandStatus.USER_ON_LEGACY_SERVER_CANT_SYNC ||
+ status == CommandStatus.DEVICE_QUARANTINED ||
+ status == CommandStatus.ACCESS_DENIED ||
+ status == CommandStatus.USER_ACCOUNT_DISABLED ||
+ status == CommandStatus.NOT_PROVISIONABLE_LEGACY_DEVICE ||
+ status == CommandStatus.NOT_PROVISIONABLE_PARTIAL ||
+ status == CommandStatus.TOO_MANY_PARTNERSHIPS);
+ }
+
+ public static boolean isTransientError(int status) {
+ return status == CommandStatus.SYNC_STATE_NOT_FOUND ||
+ status == CommandStatus.SERVER_ERROR_RETRY;
+ }
+
+ public static String toString(int status) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(status);
+ sb.append(" (");
+ if (status < STATUS_TEXT_START || status > STATUS_TEXT_END) {
+ sb.append("unknown");
+ } else {
+ int offset = status - STATUS_TEXT_START;
+ if (offset <= STATUS_TEXT.length) {
+ sb.append(STATUS_TEXT[offset]);
+ }
+ }
+ sb.append(")");
+ return sb.toString();
+ }
+ }
+
+ public CommandStatusException(int status) {
+ mStatus = status;
+ mItemId = null;
+ }
+
+ public CommandStatusException(int status, String itemId) {
+ mStatus = status;
+ mItemId = itemId;
+ }
+}
diff --git a/exchange2/src/com/android/exchange/ContactsSyncAdapterService.java b/exchange2/src/com/android/exchange/ContactsSyncAdapterService.java
new file mode 100644
index 0000000..97706e7
--- /dev/null
+++ b/exchange2/src/com/android/exchange/ContactsSyncAdapterService.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import com.android.emailcommon.provider.EmailContent.AccountColumns;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.Mailbox;
+
+import android.accounts.Account;
+import android.accounts.OperationCanceledException;
+import android.app.Service;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SyncResult;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+public class ContactsSyncAdapterService extends Service {
+ private static final String TAG = "EAS ContactsSyncAdapterService";
+ private static SyncAdapterImpl sSyncAdapter = null;
+ private static final Object sSyncAdapterLock = new Object();
+
+ private static final String[] ID_PROJECTION = new String[] {"_id"};
+ private static final String ACCOUNT_AND_TYPE_CONTACTS =
+ MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_CONTACTS;
+
+ public ContactsSyncAdapterService() {
+ super();
+ }
+
+ private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
+ private Context mContext;
+
+ public SyncAdapterImpl(Context context) {
+ super(context, true /* autoInitialize */);
+ mContext = context;
+ }
+
+ @Override
+ public void onPerformSync(Account account, Bundle extras,
+ String authority, ContentProviderClient provider, SyncResult syncResult) {
+ try {
+ ContactsSyncAdapterService.performSync(mContext, account, extras,
+ authority, provider, syncResult);
+ } catch (OperationCanceledException e) {
+ }
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ synchronized (sSyncAdapterLock) {
+ if (sSyncAdapter == null) {
+ sSyncAdapter = new SyncAdapterImpl(getApplicationContext());
+ }
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return sSyncAdapter.getSyncAdapterBinder();
+ }
+
+ private static boolean hasDirtyRows(ContentResolver resolver, Uri uri, String dirtyColumn) {
+ Cursor c = resolver.query(uri, ID_PROJECTION, dirtyColumn + "=1", null, null);
+ try {
+ return c.getCount() > 0;
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Partial integration with system SyncManager; we tell our EAS ExchangeService to start a
+ * contacts sync when we get the signal from 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();
+
+ // If we've been asked to do an upload, make sure we've got work to do
+ if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
+ Uri uri = RawContacts.CONTENT_URI.buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)
+ .build();
+ // See if we've got dirty contacts or dirty groups containing our contacts
+ boolean changed = hasDirtyRows(cr, uri, RawContacts.DIRTY);
+ if (!changed) {
+ uri = Groups.CONTENT_URI.buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)
+ .build();
+ changed = hasDirtyRows(cr, uri, Groups.DIRTY);
+ }
+ if (!changed) {
+ Log.i(TAG, "Upload sync; no changes");
+ return;
+ }
+ }
+
+ // Find the (EmailProvider) account associated with this email address
+ Cursor accountCursor =
+ cr.query(com.android.emailcommon.provider.Account.CONTENT_URI, ID_PROJECTION,
+ AccountColumns.EMAIL_ADDRESS + "=?", new String[] {account.name}, null);
+ try {
+ if (accountCursor.moveToFirst()) {
+ long accountId = accountCursor.getLong(0);
+ // Now, find the contacts mailbox associated with the account
+ Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, ID_PROJECTION,
+ ACCOUNT_AND_TYPE_CONTACTS, new String[] {Long.toString(accountId)}, null);
+ try {
+ if (mailboxCursor.moveToFirst()) {
+ Log.i(TAG, "Contact sync requested for " + account.name);
+ // Ask for a sync from our sync manager
+ ExchangeService.serviceRequest(mailboxCursor.getLong(0),
+ ExchangeService.SYNC_UPSYNC);
+ }
+ } finally {
+ mailboxCursor.close();
+ }
+ }
+ } finally {
+ accountCursor.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/exchange2/src/com/android/exchange/Eas.java b/exchange2/src/com/android/exchange/Eas.java
new file mode 100644
index 0000000..9789f43
--- /dev/null
+++ b/exchange2/src/com/android/exchange/Eas.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import android.util.Log;
+
+import com.android.emailcommon.service.EmailServiceProxy;
+import com.android.emailcommon.service.SyncWindow;
+
+/**
+ * Constants used throughout the EAS implementation are stored here.
+ *
+ */
+public class Eas {
+ // For debugging
+ public static boolean WAIT_DEBUG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE
+ public static boolean DEBUG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE
+
+ // The following two are for user logging (the second providing more detail)
+ public static boolean USER_LOG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE
+ public static boolean PARSER_LOG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE
+ public static boolean FILE_LOG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE
+
+ public static final String CLIENT_VERSION = "EAS-1.3";
+ public static final String ACCOUNT_MAILBOX_PREFIX = "__eas";
+
+ // Define our default protocol version as 2.5 (Exchange 2003)
+ public static final String SUPPORTED_PROTOCOL_EX2003 = "2.5";
+ public static final double SUPPORTED_PROTOCOL_EX2003_DOUBLE = 2.5;
+ public static final String SUPPORTED_PROTOCOL_EX2007 = "12.0";
+ public static final double SUPPORTED_PROTOCOL_EX2007_DOUBLE = 12.0;
+ public static final String SUPPORTED_PROTOCOL_EX2007_SP1 = "12.1";
+ public static final double SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE = 12.1;
+ public static final String SUPPORTED_PROTOCOL_EX2010 = "14.0";
+ public static final double SUPPORTED_PROTOCOL_EX2010_DOUBLE = 14.0;
+ public static final String SUPPORTED_PROTOCOL_EX2010_SP1 = "14.1";
+ public static final double SUPPORTED_PROTOCOL_EX2010_SP1_DOUBLE = 14.1;
+ public static final String DEFAULT_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_EX2003;
+
+ public static final String EXCHANGE_ACCOUNT_MANAGER_TYPE = "com.android.exchange";
+
+ // From EAS spec
+ // Mail Cal
+ // 0 No filter Yes Yes
+ // 1 1 day ago Yes No
+ // 2 3 days ago Yes No
+ // 3 1 week ago Yes No
+ // 4 2 weeks ago Yes Yes
+ // 5 1 month ago Yes Yes
+ // 6 3 months ago No Yes
+ // 7 6 months ago No Yes
+
+ public static final String FILTER_AUTO = Integer.toString(SyncWindow.SYNC_WINDOW_AUTO);
+ // TODO Rationalize this with SYNC_WINDOW_ALL
+ public static final String FILTER_ALL = "0";
+ public static final String FILTER_1_DAY = Integer.toString(SyncWindow.SYNC_WINDOW_1_DAY);
+ public static final String FILTER_3_DAYS = Integer.toString(SyncWindow.SYNC_WINDOW_3_DAYS);
+ public static final String FILTER_1_WEEK = Integer.toString(SyncWindow.SYNC_WINDOW_1_WEEK);
+ public static final String FILTER_2_WEEKS = Integer.toString(SyncWindow.SYNC_WINDOW_2_WEEKS);
+ public static final String FILTER_1_MONTH = Integer.toString(SyncWindow.SYNC_WINDOW_1_MONTH);
+ public static final String FILTER_3_MONTHS = "6";
+ public static final String FILTER_6_MONTHS = "7";
+
+ public static final String BODY_PREFERENCE_TEXT = "1";
+ public static final String BODY_PREFERENCE_HTML = "2";
+
+ public static final String MIME_BODY_PREFERENCE_TEXT = "0";
+ public static final String MIME_BODY_PREFERENCE_MIME = "2";
+
+ // For EAS 12, we use HTML, so we want a larger size than in EAS 2.5
+ public static final String EAS12_TRUNCATION_SIZE = "200000";
+ // For EAS 2.5, truncation is a code; the largest is "7", which is 100k
+ public static final String EAS2_5_TRUNCATION_SIZE = "7";
+
+ public static final int FOLDER_STATUS_OK = 1;
+ public static final int FOLDER_STATUS_INVALID_KEY = 9;
+
+ public static final int EXCHANGE_ERROR_NOTIFICATION = 0x10;
+
+ public static void setUserDebug(int state) {
+ // DEBUG takes precedence and is never true in a user build
+ if (!DEBUG) {
+ USER_LOG = (state & EmailServiceProxy.DEBUG_BIT) != 0;
+ PARSER_LOG = (state & EmailServiceProxy.DEBUG_VERBOSE_BIT) != 0;
+ FILE_LOG = (state & EmailServiceProxy.DEBUG_FILE_BIT) != 0;
+ if (FILE_LOG || PARSER_LOG) {
+ USER_LOG = true;
+ }
+ Log.d("Eas Debug", "Logging: " + (USER_LOG ? "User " : "") +
+ (PARSER_LOG ? "Parser " : "") + (FILE_LOG ? "File" : ""));
+ }
+ }
+
+ static public Double getProtocolVersionDouble(String version) {
+ if (SUPPORTED_PROTOCOL_EX2003.equals(version)) {
+ return SUPPORTED_PROTOCOL_EX2003_DOUBLE;
+ } else if (SUPPORTED_PROTOCOL_EX2007.equals(version)) {
+ return SUPPORTED_PROTOCOL_EX2007_DOUBLE;
+ } if (SUPPORTED_PROTOCOL_EX2007_SP1.equals(version)) {
+ return SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE;
+ } if (SUPPORTED_PROTOCOL_EX2010.equals(version)) {
+ return SUPPORTED_PROTOCOL_EX2010_DOUBLE;
+ } if (SUPPORTED_PROTOCOL_EX2010_SP1.equals(version)) {
+ return SUPPORTED_PROTOCOL_EX2010_SP1_DOUBLE;
+ }
+ throw new IllegalArgumentException("illegal protocol version");
+ }
+}
diff --git a/src/com/android/exchange/EasAccountService.java b/exchange2/src/com/android/exchange/EasAccountService.java
similarity index 100%
rename from src/com/android/exchange/EasAccountService.java
rename to exchange2/src/com/android/exchange/EasAccountService.java
diff --git a/exchange2/src/com/android/exchange/EasAuthenticationException.java b/exchange2/src/com/android/exchange/EasAuthenticationException.java
new file mode 100644
index 0000000..f5b14b9
--- /dev/null
+++ b/exchange2/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/exchange2/src/com/android/exchange/EasCertificateRequestor.java b/exchange2/src/com/android/exchange/EasCertificateRequestor.java
new file mode 100644
index 0000000..ebafe44
--- /dev/null
+++ b/exchange2/src/com/android/exchange/EasCertificateRequestor.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.exchange;
+
+import android.security.KeyChain;
+
+import com.android.emailcommon.utility.CertificateRequestor;
+
+/**
+ * A subclass of the {@link CertificateRequestor} so that the Exchange process
+ * can request access to a certificate.
+ *
+ * They {@link KeyChain} API works in such a way that the host
+ * activity requesting the certificate must be running in the process with the UID of who will
+ * actually use the certificate. Since the Exchange process needs to establish connections and use
+ * certificates for EAS accounts, requests for certificates must be delegated by an Activity in this
+ * process.
+ */
+public class EasCertificateRequestor extends CertificateRequestor {
+ // Intentionally blank - no behavior overridden.
+}
diff --git a/exchange2/src/com/android/exchange/EasException.java b/exchange2/src/com/android/exchange/EasException.java
new file mode 100644
index 0000000..e7613d7
--- /dev/null
+++ b/exchange2/src/com/android/exchange/EasException.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+public class EasException extends Exception {
+ private static final long serialVersionUID = 5894556952470989968L;
+}
diff --git a/exchange2/src/com/android/exchange/EasOutboxService.java b/exchange2/src/com/android/exchange/EasOutboxService.java
new file mode 100644
index 0000000..9ff1ffc
--- /dev/null
+++ b/exchange2/src/com/android/exchange/EasOutboxService.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.TrafficStats;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.text.TextUtils;
+
+import com.android.emailcommon.TrafficFlags;
+import com.android.emailcommon.internet.Rfc822Output;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent.Body;
+import com.android.emailcommon.provider.EmailContent.BodyColumns;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.EmailContent.MessageColumns;
+import com.android.emailcommon.provider.EmailContent.SyncColumns;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.service.EmailServiceStatus;
+import com.android.emailcommon.utility.Utility;
+import com.android.exchange.CommandStatusException.CommandStatus;
+import com.android.exchange.adapter.Parser;
+import com.android.exchange.adapter.Parser.EmptyStreamException;
+import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.Tags;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpStatus;
+import org.apache.http.entity.InputStreamEntity;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class EasOutboxService extends EasSyncService {
+
+ public static final int SEND_FAILED = 1;
+ public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED =
+ MessageColumns.MAILBOX_KEY + "=? and (" + SyncColumns.SERVER_ID + " is null or " +
+ SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';
+ public static final String[] BODY_SOURCE_PROJECTION =
+ new String[] {BodyColumns.SOURCE_MESSAGE_KEY};
+ public static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
+
+ // This is a normal email (i.e. not one of the other types)
+ public static final int MODE_NORMAL = 0;
+ // This is a smart reply email
+ public static final int MODE_SMART_REPLY = 1;
+ // This is a smart forward email
+ public static final int MODE_SMART_FORWARD = 2;
+
+ // This needs to be long enough to send the longest reasonable message, without being so long
+ // as to effectively "hang" sending of mail. The standard 30 second timeout isn't long enough
+ // for pictures and the like. For now, we'll use 15 minutes, in the knowledge that any socket
+ // failure would probably generate an Exception before timing out anyway
+ public static final int SEND_MAIL_TIMEOUT = 15*MINUTES;
+
+ protected EasOutboxService(Context _context, Mailbox _mailbox) {
+ super(_context, _mailbox);
+ }
+
+ /**
+ * Our own HttpEntity subclass that is able to insert opaque data (in this case the MIME
+ * representation of the message body as stored in a temporary file) into the serializer stream
+ */
+ private static class SendMailEntity extends InputStreamEntity {
+ private final Context mContext;
+ private final FileInputStream mFileStream;
+ private final long mFileLength;
+ private final int mSendTag;
+ private final Message mMessage;
+
+ private static final int[] MODE_TAGS = new int[] {Tags.COMPOSE_SEND_MAIL,
+ Tags.COMPOSE_SMART_REPLY, Tags.COMPOSE_SMART_FORWARD};
+
+ public SendMailEntity(Context context, FileInputStream instream, long length, int tag,
+ Message message) {
+ super(instream, length);
+ mContext = context;
+ mFileStream = instream;
+ mFileLength = length;
+ mSendTag = tag;
+ mMessage = message;
+ }
+
+ /**
+ * We always return -1 because we don't know the actual length of the POST data (this
+ * causes HttpClient to send the data in "chunked" mode)
+ */
+ @Override
+ public long getContentLength() {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ // Calculate the overhead for the WBXML data
+ writeTo(baos, false);
+ // Return the actual size that will be sent
+ return baos.size() + mFileLength;
+ } catch (IOException e) {
+ // Just return -1 (unknown)
+ } finally {
+ try {
+ baos.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public void writeTo(OutputStream outstream) throws IOException {
+ writeTo(outstream, true);
+ }
+
+ /**
+ * Write the message to the output stream
+ * @param outstream the output stream to write
+ * @param withData whether or not the actual data is to be written; true when sending
+ * mail; false when calculating size only
+ * @throws IOException
+ */
+ public void writeTo(OutputStream outstream, boolean withData) throws IOException {
+ // Not sure if this is possible; the check is taken from the superclass
+ if (outstream == null) {
+ throw new IllegalArgumentException("Output stream may not be null");
+ }
+
+ // We'll serialize directly into the output stream
+ Serializer s = new Serializer(outstream);
+ // Send the appropriate initial tag
+ s.start(mSendTag);
+ // The Message-Id for this message (note that we cannot use the messageId stored in
+ // the message, as EAS 14 limits the length to 40 chars and we use 70+)
+ s.data(Tags.COMPOSE_CLIENT_ID, "SendMail-" + System.nanoTime());
+ // We always save sent mail
+ s.tag(Tags.COMPOSE_SAVE_IN_SENT_ITEMS);
+
+ // If we're using smart reply/forward, we need info about the original message
+ if (mSendTag != Tags.COMPOSE_SEND_MAIL) {
+ OriginalMessageInfo info = getOriginalMessageInfo(mContext, mMessage.mId);
+ if (info != null) {
+ s.start(Tags.COMPOSE_SOURCE);
+ // For search results, use the long id (stored in mProtocolSearchInfo); else,
+ // use folder id/item id combo
+ if (mMessage.mProtocolSearchInfo != null) {
+ s.data(Tags.COMPOSE_LONG_ID, mMessage.mProtocolSearchInfo);
+ } else {
+ s.data(Tags.COMPOSE_ITEM_ID, info.mItemId);
+ s.data(Tags.COMPOSE_FOLDER_ID, info.mCollectionId);
+ }
+ s.end(); // Tags.COMPOSE_SOURCE
+ }
+ }
+
+ // Start the MIME tag; this is followed by "opaque" data (byte array)
+ s.start(Tags.COMPOSE_MIME);
+ // Send opaque data from the file stream
+ if (withData) {
+ s.opaque(mFileStream, (int)mFileLength);
+ } else {
+ s.opaqueWithoutData((int)mFileLength);
+ }
+ // And we're done
+ s.end().end().done();
+ }
+ }
+
+ private static class SendMailParser extends Parser {
+ private final int mStartTag;
+ private int mStatus;
+
+ public SendMailParser(InputStream in, int startTag) throws IOException {
+ super(in);
+ mStartTag = startTag;
+ }
+
+ public int getStatus() {
+ return mStatus;
+ }
+
+ /**
+ * The only useful info in the SendMail response is the status; we capture and save it
+ */
+ @Override
+ public boolean parse() throws IOException {
+ if (nextTag(START_DOCUMENT) != mStartTag) {
+ throw new IOException();
+ }
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ if (tag == Tags.COMPOSE_STATUS) {
+ mStatus = getValueInt();
+ } else {
+ skipTag();
+ }
+ }
+ return true;
+ }
+ }
+
+ /**
+ * For OriginalMessageInfo, we use the terminology of EAS for the serverId and mailboxId of the
+ * original message
+ */
+ protected static class OriginalMessageInfo {
+ final String mItemId;
+ final String mCollectionId;
+ final String mLongId;
+
+ OriginalMessageInfo(String itemId, String collectionId, String longId) {
+ mItemId = itemId;
+ mCollectionId = collectionId;
+ mLongId = longId;
+ }
+ }
+
+ private void sendCallback(long msgId, String subject, int status) {
+ try {
+ ExchangeService.callback().sendMessageStatus(msgId, subject, status, 0);
+ } catch (RemoteException e) {
+ // It's all good
+ }
+ }
+
+ /*package*/ String generateSmartSendCmd(boolean reply, OriginalMessageInfo info) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(reply ? "SmartReply" : "SmartForward");
+ if (!TextUtils.isEmpty(info.mLongId)) {
+ sb.append("&LongId=");
+ sb.append(Uri.encode(info.mLongId, ":"));
+ } else {
+ sb.append("&ItemId=");
+ sb.append(Uri.encode(info.mItemId, ":"));
+ sb.append("&CollectionId=");
+ sb.append(Uri.encode(info.mCollectionId, ":"));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get information about the original message that is referenced by the message to be sent; this
+ * information will exist for replies and forwards
+ *
+ * @param context the caller's context
+ * @param msgId the id of the message we're sending
+ * @return a data structure with the serverId and mailboxId of the original message, or null if
+ * either or both of those pieces of information can't be found
+ */
+ private static OriginalMessageInfo getOriginalMessageInfo(Context context, long msgId) {
+ // Note: itemId and collectionId are the terms used by EAS to refer to the serverId and
+ // mailboxId of a Message
+ String itemId = null;
+ String collectionId = null;
+ String longId = null;
+
+ // First, we need to get the id of the reply/forward message
+ String[] cols = Utility.getRowColumns(context, Body.CONTENT_URI,
+ BODY_SOURCE_PROJECTION, WHERE_MESSAGE_KEY,
+ new String[] {Long.toString(msgId)});
+ if (cols != null) {
+ long refId = Long.parseLong(cols[0]);
+ // Then, we need the serverId and mailboxKey of the message
+ cols = Utility.getRowColumns(context, Message.CONTENT_URI, refId,
+ SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY,
+ MessageColumns.PROTOCOL_SEARCH_INFO);
+ if (cols != null) {
+ itemId = cols[0];
+ long boxId = Long.parseLong(cols[1]);
+ // Then, we need the serverId of the mailbox
+ cols = Utility.getRowColumns(context, Mailbox.CONTENT_URI, boxId,
+ MailboxColumns.SERVER_ID);
+ if (cols != null) {
+ collectionId = cols[0];
+ }
+ }
+ }
+ // We need either a longId or both itemId (serverId) and collectionId (mailboxId) to process
+ // a smart reply or a smart forward
+ if (longId != null || (itemId != null && collectionId != null)){
+ return new OriginalMessageInfo(itemId, collectionId, longId);
+ }
+ return null;
+ }
+
+ private void sendFailed(long msgId, int result) {
+ ContentValues cv = new ContentValues();
+ cv.put(SyncColumns.SERVER_ID, SEND_FAILED);
+ Message.update(mContext, Message.CONTENT_URI, msgId, cv);
+ sendCallback(msgId, null, result);
+ }
+
+ /**
+ * Send a single message via EAS
+ * Note that we mark messages SEND_FAILED when there is a permanent failure, rather than an
+ * IOException, which is handled by ExchangeService with retries, backoffs, etc.
+ *
+ * @param cacheDir the cache directory for this context
+ * @param msgId the _id of the message to send
+ * @throws IOException
+ */
+ int sendMessage(File cacheDir, long msgId) throws IOException, MessagingException {
+ // We always return SUCCESS unless the sending error is account-specific (security or
+ // authentication) rather than message-specific; returning anything else will terminate
+ // the Outbox sync! Message-specific errors are marked in the messages themselves.
+ int result = EmailServiceStatus.SUCCESS;
+ // Say we're starting to send this message
+ sendCallback(msgId, null, EmailServiceStatus.IN_PROGRESS);
+ // Create a temporary file (this will hold the outgoing message in RFC822 (MIME) format)
+ File tmpFile = File.createTempFile("eas_", "tmp", cacheDir);
+ try {
+ // Get the message and fail quickly if not found
+ Message msg = Message.restoreMessageWithId(mContext, msgId);
+ if (msg == null) return EmailServiceStatus.MESSAGE_NOT_FOUND;
+
+ // See what kind of outgoing messge this is
+ int flags = msg.mFlags;
+ boolean reply = (flags & Message.FLAG_TYPE_REPLY) != 0;
+ boolean forward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
+ boolean includeQuotedText = (flags & Message.FLAG_NOT_INCLUDE_QUOTED_TEXT) == 0;
+
+ // The reference message and mailbox are called item and collection in EAS
+ OriginalMessageInfo referenceInfo = null;
+ // Respect the sense of the include quoted text flag
+ if (includeQuotedText && (reply || forward)) {
+ referenceInfo = getOriginalMessageInfo(mContext, msgId);
+ }
+ // Generally, we use SmartReply/SmartForward if we've got a good reference
+ boolean smartSend = referenceInfo != null;
+ // But we won't use SmartForward if the account isn't set up for it (currently, we only
+ // use SmartForward for EAS 12.0 or later to avoid creating eml files that are
+ // potentially difficult for the recipient to handle)
+ if (forward && ((mAccount.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) {
+ smartSend = false;
+ }
+
+ // Write the message to the temporary file
+ FileOutputStream fileOutputStream = new FileOutputStream(tmpFile);
+ Rfc822Output.writeTo(mContext, msgId, fileOutputStream, smartSend, true);
+ fileOutputStream.close();
+
+ // Sending via EAS14 is a whole 'nother kettle of fish
+ boolean isEas14 = (Double.parseDouble(mAccount.mProtocolVersion) >=
+ Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE);
+
+ while (true) {
+ // Get an input stream to our temporary file and create an entity with it
+ FileInputStream fileStream = new FileInputStream(tmpFile);
+ long fileLength = tmpFile.length();
+
+ // The type of entity depends on whether we're using EAS 14
+ HttpEntity inputEntity;
+ // For EAS 14, we need to save the wbxml tag we're using
+ int modeTag = 0;
+ if (isEas14) {
+ int mode =
+ !smartSend ? MODE_NORMAL : reply ? MODE_SMART_REPLY : MODE_SMART_FORWARD;
+ modeTag = SendMailEntity.MODE_TAGS[mode];
+ inputEntity =
+ new SendMailEntity(mContext, fileStream, fileLength, modeTag, msg);
+ } else {
+ inputEntity = new InputStreamEntity(fileStream, fileLength);
+ }
+ // Create the appropriate command and POST it to the server
+ String cmd = "SendMail";
+ if (smartSend) {
+ // In EAS 14, we don't send itemId and collectionId in the command
+ if (isEas14) {
+ cmd = reply ? "SmartReply" : "SmartForward";
+ } else {
+ cmd = generateSmartSendCmd(reply, referenceInfo);
+ }
+ }
+
+ // If we're not EAS 14, add our save-in-sent setting here
+ if (!isEas14) {
+ cmd += "&SaveInSent=T";
+ }
+ userLog("Send cmd: " + cmd);
+
+ // Finally, post SendMail to the server
+ EasResponse resp = sendHttpClientPost(cmd, inputEntity, SEND_MAIL_TIMEOUT);
+ try {
+ fileStream.close();
+ int code = resp.getStatus();
+ if (code == HttpStatus.SC_OK) {
+ // HTTP OK before EAS 14 is a thumbs up; in EAS 14, we've got to parse
+ // the reply
+ if (isEas14) {
+ try {
+ // Try to parse the result
+ SendMailParser p =
+ new SendMailParser(resp.getInputStream(), modeTag);
+ // If we get here, the SendMail failed; go figure
+ p.parse();
+ // The parser holds the status
+ int status = p.getStatus();
+ userLog("SendMail error, status: " + status);
+ if (CommandStatus.isNeedsProvisioning(status)) {
+ result = EmailServiceStatus.SECURITY_FAILURE;
+ } else if (status == CommandStatus.ITEM_NOT_FOUND && smartSend) {
+ // This is the retry case for EAS 14; we'll send without "smart"
+ // commands next time
+ resp.close();
+ smartSend = false;
+ continue;
+ }
+ sendFailed(msgId, result);
+ return result;
+ } catch (EmptyStreamException e) {
+ // This is actually fine; an empty stream means SendMail succeeded
+ }
+ }
+
+ // If we're here, the SendMail command succeeded
+ userLog("Deleting message...");
+ // Delete the message from the Outbox and send callback
+ mContentResolver.delete(
+ ContentUris.withAppendedId(Message.CONTENT_URI, msgId), null, null);
+ sendCallback(-1, msg.mSubject, EmailServiceStatus.SUCCESS);
+ break;
+ } else if (code == EasSyncService.INTERNAL_SERVER_ERROR_CODE && smartSend) {
+ // This is the retry case for EAS 12.1 and below; we'll send without "smart"
+ // commands next time
+ resp.close();
+ smartSend = false;
+ } else {
+ userLog("Message sending failed, code: " + code);
+ if (EasResponse.isAuthError(code)) {
+ result = EmailServiceStatus.LOGIN_FAILED;
+ } else if (EasResponse.isProvisionError(code)) {
+ result = EmailServiceStatus.SECURITY_FAILURE;
+ }
+ sendFailed(msgId, result);
+ break;
+ }
+ } finally {
+ resp.close();
+ }
+ }
+ } catch (IOException e) {
+ // We catch this just to send the callback
+ sendCallback(msgId, null, EmailServiceStatus.CONNECTION_ERROR);
+ throw e;
+ } finally {
+ // Clean up the temporary file
+ if (tmpFile.exists()) {
+ tmpFile.delete();
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public void run() {
+ setupService();
+ // Use SMTP flags for sending mail
+ TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(mContext, mAccount));
+ File cacheDir = mContext.getCacheDir();
+ try {
+ mDeviceId = ExchangeService.getDeviceId(mContext);
+ // Get a cursor to Outbox messages
+ Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI,
+ Message.ID_COLUMN_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED,
+ new String[] {Long.toString(mMailbox.mId)}, null);
+ try {
+ // Loop through the messages, sending each one
+ while (c.moveToNext()) {
+ long msgId = c.getLong(Message.ID_COLUMNS_ID_COLUMN);
+ if (msgId != 0) {
+ if (Utility.hasUnloadedAttachments(mContext, msgId)) {
+ // We'll just have to wait on this...
+ continue;
+ }
+ int result = sendMessage(cacheDir, msgId);
+ // If there's an error, it should stop the service; we will distinguish
+ // at least between login failures and everything else
+ if (result == EmailServiceStatus.LOGIN_FAILED) {
+ mExitStatus = EXIT_LOGIN_FAILURE;
+ return;
+ } else if (result == EmailServiceStatus.SECURITY_FAILURE) {
+ mExitStatus = EXIT_SECURITY_FAILURE;
+ return;
+ } else if (result == EmailServiceStatus.REMOTE_EXCEPTION) {
+ mExitStatus = EXIT_EXCEPTION;
+ return;
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ mExitStatus = EXIT_DONE;
+ } catch (IOException e) {
+ mExitStatus = EXIT_IO_ERROR;
+ } catch (Exception e) {
+ userLog("Exception caught in EasOutboxService", e);
+ mExitStatus = EXIT_EXCEPTION;
+ } finally {
+ userLog(mMailbox.mDisplayName, ": sync finished");
+ userLog("Outbox exited with status ", mExitStatus);
+ ExchangeService.done(this);
+ }
+ }
+
+ /**
+ * Convenience method for adding a Message to an account's outbox
+ * @param context the context of the caller
+ * @param accountId the accountId for the sending account
+ * @param msg the message to send
+ */
+ public static void sendMessage(Context context, long accountId, Message msg) {
+ Mailbox mailbox = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_OUTBOX);
+ if (mailbox != null) {
+ msg.mMailboxKey = mailbox.mId;
+ msg.mAccountKey = accountId;
+ msg.save(context);
+ }
+ }
+}
\ No newline at end of file
diff --git a/exchange2/src/com/android/exchange/EasResponse.java b/exchange2/src/com/android/exchange/EasResponse.java
new file mode 100644
index 0000000..174989e
--- /dev/null
+++ b/exchange2/src/com/android/exchange/EasResponse.java
@@ -0,0 +1,167 @@
+/*
+ * 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.emailcommon.utility.EmailClientConnectionManager;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpUriRequest;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * Encapsulate a response to an HTTP POST
+ */
+public class EasResponse {
+ // MSFT's custom HTTP result code indicating the need to provision
+ static private final int HTTP_NEED_PROVISIONING = 449;
+
+ final HttpResponse mResponse;
+ private final HttpEntity mEntity;
+ private final int mLength;
+ private InputStream mInputStream;
+ private boolean mClosed;
+
+ /**
+ * Whether or not a certificate was requested by the server and missing.
+ * If this is set, it is essentially a 403 whereby the failure was due
+ */
+ private boolean mClientCertRequested = false;
+
+ private EasResponse(HttpResponse response) {
+ mResponse = response;
+ mEntity = (response == null) ? null : mResponse.getEntity();
+ if (mEntity != null) {
+ mLength = (int) mEntity.getContentLength();
+ } else {
+ mLength = 0;
+ }
+ }
+
+ public static EasResponse fromHttpRequest(
+ EmailClientConnectionManager connManager, HttpClient client, HttpUriRequest request)
+ throws IOException {
+
+ long reqTime = System.currentTimeMillis();
+ HttpResponse response = client.execute(request);
+ EasResponse result = new EasResponse(response);
+ if (isAuthError(response.getStatusLine().getStatusCode())
+ && connManager.hasDetectedUnsatisfiedCertReq(reqTime)) {
+ result.mClientCertRequested = true;
+ result.mClosed = true;
+ }
+
+ return result;
+ }
+
+ /**
+ * Determine whether an HTTP code represents an authentication error
+ * @param code the HTTP code returned by the server
+ * @return whether or not the code represents an authentication error
+ */
+ public static boolean isAuthError(int code) {
+ return (code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN);
+ }
+
+ /**
+ * Determine whether an HTTP code represents a provisioning error
+ * @param code the HTTP code returned by the server
+ * @return whether or not the code represents an provisioning error
+ */
+ public static boolean isProvisionError(int code) {
+ return (code == HTTP_NEED_PROVISIONING) || (code == HttpStatus.SC_FORBIDDEN);
+ }
+
+ /**
+ * Return an appropriate input stream for the response, either a GZIPInputStream, for
+ * compressed data, or a generic InputStream otherwise
+ * @return the input stream for the response
+ */
+ public InputStream getInputStream() {
+ if (mInputStream != null || mClosed) {
+ throw new IllegalStateException("Can't reuse stream or get closed stream");
+ } else if (mEntity == null) {
+ throw new IllegalStateException("Can't get input stream without entity");
+ }
+ InputStream is = null;
+ try {
+ // Get the default input stream for the entity
+ is = mEntity.getContent();
+ Header ceHeader = mResponse.getFirstHeader("Content-Encoding");
+ if (ceHeader != null) {
+ String encoding = ceHeader.getValue();
+ // If we're gzip encoded, wrap appropriately
+ if (encoding.toLowerCase().equals("gzip")) {
+ is = new GZIPInputStream(is);
+ }
+ }
+ } catch (IllegalStateException e1) {
+ } catch (IOException e1) {
+ }
+ mInputStream = is;
+ return is;
+ }
+
+ public boolean isEmpty() {
+ return mLength == 0;
+ }
+
+ public int getStatus() {
+ return mClientCertRequested
+ ? HttpStatus.SC_UNAUTHORIZED
+ : mResponse.getStatusLine().getStatusCode();
+ }
+
+ public boolean isMissingCertificate() {
+ return mClientCertRequested;
+ }
+
+ public Header getHeader(String name) {
+ return (mResponse == null) ? null : mResponse.getFirstHeader(name);
+ }
+
+ public int getLength() {
+ return mLength;
+ }
+
+ public void close() {
+ if (!mClosed) {
+ if (mEntity != null) {
+ try {
+ mEntity.consumeContent();
+ } catch (IOException e) {
+ // No harm, no foul
+ }
+ }
+ if (mInputStream instanceof GZIPInputStream) {
+ try {
+ mInputStream.close();
+ } catch (IOException e) {
+ // We tried
+ }
+ }
+ mClosed = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/exchange2/src/com/android/exchange/EasSyncService.java b/exchange2/src/com/android/exchange/EasSyncService.java
new file mode 100644
index 0000000..f1e544d
--- /dev/null
+++ b/exchange2/src/com/android/exchange/EasSyncService.java
@@ -0,0 +1,1975 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Entity;
+import android.database.Cursor;
+import android.net.TrafficStats;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.provider.CalendarContract.Attendees;
+import android.provider.CalendarContract.Events;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.emailcommon.TrafficFlags;
+import com.android.emailcommon.mail.Address;
+import com.android.emailcommon.mail.MeetingInfo;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.PackedString;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent.AccountColumns;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.EmailContent.MessageColumns;
+import com.android.emailcommon.provider.EmailContent.SyncColumns;
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.provider.Policy;
+import com.android.emailcommon.provider.ProviderUnavailableException;
+import com.android.emailcommon.service.EmailServiceConstants;
+import com.android.emailcommon.service.EmailServiceProxy;
+import com.android.emailcommon.service.EmailServiceStatus;
+import com.android.emailcommon.service.PolicyServiceProxy;
+import com.android.emailcommon.utility.EmailClientConnectionManager;
+import com.android.emailcommon.utility.Utility;
+import com.android.exchange.CommandStatusException.CommandStatus;
+import com.android.exchange.adapter.AbstractSyncAdapter;
+import com.android.exchange.adapter.AccountSyncAdapter;
+import com.android.exchange.adapter.AttachmentLoader;
+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.GalParser;
+import com.android.exchange.adapter.MeetingResponseParser;
+import com.android.exchange.adapter.MoveItemsParser;
+import com.android.exchange.adapter.Parser.EmptyStreamException;
+import com.android.exchange.adapter.ProvisionParser;
+import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.SettingsParser;
+import com.android.exchange.adapter.Tags;
+import com.android.exchange.provider.GalResult;
+import com.android.exchange.utility.CalendarUtilities;
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.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 java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.Thread.State;
+import java.net.URI;
+import java.security.cert.CertificateException;
+
+public class EasSyncService extends AbstractSyncService {
+ // DO NOT CHECK IN SET TO TRUE
+ public static final boolean DEBUG_GAL_SERVICE = false;
+
+ protected static final String PING_COMMAND = "Ping";
+ // Command timeout is the the time allowed for reading data from an open connection before an
+ // IOException is thrown. After a small added allowance, our watchdog alarm goes off (allowing
+ // us to detect a silently dropped connection). The allowance is defined below.
+ static public final int COMMAND_TIMEOUT = 30*SECONDS;
+ // Connection timeout is the time given to connect to the server before reporting an IOException
+ static private final int CONNECTION_TIMEOUT = 20*SECONDS;
+ // The extra time allowed beyond the COMMAND_TIMEOUT before which our watchdog alarm triggers
+ static private final int WATCHDOG_TIMEOUT_ALLOWANCE = 30*SECONDS;
+
+ 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 protected final int EAS_REDIRECT_CODE = 451;
+
+ static public final int INTERNAL_SERVER_ERROR_CODE = 500;
+
+ static public final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML";
+ static public final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML";
+
+ static public final int MESSAGE_FLAG_MOVED_MESSAGE = 1 << Message.FLAG_SYNC_ADAPTER_SHIFT;
+ // The amount of time we allow for a thread to release its post lock after receiving an alert
+ static private final int POST_LOCK_TIMEOUT = 10*SECONDS;
+
+ // The EAS protocol Provision status for "we implement all of the policies"
+ static private final String PROVISION_STATUS_OK = "1";
+ // The EAS protocol Provision status meaning "we partially implement the policies"
+ static private final String PROVISION_STATUS_PARTIAL = "2";
+
+ static /*package*/ final String DEVICE_TYPE = "Android";
+ static final String USER_AGENT = DEVICE_TYPE + '/' + Build.VERSION.RELEASE + '-' +
+ Eas.CLIENT_VERSION;
+
+ // Maximum number of times we'll allow a sync to "loop" with MoreAvailable true before
+ // forcing it to stop. This number has been determined empirically.
+ static private final int MAX_LOOPING_COUNT = 100;
+ // Reasonable default
+ public String mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
+ public Double mProtocolVersionDouble;
+ protected String mDeviceId = null;
+ @VisibleForTesting
+ String mAuthString = null;
+ @VisibleForTesting
+ String mUserString = null;
+ @VisibleForTesting
+ String mBaseUriString = null;
+ public String mHostAddress;
+ public String mUserName;
+ public String mPassword;
+
+ // The HttpPost in progress
+ private volatile HttpPost mPendingPost = null;
+ // Whether a POST was aborted due to alarm (watchdog alarm)
+ protected boolean mPostAborted = false;
+ // Whether a POST was aborted due to reset
+ protected boolean mPostReset = false;
+
+ // The parameters for the connection must be modified through setConnectionParameters
+ private boolean mSsl = true;
+ private boolean mTrustSsl = false;
+ private String mClientCertAlias = null;
+
+ public ContentResolver mContentResolver;
+ // Whether or not the sync service is valid (usable)
+ public boolean mIsValid = true;
+
+ // Whether the most recent upsync failed (status 7)
+ public boolean mUpsyncFailed = false;
+
+ protected 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) != 0;
+ }
+
+ private EasSyncService(String prefix) {
+ super(prefix);
+ }
+
+ public EasSyncService() {
+ this("EAS Validation");
+ }
+
+ public static EasSyncService getServiceForMailbox(Context context, Mailbox m) {
+ switch(m.mType) {
+ case Mailbox.TYPE_EAS_ACCOUNT_MAILBOX:
+ return new EasAccountService(context, m);
+ case Mailbox.TYPE_OUTBOX:
+ return new EasOutboxService(context, m);
+ default:
+ return new EasSyncService(context, m);
+ }
+ }
+
+ /**
+ * Try to wake up a sync thread that is waiting on an HttpClient POST and has waited past its
+ * socket timeout without having thrown an Exception
+ *
+ * @return true if the POST was successfully stopped; false if we've failed and interrupted
+ * the thread
+ */
+ @Override
+ public boolean alarm() {
+ HttpPost post;
+ if (mThread == null) return true;
+ String threadName = mThread.getName();
+
+ // Synchronize here so that we are guaranteed to have valid mPendingPost and mPostLock
+ // executePostWithTimeout (which executes the HttpPost) also uses this lock
+ synchronized(getSynchronizer()) {
+ // Get a reference to the current post lock
+ post = mPendingPost;
+ if (post != null) {
+ if (Eas.USER_LOG) {
+ URI uri = post.getURI();
+ if (uri != null) {
+ String query = uri.getQuery();
+ if (query == null) {
+ query = "POST";
+ }
+ userLog(threadName, ": Alert, aborting ", query);
+ } else {
+ userLog(threadName, ": Alert, no URI?");
+ }
+ }
+ // Abort the POST
+ mPostAborted = true;
+ post.abort();
+ } else {
+ // If there's no POST, we're done
+ userLog("Alert, no pending POST");
+ return true;
+ }
+ }
+
+ // Wait for the POST to finish
+ try {
+ Thread.sleep(POST_LOCK_TIMEOUT);
+ } catch (InterruptedException e) {
+ }
+
+ State s = mThread.getState();
+ if (Eas.USER_LOG) {
+ userLog(threadName + ": State = " + s.name());
+ }
+
+ synchronized (getSynchronizer()) {
+ // If the thread is still hanging around and the same post is pending, let's try to
+ // stop the thread with an interrupt.
+ if ((s != State.TERMINATED) && (mPendingPost != null) && (mPendingPost == post)) {
+ mStop = true;
+ mThread.interrupt();
+ userLog("Interrupting...");
+ // Let the caller know we had to interrupt the thread
+ return false;
+ }
+ }
+ // Let the caller know that the alarm was handled normally
+ return true;
+ }
+
+ @Override
+ public void reset() {
+ synchronized(getSynchronizer()) {
+ if (mPendingPost != null) {
+ URI uri = mPendingPost.getURI();
+ if (uri != null) {
+ String query = uri.getQuery();
+ if (query.startsWith("Cmd=Ping")) {
+ userLog("Reset, aborting Ping");
+ mPostReset = true;
+ mPendingPost.abort();
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void stop() {
+ mStop = true;
+ synchronized(getSynchronizer()) {
+ if (mPendingPost != null) {
+ mPendingPost.abort();
+ }
+ }
+ }
+
+ @Override
+ public void addRequest(Request request) {
+ // Don't allow duplicates of requests; just refuse them
+ if (mRequestQueue.contains(request)) return;
+ // Add the request
+ super.addRequest(request);
+ }
+
+ void setupProtocolVersion(EasSyncService service, Header versionHeader)
+ throws MessagingException {
+ // The string is a comma separated list of EAS versions in ascending order
+ // e.g. 1.0,2.0,2.5,12.0,12.1,14.0,14.1
+ String supportedVersions = versionHeader.getValue();
+ userLog("Server supports versions: ", supportedVersions);
+ String[] supportedVersionsArray = supportedVersions.split(",");
+ String ourVersion = null;
+ // Find the most recent version we support
+ for (String version: supportedVersionsArray) {
+ if (version.equals(Eas.SUPPORTED_PROTOCOL_EX2003) ||
+ version.equals(Eas.SUPPORTED_PROTOCOL_EX2007) ||
+ version.equals(Eas.SUPPORTED_PROTOCOL_EX2007_SP1) ||
+ version.equals(Eas.SUPPORTED_PROTOCOL_EX2010) ||
+ version.equals(Eas.SUPPORTED_PROTOCOL_EX2010_SP1)) {
+ ourVersion = version;
+ }
+ }
+ // If we don't support any of the servers supported versions, throw an exception here
+ // This will cause validation to fail
+ if (ourVersion == null) {
+ Log.w(TAG, "No supported EAS versions: " + supportedVersions);
+ throw new MessagingException(MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
+ } else {
+ // Debug code for testing EAS 14.0; disables support for EAS 14.1
+ // "adb shell setprop log.tag.Exchange14 VERBOSE"
+ if (ourVersion.equals(Eas.SUPPORTED_PROTOCOL_EX2010_SP1) &&
+ Log.isLoggable("Exchange14", Log.VERBOSE)) {
+ ourVersion = Eas.SUPPORTED_PROTOCOL_EX2010;
+ }
+ service.mProtocolVersion = ourVersion;
+ service.mProtocolVersionDouble = Eas.getProtocolVersionDouble(ourVersion);
+ Account account = service.mAccount;
+ if (account != null) {
+ account.mProtocolVersion = ourVersion;
+ // Fixup search flags, if they're not set
+ if (service.mProtocolVersionDouble >= 12.0 &&
+ (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) == 0) {
+ if (account.isSaved()) {
+ ContentValues cv = new ContentValues();
+ account.mFlags |=
+ Account.FLAGS_SUPPORTS_GLOBAL_SEARCH + Account.FLAGS_SUPPORTS_SEARCH;
+ cv.put(AccountColumns.FLAGS, account.mFlags);
+ account.update(service.mContext, cv);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Create an EasSyncService for the specified account
+ *
+ * @param context the caller's context
+ * @param account the account
+ * @return the service, or null if the account is on hold or hasn't been initialized
+ */
+ public static EasSyncService setupServiceForAccount(Context context, Account account) {
+ // Just return null if we're on security hold
+ if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
+ return null;
+ }
+ // If there's no protocol version, we're not initialized
+ String protocolVersion = account.mProtocolVersion;
+ if (protocolVersion == null) {
+ return null;
+ }
+ EasSyncService svc = new EasSyncService("OutOfBand");
+ HostAuth ha = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
+ svc.mProtocolVersion = protocolVersion;
+ svc.mProtocolVersionDouble = Eas.getProtocolVersionDouble(protocolVersion);
+ svc.mContext = context;
+ svc.mHostAddress = ha.mAddress;
+ svc.mUserName = ha.mLogin;
+ svc.mPassword = ha.mPassword;
+ try {
+ svc.setConnectionParameters(
+ (ha.mFlags & HostAuth.FLAG_SSL) != 0,
+ (ha.mFlags & HostAuth.FLAG_TRUST_ALL) != 0,
+ ha.mClientCertAlias);
+ svc.mDeviceId = ExchangeService.getDeviceId(context);
+ } catch (IOException e) {
+ return null;
+ } catch (CertificateException e) {
+ return null;
+ }
+ svc.mAccount = account;
+ return svc;
+ }
+
+ /**
+ * Get a redirect address and validate against it
+ * @param resp the EasResponse to our POST
+ * @param hostAuth the HostAuth we're using to validate
+ * @return true if we have an updated HostAuth (with redirect address); false otherwise
+ */
+ protected boolean getValidateRedirect(EasResponse resp, HostAuth hostAuth) {
+ Header locHeader = resp.getHeader("X-MS-Location");
+ if (locHeader != null) {
+ String loc;
+ try {
+ loc = locHeader.getValue();
+ // Reset our host address and uncache our base uri
+ mHostAddress = Uri.parse(loc).getHost();
+ mBaseUriString = null;
+ hostAuth.mAddress = mHostAddress;
+ userLog("Redirecting to: " + loc);
+ return true;
+ } catch (RuntimeException e) {
+ // Just don't crash if the Uri is illegal
+ }
+ }
+ return false;
+ }
+
+ private static final int MAX_REDIRECTS = 3;
+ private int mRedirectCount = 0;
+
+ @Override
+ public Bundle validateAccount(HostAuth hostAuth, Context context) {
+ Bundle bundle = new Bundle();
+ int resultCode = MessagingException.NO_ERROR;
+ try {
+ userLog("Testing EAS: ", hostAuth.mAddress, ", ", hostAuth.mLogin,
+ ", ssl = ", hostAuth.shouldUseSsl() ? "1" : "0");
+ mContext = context;
+ mHostAddress = hostAuth.mAddress;
+ mUserName = hostAuth.mLogin;
+ mPassword = hostAuth.mPassword;
+
+ setConnectionParameters(
+ hostAuth.shouldUseSsl(),
+ hostAuth.shouldTrustAllServerCerts(),
+ hostAuth.mClientCertAlias);
+ mDeviceId = ExchangeService.getDeviceId(context);
+ mAccount = new Account();
+ mAccount.mEmailAddress = hostAuth.mLogin;
+ EasResponse resp = sendHttpClientOptions();
+ try {
+ int code = resp.getStatus();
+ userLog("Validation (OPTIONS) response: " + code);
+ if (code == HttpStatus.SC_OK) {
+ // No exception means successful validation
+ Header commands = resp.getHeader("MS-ASProtocolCommands");
+ Header versions = resp.getHeader("ms-asprotocolversions");
+ // Make sure we've got the right protocol version set up
+ try {
+ if (commands == null || versions == null) {
+ userLog("OPTIONS response without commands or versions");
+ // We'll treat this as a protocol exception
+ throw new MessagingException(0);
+ }
+ setupProtocolVersion(this, versions);
+ } catch (MessagingException e) {
+ bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE,
+ MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
+ return bundle;
+ }
+
+ // Run second test here for provisioning failures using FolderSync
+ userLog("Try folder sync");
+ // Send "0" as the sync key for new accounts; otherwise, use the current key
+ String syncKey = "0";
+ Account existingAccount = Utility.findExistingAccount(
+ context, -1L, hostAuth.mAddress, hostAuth.mLogin);
+ if (existingAccount != null && existingAccount.mSyncKey != null) {
+ syncKey = existingAccount.mSyncKey;
+ }
+ Serializer s = new Serializer();
+ s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text(syncKey)
+ .end().end().done();
+ resp = sendHttpClientPost("FolderSync", s.toByteArray());
+ code = resp.getStatus();
+ // Handle HTTP error responses accordingly
+ if (code == HttpStatus.SC_FORBIDDEN) {
+ // For validation only, we take 403 as ACCESS_DENIED (the account isn't
+ // authorized, possibly due to device type)
+ resultCode = MessagingException.ACCESS_DENIED;
+ } else if (EasResponse.isProvisionError(code)) {
+ // The device needs to have security policies enforced
+ throw new CommandStatusException(CommandStatus.NEEDS_PROVISIONING);
+ } else if (code == HttpStatus.SC_NOT_FOUND) {
+ // We get a 404 from OWA addresses (which are NOT EAS addresses)
+ resultCode = MessagingException.PROTOCOL_VERSION_UNSUPPORTED;
+ } else if (code == HttpStatus.SC_UNAUTHORIZED) {
+ resultCode = resp.isMissingCertificate()
+ ? MessagingException.CLIENT_CERTIFICATE_REQUIRED
+ : MessagingException.AUTHENTICATION_FAILED;
+ } else if (code != HttpStatus.SC_OK) {
+ if ((code == EAS_REDIRECT_CODE) && (mRedirectCount++ < MAX_REDIRECTS) &&
+ getValidateRedirect(resp, hostAuth)) {
+ return validateAccount(hostAuth, context);
+ }
+ // Fail generically with anything other than success
+ userLog("Unexpected response for FolderSync: ", code);
+ resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
+ } else {
+ // We need to parse the result to see if we've got a provisioning issue
+ // (EAS 14.0 only)
+ if (!resp.isEmpty()) {
+ InputStream is = resp.getInputStream();
+ // Create the parser with statusOnly set to true; we only care about
+ // seeing if a CommandStatusException is thrown (indicating a
+ // provisioning failure)
+ new FolderSyncParser(is, new AccountSyncAdapter(this), true).parse();
+ }
+ userLog("Validation successful");
+ }
+ } else if (EasResponse.isAuthError(code)) {
+ userLog("Authentication failed");
+ resultCode = resp.isMissingCertificate()
+ ? MessagingException.CLIENT_CERTIFICATE_REQUIRED
+ : MessagingException.AUTHENTICATION_FAILED;
+ } else if (code == INTERNAL_SERVER_ERROR_CODE) {
+ // For Exchange 2003, this could mean an authentication failure OR server error
+ userLog("Internal server error");
+ resultCode = MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR;
+ } else {
+ if ((code == EAS_REDIRECT_CODE) && (mRedirectCount++ < MAX_REDIRECTS) &&
+ getValidateRedirect(resp, hostAuth)) {
+ return validateAccount(hostAuth, context);
+ }
+ // TODO Need to catch other kinds of errors (e.g. policy) For now, report code.
+ userLog("Validation failed, reporting I/O error: ", code);
+ resultCode = MessagingException.IOERROR;
+ }
+ } catch (CommandStatusException e) {
+ int status = e.mStatus;
+ if (CommandStatus.isNeedsProvisioning(status)) {
+ // Get the policies and see if we are able to support them
+ ProvisionParser pp = canProvision(this);
+ if (pp != null && pp.hasSupportablePolicySet()) {
+ // Set the proper result code and save the PolicySet in our Bundle
+ resultCode = MessagingException.SECURITY_POLICIES_REQUIRED;
+ bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET,
+ pp.getPolicy());
+ if (mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
+ mAccount.mSecuritySyncKey = pp.getSecuritySyncKey();
+ if (!sendSettings()) {
+ userLog("Denied access: ", CommandStatus.toString(status));
+ resultCode = MessagingException.ACCESS_DENIED;
+ }
+ }
+ } else {
+ // If not, set the proper code (the account will not be created)
+ resultCode = MessagingException.SECURITY_POLICIES_UNSUPPORTED;
+ bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET,
+ pp.getPolicy());
+ }
+ } else if (CommandStatus.isDeniedAccess(status)) {
+ userLog("Denied access: ", CommandStatus.toString(status));
+ resultCode = MessagingException.ACCESS_DENIED;
+ } else if (CommandStatus.isTransientError(status)) {
+ userLog("Transient error: ", CommandStatus.toString(status));
+ resultCode = MessagingException.IOERROR;
+ } else {
+ userLog("Unexpected response: ", CommandStatus.toString(status));
+ resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
+ }
+ } finally {
+ resp.close();
+ }
+ } catch (IOException e) {
+ Throwable cause = e.getCause();
+ if (cause != null && cause instanceof CertificateException) {
+ // This could be because the server's certificate failed to validate.
+ userLog("CertificateException caught: ", e.getMessage());
+ resultCode = MessagingException.GENERAL_SECURITY;
+ }
+ userLog("IOException caught: ", e.getMessage());
+ resultCode = MessagingException.IOERROR;
+ } catch (CertificateException e) {
+ // This occurs if the client certificate the user specified is invalid/inaccessible.
+ userLog("CertificateException caught: ", e.getMessage());
+ resultCode = MessagingException.CLIENT_CERTIFICATE_ERROR;
+ }
+ bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, resultCode);
+ return bundle;
+ }
+
+ /**
+ * 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. If we get a 401 (unauthorized) error and we're using the
+ * full email address, try the bare user name instead (e.g. foo instead of foo@bar.com)
+ *
+ * @param client the HttpClient to be used for the request
+ * @param post the HttpPost we're going to send
+ * @param canRetry whether we can retry using the bare name on an authentication failure (401)
+ * @return an HttpResponse from the original or redirect server
+ * @throws IOException on any IOException within the HttpClient code
+ * @throws MessagingException
+ */
+ private EasResponse postAutodiscover(HttpClient client, HttpPost post, boolean canRetry)
+ throws IOException, MessagingException {
+ userLog("Posting autodiscover to: " + post.getURI());
+ EasResponse resp = executePostWithTimeout(client, post, COMMAND_TIMEOUT);
+ int code = resp.getStatus();
+ // On a redirect, try the new location
+ if (code == EAS_REDIRECT_CODE) {
+ post = getRedirect(resp.mResponse, post);
+ if (post != null) {
+ userLog("Posting autodiscover to redirect: " + post.getURI());
+ return executePostWithTimeout(client, post, COMMAND_TIMEOUT);
+ }
+ // 401 (Unauthorized) is for true auth errors when used in Autodiscover
+ } else if (code == HttpStatus.SC_UNAUTHORIZED) {
+ if (canRetry && mUserName.contains("@")) {
+ // Try again using the bare user name
+ int atSignIndex = mUserName.indexOf('@');
+ mUserName = mUserName.substring(0, atSignIndex);
+ cacheAuthUserAndBaseUriStrings();
+ userLog("401 received; trying username: ", mUserName);
+ // Recreate the basic authentication string and reset the header
+ post.removeHeaders("Authorization");
+ post.setHeader("Authorization", mAuthString);
+ return postAutodiscover(client, post, false);
+ }
+ throw new MessagingException(MessagingException.AUTHENTICATION_FAILED);
+ // 403 (and others) we'll just punt on
+ } 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;
+ }
+
+ /**
+ * Convert an EAS server url to a HostAuth host address
+ * @param url a url, as provided by the Exchange server
+ * @return our equivalent host address
+ */
+ protected String autodiscoverUrlToHostAddress(String url) {
+ if (url == null) return null;
+ // We need to extract the server address from a url
+ return Uri.parse(url).getHost();
+ }
+
+ /**
+ * 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 recreated and cached
+ cacheAuthUserAndBaseUriStrings();
+
+ // 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)
+ // Note: The expectation is that, of these four attempts, only a single server will
+ // actually be identified as the autodiscover server. For the identified server,
+ // we may also try a 2nd connection with a different format (bare name).
+
+ // Try the domain first and see if we can get a response
+ HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE);
+ setHeaders(post, false);
+ post.setHeader("Content-Type", "text/xml");
+ post.setEntity(new StringEntity(req));
+ HttpClient client = getHttpClient(COMMAND_TIMEOUT);
+ EasResponse resp;
+ try {
+ resp = postAutodiscover(client, post, true /*canRetry*/);
+ } catch (IOException e1) {
+ userLog("IOException in autodiscover; trying alternate address");
+ // 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, true /*canRetry*/);
+ }
+
+ try {
+ // Get the "final" code; if it's not 200, just return null
+ int code = resp.getStatus();
+ userLog("Code: " + code);
+ if (code != HttpStatus.SC_OK) return null;
+
+ InputStream is = resp.getInputStream();
+ // 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) {
+ // Fill in the rest of the HostAuth
+ // We use the user name and password that were successful during
+ // the autodiscover process
+ hostAuth.mLogin = mUserName;
+ hostAuth.mPassword = mPassword;
+ // Note: there is no way we can auto-discover the proper client
+ // SSL certificate to use, if one is needed.
+ 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
+ } finally {
+ resp.close();
+ }
+ // 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.AUTODISCOVER_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 hostAddress =
+ autodiscoverUrlToHostAddress(parser.nextText());
+ if (hostAddress != null) {
+ hostAuth.mAddress = hostAddress;
+ userLog("Autodiscover, server: " + hostAddress);
+ }
+ }
+ }
+ }
+ }
+
+ 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();
+ userLog("Autodiscover, email: " + 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);
+ }
+ }
+ }
+
+ /**
+ * Contact the GAL and obtain a list of matching accounts
+ * @param context caller's context
+ * @param accountId the account Id to search
+ * @param filter the characters entered so far
+ * @return a result record or null for no data
+ *
+ * TODO: shorter timeout for interactive lookup
+ * TODO: make watchdog actually work (it doesn't understand our service w/Mailbox == 0)
+ * TODO: figure out why sendHttpClientPost() hangs - possibly pool exhaustion
+ */
+ static public GalResult searchGal(Context context, long accountId, String filter, int limit) {
+ Account acct = Account.restoreAccountWithId(context, accountId);
+ if (acct != null) {
+ EasSyncService svc = setupServiceForAccount(context, acct);
+ if (svc == null) return null;
+ try {
+ Serializer s = new Serializer();
+ s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE);
+ s.data(Tags.SEARCH_NAME, "GAL").data(Tags.SEARCH_QUERY, filter);
+ s.start(Tags.SEARCH_OPTIONS);
+ s.data(Tags.SEARCH_RANGE, "0-" + Integer.toString(limit - 1));
+ s.end().end().end().done();
+ EasResponse resp = svc.sendHttpClientPost("Search", s.toByteArray());
+ try {
+ int code = resp.getStatus();
+ if (code == HttpStatus.SC_OK) {
+ InputStream is = resp.getInputStream();
+ try {
+ GalParser gp = new GalParser(is, svc);
+ if (gp.parse()) {
+ return gp.getGalResult();
+ }
+ } finally {
+ is.close();
+ }
+ } else {
+ svc.userLog("GAL lookup returned " + code);
+ }
+ } finally {
+ resp.close();
+ }
+ } catch (IOException e) {
+ // GAL is non-critical; we'll just go on
+ svc.userLog("GAL lookup exception " + e);
+ }
+ }
+ return null;
+ }
+ /**
+ * Send an email responding to a Message that has been marked as a meeting request. The message
+ * will consist a little bit of event information and an iCalendar attachment
+ * @param msg the meeting request email
+ */
+ private void sendMeetingResponseMail(Message msg, int response) {
+ // Get the meeting information; we'd better have some...
+ if (msg.mMeetingInfo == null) return;
+ PackedString meetingInfo = new PackedString(msg.mMeetingInfo);
+
+ // This will come as "First Last" <box@server.blah>, so we use Address to
+ // parse it into parts; we only need the email address part for the ics file
+ Address[] addrs = Address.parse(meetingInfo.get(MeetingInfo.MEETING_ORGANIZER_EMAIL));
+ // It shouldn't be possible, but handle it anyway
+ if (addrs.length != 1) return;
+ String organizerEmail = addrs[0].getAddress();
+
+ String dtStamp = meetingInfo.get(MeetingInfo.MEETING_DTSTAMP);
+ String dtStart = meetingInfo.get(MeetingInfo.MEETING_DTSTART);
+ String dtEnd = meetingInfo.get(MeetingInfo.MEETING_DTEND);
+
+ // What we're doing here is to create an Entity that looks like an Event as it would be
+ // stored by CalendarProvider
+ ContentValues entityValues = new ContentValues();
+ Entity entity = new Entity(entityValues);
+
+ // Fill in times, location, title, and organizer
+ entityValues.put("DTSTAMP",
+ CalendarUtilities.convertEmailDateTimeToCalendarDateTime(dtStamp));
+ entityValues.put(Events.DTSTART, Utility.parseEmailDateTimeToMillis(dtStart));
+ entityValues.put(Events.DTEND, Utility.parseEmailDateTimeToMillis(dtEnd));
+ entityValues.put(Events.EVENT_LOCATION, meetingInfo.get(MeetingInfo.MEETING_LOCATION));
+ entityValues.put(Events.TITLE, meetingInfo.get(MeetingInfo.MEETING_TITLE));
+ entityValues.put(Events.ORGANIZER, organizerEmail);
+
+ // Add ourselves as an attendee, using our account email address
+ ContentValues attendeeValues = new ContentValues();
+ attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP,
+ Attendees.RELATIONSHIP_ATTENDEE);
+ attendeeValues.put(Attendees.ATTENDEE_EMAIL, mAccount.mEmailAddress);
+ entity.addSubValue(Attendees.CONTENT_URI, attendeeValues);
+
+ // Add the organizer
+ ContentValues organizerValues = new ContentValues();
+ organizerValues.put(Attendees.ATTENDEE_RELATIONSHIP,
+ Attendees.RELATIONSHIP_ORGANIZER);
+ organizerValues.put(Attendees.ATTENDEE_EMAIL, organizerEmail);
+ entity.addSubValue(Attendees.CONTENT_URI, organizerValues);
+
+ // Create a message from the Entity we've built. The message will have fields like
+ // to, subject, date, and text filled in. There will also be an "inline" attachment
+ // which is in iCalendar format
+ int flag;
+ switch(response) {
+ case EmailServiceConstants.MEETING_REQUEST_ACCEPTED:
+ flag = Message.FLAG_OUTGOING_MEETING_ACCEPT;
+ break;
+ case EmailServiceConstants.MEETING_REQUEST_DECLINED:
+ flag = Message.FLAG_OUTGOING_MEETING_DECLINE;
+ break;
+ case EmailServiceConstants.MEETING_REQUEST_TENTATIVE:
+ default:
+ flag = Message.FLAG_OUTGOING_MEETING_TENTATIVE;
+ break;
+ }
+ Message outgoingMsg =
+ CalendarUtilities.createMessageForEntity(mContext, entity, flag,
+ meetingInfo.get(MeetingInfo.MEETING_UID), mAccount);
+ // Assuming we got a message back (we might not if the event has been deleted), send it
+ if (outgoingMsg != null) {
+ EasOutboxService.sendMessage(mContext, mAccount.mId, outgoingMsg);
+ }
+ }
+
+ /**
+ * Responds to a move request. The MessageMoveRequest is basically our
+ * wrapper for the MoveItems service call
+ * @param req the request (message id and "to" mailbox id)
+ * @throws IOException
+ */
+ protected void messageMoveRequest(MessageMoveRequest req) throws IOException {
+ // Retrieve the message and mailbox; punt if either are null
+ Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
+ if (msg == null) return;
+ Cursor c = mContentResolver.query(ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI,
+ msg.mId), new String[] {MessageColumns.MAILBOX_KEY}, null, null, null);
+ if (c == null) throw new ProviderUnavailableException();
+ Mailbox srcMailbox = null;
+ try {
+ if (!c.moveToNext()) return;
+ srcMailbox = Mailbox.restoreMailboxWithId(mContext, c.getLong(0));
+ } finally {
+ c.close();
+ }
+ if (srcMailbox == null) return;
+ Mailbox dstMailbox = Mailbox.restoreMailboxWithId(mContext, req.mMailboxId);
+ if (dstMailbox == null) return;
+ Serializer s = new Serializer();
+ s.start(Tags.MOVE_MOVE_ITEMS).start(Tags.MOVE_MOVE);
+ s.data(Tags.MOVE_SRCMSGID, msg.mServerId);
+ s.data(Tags.MOVE_SRCFLDID, srcMailbox.mServerId);
+ s.data(Tags.MOVE_DSTFLDID, dstMailbox.mServerId);
+ s.end().end().done();
+ EasResponse resp = sendHttpClientPost("MoveItems", s.toByteArray());
+ try {
+ int status = resp.getStatus();
+ if (status == HttpStatus.SC_OK) {
+ if (!resp.isEmpty()) {
+ InputStream is = resp.getInputStream();
+ MoveItemsParser p = new MoveItemsParser(is, this);
+ p.parse();
+ int statusCode = p.getStatusCode();
+ ContentValues cv = new ContentValues();
+ if (statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
+ // Restore the old mailbox id
+ cv.put(MessageColumns.MAILBOX_KEY, srcMailbox.mServerId);
+ mContentResolver.update(
+ ContentUris.withAppendedId(Message.CONTENT_URI, req.mMessageId),
+ cv, null, null);
+ } else if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS) {
+ // Update with the new server id
+ cv.put(SyncColumns.SERVER_ID, p.getNewServerId());
+ cv.put(Message.FLAGS, msg.mFlags | MESSAGE_FLAG_MOVED_MESSAGE);
+ mContentResolver.update(
+ ContentUris.withAppendedId(Message.CONTENT_URI, req.mMessageId),
+ cv, null, null);
+ }
+ if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS
+ || statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
+ // If we revert or succeed, we no longer need the update information
+ // OR the now-duplicate email (the new copy will be synced down)
+ mContentResolver.delete(ContentUris.withAppendedId(
+ Message.UPDATED_CONTENT_URI, req.mMessageId), null, null);
+ } else {
+ // In this case, we're retrying, so do nothing. The request will be
+ // handled next sync
+ }
+ }
+ } else if (EasResponse.isAuthError(status)) {
+ throw new EasAuthenticationException();
+ } else {
+ userLog("Move items request failed, code: " + status);
+ throw new IOException();
+ }
+ } finally {
+ resp.close();
+ }
+ }
+
+ /**
+ * 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 {
+ // Retrieve the message and mailbox; punt if either are null
+ Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
+ if (msg == null) return;
+ Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, msg.mMailboxKey);
+ if (mailbox == null) return;
+ 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, mailbox.mServerId);
+ s.data(Tags.MREQ_REQ_ID, msg.mServerId);
+ s.end().end().done();
+ EasResponse resp = sendHttpClientPost("MeetingResponse", s.toByteArray());
+ try {
+ int status = resp.getStatus();
+ if (status == HttpStatus.SC_OK) {
+ if (!resp.isEmpty()) {
+ InputStream is = resp.getInputStream();
+ new MeetingResponseParser(is, this).parse();
+ String meetingInfo = msg.mMeetingInfo;
+ if (meetingInfo != null) {
+ String responseRequested = new PackedString(meetingInfo).get(
+ MeetingInfo.MEETING_RESPONSE_REQUESTED);
+ // If there's no tag, or a non-zero tag, we send the response mail
+ if ("0".equals(responseRequested)) {
+ return;
+ }
+ }
+ sendMeetingResponseMail(msg, req.mResponse);
+ }
+ } else if (EasResponse.isAuthError(status)) {
+ throw new EasAuthenticationException();
+ } else {
+ userLog("Meeting response request failed, code: " + status);
+ throw new IOException();
+ }
+ } finally {
+ resp.close();
+ }
+ }
+
+ /**
+ * Using mUserName and mPassword, lazily create the strings that are commonly used in our HTTP
+ * POSTs, including the authentication header string, the base URI we use to communicate with
+ * EAS, and the user information string (user, deviceId, and deviceType)
+ */
+ private void cacheAuthUserAndBaseUriStrings() {
+ if (mAuthString == null || mUserString == null || mBaseUriString == null) {
+ String safeUserName = Uri.encode(mUserName);
+ String cs = mUserName + ':' + mPassword;
+ mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP);
+ mUserString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId +
+ "&DeviceType=" + DEVICE_TYPE;
+ String scheme =
+ EmailClientConnectionManager.makeScheme(mSsl, mTrustSsl, mClientCertAlias);
+ mBaseUriString = scheme + "://" + mHostAddress + "/Microsoft-Server-ActiveSync";
+ }
+ }
+
+ @VisibleForTesting
+ String makeUriString(String cmd, String extra) {
+ cacheAuthUserAndBaseUriStrings();
+ String uriString = mBaseUriString;
+ if (cmd != null) {
+ uriString += "?Cmd=" + cmd + mUserString;
+ }
+ if (extra != null) {
+ uriString += extra;
+ }
+ return uriString;
+ }
+
+ /**
+ * Set standard HTTP headers, using a policy key if required
+ * @param method the method we are going to send
+ * @param usePolicyKey whether or not a policy key should be sent in the headers
+ */
+ /*package*/ void setHeaders(HttpRequestBase method, boolean usePolicyKey) {
+ method.setHeader("Authorization", mAuthString);
+ method.setHeader("MS-ASProtocolVersion", mProtocolVersion);
+ method.setHeader("User-Agent", USER_AGENT);
+ method.setHeader("Accept-Encoding", "gzip");
+ if (usePolicyKey) {
+ // If there's an account in existence, use its key; otherwise (we're creating the
+ // account), send "0". The server will respond with code 449 if there are policies
+ // to be enforced
+ String key = "0";
+ if (mAccount != null) {
+ String accountKey = mAccount.mSecuritySyncKey;
+ if (!TextUtils.isEmpty(accountKey)) {
+ key = accountKey;
+ }
+ }
+ method.setHeader("X-MS-PolicyKey", key);
+ }
+ }
+
+ protected void setConnectionParameters(
+ boolean useSsl, boolean trustAllServerCerts, String clientCertAlias)
+ throws CertificateException {
+
+ EmailClientConnectionManager connManager = getClientConnectionManager();
+
+ mSsl = useSsl;
+ mTrustSsl = trustAllServerCerts;
+ mClientCertAlias = clientCertAlias;
+
+ // Register the new alias, if needed.
+ if (mClientCertAlias != null) {
+ // Ensure that the connection manager knows to use the proper client certificate
+ // when establishing connections for this service.
+ connManager.registerClientCert(mContext, mClientCertAlias, mTrustSsl);
+ }
+ }
+
+ private EmailClientConnectionManager getClientConnectionManager() {
+ return ExchangeService.getClientConnectionManager();
+ }
+
+ private HttpClient getHttpClient(int timeout) {
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
+ HttpConnectionParams.setSoTimeout(params, timeout);
+ HttpConnectionParams.setSocketBufferSize(params, 8192);
+ HttpClient client = new DefaultHttpClient(getClientConnectionManager(), params);
+ return client;
+ }
+
+ public EasResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException {
+ return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), COMMAND_TIMEOUT);
+ }
+
+ protected EasResponse sendHttpClientPost(String cmd, HttpEntity entity) throws IOException {
+ return sendHttpClientPost(cmd, entity, COMMAND_TIMEOUT);
+ }
+
+ protected EasResponse sendPing(byte[] bytes, int heartbeat) throws IOException {
+ Thread.currentThread().setName(mAccount.mDisplayName + ": Ping");
+ return sendHttpClientPost(PING_COMMAND, new ByteArrayEntity(bytes), (heartbeat+5)*SECONDS);
+ }
+
+ /**
+ * Convenience method for executePostWithTimeout for use other than with the Ping command
+ */
+ protected EasResponse executePostWithTimeout(HttpClient client, HttpPost method, int timeout)
+ throws IOException {
+ return executePostWithTimeout(client, method, timeout, false);
+ }
+
+ /**
+ * Handle executing an HTTP POST command with proper timeout, watchdog, and ping behavior
+ * @param client the HttpClient
+ * @param method the HttpPost
+ * @param timeout the timeout before failure, in ms
+ * @param isPingCommand whether the POST is for the Ping command (requires wakelock logic)
+ * @return the HttpResponse
+ * @throws IOException
+ */
+ protected EasResponse executePostWithTimeout(HttpClient client, HttpPost method, int timeout,
+ boolean isPingCommand) throws IOException {
+ synchronized(getSynchronizer()) {
+ mPendingPost = method;
+ long alarmTime = timeout + WATCHDOG_TIMEOUT_ALLOWANCE;
+ if (isPingCommand) {
+ ExchangeService.runAsleep(mMailboxId, alarmTime);
+ } else {
+ ExchangeService.setWatchdogAlarm(mMailboxId, alarmTime);
+ }
+ }
+ try {
+ return EasResponse.fromHttpRequest(getClientConnectionManager(), client, method);
+ } finally {
+ synchronized(getSynchronizer()) {
+ if (isPingCommand) {
+ ExchangeService.runAwake(mMailboxId);
+ } else {
+ ExchangeService.clearWatchdogAlarm(mMailboxId);
+ }
+ mPendingPost = null;
+ }
+ }
+ }
+
+ public EasResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout)
+ throws IOException {
+ HttpClient client = getHttpClient(timeout);
+ boolean isPingCommand = cmd.equals(PING_COMMAND);
+
+ // Split the mail sending commands
+ String extra = null;
+ boolean msg = false;
+ if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) {
+ int cmdLength = cmd.indexOf('&');
+ extra = cmd.substring(cmdLength);
+ cmd = cmd.substring(0, cmdLength);
+ msg = true;
+ } else if (cmd.startsWith("SendMail&")) {
+ msg = true;
+ }
+
+ String us = makeUriString(cmd, extra);
+ HttpPost method = new HttpPost(URI.create(us));
+ // Send the proper Content-Type header; it's always wbxml except for messages when
+ // the EAS protocol version is < 14.0
+ // If entity is null (e.g. for attachments), don't set this header
+ if (msg && (mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE)) {
+ method.setHeader("Content-Type", "message/rfc822");
+ } else if (entity != null) {
+ method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml");
+ }
+ setHeaders(method, !isPingCommand);
+ // NOTE
+ // The next lines are added at the insistence of $VENDOR, who is seeing inappropriate
+ // network activity related to the Ping command on some networks with some servers.
+ // This code should be removed when the underlying issue is resolved
+ if (isPingCommand) {
+ method.setHeader("Connection", "close");
+ }
+ method.setEntity(entity);
+ return executePostWithTimeout(client, method, timeout, isPingCommand);
+ }
+
+ protected EasResponse sendHttpClientOptions() throws IOException {
+ cacheAuthUserAndBaseUriStrings();
+ // For OPTIONS, just use the base string and the single header
+ String uriString = mBaseUriString;
+ HttpOptions method = new HttpOptions(URI.create(uriString));
+ method.setHeader("Authorization", mAuthString);
+ method.setHeader("User-Agent", USER_AGENT);
+ HttpClient client = getHttpClient(COMMAND_TIMEOUT);
+ return EasResponse.fromHttpRequest(getClientConnectionManager(), client, method);
+ }
+
+ String getTargetCollectionClassFromCursor(Cursor c) {
+ int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
+ if (type == Mailbox.TYPE_CONTACTS) {
+ return "Contacts";
+ } else if (type == Mailbox.TYPE_CALENDAR) {
+ return "Calendar";
+ } else {
+ return "Email";
+ }
+ }
+
+ /**
+ * Negotiate provisioning with the server. First, get policies form the server and see if
+ * the policies are supported by the device. Then, write the policies to the account and
+ * tell SecurityPolicy that we have policies in effect. Finally, see if those policies are
+ * active; if so, acknowledge the policies to the server and get a final policy key that we
+ * use in future EAS commands and write this key to the account.
+ * @return whether or not provisioning has been successful
+ * @throws IOException
+ */
+ public static boolean tryProvision(EasSyncService svc) throws IOException {
+ // First, see if provisioning is even possible, i.e. do we support the policies required
+ // by the server
+ ProvisionParser pp = canProvision(svc);
+ if (pp == null) return false;
+ Context context = svc.mContext;
+ Account account = svc.mAccount;
+ // Get the policies from ProvisionParser
+ Policy policy = pp.getPolicy();
+ Policy oldPolicy = null;
+ // Grab the old policy (if any)
+ if (svc.mAccount.mPolicyKey > 0) {
+ oldPolicy = Policy.restorePolicyWithId(context, account.mPolicyKey);
+ }
+ // Update the account with a null policyKey (the key we've gotten is
+ // temporary and cannot be used for syncing)
+ PolicyServiceProxy.setAccountPolicy(context, account.mId, policy, null);
+ // Make sure mAccount is current (with latest policy key)
+ account.refresh(context);
+ if (pp.getRemoteWipe()) {
+ // We've gotten a remote wipe command
+ ExchangeService.alwaysLog("!!! Remote wipe request received");
+ // Start by setting the account to security hold
+ PolicyServiceProxy.setAccountHoldFlag(context, account, true);
+ // Force a stop to any running syncs for this account (except this one)
+ ExchangeService.stopNonAccountMailboxSyncsForAccount(account.mId);
+
+ // First, we've got to acknowledge it, but wrap the wipe in try/catch so that
+ // we wipe the device regardless of any errors in acknowledgment
+ try {
+ ExchangeService.alwaysLog("!!! Acknowledging remote wipe to server");
+ acknowledgeRemoteWipe(svc, pp.getSecuritySyncKey());
+ } catch (Exception e) {
+ // Because remote wipe is such a high priority task, we don't want to
+ // circumvent it if there's an exception in acknowledgment
+ }
+ // Then, tell SecurityPolicy to wipe the device
+ ExchangeService.alwaysLog("!!! Executing remote wipe");
+ PolicyServiceProxy.remoteWipe(context);
+ return false;
+ } else if (pp.hasSupportablePolicySet() && PolicyServiceProxy.isActive(context, policy)) {
+ // See if the required policies are in force; if they are, acknowledge the policies
+ // to the server and get the final policy key
+ // NOTE: For EAS 14.0, we already have the acknowledgment in the ProvisionParser
+ String securitySyncKey;
+ if (svc.mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
+ securitySyncKey = pp.getSecuritySyncKey();
+ } else {
+ securitySyncKey = acknowledgeProvision(svc, pp.getSecuritySyncKey(),
+ PROVISION_STATUS_OK);
+ }
+ if (securitySyncKey != null) {
+ // If attachment policies have changed, fix up any affected attachment records
+ if (oldPolicy != null) {
+ if ((oldPolicy.mDontAllowAttachments != policy.mDontAllowAttachments) ||
+ (oldPolicy.mMaxAttachmentSize != policy.mMaxAttachmentSize)) {
+ Policy.setAttachmentFlagsForNewPolicy(context, account, policy);
+ }
+ }
+ // Write the final policy key to the Account and say we've been successful
+ PolicyServiceProxy.setAccountPolicy(context, account.mId, policy, securitySyncKey);
+ // Release any mailboxes that might be in a security hold
+ ExchangeService.releaseSecurityHold(account);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String getPolicyType(Double protocolVersion) {
+ return (protocolVersion >=
+ Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) ? EAS_12_POLICY_TYPE : EAS_2_POLICY_TYPE;
+ }
+
+ /**
+ * Obtain a set of policies from the server and determine whether those policies are supported
+ * by the device.
+ * @return the ProvisionParser (holds policies and key) if we receive policies; null otherwise
+ * @throws IOException
+ */
+ public static ProvisionParser canProvision(EasSyncService svc) throws IOException {
+ Serializer s = new Serializer();
+ Double protocolVersion = svc.mProtocolVersionDouble;
+ s.start(Tags.PROVISION_PROVISION);
+ if (svc.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_SP1_DOUBLE) {
+ // Send settings information in 14.1 and greater
+ s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
+ s.data(Tags.SETTINGS_MODEL, Build.MODEL);
+ //s.data(Tags.SETTINGS_IMEI, "");
+ //s.data(Tags.SETTINGS_FRIENDLY_NAME, "Friendly Name");
+ s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
+ //s.data(Tags.SETTINGS_OS_LANGUAGE, "");
+ //s.data(Tags.SETTINGS_PHONE_NUMBER, "");
+ //s.data(Tags.SETTINGS_MOBILE_OPERATOR, "");
+ s.data(Tags.SETTINGS_USER_AGENT, EasSyncService.USER_AGENT);
+ s.end().end(); // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION
+ }
+ s.start(Tags.PROVISION_POLICIES);
+ s.start(Tags.PROVISION_POLICY);
+ s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType(protocolVersion));
+ s.end().end().end().done(); // PROVISION_POLICY, PROVISION_POLICIES, PROVISION_PROVISION
+ EasResponse resp = svc.sendHttpClientPost("Provision", s.toByteArray());
+ try {
+ int code = resp.getStatus();
+ if (code == HttpStatus.SC_OK) {
+ InputStream is = resp.getInputStream();
+ ProvisionParser pp = new ProvisionParser(is, svc);
+ if (pp.parse()) {
+ // The PolicySet in the ProvisionParser will have the requirements for all KNOWN
+ // policies. If others are required, hasSupportablePolicySet will be false
+ if (pp.hasSupportablePolicySet() &&
+ svc.mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
+ // In EAS 14.0, we need the final security key in order to use the settings
+ // command
+ String policyKey = acknowledgeProvision(svc, pp.getSecuritySyncKey(),
+ PROVISION_STATUS_OK);
+ if (policyKey != null) {
+ pp.setSecuritySyncKey(policyKey);
+ }
+ } else if (!pp.hasSupportablePolicySet()) {
+ // Try to acknowledge using the "partial" status (i.e. we can partially
+ // accommodate the required policies). The server will agree to this if the
+ // "allow non-provisionable devices" setting is enabled on the server
+ ExchangeService.log("PolicySet is NOT fully supportable");
+ if (acknowledgeProvision(svc, pp.getSecuritySyncKey(),
+ PROVISION_STATUS_PARTIAL) != null) {
+ // The server's ok with our inability to support policies, so we'll
+ // clear them
+ pp.clearUnsupportablePolicies();
+ }
+ }
+ return pp;
+ }
+ }
+ } finally {
+ resp.close();
+ }
+
+ // On failures, simply return null
+ return null;
+ }
+
+ /**
+ * Acknowledge that we support the policies provided by the server, and that these policies
+ * are in force.
+ * @param tempKey the initial (temporary) policy key sent by the server
+ * @return the final policy key, which can be used for syncing
+ * @throws IOException
+ */
+ private static void acknowledgeRemoteWipe(EasSyncService svc, String tempKey)
+ throws IOException {
+ acknowledgeProvisionImpl(svc, tempKey, PROVISION_STATUS_OK, true);
+ }
+
+ private static String acknowledgeProvision(EasSyncService svc, String tempKey, String result)
+ throws IOException {
+ return acknowledgeProvisionImpl(svc, tempKey, result, false);
+ }
+
+ private static String acknowledgeProvisionImpl(EasSyncService svc, String tempKey,
+ String status, boolean remoteWipe) throws IOException {
+ Serializer s = new Serializer();
+ s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES);
+ s.start(Tags.PROVISION_POLICY);
+
+ // Use the proper policy type, depending on EAS version
+ s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType(svc.mProtocolVersionDouble));
+
+ s.data(Tags.PROVISION_POLICY_KEY, tempKey);
+ s.data(Tags.PROVISION_STATUS, status);
+ s.end().end(); // PROVISION_POLICY, PROVISION_POLICIES
+ if (remoteWipe) {
+ s.start(Tags.PROVISION_REMOTE_WIPE);
+ s.data(Tags.PROVISION_STATUS, PROVISION_STATUS_OK);
+ s.end();
+ }
+ s.end().done(); // PROVISION_PROVISION
+ EasResponse resp = svc.sendHttpClientPost("Provision", s.toByteArray());
+ try {
+ int code = resp.getStatus();
+ if (code == HttpStatus.SC_OK) {
+ InputStream is = resp.getInputStream();
+ ProvisionParser pp = new ProvisionParser(is, svc);
+ if (pp.parse()) {
+ // Return the final policy key from the ProvisionParser
+ String result = (pp.getSecuritySyncKey() == null) ? "failed" : "confirmed";
+ ExchangeService.log("Provision " + result + " for " +
+ (PROVISION_STATUS_PARTIAL.equals(status) ? "PART" : "FULL") + " set");
+ return pp.getSecuritySyncKey();
+ }
+ }
+ } finally {
+ resp.close();
+ }
+ // On failures, log issue and return null
+ ExchangeService.log("Provisioning failed for" +
+ (PROVISION_STATUS_PARTIAL.equals(status) ? "PART" : "FULL") + " set");
+ return null;
+ }
+
+ private boolean sendSettings() throws IOException {
+ Serializer s = new Serializer();
+ s.start(Tags.SETTINGS_SETTINGS);
+ s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
+ s.data(Tags.SETTINGS_MODEL, Build.MODEL);
+ s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
+ s.data(Tags.SETTINGS_USER_AGENT, USER_AGENT);
+ s.end().end().end().done(); // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION, SETTINGS_SETTINGS
+ EasResponse resp = sendHttpClientPost("Settings", s.toByteArray());
+ try {
+ int code = resp.getStatus();
+ if (code == HttpStatus.SC_OK) {
+ InputStream is = resp.getInputStream();
+ SettingsParser sp = new SettingsParser(is, this);
+ return sp.parse();
+ }
+ } finally {
+ resp.close();
+ }
+ // On failures, simply return false
+ return false;
+ }
+
+ /**
+ * Common code to sync E+PIM data
+ * @param target an EasMailbox, EasContacts, or EasCalendar object
+ */
+ public void sync(AbstractSyncAdapter target) throws IOException {
+ Mailbox mailbox = target.mMailbox;
+
+ boolean moreAvailable = true;
+ int loopingCount = 0;
+ while (!mStop && (moreAvailable || hasPendingRequests())) {
+ // If we have no connectivity, just exit cleanly. ExchangeService will start us up again
+ // when connectivity has returned
+ if (!hasConnectivity()) {
+ userLog("No connectivity in sync; finishing sync");
+ mExitStatus = EXIT_DONE;
+ return;
+ }
+
+ // Every time through the loop we check to see if we're still syncable
+ if (!target.isSyncable()) {
+ mExitStatus = EXIT_DONE;
+ return;
+ }
+
+ // Now, handle various requests
+ while (true) {
+ Request req = null;
+
+ if (mRequestQueue.isEmpty()) {
+ break;
+ } else {
+ req = mRequestQueue.peek();
+ }
+
+ // Our two request types are PartRequest (loading attachment) and
+ // MeetingResponseRequest (respond to a meeting request)
+ if (req instanceof PartRequest) {
+ TrafficStats.setThreadStatsTag(
+ TrafficFlags.getAttachmentFlags(mContext, mAccount));
+ new AttachmentLoader(this, (PartRequest)req).loadAttachment();
+ TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, mAccount));
+ } else if (req instanceof MeetingResponseRequest) {
+ sendMeetingResponse((MeetingResponseRequest)req);
+ } else if (req instanceof MessageMoveRequest) {
+ messageMoveRequest((MessageMoveRequest)req);
+ }
+
+ // If there's an exception handling the request, we'll throw it
+ // Otherwise, we remove the request
+ mRequestQueue.remove();
+ }
+
+ // Don't sync if we've got nothing to do
+ if (!moreAvailable) {
+ continue;
+ }
+
+ Serializer s = new Serializer();
+
+ String className = target.getCollectionName();
+ String syncKey = target.getSyncKey();
+ userLog("sync, sending ", className, " syncKey: ", syncKey);
+ s.start(Tags.SYNC_SYNC)
+ .start(Tags.SYNC_COLLECTIONS)
+ .start(Tags.SYNC_COLLECTION);
+ // The "Class" element is removed in EAS 12.1 and later versions
+ if (mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
+ s.data(Tags.SYNC_CLASS, className);
+ }
+ s.data(Tags.SYNC_SYNC_KEY, syncKey)
+ .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId);
+
+ // Start with the default timeout
+ int timeout = COMMAND_TIMEOUT;
+ if (!syncKey.equals("0")) {
+ // EAS doesn't allow GetChanges in an initial sync; sending other options
+ // appears to cause the server to delay its response in some cases, and this delay
+ // can be long enough to result in an IOException and total failure to sync.
+ // Therefore, we don't send any options with the initial sync.
+ // Set the truncation amount, body preference, lookback, etc.
+ target.sendSyncOptions(mProtocolVersionDouble, s);
+ } else {
+ // Use enormous timeout for initial sync, which empirically can take a while longer
+ timeout = 120*SECONDS;
+ }
+ // Send our changes up to the server
+ if (mUpsyncFailed) {
+ if (Eas.USER_LOG) {
+ Log.d(TAG, "Inhibiting upsync this cycle");
+ }
+ } else {
+ target.sendLocalChanges(s);
+ }
+
+ s.end().end().end().done();
+ EasResponse resp = sendHttpClientPost("Sync", new ByteArrayEntity(s.toByteArray()),
+ timeout);
+ try {
+ int code = resp.getStatus();
+ if (code == HttpStatus.SC_OK) {
+ // In EAS 12.1, we can get "empty" sync responses, which indicate that there are
+ // no changes in the mailbox; handle that case here
+ // There are two cases here; if we get back a compressed stream (GZIP), we won't
+ // know until we try to parse it (and generate an EmptyStreamException). If we
+ // get uncompressed data, the response will be empty (i.e. have zero length)
+ boolean emptyStream = false;
+ if (!resp.isEmpty()) {
+ InputStream is = resp.getInputStream();
+ try {
+ moreAvailable = target.parse(is);
+ // If we inhibited upsync, we need yet another sync
+ if (mUpsyncFailed) {
+ moreAvailable = true;
+ }
+
+ if (target.isLooping()) {
+ loopingCount++;
+ userLog("** Looping: " + loopingCount);
+ // After the maximum number of loops, we'll set moreAvailable to
+ // false and allow the sync loop to terminate
+ if (moreAvailable && (loopingCount > MAX_LOOPING_COUNT)) {
+ userLog("** Looping force stopped");
+ moreAvailable = false;
+ }
+ } else {
+ loopingCount = 0;
+ }
+
+ // Cleanup clears out the updated/deleted tables, and we don't want to
+ // do that if our upsync failed; clear the flag otherwise
+ if (!mUpsyncFailed) {
+ target.cleanup();
+ } else {
+ mUpsyncFailed = false;
+ }
+ } catch (EmptyStreamException e) {
+ userLog("Empty stream detected in GZIP response");
+ emptyStream = true;
+ } catch (CommandStatusException e) {
+ // TODO 14.1
+ int status = e.mStatus;
+ if (CommandStatus.isNeedsProvisioning(status)) {
+ mExitStatus = EXIT_SECURITY_FAILURE;
+ } else if (CommandStatus.isDeniedAccess(status)) {
+ mExitStatus = EXIT_ACCESS_DENIED;
+ } else if (CommandStatus.isTransientError(status)) {
+ mExitStatus = EXIT_IO_ERROR;
+ } else {
+ mExitStatus = EXIT_EXCEPTION;
+ }
+ return;
+ }
+ } else {
+ emptyStream = true;
+ }
+
+ if (emptyStream) {
+ // If this happens, exit cleanly, and change the interval from push to ping
+ // if necessary
+ userLog("Empty sync response; finishing");
+ if (mMailbox.mSyncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
+ userLog("Changing mailbox from push to ping");
+ ContentValues cv = new ContentValues();
+ cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PING);
+ mContentResolver.update(
+ ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId),
+ cv, null, null);
+ }
+ if (mRequestQueue.isEmpty()) {
+ mExitStatus = EXIT_DONE;
+ return;
+ } else {
+ continue;
+ }
+ }
+ } else {
+ userLog("Sync response error: ", code);
+ if (EasResponse.isProvisionError(code)) {
+ mExitStatus = EXIT_SECURITY_FAILURE;
+ } else if (EasResponse.isAuthError(code)) {
+ mExitStatus = EXIT_LOGIN_FAILURE;
+ } else {
+ mExitStatus = EXIT_IO_ERROR;
+ }
+ return;
+ }
+ } finally {
+ resp.close();
+ }
+ }
+ mExitStatus = EXIT_DONE;
+ }
+
+ protected boolean setupService() {
+ synchronized(getSynchronizer()) {
+ mThread = Thread.currentThread();
+ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ TAG = mThread.getName();
+ }
+ // Make sure account and mailbox are always the latest from the database
+ mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
+ if (mAccount == null) return false;
+ mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId);
+ if (mMailbox == null) return false;
+ HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
+ if (ha == null) return false;
+ mHostAddress = ha.mAddress;
+ mUserName = ha.mLogin;
+ mPassword = ha.mPassword;
+
+ try {
+ setConnectionParameters(
+ (ha.mFlags & HostAuth.FLAG_SSL) != 0,
+ (ha.mFlags & HostAuth.FLAG_TRUST_ALL) != 0,
+ ha.mClientCertAlias);
+ } catch (CertificateException e) {
+ userLog("Couldn't retrieve certificate for connection");
+ try {
+ ExchangeService.callback().syncMailboxStatus(mMailboxId,
+ EmailServiceStatus.CLIENT_CERTIFICATE_ERROR, 0);
+ } catch (RemoteException e1) {
+ // Don't care if this fails.
+ }
+ return false;
+ }
+
+ // Set up our protocol version from the Account
+ mProtocolVersion = mAccount.mProtocolVersion;
+ // If it hasn't been set up, start with default version
+ if (mProtocolVersion == null) {
+ mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
+ }
+ mProtocolVersionDouble = Eas.getProtocolVersionDouble(mProtocolVersion);
+
+ // Do checks to address historical policy sets.
+ Policy policy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
+ if ((policy != null) && policy.mRequireEncryptionExternal) {
+ // External storage encryption is not supported at this time. In a previous release,
+ // prior to the system supporting true removable storage on Honeycomb, we accepted
+ // this since we emulated external storage on partitions that could be encrypted.
+ // If that was set before, we must clear it out now that the system supports true
+ // removable storage (which can't be encrypted).
+ resetSecurityPolicies();
+ }
+ return true;
+ }
+
+ /**
+ * Clears out the security policies associated with the account, forcing a provision error
+ * and a re-sync of the policy information for the account.
+ */
+ @SuppressWarnings("deprecation")
+ void resetSecurityPolicies() {
+ ContentValues cv = new ContentValues();
+ cv.put(AccountColumns.SECURITY_FLAGS, 0);
+ cv.putNull(AccountColumns.SECURITY_SYNC_KEY);
+ long accountId = mAccount.mId;
+ mContentResolver.update(ContentUris.withAppendedId(
+ Account.CONTENT_URI, accountId), cv, null, null);
+ }
+
+ @Override
+ public void run() {
+ try {
+ // Make sure account and mailbox are still valid
+ if (!setupService()) return;
+ // If we've been stopped, we're done
+ if (mStop) return;
+
+ // Whether or not we're the account mailbox
+ try {
+ mDeviceId = ExchangeService.getDeviceId(mContext);
+ int trafficFlags = TrafficFlags.getSyncFlags(mContext, mAccount);
+ if ((mMailbox == null) || (mAccount == null)) {
+ return;
+ } else {
+ AbstractSyncAdapter target;
+ if (mMailbox.mType == Mailbox.TYPE_CONTACTS) {
+ TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_CONTACTS);
+ target = new ContactsSyncAdapter( this);
+ } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) {
+ TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_CALENDAR);
+ target = new CalendarSyncAdapter(this);
+ } else {
+ TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_EMAIL);
+ target = new EmailSyncAdapter(this);
+ }
+ // We loop because someone might have put a request in while we were syncing
+ // and we've missed that opportunity...
+ do {
+ if (mRequestTime != 0) {
+ userLog("Looping for user request...");
+ mRequestTime = 0;
+ }
+ String syncKey = target.getSyncKey();
+ if (mSyncReason >= ExchangeService.SYNC_CALLBACK_START ||
+ "0".equals(syncKey)) {
+ try {
+ ExchangeService.callback().syncMailboxStatus(mMailboxId,
+ EmailServiceStatus.IN_PROGRESS, 0);
+ } catch (RemoteException e1) {
+ // Don't care if this fails
+ }
+ }
+ 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);
+ mExitStatus = EXIT_IO_ERROR;
+ } catch (Exception e) {
+ userLog("Uncaught exception in EasSyncService", e);
+ } finally {
+ int status;
+ ExchangeService.done(this);
+ if (!mStop) {
+ userLog("Sync finished");
+ 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;
+ break;
+ case EXIT_SECURITY_FAILURE:
+ status = EmailServiceStatus.SECURITY_FAILURE;
+ // Ask for a new folder list. This should wake up the account mailbox; a
+ // security error in account mailbox should start provisioning
+ ExchangeService.reloadFolderList(mContext, mAccount.mId, true);
+ break;
+ case EXIT_ACCESS_DENIED:
+ status = EmailServiceStatus.ACCESS_DENIED;
+ break;
+ default:
+ status = EmailServiceStatus.REMOTE_EXCEPTION;
+ errorLog("Sync ended due to an exception.");
+ break;
+ }
+ } else {
+ userLog("Stopped sync finished.");
+ status = EmailServiceStatus.SUCCESS;
+ }
+
+ // Send a callback (doesn't matter how the sync was started)
+ try {
+ // Unless the user specifically asked for a sync, we don't want to report
+ // connection issues, as they are likely to be transient. In this case, we
+ // simply report success, so that the progress indicator terminates without
+ // putting up an error banner
+ if (mSyncReason != ExchangeService.SYNC_UI_REQUEST &&
+ status == EmailServiceStatus.CONNECTION_ERROR) {
+ status = EmailServiceStatus.SUCCESS;
+ }
+ ExchangeService.callback().syncMailboxStatus(mMailboxId, status, 0);
+ } catch (RemoteException e1) {
+ // Don't care if this fails
+ }
+
+ // Make sure ExchangeService knows about this
+ ExchangeService.kick("sync finished");
+ }
+ } catch (ProviderUnavailableException e) {
+ Log.e(TAG, "EmailProvider unavailable; sync ended prematurely");
+ }
+ }
+}
diff --git a/exchange2/src/com/android/exchange/EmailSyncAdapterService.java b/exchange2/src/com/android/exchange/EmailSyncAdapterService.java
new file mode 100644
index 0000000..f91ee13
--- /dev/null
+++ b/exchange2/src/com/android/exchange/EmailSyncAdapterService.java
@@ -0,0 +1,121 @@
+/*
+ * 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.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.AccountColumns;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.Mailbox;
+
+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.util.Log;
+
+public class EmailSyncAdapterService extends Service {
+ private static final String TAG = "EAS EmailSyncAdapterService";
+ private static SyncAdapterImpl sSyncAdapter = null;
+ private static final Object sSyncAdapterLock = new Object();
+
+ private static final String[] ID_PROJECTION = new String[] {EmailContent.RECORD_ID};
+ private static final String ACCOUNT_AND_TYPE_INBOX =
+ MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_INBOX;
+
+ public EmailSyncAdapterService() {
+ 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 {
+ EmailSyncAdapterService.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 ExchangeService to start an
+ * inbox sync when we get the signal from the system SyncManager.
+ */
+ private static void performSync(Context context, Account account, Bundle extras,
+ String authority, ContentProviderClient provider, SyncResult syncResult)
+ throws OperationCanceledException {
+ ContentResolver cr = context.getContentResolver();
+ Log.i(TAG, "performSync");
+
+ // Find the (EmailProvider) account associated with this email address
+ Cursor accountCursor =
+ cr.query(com.android.emailcommon.provider.Account.CONTENT_URI,
+ ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?", new String[] {account.name},
+ null);
+ try {
+ if (accountCursor.moveToFirst()) {
+ long accountId = accountCursor.getLong(0);
+ // Now, find the inbox associated with the account
+ Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, ID_PROJECTION,
+ ACCOUNT_AND_TYPE_INBOX, new String[] {Long.toString(accountId)}, null);
+ try {
+ if (mailboxCursor.moveToFirst()) {
+ Log.i(TAG, "Mail sync requested for " + account.name);
+ // Ask for a sync from our sync manager
+ ExchangeService.serviceRequest(mailboxCursor.getLong(0),
+ ExchangeService.SYNC_KICK);
+ }
+ } finally {
+ mailboxCursor.close();
+ }
+ }
+ } finally {
+ accountCursor.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/exchange2/src/com/android/exchange/EmailSyncAlarmReceiver.java b/exchange2/src/com/android/exchange/EmailSyncAlarmReceiver.java
new file mode 100644
index 0000000..8006016
--- /dev/null
+++ b/exchange2/src/com/android/exchange/EmailSyncAlarmReceiver.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.util.Log;
+
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.EmailContent.MessageColumns;
+import com.android.emailcommon.provider.ProviderUnavailableException;
+
+import java.util.ArrayList;
+
+/**
+ * EmailSyncAlarmReceiver (USAR) is used by the SyncManager to start up-syncs of user-modified data
+ * back to the Exchange server.
+ *
+ * Here's how this works for Email, for example:
+ *
+ * 1) User modifies or deletes an email from the UI.
+ * 2) SyncManager, which has a ContentObserver watching the Message class, is alerted to a change
+ * 3) SyncManager sets an alarm (to be received by USAR) for a few seconds in the
+ * future (currently 15), the delay preventing excess syncing (think of it as a debounce mechanism).
+ * 4) ESAR Receiver's onReceive method is called
+ * 5) ESAR goes through all change and deletion records and compiles a list of mailboxes which have
+ * changes to be uploaded.
+ * 6) ESAR calls SyncManager to start syncs of those mailboxes
+ *
+ * If EmailProvider isn't available, the upsyncs will happen the next time ExchangeService starts
+ *
+ */
+public class EmailSyncAlarmReceiver extends BroadcastReceiver {
+ final String[] MAILBOX_DATA_PROJECTION = {MessageColumns.MAILBOX_KEY};
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ 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;
+
+ // Get a selector for EAS accounts (we don't want to sync on changes to POP/IMAP messages)
+ String selector = ExchangeService.getEasAccountSelector();
+
+ try {
+ // Find all of the deletions
+ Cursor c = cr.query(Message.DELETED_CONTENT_URI, MAILBOX_DATA_PROJECTION, selector,
+ null, null);
+ if (c == null) throw new ProviderUnavailableException();
+ try {
+ // Keep track of which mailboxes to notify; we'll only notify each one once
+ while (c.moveToNext()) {
+ messageCount++;
+ long mailboxId = c.getLong(0);
+ if (!mailboxesToNotify.contains(mailboxId)) {
+ mailboxesToNotify.add(mailboxId);
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ // Now, find changed messages
+ c = cr.query(Message.UPDATED_CONTENT_URI, MAILBOX_DATA_PROJECTION, selector,
+ null, null);
+ if (c == null) throw new ProviderUnavailableException();
+ try {
+ // Keep track of which mailboxes to notify; we'll only notify each one once
+ while (c.moveToNext()) {
+ messageCount++;
+ long mailboxId = c.getLong(0);
+ if (!mailboxesToNotify.contains(mailboxId)) {
+ mailboxesToNotify.add(mailboxId);
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ // Request service from the mailbox
+ for (Long mailboxId: mailboxesToNotify) {
+ ExchangeService.serviceRequest(mailboxId, ExchangeService.SYNC_UPSYNC);
+ }
+ } catch (ProviderUnavailableException e) {
+ Log.e("EmailSyncAlarmReceiver", "EmailProvider unavailable; aborting alarm receiver");
+ }
+ }
+}
diff --git a/exchange2/src/com/android/exchange/Exchange.java b/exchange2/src/com/android/exchange/Exchange.java
new file mode 100644
index 0000000..496e1f5
--- /dev/null
+++ b/exchange2/src/com/android/exchange/Exchange.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import android.app.Application;
+
+public class Exchange extends Application {
+ // TODO Investigate whether this class is needed
+}
diff --git a/exchange2/src/com/android/exchange/ExchangeService.java b/exchange2/src/com/android/exchange/ExchangeService.java
new file mode 100644
index 0000000..0107d55
--- /dev/null
+++ b/exchange2/src/com/android/exchange/ExchangeService.java
@@ -0,0 +1,2685 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.State;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.Process;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.provider.CalendarContract;
+import android.provider.CalendarContract.Calendars;
+import android.provider.CalendarContract.Events;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import com.android.emailcommon.Api;
+import com.android.emailcommon.TempDirectory;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.Attachment;
+import com.android.emailcommon.provider.EmailContent.Body;
+import com.android.emailcommon.provider.EmailContent.BodyColumns;
+import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.EmailContent.SyncColumns;
+import com.android.emailcommon.provider.HostAuth;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.provider.Policy;
+import com.android.emailcommon.provider.ProviderUnavailableException;
+import com.android.emailcommon.service.AccountServiceProxy;
+import com.android.emailcommon.service.EmailServiceProxy;
+import com.android.emailcommon.service.EmailServiceStatus;
+import com.android.emailcommon.service.IEmailService;
+import com.android.emailcommon.service.IEmailServiceCallback;
+import com.android.emailcommon.service.PolicyServiceProxy;
+import com.android.emailcommon.service.SearchParams;
+import com.android.emailcommon.utility.EmailAsyncTask;
+import com.android.emailcommon.utility.EmailClientConnectionManager;
+import com.android.emailcommon.utility.Utility;
+import com.android.exchange.adapter.CalendarSyncAdapter;
+import com.android.exchange.adapter.ContactsSyncAdapter;
+import com.android.exchange.adapter.Search;
+import com.android.exchange.provider.MailboxUtilities;
+import com.android.exchange.utility.FileLogger;
+
+import org.apache.http.conn.params.ConnManagerPNames;
+import org.apache.http.conn.params.ConnPerRoute;
+import org.apache.http.conn.routing.HttpRoute;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpParams;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * The ExchangeService handles all aspects of starting, maintaining, and stopping the various sync
+ * adapters used by Exchange. However, it is capable of handing any kind of email sync, and it
+ * would be appropriate to use for IMAP push, when that functionality is added to the Email
+ * application.
+ *
+ * The Email application communicates with EAS sync adapters via ExchangeService's binder interface,
+ * which exposes UI-related functionality to the application (see the definitions below)
+ *
+ * ExchangeService uses ContentObservers to detect changes to accounts, mailboxes, and messages in
+ * order to maintain proper 2-way syncing of data. (More documentation to follow)
+ *
+ */
+public class ExchangeService extends Service implements Runnable {
+
+ private static final String TAG = "ExchangeService";
+
+ // The ExchangeService's mailbox "id"
+ public static final int EXTRA_MAILBOX_ID = -1;
+ public static final int EXCHANGE_SERVICE_MAILBOX_ID = 0;
+
+ private static final int SECONDS = 1000;
+ private static final int MINUTES = 60*SECONDS;
+ private static final int ONE_DAY_MINUTES = 1440;
+
+ private static final int EXCHANGE_SERVICE_HEARTBEAT_TIME = 15*MINUTES;
+ private static final int CONNECTIVITY_WAIT_TIME = 10*MINUTES;
+
+ // Sync hold constants for services with transient errors
+ private static final int HOLD_DELAY_MAXIMUM = 4*MINUTES;
+
+ // Reason codes when ExchangeService.kick is called (mainly for debugging)
+ // UI has changed data, requiring an upsync of changes
+ public static final int SYNC_UPSYNC = 0;
+ // A scheduled sync (when not using push)
+ public static final int SYNC_SCHEDULED = 1;
+ // Mailbox was marked push
+ public static final int SYNC_PUSH = 2;
+ // A ping (EAS push signal) was received
+ public static final int SYNC_PING = 3;
+ // Misc.
+ public static final int SYNC_KICK = 4;
+ // A part request (attachment load, for now) was sent to ExchangeService
+ public static final int SYNC_SERVICE_PART_REQUEST = 5;
+
+ // Requests >= SYNC_CALLBACK_START generate callbacks to the UI
+ public static final int SYNC_CALLBACK_START = 6;
+ // startSync was requested of ExchangeService (other than due to user request)
+ public static final int SYNC_SERVICE_START_SYNC = SYNC_CALLBACK_START + 0;
+ // startSync was requested of ExchangeService (due to user request)
+ public static final int SYNC_UI_REQUEST = SYNC_CALLBACK_START + 1;
+
+ private static final String WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX =
+ MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.TYPE + "!=" +
+ Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + " and " + MailboxColumns.SYNC_INTERVAL +
+ " IN (" + Mailbox.CHECK_INTERVAL_PING + ',' + Mailbox.CHECK_INTERVAL_PUSH + ')';
+ protected static final String WHERE_IN_ACCOUNT_AND_PUSHABLE =
+ MailboxColumns.ACCOUNT_KEY + "=? and type in (" + Mailbox.TYPE_INBOX + ','
+ + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + ',' + Mailbox.TYPE_CONTACTS + ','
+ + Mailbox.TYPE_CALENDAR + ')';
+ protected static final String WHERE_IN_ACCOUNT_AND_TYPE_INBOX =
+ MailboxColumns.ACCOUNT_KEY + "=? and type = " + Mailbox.TYPE_INBOX ;
+ private static final String WHERE_MAILBOX_KEY = Message.MAILBOX_KEY + "=?";
+ private static final String WHERE_PROTOCOL_EAS = HostAuthColumns.PROTOCOL + "=\"" +
+ AbstractSyncService.EAS_PROTOCOL + "\"";
+ private static final String WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN =
+ "(" + MailboxColumns.TYPE + '=' + Mailbox.TYPE_OUTBOX
+ + " or " + MailboxColumns.SYNC_INTERVAL + "!=" + Mailbox.CHECK_INTERVAL_NEVER + ')'
+ + " and " + MailboxColumns.ACCOUNT_KEY + " in (";
+ private static final String ACCOUNT_KEY_IN = MailboxColumns.ACCOUNT_KEY + " in (";
+ private static final String WHERE_CALENDAR_ID = Events.CALENDAR_ID + "=?";
+
+ // Offsets into the syncStatus data for EAS that indicate type, exit status, and change count
+ // The format is S<type_char>:<exit_char>:<change_count>
+ public static final int STATUS_TYPE_CHAR = 1;
+ public static final int STATUS_EXIT_CHAR = 3;
+ public static final int STATUS_CHANGE_COUNT_OFFSET = 5;
+
+ // Ready for ping
+ public static final int PING_STATUS_OK = 0;
+ // Service already running (can't ping)
+ public static final int PING_STATUS_RUNNING = 1;
+ // Service waiting after I/O error (can't ping)
+ public static final int PING_STATUS_WAITING = 2;
+ // Service had a fatal error; can't run
+ public static final int PING_STATUS_UNABLE = 3;
+ // Service is disabled by user (checkbox)
+ public static final int PING_STATUS_DISABLED = 4;
+
+ private static final int MAX_CLIENT_CONNECTION_MANAGER_SHUTDOWNS = 1;
+
+ // We synchronize on this for all actions affecting the service and error maps
+ private static final Object sSyncLock = new Object();
+ // All threads can use this lock to wait for connectivity
+ public static final Object sConnectivityLock = new Object();
+ public static boolean sConnectivityHold = false;
+
+ // Keeps track of running services (by mailbox id)
+ private final HashMap<Long, AbstractSyncService> mServiceMap =
+ new HashMap<Long, AbstractSyncService>();
+ // Keeps track of services whose last sync ended with an error (by mailbox id)
+ /*package*/ ConcurrentHashMap<Long, SyncError> mSyncErrorMap =
+ new ConcurrentHashMap<Long, SyncError>();
+ // Keeps track of which services require a wake lock (by mailbox id)
+ private final HashMap<Long, Boolean> mWakeLocks = new HashMap<Long, Boolean>();
+ // Keeps track of PendingIntents for mailbox alarms (by mailbox id)
+ private final HashMap<Long, PendingIntent> mPendingIntents = new HashMap<Long, PendingIntent>();
+ // The actual WakeLock obtained by ExchangeService
+ private WakeLock mWakeLock = null;
+ // Keep our cached list of active Accounts here
+ public final AccountList mAccountList = new AccountList();
+
+ // Observers that we use to look for changed mail-related data
+ private final Handler mHandler = new Handler();
+ private AccountObserver mAccountObserver;
+ private MailboxObserver mMailboxObserver;
+ private SyncedMessageObserver mSyncedMessageObserver;
+
+ // Concurrent because CalendarSyncAdapter can modify the map during a wipe
+ private final ConcurrentHashMap<Long, CalendarObserver> mCalendarObservers =
+ new ConcurrentHashMap<Long, CalendarObserver>();
+
+ private ContentResolver mResolver;
+
+ // The singleton ExchangeService object, with its thread and stop flag
+ protected static ExchangeService INSTANCE;
+ private static Thread sServiceThread = null;
+ // Cached unique device id
+ private static String sDeviceId = null;
+ // ConnectionManager that all EAS threads can use
+ private static EmailClientConnectionManager sClientConnectionManager = null;
+ // Count of ClientConnectionManager shutdowns
+ private static volatile int sClientConnectionManagerShutdownCount = 0;
+
+ private static volatile boolean sStartingUp = false;
+ private static volatile boolean sStop = false;
+
+ // The reason for ExchangeService's next wakeup call
+ private String mNextWaitReason;
+ // Whether we have an unsatisfied "kick" pending
+ private boolean mKicked = false;
+
+ // Receiver of connectivity broadcasts
+ private ConnectivityReceiver mConnectivityReceiver = null;
+ private ConnectivityReceiver mBackgroundDataSettingReceiver = null;
+ private volatile boolean mBackgroundData = true;
+ // The most current NetworkInfo (from ConnectivityManager)
+ private NetworkInfo mNetworkInfo;
+
+ // Callbacks as set up via setCallback
+ private final RemoteCallbackList<IEmailServiceCallback> mCallbackList =
+ new RemoteCallbackList<IEmailServiceCallback>();
+
+ private interface ServiceCallbackWrapper {
+ public void call(IEmailServiceCallback cb) throws RemoteException;
+ }
+
+ /**
+ * Proxy that can be used by various sync adapters to tie into ExchangeService's callback system
+ * Used this way: ExchangeService.callback().callbackMethod(args...);
+ * The proxy wraps checking for existence of a ExchangeService instance
+ * Failures of these callbacks can be safely ignored.
+ */
+ static private final IEmailServiceCallback.Stub sCallbackProxy =
+ new IEmailServiceCallback.Stub() {
+
+ /**
+ * Broadcast a callback to the everyone that's registered
+ *
+ * @param wrapper the ServiceCallbackWrapper used in the broadcast
+ */
+ private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) {
+ RemoteCallbackList<IEmailServiceCallback> callbackList =
+ (INSTANCE == null) ? null: INSTANCE.mCallbackList;
+ if (callbackList != null) {
+ // Call everyone on our callback list
+ int count = callbackList.beginBroadcast();
+ try {
+ for (int i = 0; i < count; i++) {
+ try {
+ wrapper.call(callbackList.getBroadcastItem(i));
+ } catch (RemoteException e) {
+ // Safe to ignore
+ } catch (RuntimeException e) {
+ // We don't want an exception in one call to prevent other calls, so
+ // we'll just log this and continue
+ Log.e(TAG, "Caught RuntimeException in broadcast", e);
+ }
+ }
+ } finally {
+ // No matter what, we need to finish the broadcast
+ callbackList.finishBroadcast();
+ }
+ }
+ }
+
+ @Override
+ public void loadAttachmentStatus(final long messageId, final long attachmentId,
+ final int status, final int progress) {
+ broadcastCallback(new ServiceCallbackWrapper() {
+ @Override
+ public void call(IEmailServiceCallback cb) throws RemoteException {
+ cb.loadAttachmentStatus(messageId, attachmentId, status, progress);
+ }
+ });
+ }
+
+ @Override
+ public void sendMessageStatus(final long messageId, final String subject, final int status,
+ final int progress) {
+ broadcastCallback(new ServiceCallbackWrapper() {
+ @Override
+ public void call(IEmailServiceCallback cb) throws RemoteException {
+ cb.sendMessageStatus(messageId, subject, status, progress);
+ }
+ });
+ }
+
+ @Override
+ public void syncMailboxListStatus(final long accountId, final int status,
+ final int progress) {
+ broadcastCallback(new ServiceCallbackWrapper() {
+ @Override
+ public void call(IEmailServiceCallback cb) throws RemoteException {
+ cb.syncMailboxListStatus(accountId, status, progress);
+ }
+ });
+ }
+
+ @Override
+ public void syncMailboxStatus(final long mailboxId, final int status,
+ final int progress) {
+ broadcastCallback(new ServiceCallbackWrapper() {
+ @Override
+ public void call(IEmailServiceCallback cb) throws RemoteException {
+ cb.syncMailboxStatus(mailboxId, status, progress);
+ }
+ });
+ }
+
+ @Override
+ public void loadMessageStatus(long messageId, int statusCode, int progress)
+ throws RemoteException {
+ }
+ };
+
+ /**
+ * Create our EmailService implementation here.
+ */
+ private final IEmailService.Stub mBinder = new IEmailService.Stub() {
+
+ @Override
+ public int getApiLevel() {
+ return Api.LEVEL;
+ }
+
+ @Override
+ public Bundle validate(HostAuth hostAuth) throws RemoteException {
+ return AbstractSyncService.validate(EasSyncService.class,
+ hostAuth, ExchangeService.this);
+ }
+
+ @Override
+ public Bundle autoDiscover(String userName, String password) throws RemoteException {
+ return new EasSyncService().tryAutodiscover(userName, password);
+ }
+
+ @Override
+ public void startSync(long mailboxId, boolean userRequest) throws RemoteException {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return;
+ checkExchangeServiceServiceRunning();
+ Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
+ if (m == null) return;
+ Account acct = Account.restoreAccountWithId(exchangeService, m.mAccountKey);
+ if (acct == null) return;
+ // If this is a user request and we're being held, release the hold; this allows us to
+ // try again (the hold might have been specific to this account and released already)
+ if (userRequest) {
+ if (onSyncDisabledHold(acct)) {
+ releaseSyncHolds(exchangeService, AbstractSyncService.EXIT_ACCESS_DENIED, acct);
+ log("User requested sync of account in sync disabled hold; releasing");
+ } else if (onSecurityHold(acct)) {
+ releaseSyncHolds(exchangeService, AbstractSyncService.EXIT_SECURITY_FAILURE,
+ acct);
+ log("User requested sync of account in security hold; releasing");
+ }
+ if (sConnectivityHold) {
+ try {
+ // UI is expecting the callbacks....
+ sCallbackProxy.syncMailboxStatus(mailboxId, EmailServiceStatus.IN_PROGRESS,
+ 0);
+ sCallbackProxy.syncMailboxStatus(mailboxId,
+ EmailServiceStatus.CONNECTION_ERROR, 0);
+ } catch (RemoteException ignore) {
+ }
+ return;
+ }
+ }
+ if (m.mType == Mailbox.TYPE_OUTBOX) {
+ // We're using SERVER_ID to indicate an error condition (it has no other use for
+ // sent mail) Upon request to sync the Outbox, we clear this so that all messages
+ // are candidates for sending.
+ ContentValues cv = new ContentValues();
+ cv.put(SyncColumns.SERVER_ID, 0);
+ exchangeService.getContentResolver().update(Message.CONTENT_URI,
+ cv, WHERE_MAILBOX_KEY, new String[] {Long.toString(mailboxId)});
+ // Clear the error state; the Outbox sync will be started from checkMailboxes
+ exchangeService.mSyncErrorMap.remove(mailboxId);
+ kick("start outbox");
+ // Outbox can't be synced in EAS
+ return;
+ } else if (!isSyncable(m)) {
+ try {
+ // UI may be expecting the callbacks, so send them
+ sCallbackProxy.syncMailboxStatus(mailboxId, EmailServiceStatus.IN_PROGRESS, 0);
+ sCallbackProxy.syncMailboxStatus(mailboxId, EmailServiceStatus.SUCCESS, 0);
+ } catch (RemoteException ignore) {
+ // We tried
+ }
+ return;
+ }
+ startManualSync(mailboxId, userRequest ? ExchangeService.SYNC_UI_REQUEST :
+ ExchangeService.SYNC_SERVICE_START_SYNC, null);
+ }
+
+ @Override
+ public void stopSync(long mailboxId) throws RemoteException {
+ stopManualSync(mailboxId);
+ }
+
+ @Override
+ public void loadAttachment(long attachmentId, boolean background) throws RemoteException {
+ Attachment att = Attachment.restoreAttachmentWithId(ExchangeService.this, attachmentId);
+ log("loadAttachment " + attachmentId + ": " + att.mFileName);
+ sendMessageRequest(new PartRequest(att, null, null));
+ }
+
+ @Override
+ public void updateFolderList(long accountId) throws RemoteException {
+ reloadFolderList(ExchangeService.this, accountId, false);
+ }
+
+ @Override
+ public void hostChanged(long accountId) throws RemoteException {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return;
+ ConcurrentHashMap<Long, SyncError> syncErrorMap = exchangeService.mSyncErrorMap;
+ // Go through the various error mailboxes
+ for (long mailboxId: syncErrorMap.keySet()) {
+ SyncError error = syncErrorMap.get(mailboxId);
+ // If it's a login failure, look a little harder
+ Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
+ // If it's for the account whose host has changed, clear the error
+ // If the mailbox is no longer around, remove the entry in the map
+ if (m == null) {
+ syncErrorMap.remove(mailboxId);
+ } else if (error != null && m.mAccountKey == accountId) {
+ error.fatal = false;
+ error.holdEndTime = 0;
+ }
+ }
+ // Stop any running syncs
+ exchangeService.stopAccountSyncs(accountId, true);
+ // Kick ExchangeService
+ kick("host changed");
+ }
+
+ @Override
+ public void setLogging(int flags) throws RemoteException {
+ Eas.setUserDebug(flags);
+ }
+
+ @Override
+ public void sendMeetingResponse(long messageId, int response) throws RemoteException {
+ sendMessageRequest(new MeetingResponseRequest(messageId, response));
+ }
+
+ @Override
+ public void loadMore(long messageId) throws RemoteException {
+ }
+
+ // The following three methods are not implemented in this version
+ @Override
+ public boolean createFolder(long accountId, String name) throws RemoteException {
+ return false;
+ }
+
+ @Override
+ public boolean deleteFolder(long accountId, String name) throws RemoteException {
+ return false;
+ }
+
+ @Override
+ public boolean renameFolder(long accountId, String oldName, String newName)
+ throws RemoteException {
+ return false;
+ }
+
+ @Override
+ public void setCallback(IEmailServiceCallback cb) throws RemoteException {
+ mCallbackList.register(cb);
+ }
+
+ /**
+ * Delete PIM (calendar, contacts) data for the specified account
+ *
+ * @param accountId the account whose data should be deleted
+ * @throws RemoteException
+ */
+ @Override
+ public void deleteAccountPIMData(long accountId) throws RemoteException {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return;
+ // Stop any running syncs
+ ExchangeService.stopAccountSyncs(accountId);
+ // Delete the data
+ ExchangeService.deleteAccountPIMData(accountId);
+ long accountMailboxId = Mailbox.findMailboxOfType(exchangeService, accountId,
+ Mailbox.TYPE_EAS_ACCOUNT_MAILBOX);
+ if (accountMailboxId != Mailbox.NO_MAILBOX) {
+ // Make sure the account mailbox is held due to security
+ synchronized(sSyncLock) {
+ mSyncErrorMap.put(accountMailboxId, exchangeService.new SyncError(
+ AbstractSyncService.EXIT_SECURITY_FAILURE, false));
+
+ }
+ }
+ // Make sure the reconciler runs
+ runAccountReconcilerSync(ExchangeService.this);
+ }
+
+ @Override
+ public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return 0;
+ return Search.searchMessages(exchangeService, accountId, searchParams,
+ destMailboxId);
+ }
+
+ @Override
+ public void sendMail(long accountId) throws RemoteException {
+ }
+ };
+
+ /**
+ * Return a list of all Accounts in EmailProvider. Because the result of this call may be used
+ * in account reconciliation, an exception is thrown if the result cannot be guaranteed accurate
+ * @param context the caller's context
+ * @param accounts a list that Accounts will be added into
+ * @return the list of Accounts
+ * @throws ProviderUnavailableException if the list of Accounts cannot be guaranteed valid
+ */
+ private static AccountList collectEasAccounts(Context context, AccountList accounts) {
+ ContentResolver resolver = context.getContentResolver();
+ Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null,
+ null);
+ // We must throw here; callers might use the information we provide for reconciliation, etc.
+ if (c == null) throw new ProviderUnavailableException();
+ try {
+ ContentValues cv = new ContentValues();
+ while (c.moveToNext()) {
+ long hostAuthId = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
+ if (hostAuthId > 0) {
+ HostAuth ha = HostAuth.restoreHostAuthWithId(context, hostAuthId);
+ if (ha != null && ha.mProtocol.equals("eas")) {
+ Account account = new Account();
+ account.restore(c);
+ // Cache the HostAuth
+ account.mHostAuthRecv = ha;
+ accounts.add(account);
+ // Fixup flags for inbox (should accept moved mail)
+ Mailbox inbox = Mailbox.restoreMailboxOfType(context, account.mId,
+ Mailbox.TYPE_INBOX);
+ if (inbox != null &&
+ ((inbox.mFlags & Mailbox.FLAG_ACCEPTS_MOVED_MAIL) == 0)) {
+ cv.put(MailboxColumns.FLAGS,
+ inbox.mFlags | Mailbox.FLAG_ACCEPTS_MOVED_MAIL);
+ resolver.update(
+ ContentUris.withAppendedId(Mailbox.CONTENT_URI, inbox.mId), cv,
+ null, null);
+ }
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ return accounts;
+ }
+
+ static class AccountList extends ArrayList<Account> {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public boolean add(Account account) {
+ // Cache the account manager account
+ account.mAmAccount = new android.accounts.Account(account.mEmailAddress,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ super.add(account);
+ return true;
+ }
+
+ public boolean contains(long id) {
+ for (Account account : this) {
+ if (account.mId == id) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public Account getById(long id) {
+ for (Account account : this) {
+ if (account.mId == id) {
+ return account;
+ }
+ }
+ return null;
+ }
+
+ public Account getByName(String accountName) {
+ for (Account account : this) {
+ if (account.mEmailAddress.equalsIgnoreCase(accountName)) {
+ return account;
+ }
+ }
+ return null;
+ }
+ }
+
+ public static void deleteAccountPIMData(long accountId) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return;
+ Mailbox mailbox =
+ Mailbox.restoreMailboxOfType(exchangeService, accountId, Mailbox.TYPE_CONTACTS);
+ if (mailbox != null) {
+ EasSyncService service = EasSyncService.getServiceForMailbox(exchangeService, mailbox);
+ ContactsSyncAdapter adapter = new ContactsSyncAdapter(service);
+ adapter.wipe();
+ }
+ mailbox =
+ Mailbox.restoreMailboxOfType(exchangeService, accountId, Mailbox.TYPE_CALENDAR);
+ if (mailbox != null) {
+ EasSyncService service = EasSyncService.getServiceForMailbox(exchangeService, mailbox);
+ CalendarSyncAdapter adapter = new CalendarSyncAdapter(service);
+ adapter.wipe();
+ }
+ }
+
+ private boolean onSecurityHold(Account account) {
+ return (account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0;
+ }
+
+ private boolean onSyncDisabledHold(Account account) {
+ return (account.mFlags & Account.FLAGS_SYNC_DISABLED) != 0;
+ }
+
+ class AccountObserver extends ContentObserver {
+ String mSyncableEasMailboxSelector = null;
+ String mEasAccountSelector = null;
+
+ // Runs when ExchangeService first starts
+ public AccountObserver(Handler handler) {
+ super(handler);
+ // At startup, we want to see what EAS accounts exist and cache them
+ // TODO: Move database work out of UI thread
+ Context context = getContext();
+ synchronized (mAccountList) {
+ try {
+ collectEasAccounts(context, mAccountList);
+ } catch (ProviderUnavailableException e) {
+ // Just leave if EmailProvider is unavailable
+ return;
+ }
+ // Create an account mailbox for any account without one
+ for (Account account : mAccountList) {
+ int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, "accountKey="
+ + account.mId, null);
+ if (cnt == 0) {
+ // This case handles a newly created account
+ addAccountMailbox(account.mId);
+ }
+ }
+ }
+ // Run through accounts and update account hold information
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mAccountList) {
+ for (Account account : mAccountList) {
+ if (onSecurityHold(account)) {
+ // If we're in a security hold, and our policies are active, release
+ // the hold
+ if (PolicyServiceProxy.isActive(ExchangeService.this, null)) {
+ PolicyServiceProxy.setAccountHoldFlag(ExchangeService.this,
+ account, false);
+ log("isActive true; release hold for " + account.mDisplayName);
+ }
+ }
+ }
+ }
+ }});
+ }
+
+ /**
+ * Returns a String suitable for appending to a where clause that selects for all syncable
+ * mailboxes in all eas accounts
+ * @return a complex selection string that is not to be cached
+ */
+ public String getSyncableEasMailboxWhere() {
+ if (mSyncableEasMailboxSelector == null) {
+ StringBuilder sb = new StringBuilder(WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN);
+ boolean first = true;
+ synchronized (mAccountList) {
+ for (Account account : mAccountList) {
+ if (!first) {
+ sb.append(',');
+ } else {
+ first = false;
+ }
+ sb.append(account.mId);
+ }
+ }
+ sb.append(')');
+ mSyncableEasMailboxSelector = sb.toString();
+ }
+ return mSyncableEasMailboxSelector;
+ }
+
+ /**
+ * Returns a String suitable for appending to a where clause that selects for all eas
+ * accounts.
+ * @return a String in the form "accountKey in (a, b, c...)" that is not to be cached
+ */
+ public String getAccountKeyWhere() {
+ if (mEasAccountSelector == null) {
+ StringBuilder sb = new StringBuilder(ACCOUNT_KEY_IN);
+ boolean first = true;
+ synchronized (mAccountList) {
+ for (Account account : mAccountList) {
+ if (!first) {
+ sb.append(',');
+ } else {
+ first = false;
+ }
+ sb.append(account.mId);
+ }
+ }
+ sb.append(')');
+ mEasAccountSelector = sb.toString();
+ }
+ return mEasAccountSelector;
+ }
+
+ private void onAccountChanged() {
+ try {
+ maybeStartExchangeServiceThread();
+ Context context = getContext();
+
+ // A change to the list requires us to scan for deletions (stop running syncs)
+ // At startup, we want to see what accounts exist and cache them
+ AccountList currentAccounts = new AccountList();
+ try {
+ collectEasAccounts(context, currentAccounts);
+ } catch (ProviderUnavailableException e) {
+ // Just leave if EmailProvider is unavailable
+ return;
+ }
+ synchronized (mAccountList) {
+ for (Account account : mAccountList) {
+ boolean accountIncomplete =
+ (account.mFlags & Account.FLAGS_INCOMPLETE) != 0;
+ // If the current list doesn't include this account and the account wasn't
+ // incomplete, then this is a deletion
+ if (!currentAccounts.contains(account.mId) && !accountIncomplete) {
+ // The implication is that the account has been deleted; let's find out
+ alwaysLog("Observer found deleted account: " + account.mDisplayName);
+ // Run the reconciler (the reconciliation itself runs in the Email app)
+ runAccountReconcilerSync(ExchangeService.this);
+ // See if the account is still around
+ Account deletedAccount =
+ Account.restoreAccountWithId(context, account.mId);
+ if (deletedAccount != null) {
+ // It is; add it to our account list
+ alwaysLog("Account still in provider: " + account.mDisplayName);
+ currentAccounts.add(account);
+ } else {
+ // It isn't; stop syncs and clear our selectors
+ alwaysLog("Account deletion confirmed: " + account.mDisplayName);
+ stopAccountSyncs(account.mId, true);
+ mSyncableEasMailboxSelector = null;
+ mEasAccountSelector = null;
+ }
+ } else {
+ // Get the newest version of this account
+ Account updatedAccount =
+ Account.restoreAccountWithId(context, account.mId);
+ if (updatedAccount == null) continue;
+ if (account.mSyncInterval != updatedAccount.mSyncInterval
+ || account.mSyncLookback != updatedAccount.mSyncLookback) {
+ // Set the inbox interval to the interval of the Account
+ // This setting should NOT affect other boxes
+ ContentValues cv = new ContentValues();
+ cv.put(MailboxColumns.SYNC_INTERVAL, updatedAccount.mSyncInterval);
+ getContentResolver().update(Mailbox.CONTENT_URI, cv,
+ WHERE_IN_ACCOUNT_AND_TYPE_INBOX, new String[] {
+ Long.toString(account.mId)
+ });
+ // Stop all current syncs; the appropriate ones will restart
+ log("Account " + account.mDisplayName + " changed; stop syncs");
+ stopAccountSyncs(account.mId, true);
+ }
+
+ // See if this account is no longer on security hold
+ if (onSecurityHold(account) && !onSecurityHold(updatedAccount)) {
+ releaseSyncHolds(ExchangeService.this,
+ AbstractSyncService.EXIT_SECURITY_FAILURE, account);
+ }
+
+ // Put current values into our cached account
+ account.mSyncInterval = updatedAccount.mSyncInterval;
+ account.mSyncLookback = updatedAccount.mSyncLookback;
+ account.mFlags = updatedAccount.mFlags;
+ }
+ }
+ // Look for new accounts
+ for (Account account : currentAccounts) {
+ if (!mAccountList.contains(account.mId)) {
+ // Don't forget to cache the HostAuth
+ HostAuth ha = HostAuth.restoreHostAuthWithId(getContext(),
+ account.mHostAuthKeyRecv);
+ if (ha == null) continue;
+ account.mHostAuthRecv = ha;
+ // This is an addition; create our magic hidden mailbox...
+ log("Account observer found new account: " + account.mDisplayName);
+ addAccountMailbox(account.mId);
+ mAccountList.add(account);
+ mSyncableEasMailboxSelector = null;
+ mEasAccountSelector = null;
+ }
+ }
+ // Finally, make sure our account list is up to date
+ mAccountList.clear();
+ mAccountList.addAll(currentAccounts);
+ }
+
+ // See if there's anything to do...
+ kick("account changed");
+ } catch (ProviderUnavailableException e) {
+ alwaysLog("Observer failed; provider unavailable");
+ }
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ onAccountChanged();
+ }}, "Account Observer").start();
+ }
+
+ private void addAccountMailbox(long acctId) {
+ Account acct = Account.restoreAccountWithId(getContext(), acctId);
+ Mailbox main = new Mailbox();
+ main.mDisplayName = Eas.ACCOUNT_MAILBOX_PREFIX;
+ main.mServerId = Eas.ACCOUNT_MAILBOX_PREFIX + System.nanoTime();
+ main.mAccountKey = acct.mId;
+ main.mType = Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
+ main.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH;
+ main.mFlagVisible = false;
+ main.save(getContext());
+ log("Initializing account: " + acct.mDisplayName);
+ }
+
+ }
+
+ /**
+ * Register a specific Calendar's data observer; we need to recognize when the SYNC_EVENTS
+ * column has changed (when sync has turned off or on)
+ * @param account the Account whose Calendar we're observing
+ */
+ private void registerCalendarObserver(Account account) {
+ // Get a new observer
+ CalendarObserver observer = new CalendarObserver(mHandler, account);
+ if (observer.mCalendarId != 0) {
+ // If we find the Calendar (and we'd better) register it and store it in the map
+ mCalendarObservers.put(account.mId, observer);
+ mResolver.registerContentObserver(
+ ContentUris.withAppendedId(Calendars.CONTENT_URI, observer.mCalendarId), false,
+ observer);
+ }
+ }
+
+ /**
+ * Unregister all CalendarObserver's
+ */
+ static public void unregisterCalendarObservers() {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return;
+ ContentResolver resolver = exchangeService.mResolver;
+ for (CalendarObserver observer: exchangeService.mCalendarObservers.values()) {
+ resolver.unregisterContentObserver(observer);
+ }
+ exchangeService.mCalendarObservers.clear();
+ }
+
+ /**
+ * Return the syncable state of an account's calendar, as determined by the sync_events column
+ * of our Calendar (from CalendarProvider2)
+ * Note that the current state of sync_events is cached in our CalendarObserver
+ * @param accountId the id of the account whose calendar we are checking
+ * @return whether or not syncing of events is enabled
+ */
+ private boolean isCalendarEnabled(long accountId) {
+ CalendarObserver observer = mCalendarObservers.get(accountId);
+ if (observer != null) {
+ return (observer.mSyncEvents == 1);
+ }
+ // If there's no observer, there's no Calendar in CalendarProvider2, so we return true
+ // to allow Calendar creation
+ return true;
+ }
+
+ private class CalendarObserver extends ContentObserver {
+ long mAccountId;
+ long mCalendarId;
+ long mSyncEvents;
+ String mAccountName;
+
+ public CalendarObserver(Handler handler, Account account) {
+ super(handler);
+ mAccountId = account.mId;
+ mAccountName = account.mEmailAddress;
+
+ // Find the Calendar for this account
+ Cursor c = mResolver.query(Calendars.CONTENT_URI,
+ new String[] {Calendars._ID, Calendars.SYNC_EVENTS},
+ CalendarSyncAdapter.CALENDAR_SELECTION,
+ new String[] {account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE},
+ null);
+ if (c != null) {
+ // Save its id and its sync events status
+ try {
+ if (c.moveToFirst()) {
+ mCalendarId = c.getLong(0);
+ mSyncEvents = c.getLong(1);
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ @Override
+ public synchronized void onChange(boolean selfChange) {
+ // See if the user has changed syncing of our calendar
+ if (!selfChange) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Cursor c = mResolver.query(Calendars.CONTENT_URI,
+ new String[] {Calendars.SYNC_EVENTS}, Calendars._ID + "=?",
+ new String[] {Long.toString(mCalendarId)}, null);
+ if (c == null) return;
+ // Get its sync events; if it's changed, we've got work to do
+ try {
+ if (c.moveToFirst()) {
+ long newSyncEvents = c.getLong(0);
+ if (newSyncEvents != mSyncEvents) {
+ log("_sync_events changed for calendar in " + mAccountName);
+ Mailbox mailbox = Mailbox.restoreMailboxOfType(INSTANCE,
+ mAccountId, Mailbox.TYPE_CALENDAR);
+ // Sanity check for mailbox deletion
+ if (mailbox == null) return;
+ ContentValues cv = new ContentValues();
+ if (newSyncEvents == 0) {
+ // When sync is disabled, we're supposed to delete
+ // all events in the calendar
+ log("Deleting events and setting syncKey to 0 for " +
+ mAccountName);
+ // First, stop any sync that's ongoing
+ stopManualSync(mailbox.mId);
+ // Set the syncKey to 0 (reset)
+ EasSyncService service =
+ EasSyncService.getServiceForMailbox(
+ INSTANCE, mailbox);
+ CalendarSyncAdapter adapter =
+ new CalendarSyncAdapter(service);
+ try {
+ adapter.setSyncKey("0", false);
+ } catch (IOException e) {
+ // The provider can't be reached; nothing to be done
+ }
+ // Reset the sync key locally and stop syncing
+ cv.put(Mailbox.SYNC_KEY, "0");
+ cv.put(Mailbox.SYNC_INTERVAL,
+ Mailbox.CHECK_INTERVAL_NEVER);
+ mResolver.update(ContentUris.withAppendedId(
+ Mailbox.CONTENT_URI, mailbox.mId), cv, null,
+ null);
+ // Delete all events using the sync adapter
+ // parameter so that the deletion is only local
+ Uri eventsAsSyncAdapter =
+ CalendarSyncAdapter.asSyncAdapter(
+ Events.CONTENT_URI,
+ mAccountName,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ mResolver.delete(eventsAsSyncAdapter, WHERE_CALENDAR_ID,
+ new String[] {Long.toString(mCalendarId)});
+ } else {
+ // Make this a push mailbox and kick; this will start
+ // a resync of the Calendar; the account mailbox will
+ // ping on this during the next cycle of the ping loop
+ cv.put(Mailbox.SYNC_INTERVAL,
+ Mailbox.CHECK_INTERVAL_PUSH);
+ mResolver.update(ContentUris.withAppendedId(
+ Mailbox.CONTENT_URI, mailbox.mId), cv, null,
+ null);
+ kick("calendar sync changed");
+ }
+
+ // Save away the new value
+ mSyncEvents = newSyncEvents;
+ }
+ }
+ } finally {
+ c.close();
+ }
+ } catch (ProviderUnavailableException e) {
+ Log.w(TAG, "Observer failed; provider unavailable");
+ }
+ }}, "Calendar Observer").start();
+ }
+ }
+ }
+
+ private class MailboxObserver extends ContentObserver {
+ public MailboxObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ // See if there's anything to do...
+ if (!selfChange) {
+ kick("mailbox changed");
+ }
+ }
+ }
+
+ private class SyncedMessageObserver extends ContentObserver {
+ Intent syncAlarmIntent = new Intent(INSTANCE, EmailSyncAlarmReceiver.class);
+ PendingIntent syncAlarmPendingIntent =
+ PendingIntent.getBroadcast(INSTANCE, 0, syncAlarmIntent, 0);
+ AlarmManager alarmManager = (AlarmManager)INSTANCE.getSystemService(Context.ALARM_SERVICE);
+
+ public SyncedMessageObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ alarmManager.set(AlarmManager.RTC_WAKEUP,
+ System.currentTimeMillis() + 10*SECONDS, syncAlarmPendingIntent);
+ }
+ }
+
+ static public IEmailServiceCallback callback() {
+ return sCallbackProxy;
+ }
+
+ static public Account getAccountById(long accountId) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService != null) {
+ AccountList accountList = exchangeService.mAccountList;
+ synchronized (accountList) {
+ return accountList.getById(accountId);
+ }
+ }
+ return null;
+ }
+
+ static public Account getAccountByName(String accountName) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService != null) {
+ AccountList accountList = exchangeService.mAccountList;
+ synchronized (accountList) {
+ return accountList.getByName(accountName);
+ }
+ }
+ return null;
+ }
+
+ static public String getEasAccountSelector() {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService != null && exchangeService.mAccountObserver != null) {
+ return exchangeService.mAccountObserver.getAccountKeyWhere();
+ }
+ return null;
+ }
+
+ public class SyncStatus {
+ static public final int NOT_RUNNING = 0;
+ static public final int DIED = 1;
+ static public final int SYNC = 2;
+ static public final int IDLE = 3;
+ }
+
+ /*package*/ class SyncError {
+ int reason;
+ boolean fatal = false;
+ long holdDelay = 15*SECONDS;
+ long holdEndTime = System.currentTimeMillis() + holdDelay;
+
+ SyncError(int _reason, boolean _fatal) {
+ reason = _reason;
+ fatal = _fatal;
+ }
+
+ /**
+ * We double the holdDelay from 15 seconds through 4 mins
+ */
+ void escalate() {
+ if (holdDelay < HOLD_DELAY_MAXIMUM) {
+ holdDelay *= 2;
+ }
+ holdEndTime = System.currentTimeMillis() + holdDelay;
+ }
+ }
+
+ private void logSyncHolds() {
+ if (Eas.USER_LOG) {
+ log("Sync holds:");
+ long time = System.currentTimeMillis();
+ for (long mailboxId : mSyncErrorMap.keySet()) {
+ Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
+ if (m == null) {
+ log("Mailbox " + mailboxId + " no longer exists");
+ } else {
+ SyncError error = mSyncErrorMap.get(mailboxId);
+ if (error != null) {
+ log("Mailbox " + m.mDisplayName + ", error = " + error.reason
+ + ", fatal = " + error.fatal);
+ if (error.holdEndTime > 0) {
+ log("Hold ends in " + ((error.holdEndTime - time) / 1000) + "s");
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Release security holds for the specified account
+ * @param account the account whose Mailboxes should be released from security hold
+ */
+ static public void releaseSecurityHold(Account account) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService != null) {
+ exchangeService.releaseSyncHolds(INSTANCE, AbstractSyncService.EXIT_SECURITY_FAILURE,
+ account);
+ }
+ }
+
+ /**
+ * Release a specific type of hold (the reason) for the specified Account; if the account
+ * is null, mailboxes from all accounts with the specified hold will be released
+ * @param reason the reason for the SyncError (AbstractSyncService.EXIT_XXX)
+ * @param account an Account whose mailboxes should be released (or all if null)
+ * @return whether or not any mailboxes were released
+ */
+ /*package*/ boolean releaseSyncHolds(Context context, int reason, Account account) {
+ boolean holdWasReleased = releaseSyncHoldsImpl(context, reason, account);
+ kick("security release");
+ return holdWasReleased;
+ }
+
+ private boolean releaseSyncHoldsImpl(Context context, int reason, Account account) {
+ boolean holdWasReleased = false;
+ for (long mailboxId: mSyncErrorMap.keySet()) {
+ if (account != null) {
+ Mailbox m = Mailbox.restoreMailboxWithId(context, mailboxId);
+ if (m == null) {
+ mSyncErrorMap.remove(mailboxId);
+ } else if (m.mAccountKey != account.mId) {
+ continue;
+ }
+ }
+ SyncError error = mSyncErrorMap.get(mailboxId);
+ if (error != null && error.reason == reason) {
+ mSyncErrorMap.remove(mailboxId);
+ holdWasReleased = true;
+ }
+ }
+ return holdWasReleased;
+ }
+
+ /**
+ * Reconcile Exchange accounts with AccountManager (asynchronous)
+ * @param context the caller's Context
+ */
+ public static void reconcileAccounts(final Context context) {
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService != null) {
+ exchangeService.runAccountReconcilerSync(context);
+ }
+ }});
+ }
+
+ /**
+ * Blocking call to the account reconciler
+ */
+ public static void runAccountReconcilerSync(Context context) {
+ alwaysLog("Reconciling accounts...");
+ new AccountServiceProxy(context).reconcileAccounts(
+ HostAuth.SCHEME_EAS, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ }
+
+ public static void log(String str) {
+ log(TAG, str);
+ }
+
+ public static void log(String tag, String str) {
+ if (Eas.USER_LOG) {
+ Log.d(tag, str);
+ if (Eas.FILE_LOG) {
+ FileLogger.log(tag, str);
+ }
+ }
+ }
+
+ public static void alwaysLog(String str) {
+ if (!Eas.USER_LOG) {
+ Log.d(TAG, str);
+ } else {
+ log(str);
+ }
+ }
+
+ /**
+ * EAS requires a unique device id, so that sync is possible from a variety of different
+ * devices (e.g. the syncKey is specific to a device) If we're on an emulator or some other
+ * device that doesn't provide one, we can create it as "device".
+ * This would work on a real device as well, but it would be better to use the "real" id if
+ * it's available
+ */
+ static public String getDeviceId(Context context) throws IOException {
+ if (sDeviceId == null) {
+ sDeviceId = new AccountServiceProxy(context).getDeviceId();
+ alwaysLog("Received deviceId from Email app: " + sDeviceId);
+ }
+ return sDeviceId;
+ }
+
+ @Override
+ public IBinder onBind(Intent arg0) {
+ return mBinder;
+ }
+
+ static public ConnPerRoute sConnPerRoute = new ConnPerRoute() {
+ @Override
+ public int getMaxForRoute(HttpRoute route) {
+ return 8;
+ }
+ };
+
+ static public synchronized EmailClientConnectionManager getClientConnectionManager() {
+ if (sClientConnectionManager == null) {
+ // After two tries, kill the process. Most likely, this will happen in the background
+ // The service will restart itself after about 5 seconds
+ if (sClientConnectionManagerShutdownCount > MAX_CLIENT_CONNECTION_MANAGER_SHUTDOWNS) {
+ alwaysLog("Shutting down process to unblock threads");
+ Process.killProcess(Process.myPid());
+ }
+ HttpParams params = new BasicHttpParams();
+ params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 25);
+ params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, sConnPerRoute);
+ sClientConnectionManager = EmailClientConnectionManager.newInstance(params);
+ }
+ // Null is a valid return result if we get an exception
+ return sClientConnectionManager;
+ }
+
+ static private synchronized void shutdownConnectionManager() {
+ if (sClientConnectionManager != null) {
+ log("Shutting down ClientConnectionManager");
+ sClientConnectionManager.shutdown();
+ sClientConnectionManagerShutdownCount++;
+ sClientConnectionManager = null;
+ }
+ }
+
+ public static void stopAccountSyncs(long acctId) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService != null) {
+ exchangeService.stopAccountSyncs(acctId, true);
+ }
+ }
+
+ private void stopAccountSyncs(long acctId, boolean includeAccountMailbox) {
+ synchronized (sSyncLock) {
+ List<Long> deletedBoxes = new ArrayList<Long>();
+ for (Long mid : mServiceMap.keySet()) {
+ Mailbox box = Mailbox.restoreMailboxWithId(this, mid);
+ if (box != null) {
+ if (box.mAccountKey == acctId) {
+ if (!includeAccountMailbox &&
+ box.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
+ AbstractSyncService svc = mServiceMap.get(mid);
+ if (svc != null) {
+ svc.stop();
+ }
+ continue;
+ }
+ AbstractSyncService svc = mServiceMap.get(mid);
+ if (svc != null) {
+ svc.stop();
+ Thread t = svc.mThread;
+ if (t != null) {
+ t.interrupt();
+ }
+ }
+ deletedBoxes.add(mid);
+ }
+ }
+ }
+ for (Long mid : deletedBoxes) {
+ releaseMailbox(mid);
+ }
+ }
+ }
+
+ static private void reloadFolderListFailed(long accountId) {
+ try {
+ callback().syncMailboxListStatus(accountId,
+ EmailServiceStatus.ACCOUNT_UNINITIALIZED, 0);
+ } catch (RemoteException e1) {
+ // Don't care if this fails
+ }
+ }
+
+ static public void reloadFolderList(Context context, long accountId, boolean force) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return;
+ Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
+ Mailbox.CONTENT_PROJECTION, MailboxColumns.ACCOUNT_KEY + "=? AND " +
+ MailboxColumns.TYPE + "=?",
+ new String[] {Long.toString(accountId),
+ Long.toString(Mailbox.TYPE_EAS_ACCOUNT_MAILBOX)}, null);
+ try {
+ if (c.moveToFirst()) {
+ synchronized(sSyncLock) {
+ Mailbox mailbox = new Mailbox();
+ mailbox.restore(c);
+ Account acct = Account.restoreAccountWithId(context, accountId);
+ if (acct == null) {
+ reloadFolderListFailed(accountId);
+ return;
+ }
+ String syncKey = acct.mSyncKey;
+ // No need to reload the list if we don't have one
+ if (!force && (syncKey == null || syncKey.equals("0"))) {
+ reloadFolderListFailed(accountId);
+ return;
+ }
+
+ // Change all ping/push boxes to push/hold
+ ContentValues cv = new ContentValues();
+ cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH_HOLD);
+ context.getContentResolver().update(Mailbox.CONTENT_URI, cv,
+ WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX,
+ new String[] {Long.toString(accountId)});
+ log("Set push/ping boxes to push/hold");
+
+ long id = mailbox.mId;
+ AbstractSyncService svc = exchangeService.mServiceMap.get(id);
+ // Tell the service we're done
+ if (svc != null) {
+ synchronized (svc.getSynchronizer()) {
+ svc.stop();
+ // Interrupt the thread so that it can stop
+ Thread thread = svc.mThread;
+ if (thread != null) {
+ thread.setName(thread.getName() + " (Stopped)");
+ thread.interrupt();
+ }
+ }
+ // Abandon the service
+ exchangeService.releaseMailbox(id);
+ // And have it start naturally
+ kick("reload folder list");
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Informs ExchangeService that an account has a new folder list; as a result, any existing
+ * folder might have become invalid. Therefore, we act as if the account has been deleted, and
+ * then we reinitialize it.
+ *
+ * @param acctId
+ */
+ static public void stopNonAccountMailboxSyncsForAccount(long acctId) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService != null) {
+ exchangeService.stopAccountSyncs(acctId, false);
+ kick("reload folder list");
+ }
+ }
+
+ private void acquireWakeLock(long id) {
+ synchronized (mWakeLocks) {
+ Boolean lock = mWakeLocks.get(id);
+ if (lock == null) {
+ if (mWakeLock == null) {
+ PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MAIL_SERVICE");
+ mWakeLock.acquire();
+ //log("+WAKE LOCK ACQUIRED");
+ }
+ mWakeLocks.put(id, true);
+ }
+ }
+ }
+
+ private void releaseWakeLock(long id) {
+ synchronized (mWakeLocks) {
+ Boolean lock = mWakeLocks.get(id);
+ if (lock != null) {
+ mWakeLocks.remove(id);
+ if (mWakeLocks.isEmpty()) {
+ if (mWakeLock != null) {
+ mWakeLock.release();
+ }
+ mWakeLock = null;
+ //log("+WAKE LOCK RELEASED");
+ } else {
+ }
+ }
+ }
+ }
+
+ static public String alarmOwner(long id) {
+ if (id == EXTRA_MAILBOX_ID) {
+ return "ExchangeService";
+ } else {
+ String name = Long.toString(id);
+ if (Eas.USER_LOG && INSTANCE != null) {
+ Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, id);
+ if (m != null) {
+ name = m.mDisplayName + '(' + m.mAccountKey + ')';
+ }
+ }
+ return "Mailbox " + name;
+ }
+ }
+
+ private void clearAlarm(long id) {
+ synchronized (mPendingIntents) {
+ PendingIntent pi = mPendingIntents.get(id);
+ if (pi != null) {
+ AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+ alarmManager.cancel(pi);
+ //log("+Alarm cleared for " + alarmOwner(id));
+ mPendingIntents.remove(id);
+ }
+ }
+ }
+
+ private void setAlarm(long id, long millis) {
+ synchronized (mPendingIntents) {
+ PendingIntent pi = mPendingIntents.get(id);
+ if (pi == null) {
+ Intent i = new Intent(this, MailboxAlarmReceiver.class);
+ i.putExtra("mailbox", id);
+ i.setData(Uri.parse("Box" + id));
+ pi = PendingIntent.getBroadcast(this, 0, i, 0);
+ mPendingIntents.put(id, pi);
+
+ AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+ alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + millis, pi);
+ //log("+Alarm set for " + alarmOwner(id) + ", " + millis/1000 + "s");
+ }
+ }
+ }
+
+ private void clearAlarms() {
+ AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+ synchronized (mPendingIntents) {
+ for (PendingIntent pi : mPendingIntents.values()) {
+ alarmManager.cancel(pi);
+ }
+ mPendingIntents.clear();
+ }
+ }
+
+ static public void runAwake(long id) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService != null) {
+ exchangeService.acquireWakeLock(id);
+ exchangeService.clearAlarm(id);
+ }
+ }
+
+ static public void runAsleep(long id, long millis) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService != null) {
+ exchangeService.setAlarm(id, millis);
+ exchangeService.releaseWakeLock(id);
+ }
+ }
+
+ static public void clearWatchdogAlarm(long id) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService != null) {
+ exchangeService.clearAlarm(id);
+ }
+ }
+
+ static public void setWatchdogAlarm(long id, long millis) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService != null) {
+ exchangeService.setAlarm(id, millis);
+ }
+ }
+
+ static public void alert(Context context, final long id) {
+ final ExchangeService exchangeService = INSTANCE;
+ checkExchangeServiceServiceRunning();
+ if (id < 0) {
+ log("ExchangeService alert");
+ kick("ping ExchangeService");
+ } else if (exchangeService == null) {
+ context.startService(new Intent(context, ExchangeService.class));
+ } else {
+ final AbstractSyncService service = exchangeService.mServiceMap.get(id);
+ if (service != null) {
+ // Handle alerts in a background thread, as we are typically called from a
+ // broadcast receiver, and are therefore running in the UI thread
+ String threadName = "ExchangeService Alert: ";
+ if (service.mMailbox != null) {
+ threadName += service.mMailbox.mDisplayName;
+ }
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, id);
+ if (m != null) {
+ // We ignore drafts completely (doesn't sync). Changes in Outbox are
+ // handled in the checkMailboxes loop, so we can ignore these pings.
+ if (Eas.USER_LOG) {
+ Log.d(TAG, "Alert for mailbox " + id + " (" + m.mDisplayName + ")");
+ }
+ 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);
+ service.mMailbox = m;
+ // Send the alarm to the sync service
+ if (!service.alarm()) {
+ // A false return means that we were forced to interrupt the thread
+ // In this case, we release the mailbox so that we can start another
+ // thread to do the work
+ log("Alarm failed; releasing mailbox");
+ synchronized(sSyncLock) {
+ exchangeService.releaseMailbox(id);
+ }
+ // Shutdown the connection manager; this should close all of our
+ // sockets and generate IOExceptions all around.
+ ExchangeService.shutdownConnectionManager();
+ }
+ }
+ }}, threadName).start();
+ }
+ }
+ }
+
+ public class ConnectivityReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+ Bundle b = intent.getExtras();
+ if (b != null) {
+ NetworkInfo a = (NetworkInfo)b.get(ConnectivityManager.EXTRA_NETWORK_INFO);
+ String info = "Connectivity alert for " + a.getTypeName();
+ State state = a.getState();
+ if (state == State.CONNECTED) {
+ info += " CONNECTED";
+ log(info);
+ synchronized (sConnectivityLock) {
+ sConnectivityLock.notifyAll();
+ }
+ kick("connected");
+ } else if (state == State.DISCONNECTED) {
+ info += " DISCONNECTED";
+ log(info);
+ kick("disconnected");
+ }
+ }
+ } else if (intent.getAction().equals(
+ ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED)) {
+ ConnectivityManager cm =
+ (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
+ mBackgroundData = cm.getBackgroundDataSetting();
+ // If background data is now on, we want to kick ExchangeService
+ if (mBackgroundData) {
+ kick("background data on");
+ log("Background data on; restart syncs");
+ // Otherwise, stop all syncs
+ } else {
+ log("Background data off: stop all syncs");
+ EmailAsyncTask.runAsyncParallel(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mAccountList) {
+ for (Account account : mAccountList)
+ ExchangeService.stopAccountSyncs(account.mId);
+ }
+ }});
+ }
+ }
+ }
+ }
+
+ /**
+ * Starts a service thread and enters it into the service map
+ * This is the point of instantiation of all sync threads
+ * @param service the service to start
+ * @param m the Mailbox on which the service will operate
+ */
+ private void startServiceThread(AbstractSyncService service) {
+ synchronized (sSyncLock) {
+ Mailbox mailbox = service.mMailbox;
+ String mailboxName = mailbox.mDisplayName;
+ String accountName = service.mAccount.mDisplayName;
+ Thread thread = new Thread(service, mailboxName + "[" + accountName + "]");
+ log("Starting thread for " + mailboxName + " in account " + accountName);
+ thread.start();
+ mServiceMap.put(mailbox.mId, service);
+ runAwake(mailbox.mId);
+ if (mailbox.mServerId != null && mailbox.mType != Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
+ stopPing(mailbox.mAccountKey);
+ }
+ }
+ }
+
+ /**
+ * Stop any ping in progress for the given account
+ * @param accountId
+ */
+ private void stopPing(long accountId) {
+ // Go through our active mailboxes looking for the right one
+ synchronized (sSyncLock) {
+ for (long mailboxId: mServiceMap.keySet()) {
+ Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
+ if (m != null) {
+ String serverId = m.mServerId;
+ if (m.mAccountKey == accountId && serverId != null &&
+ m.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
+ // Here's our account mailbox; reset him (stopping pings)
+ AbstractSyncService svc = mServiceMap.get(mailboxId);
+ svc.reset();
+ }
+ }
+ }
+ }
+ }
+
+ private void requestSync(Mailbox m, int reason, Request req) {
+ int syncStatus = EmailContent.SYNC_STATUS_BACKGROUND;
+ // Don't sync if there's no connectivity
+ if (sConnectivityHold || (m == null) || sStop) {
+ if (reason >= SYNC_CALLBACK_START) {
+ try {
+ sCallbackProxy.syncMailboxStatus(m.mId, EmailServiceStatus.CONNECTION_ERROR, 0);
+ } catch (RemoteException e) {
+ // We tried...
+ }
+ }
+ return;
+ }
+ synchronized (sSyncLock) {
+ Account acct = Account.restoreAccountWithId(this, m.mAccountKey);
+ if (acct != null) {
+ // Always make sure there's not a running instance of this service
+ AbstractSyncService service = mServiceMap.get(m.mId);
+ if (service == null) {
+ service = EasSyncService.getServiceForMailbox(this, m);
+ if (!((EasSyncService)service).mIsValid) return;
+ service.mSyncReason = reason;
+ if (req != null) {
+ service.addRequest(req);
+ }
+ startServiceThread(service);
+ if (reason >= SYNC_CALLBACK_START) {
+ syncStatus = EmailContent.SYNC_STATUS_USER;
+ }
+ setMailboxSyncStatus(m.mId, syncStatus);
+ }
+ }
+ }
+ }
+
+ private void setMailboxSyncStatus(long id, int status) {
+ ContentValues values = new ContentValues();
+ values.put(Mailbox.UI_SYNC_STATUS, status);
+ mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), values, null, null);
+ }
+
+ private void setMailboxLastSyncResult(long id, int result) {
+ ContentValues values = new ContentValues();
+ values.put(Mailbox.UI_LAST_SYNC_RESULT, result);
+ mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), values, null, null);
+ }
+
+ private void stopServiceThreads() {
+ synchronized (sSyncLock) {
+ ArrayList<Long> toStop = new ArrayList<Long>();
+
+ // Keep track of which services to stop
+ for (Long mailboxId : mServiceMap.keySet()) {
+ toStop.add(mailboxId);
+ }
+
+ // Shut down all of those running services
+ for (Long mailboxId : toStop) {
+ AbstractSyncService svc = mServiceMap.get(mailboxId);
+ if (svc != null) {
+ log("Stopping " + svc.mAccount.mDisplayName + '/' + svc.mMailbox.mDisplayName);
+ svc.stop();
+ if (svc.mThread != null) {
+ svc.mThread.interrupt();
+ }
+ }
+ releaseWakeLock(mailboxId);
+ }
+ }
+ }
+
+ private void waitForConnectivity() {
+ boolean waiting = false;
+ ConnectivityManager cm =
+ (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
+ while (!sStop) {
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ if (info != null) {
+ mNetworkInfo = info;
+ // We're done if there's an active network
+ if (waiting) {
+ // If we've been waiting, release any I/O error holds
+ releaseSyncHolds(this, AbstractSyncService.EXIT_IO_ERROR, null);
+ // And log what's still being held
+ logSyncHolds();
+ }
+ return;
+ } else {
+ // If this is our first time through the loop, shut down running service threads
+ if (!waiting) {
+ waiting = true;
+ stopServiceThreads();
+ }
+ // Wait until a network is connected (or 10 mins), but let the device sleep
+ // We'll set an alarm just in case we don't get notified (bugs happen)
+ synchronized (sConnectivityLock) {
+ runAsleep(EXTRA_MAILBOX_ID, CONNECTIVITY_WAIT_TIME+5*SECONDS);
+ try {
+ log("Connectivity lock...");
+ sConnectivityHold = true;
+ sConnectivityLock.wait(CONNECTIVITY_WAIT_TIME);
+ log("Connectivity lock released...");
+ } catch (InterruptedException e) {
+ // This is fine; we just go around the loop again
+ } finally {
+ sConnectivityHold = false;
+ }
+ runAwake(EXTRA_MAILBOX_ID);
+ }
+ }
+ }
+ }
+
+ /**
+ * Note that there are two ways the EAS ExchangeService service can be created:
+ *
+ * 1) as a background service instantiated via startService (which happens on boot, when the
+ * first EAS account is created, etc), in which case the service thread is spun up, mailboxes
+ * sync, etc. and
+ * 2) to execute an RPC call from the UI, in which case the background service will already be
+ * running most of the time (unless we're creating a first EAS account)
+ *
+ * If the running background service detects that there are no EAS accounts (on boot, if none
+ * were created, or afterward if the last remaining EAS account is deleted), it will call
+ * stopSelf() to terminate operation.
+ *
+ * The goal is to ensure that the background service is running at all times when there is at
+ * least one EAS account in existence
+ *
+ * Because there are edge cases in which our process can crash (typically, this has been seen
+ * in UI crashes, ANR's, etc.), it's possible for the UI to start up again without the
+ * background service having been started. We explicitly try to start the service in Welcome
+ * (to handle the case of the app having been reloaded). We also start the service on any
+ * startSync call (if it isn't already running)
+ */
+ @Override
+ public void onCreate() {
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ // Quick checks first, before getting the lock
+ if (sStartingUp) return;
+ synchronized (sSyncLock) {
+ alwaysLog("!!! EAS ExchangeService, onCreate");
+ // Try to start up properly; we might be coming back from a crash that the Email
+ // application isn't aware of.
+ startService(new Intent(EmailServiceProxy.EXCHANGE_INTENT));
+ if (sStop) {
+ return;
+ }
+ }
+ }});
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ alwaysLog("!!! EAS ExchangeService, onStartCommand, startingUp = " + sStartingUp +
+ ", running = " + (INSTANCE != null));
+ if (!sStartingUp && INSTANCE == null) {
+ sStartingUp = true;
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ synchronized (sSyncLock) {
+ // ExchangeService cannot start unless we can connect to AccountService
+ if (!new AccountServiceProxy(ExchangeService.this).test()) {
+ alwaysLog("!!! Email application not found; stopping self");
+ stopSelf();
+ }
+ if (sDeviceId == null) {
+ try {
+ String deviceId = getDeviceId(ExchangeService.this);
+ if (deviceId != null) {
+ sDeviceId = deviceId;
+ }
+ } catch (IOException e) {
+ }
+ if (sDeviceId == null) {
+ alwaysLog("!!! deviceId unknown; stopping self and retrying");
+ stopSelf();
+ // Try to restart ourselves in a few seconds
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ Thread.sleep(5000);
+ } catch (InterruptedException e) {
+ }
+ startService(new Intent(
+ EmailServiceProxy.EXCHANGE_INTENT));
+ }});
+ return;
+ }
+ }
+ // Run the reconciler and clean up mismatched accounts - if we weren't
+ // running when accounts were deleted, it won't have been called.
+ runAccountReconcilerSync(ExchangeService.this);
+ // Update other services depending on final account configuration
+ maybeStartExchangeServiceThread();
+ if (sServiceThread == null) {
+ log("!!! EAS ExchangeService, stopping self");
+ stopSelf();
+ } else if (sStop) {
+ // If we were trying to stop, attempt a restart in 5 secs
+ setAlarm(EXCHANGE_SERVICE_MAILBOX_ID, 5*SECONDS);
+ }
+ }
+ } finally {
+ sStartingUp = false;
+ }
+ }});
+ }
+ return Service.START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ log("!!! EAS ExchangeService, onDestroy");
+ // Handle shutting down off the UI thread
+ Utility.runAsync(new Runnable() {
+ @Override
+ public void run() {
+ // Quick checks first, before getting the lock
+ if (INSTANCE == null || sServiceThread == null) return;
+ synchronized(sSyncLock) {
+ // Stop the sync manager thread and return
+ if (sServiceThread != null) {
+ sStop = true;
+ sServiceThread.interrupt();
+ }
+ }
+ }});
+ }
+
+ void maybeStartExchangeServiceThread() {
+ // Start our thread...
+ // See if there are any EAS accounts; otherwise, just go away
+ if (sServiceThread == null || !sServiceThread.isAlive()) {
+ if (EmailContent.count(this, HostAuth.CONTENT_URI, WHERE_PROTOCOL_EAS, null) > 0) {
+ log(sServiceThread == null ? "Starting thread..." : "Restarting thread...");
+ sServiceThread = new Thread(this, "ExchangeService");
+ INSTANCE = this;
+ sServiceThread.start();
+ }
+ }
+ }
+
+ /**
+ * Start up the ExchangeService service if it's not already running
+ * This is a stopgap for cases in which ExchangeService died (due to a crash somewhere in
+ * com.android.email) and hasn't been restarted. See the comment for onCreate for details
+ */
+ static void checkExchangeServiceServiceRunning() {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return;
+ if (sServiceThread == null) {
+ log("!!! checkExchangeServiceServiceRunning; starting service...");
+ exchangeService.startService(new Intent(exchangeService, ExchangeService.class));
+ }
+ }
+
+ @Override
+ public void run() {
+ sStop = false;
+ alwaysLog("ExchangeService thread running");
+ // If we're really debugging, turn on all logging
+ if (Eas.DEBUG) {
+ Eas.USER_LOG = true;
+ Eas.PARSER_LOG = true;
+ Eas.FILE_LOG = true;
+ }
+
+ TempDirectory.setTempDirectory(this);
+
+ // If we need to wait for the debugger, do so
+ if (Eas.WAIT_DEBUG) {
+ Debug.waitForDebugger();
+ }
+
+ // Synchronize here to prevent a shutdown from happening while we initialize our observers
+ // and receivers
+ synchronized (sSyncLock) {
+ if (INSTANCE != null) {
+ mResolver = getContentResolver();
+
+ // Set up our observers; we need them to know when to start/stop various syncs based
+ // on the insert/delete/update of mailboxes and accounts
+ // We also observe synced messages to trigger upsyncs at the appropriate time
+ mAccountObserver = new AccountObserver(mHandler);
+ mResolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver);
+ mMailboxObserver = new MailboxObserver(mHandler);
+ mResolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver);
+ mSyncedMessageObserver = new SyncedMessageObserver(mHandler);
+ mResolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true,
+ mSyncedMessageObserver);
+
+ // Set up receivers for connectivity and background data setting
+ mConnectivityReceiver = new ConnectivityReceiver();
+ registerReceiver(mConnectivityReceiver, new IntentFilter(
+ ConnectivityManager.CONNECTIVITY_ACTION));
+
+ mBackgroundDataSettingReceiver = new ConnectivityReceiver();
+ registerReceiver(mBackgroundDataSettingReceiver, new IntentFilter(
+ ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED));
+ // Save away the current background data setting; we'll keep track of it with the
+ // receiver we just registered
+ ConnectivityManager cm = (ConnectivityManager)getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ mBackgroundData = cm.getBackgroundDataSetting();
+
+ // Do any required work to clean up our Mailboxes (this serves to upgrade
+ // mailboxes that existed prior to EmailProvider database version 17)
+ MailboxUtilities.fixupUninitializedParentKeys(this, getEasAccountSelector());
+ }
+ }
+
+ try {
+ // Loop indefinitely until we're shut down
+ while (!sStop) {
+ runAwake(EXTRA_MAILBOX_ID);
+ waitForConnectivity();
+ mNextWaitReason = null;
+ long nextWait = checkMailboxes();
+ try {
+ synchronized (this) {
+ if (!mKicked) {
+ if (nextWait < 0) {
+ log("Negative wait? Setting to 1s");
+ nextWait = 1*SECONDS;
+ }
+ if (nextWait > 10*SECONDS) {
+ if (mNextWaitReason != null) {
+ log("Next awake " + nextWait / 1000 + "s: " + mNextWaitReason);
+ }
+ runAsleep(EXTRA_MAILBOX_ID, nextWait + (3*SECONDS));
+ }
+ wait(nextWait);
+ }
+ }
+ } catch (InterruptedException e) {
+ // Needs to be caught, but causes no problem
+ log("ExchangeService interrupted");
+ } finally {
+ synchronized (this) {
+ if (mKicked) {
+ //log("Wait deferred due to kick");
+ mKicked = false;
+ }
+ }
+ }
+ }
+ log("Shutdown requested");
+ } catch (ProviderUnavailableException pue) {
+ // Shutdown cleanly in this case
+ // NOTE: Sync adapters will also crash with this error, but that is already handled
+ // in the adapters themselves, i.e. they return cleanly via done(). When the Email
+ // process starts running again, the Exchange process will be started again in due
+ // course, assuming there is at least one existing EAS account.
+ Log.e(TAG, "EmailProvider unavailable; shutting down");
+ // Ask for our service to be restarted; this should kick-start the Email process as well
+ startService(new Intent(this, ExchangeService.class));
+ } catch (RuntimeException e) {
+ // Crash; this is a completely unexpected runtime error
+ Log.e(TAG, "RuntimeException in ExchangeService", e);
+ throw e;
+ } finally {
+ shutdown();
+ }
+ }
+
+ private void shutdown() {
+ synchronized (sSyncLock) {
+ // If INSTANCE is null, we've already been shut down
+ if (INSTANCE != null) {
+ log("ExchangeService shutting down...");
+
+ // Stop our running syncs
+ stopServiceThreads();
+
+ // Stop receivers
+ if (mConnectivityReceiver != null) {
+ unregisterReceiver(mConnectivityReceiver);
+ }
+ if (mBackgroundDataSettingReceiver != null) {
+ unregisterReceiver(mBackgroundDataSettingReceiver);
+ }
+
+ // Unregister observers
+ ContentResolver resolver = getContentResolver();
+ if (mSyncedMessageObserver != null) {
+ resolver.unregisterContentObserver(mSyncedMessageObserver);
+ mSyncedMessageObserver = null;
+ }
+ if (mAccountObserver != null) {
+ resolver.unregisterContentObserver(mAccountObserver);
+ mAccountObserver = null;
+ }
+ if (mMailboxObserver != null) {
+ resolver.unregisterContentObserver(mMailboxObserver);
+ mMailboxObserver = null;
+ }
+ unregisterCalendarObservers();
+
+ // Clear pending alarms and associated Intents
+ clearAlarms();
+
+ // Release our wake lock, if we have one
+ synchronized (mWakeLocks) {
+ if (mWakeLock != null) {
+ mWakeLock.release();
+ mWakeLock = null;
+ }
+ }
+
+ INSTANCE = null;
+ sServiceThread = null;
+ sStop = false;
+ log("Goodbye");
+ }
+ }
+ }
+
+ /**
+ * Release a mailbox from the service map and release its wake lock.
+ * NOTE: This method MUST be called while holding sSyncLock!
+ *
+ * @param mailboxId the id of the mailbox to be released
+ */
+ private void releaseMailbox(long mailboxId) {
+ mServiceMap.remove(mailboxId);
+ releaseWakeLock(mailboxId);
+ }
+
+ /**
+ * Check whether an Outbox (referenced by a Cursor) has any messages that can be sent
+ * @param c the cursor to an Outbox
+ * @return true if there is mail to be sent
+ */
+ private boolean hasSendableMessages(Cursor outboxCursor) {
+ Cursor c = mResolver.query(Message.CONTENT_URI, Message.ID_COLUMN_PROJECTION,
+ EasOutboxService.MAILBOX_KEY_AND_NOT_SEND_FAILED,
+ new String[] {Long.toString(outboxCursor.getLong(Mailbox.CONTENT_ID_COLUMN))},
+ null);
+ try {
+ while (c.moveToNext()) {
+ if (!Utility.hasUnloadedAttachments(this, c.getLong(Message.CONTENT_ID_COLUMN))) {
+ return true;
+ }
+ }
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Determine whether the account is allowed to sync automatically, as opposed to manually, based
+ * on whether the "require manual sync when roaming" policy is in force and applicable
+ * @param account the account
+ * @return whether or not the account can sync automatically
+ */
+ /*package*/ static boolean canAutoSync(Account account) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) {
+ return false;
+ }
+ NetworkInfo networkInfo = exchangeService.mNetworkInfo;
+
+ // Enforce manual sync only while roaming here
+ long policyKey = account.mPolicyKey;
+ // Quick exit from this check
+ if ((policyKey != 0) && (networkInfo != null) &&
+ (ConnectivityManager.isNetworkTypeMobile(networkInfo.getType()))) {
+ // We'll cache the Policy data here
+ Policy policy = account.mPolicy;
+ if (policy == null) {
+ policy = Policy.restorePolicyWithId(INSTANCE, policyKey);
+ account.mPolicy = policy;
+ if (!PolicyServiceProxy.isActive(exchangeService, policy)) return false;
+ }
+ if (policy != null && policy.mRequireManualSyncWhenRoaming && networkInfo.isRoaming()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Convenience method to determine whether Email sync is enabled for a given account
+ * @param account the Account in question
+ * @return whether Email sync is enabled
+ */
+ private boolean canSyncEmail(android.accounts.Account account) {
+ return ContentResolver.getSyncAutomatically(account, EmailContent.AUTHORITY);
+ }
+
+ /**
+ * Determine whether a mailbox of a given type in a given account can be synced automatically
+ * by ExchangeService. This is an increasingly complex determination, taking into account
+ * security policies and user settings (both within the Email application and in the Settings
+ * application)
+ *
+ * @param account the Account that the mailbox is in
+ * @param type the type of the Mailbox
+ * @return whether or not to start a sync
+ */
+ private boolean isMailboxSyncable(Account account, int type) {
+ // This 'if' statement performs checks to see whether or not a mailbox is a
+ // candidate for syncing based on policies, user settings, & other restrictions
+ if (type == Mailbox.TYPE_OUTBOX) {
+ // Outbox is always syncable
+ return true;
+ } else if (type == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
+ // Always sync EAS mailbox unless master sync is off
+ return ContentResolver.getMasterSyncAutomatically();
+ } else if (type == Mailbox.TYPE_CONTACTS || type == Mailbox.TYPE_CALENDAR) {
+ // Contacts/Calendar obey this setting from ContentResolver
+ if (!ContentResolver.getMasterSyncAutomatically()) {
+ return false;
+ }
+ // Get the right authority for the mailbox
+ String authority;
+ if (type == Mailbox.TYPE_CONTACTS) {
+ authority = ContactsContract.AUTHORITY;
+ } else {
+ authority = CalendarContract.AUTHORITY;
+ if (!mCalendarObservers.containsKey(account.mId)){
+ // Make sure we have an observer for this Calendar, as
+ // we need to be able to detect sync state changes, sigh
+ registerCalendarObserver(account);
+ }
+ }
+ // See if "sync automatically" is set; if not, punt
+ if (!ContentResolver.getSyncAutomatically(account.mAmAccount, authority)) {
+ return false;
+ // See if the calendar is enabled from the Calendar app UI; if not, punt
+ } else if ((type == Mailbox.TYPE_CALENDAR) && !isCalendarEnabled(account.mId)) {
+ return false;
+ }
+ // Never automatically sync trash
+ } else if (type == Mailbox.TYPE_TRASH) {
+ return false;
+ // For non-outbox, non-account mail, we do three checks:
+ // 1) are we restricted by policy (i.e. manual sync only),
+ // 2) has the user checked the "Sync Email" box in Account Settings, and
+ // 3) does the user have the master "background data" box checked in Settings
+ } else if (!canAutoSync(account) || !canSyncEmail(account.mAmAccount) || !mBackgroundData) {
+ return false;
+ }
+ return true;
+ }
+
+ private long checkMailboxes () {
+ // First, see if any running mailboxes have been deleted
+ ArrayList<Long> deletedMailboxes = new ArrayList<Long>();
+ synchronized (sSyncLock) {
+ for (long mailboxId: mServiceMap.keySet()) {
+ Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
+ if (m == null) {
+ deletedMailboxes.add(mailboxId);
+ }
+ }
+ // If so, stop them or remove them from the map
+ for (Long mailboxId: deletedMailboxes) {
+ AbstractSyncService svc = mServiceMap.get(mailboxId);
+ if (svc == null || 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);
+ }
+ }
+ }
+ }
+
+ long nextWait = EXCHANGE_SERVICE_HEARTBEAT_TIME;
+ long now = System.currentTimeMillis();
+
+ // Start up threads that need it; use a query which finds eas mailboxes where the
+ // the sync interval is not "never". This is the set of mailboxes that we control
+ if (mAccountObserver == null) {
+ log("mAccountObserver null; service died??");
+ return nextWait;
+ }
+
+ Cursor c = getContentResolver().query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
+ mAccountObserver.getSyncableEasMailboxWhere(), null, null);
+ if (c == null) throw new ProviderUnavailableException();
+ try {
+ while (c.moveToNext()) {
+ long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
+ AbstractSyncService service = null;
+ synchronized (sSyncLock) {
+ service = mServiceMap.get(mailboxId);
+ }
+ if (service == null) {
+ // Get the cached account
+ Account account = getAccountById(c.getInt(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN));
+ if (account == null) continue;
+
+ // We handle a few types of mailboxes specially
+ int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
+ if (!isMailboxSyncable(account, mailboxType)) {
+ continue;
+ }
+
+ // Check whether we're in a hold (temporary or permanent)
+ SyncError syncError = mSyncErrorMap.get(mailboxId);
+ if (syncError != null) {
+ // Nothing we can do about fatal errors
+ if (syncError.fatal) continue;
+ if (now < syncError.holdEndTime) {
+ // If release time is earlier than next wait time,
+ // move next wait time up to the release time
+ if (syncError.holdEndTime < now + nextWait) {
+ nextWait = syncError.holdEndTime - now;
+ mNextWaitReason = "Release hold";
+ }
+ continue;
+ } else {
+ // Keep the error around, but clear the end time
+ syncError.holdEndTime = 0;
+ }
+ }
+
+ // Otherwise, we use the sync interval
+ long syncInterval = c.getInt(Mailbox.CONTENT_SYNC_INTERVAL_COLUMN);
+ if (syncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
+ Mailbox m = EmailContent.getContent(c, Mailbox.class);
+ requestSync(m, SYNC_PUSH, null);
+ } else if (mailboxType == Mailbox.TYPE_OUTBOX) {
+ if (hasSendableMessages(c)) {
+ Mailbox m = EmailContent.getContent(c, Mailbox.class);
+ startServiceThread(EasSyncService.getServiceForMailbox(this, m));
+ }
+ } else if (syncInterval > 0 && syncInterval <= ONE_DAY_MINUTES) {
+ long lastSync = c.getLong(Mailbox.CONTENT_SYNC_TIME_COLUMN);
+ long sinceLastSync = now - lastSync;
+ long toNextSync = syncInterval*MINUTES - sinceLastSync;
+ String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
+ if (toNextSync <= 0) {
+ Mailbox m = EmailContent.getContent(c, Mailbox.class);
+ requestSync(m, SYNC_SCHEDULED, null);
+ } else if (toNextSync < nextWait) {
+ nextWait = toNextSync;
+ if (Eas.USER_LOG) {
+ log("Next sync for " + name + " in " + nextWait/1000 + "s");
+ }
+ mNextWaitReason = "Scheduled sync, " + name;
+ } else if (Eas.USER_LOG) {
+ log("Next sync for " + name + " in " + toNextSync/1000 + "s");
+ }
+ }
+ } else {
+ Thread thread = service.mThread;
+ // Look for threads that have died and remove them from the map
+ if (thread != null && !thread.isAlive()) {
+ if (Eas.USER_LOG) {
+ log("Dead thread, mailbox released: " +
+ c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN));
+ }
+ releaseMailbox(mailboxId);
+ // Restart this if necessary
+ if (nextWait > 3*SECONDS) {
+ nextWait = 3*SECONDS;
+ mNextWaitReason = "Clean up dead thread(s)";
+ }
+ } else {
+ long requestTime = service.mRequestTime;
+ if (requestTime > 0) {
+ long timeToRequest = requestTime - now;
+ if (timeToRequest <= 0) {
+ service.mRequestTime = 0;
+ service.alarm();
+ } else if (requestTime > 0 && timeToRequest < nextWait) {
+ if (timeToRequest < 11*MINUTES) {
+ nextWait = timeToRequest < 250 ? 250 : timeToRequest;
+ mNextWaitReason = "Sync data change";
+ } else {
+ log("Illegal timeToRequest: " + timeToRequest);
+ }
+ }
+ }
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ return nextWait;
+ }
+
+ static public void serviceRequest(long mailboxId, int reason) {
+ serviceRequest(mailboxId, 5*SECONDS, reason);
+ }
+
+ /**
+ * Return a boolean indicating whether the mailbox can be synced
+ * @param m the mailbox
+ * @return whether or not the mailbox can be synced
+ */
+ public static boolean isSyncable(Mailbox m) {
+ return m.loadsFromServer(HostAuth.SCHEME_EAS);
+ }
+
+ static public void serviceRequest(long mailboxId, long ms, int reason) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return;
+ Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
+ if (m == null || !isSyncable(m)) return;
+ try {
+ AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId);
+ if (service != null) {
+ service.mRequestTime = System.currentTimeMillis() + ms;
+ kick("service request");
+ } else {
+ startManualSync(mailboxId, reason, null);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ static public void serviceRequestImmediate(long mailboxId) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return;
+ AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId);
+ if (service != null) {
+ service.mRequestTime = System.currentTimeMillis();
+ Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
+ if (m != null) {
+ service.mAccount = Account.restoreAccountWithId(exchangeService, m.mAccountKey);
+ service.mMailbox = m;
+ kick("service request immediate");
+ }
+ }
+ }
+
+ static public void sendMessageRequest(Request req) {
+ ExchangeService exchangeService = INSTANCE;
+ Message msg = Message.restoreMessageWithId(exchangeService, req.mMessageId);
+ if (msg == null) return;
+ long mailboxId = msg.mMailboxKey;
+ Mailbox mailbox = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
+ if (mailbox == null) return;
+
+ // If we're loading an attachment for Outbox, we want to look at the source message
+ // to find the loading mailbox
+ if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
+ long sourceId = Utility.getFirstRowLong(exchangeService, Body.CONTENT_URI,
+ new String[] {BodyColumns.SOURCE_MESSAGE_KEY},
+ BodyColumns.MESSAGE_KEY + "=?",
+ new String[] {Long.toString(msg.mId)}, null, 0, -1L);
+ if (sourceId != -1L) {
+ EmailContent.Message sourceMsg =
+ EmailContent.Message.restoreMessageWithId(exchangeService, sourceId);
+ if (sourceMsg != null) {
+ mailboxId = sourceMsg.mMailboxKey;
+ }
+ }
+ }
+
+ AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId);
+ if (service == null) {
+ startManualSync(mailboxId, SYNC_SERVICE_PART_REQUEST, req);
+ kick("part request");
+ } else {
+ service.addRequest(req);
+ }
+ }
+
+ /**
+ * Determine whether a given Mailbox can be synced, i.e. is not already syncing and is not in
+ * an error state
+ *
+ * @param mailboxId
+ * @return whether or not the Mailbox is available for syncing (i.e. is a valid push target)
+ */
+ static public int pingStatus(long mailboxId) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return PING_STATUS_OK;
+ // Already syncing...
+ if (exchangeService.mServiceMap.get(mailboxId) != null) {
+ return PING_STATUS_RUNNING;
+ }
+ // No errors or a transient error, don't ping...
+ SyncError error = exchangeService.mSyncErrorMap.get(mailboxId);
+ if (error != null) {
+ if (error.fatal) {
+ return PING_STATUS_UNABLE;
+ } else if (error.holdEndTime > 0) {
+ return PING_STATUS_WAITING;
+ }
+ }
+ return PING_STATUS_OK;
+ }
+
+ static public void startManualSync(long mailboxId, int reason, Request req) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return;
+ synchronized (sSyncLock) {
+ AbstractSyncService svc = exchangeService.mServiceMap.get(mailboxId);
+ if (svc == null) {
+ exchangeService.mSyncErrorMap.remove(mailboxId);
+ Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
+ if (m != null) {
+ log("Starting sync for " + m.mDisplayName);
+ exchangeService.requestSync(m, reason, req);
+ }
+ } else {
+ // If this is a ui request, set the sync reason for the service
+ if (reason >= SYNC_CALLBACK_START) {
+ svc.mSyncReason = reason;
+ }
+ }
+ }
+ }
+
+ // DO NOT CALL THIS IN A LOOP ON THE SERVICEMAP
+ static public void stopManualSync(long mailboxId) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return;
+ synchronized (sSyncLock) {
+ AbstractSyncService svc = exchangeService.mServiceMap.get(mailboxId);
+ if (svc != null) {
+ log("Stopping sync for " + svc.mMailboxName);
+ svc.stop();
+ svc.mThread.interrupt();
+ exchangeService.releaseWakeLock(mailboxId);
+ }
+ }
+ }
+
+ /**
+ * Wake up ExchangeService to check for mailboxes needing service
+ */
+ static public void kick(String reason) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService != null) {
+ synchronized (exchangeService) {
+ //INSTANCE.log("Kick: " + reason);
+ exchangeService.mKicked = true;
+ exchangeService.notify();
+ }
+ }
+ if (sConnectivityLock != null) {
+ synchronized (sConnectivityLock) {
+ sConnectivityLock.notify();
+ }
+ }
+ }
+
+ static public void accountUpdated(long acctId) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return;
+ synchronized (sSyncLock) {
+ for (AbstractSyncService svc : exchangeService.mServiceMap.values()) {
+ if (svc.mAccount.mId == acctId) {
+ svc.mAccount = Account.restoreAccountWithId(exchangeService, acctId);
+ }
+ }
+ }
+ }
+
+ /**
+ * Tell ExchangeService to remove the mailbox from the map of mailboxes with sync errors
+ * @param mailboxId the id of the mailbox
+ */
+ static public void removeFromSyncErrorMap(long mailboxId) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService != null) {
+ exchangeService.mSyncErrorMap.remove(mailboxId);
+ }
+ }
+
+ private boolean isRunningInServiceThread(long mailboxId) {
+ AbstractSyncService syncService = mServiceMap.get(mailboxId);
+ Thread thisThread = Thread.currentThread();
+ return syncService != null && syncService.mThread != null &&
+ thisThread == syncService.mThread;
+ }
+
+ /**
+ * Sent by services indicating that their thread is finished; action depends on the exitStatus
+ * of the service.
+ *
+ * @param svc the service that is finished
+ */
+ static public void done(AbstractSyncService svc) {
+ ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return;
+ synchronized(sSyncLock) {
+ long mailboxId = svc.mMailboxId;
+ // If we're no longer the syncing thread for the mailbox, just return
+ if (!exchangeService.isRunningInServiceThread(mailboxId)) {
+ return;
+ }
+ exchangeService.releaseMailbox(mailboxId);
+ exchangeService.setMailboxSyncStatus(mailboxId, EmailContent.SYNC_STATUS_NONE);
+
+ ConcurrentHashMap<Long, SyncError> errorMap = exchangeService.mSyncErrorMap;
+ SyncError syncError = errorMap.get(mailboxId);
+
+ int exitStatus = svc.mExitStatus;
+ Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
+ if (m == null) return;
+
+ if (exitStatus != AbstractSyncService.EXIT_LOGIN_FAILURE) {
+ long accountId = m.mAccountKey;
+ Account account = Account.restoreAccountWithId(exchangeService, accountId);
+ if (account == null) return;
+ if (exchangeService.releaseSyncHolds(exchangeService,
+ AbstractSyncService.EXIT_LOGIN_FAILURE, account)) {
+ new AccountServiceProxy(exchangeService).notifyLoginSucceeded(accountId);
+ }
+ }
+
+ int lastResult = EmailContent.LAST_SYNC_RESULT_SUCCESS;
+ // For error states, whether the error is fatal (won't automatically be retried)
+ boolean errorIsFatal = true;
+ try {
+ switch (exitStatus) {
+ case AbstractSyncService.EXIT_DONE:
+ if (svc.hasPendingRequests()) {
+ // TODO Handle this case
+ }
+ errorMap.remove(mailboxId);
+ // If we've had a successful sync, clear the shutdown count
+ synchronized (ExchangeService.class) {
+ sClientConnectionManagerShutdownCount = 0;
+ }
+ // Leave now; other statuses are errors
+ return;
+ // I/O errors get retried at increasing intervals
+ case AbstractSyncService.EXIT_IO_ERROR:
+ if (syncError != null) {
+ syncError.escalate();
+ log(m.mDisplayName + " held for " + syncError.holdDelay + "ms");
+ } else {
+ log(m.mDisplayName + " added to syncErrorMap, hold for 15s");
+ }
+ lastResult = EmailContent.LAST_SYNC_RESULT_CONNECTION_ERROR;
+ errorIsFatal = false;
+ break;
+ // These errors are not retried automatically
+ case AbstractSyncService.EXIT_LOGIN_FAILURE:
+ new AccountServiceProxy(exchangeService).notifyLoginFailed(m.mAccountKey);
+ lastResult = EmailContent.LAST_SYNC_RESULT_AUTH_ERROR;
+ break;
+ case AbstractSyncService.EXIT_SECURITY_FAILURE:
+ case AbstractSyncService.EXIT_ACCESS_DENIED:
+ lastResult = EmailContent.LAST_SYNC_RESULT_SECURITY_ERROR;
+ break;
+ case AbstractSyncService.EXIT_EXCEPTION:
+ lastResult = EmailContent.LAST_SYNC_RESULT_INTERNAL_ERROR;
+ break;
+ }
+ // Add this box to the error map
+ errorMap.put(mailboxId, exchangeService.new SyncError(exitStatus, errorIsFatal));
+ } finally {
+ // Always set the last result
+ exchangeService.setMailboxLastSyncResult(mailboxId, lastResult);
+ kick("sync completed");
+ }
+ }
+ }
+
+ /**
+ * Given the status string from a Mailbox, return the type code for the last sync
+ * @param status the syncStatus column of a Mailbox
+ * @return
+ */
+ static public int getStatusType(String status) {
+ if (status == null) {
+ return -1;
+ } else {
+ return status.charAt(STATUS_TYPE_CHAR) - '0';
+ }
+ }
+
+ /**
+ * Given the status string from a Mailbox, return the change count for the last sync
+ * The change count is the number of adds + deletes + changes in the last sync
+ * @param status the syncStatus column of a Mailbox
+ * @return
+ */
+ static public int getStatusChangeCount(String status) {
+ try {
+ String s = status.substring(STATUS_CHANGE_COUNT_OFFSET);
+ return Integer.parseInt(s);
+ } catch (RuntimeException e) {
+ return -1;
+ }
+ }
+
+ static public Context getContext() {
+ return INSTANCE;
+ }
+}
diff --git a/exchange2/src/com/android/exchange/IllegalHeartbeatException.java b/exchange2/src/com/android/exchange/IllegalHeartbeatException.java
new file mode 100644
index 0000000..e480aa7
--- /dev/null
+++ b/exchange2/src/com/android/exchange/IllegalHeartbeatException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+public class IllegalHeartbeatException extends EasException {
+ private static final long serialVersionUID = 1L;
+ public final int mLegalHeartbeat;
+
+ public IllegalHeartbeatException(int legalHeartbeat) {
+ mLegalHeartbeat = legalHeartbeat;
+ }
+}
diff --git a/exchange2/src/com/android/exchange/MailboxAlarmReceiver.java b/exchange2/src/com/android/exchange/MailboxAlarmReceiver.java
new file mode 100644
index 0000000..7958c03
--- /dev/null
+++ b/exchange2/src/com/android/exchange/MailboxAlarmReceiver.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * MailboxAlarmReceiver is used to "wake up" the ExchangeService at the appropriate time(s). It may
+ * also be used for individual sync adapters, but this isn't implemented at the present time.
+ *
+ */
+public class MailboxAlarmReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ long mailboxId = intent.getLongExtra("mailbox", ExchangeService.EXTRA_MAILBOX_ID);
+ // EXCHANGE_SERVICE_MAILBOX_ID tells us that the service is asking to be started
+ if (mailboxId == ExchangeService.EXCHANGE_SERVICE_MAILBOX_ID) {
+ context.startService(new Intent(context, ExchangeService.class));
+ } else {
+ ExchangeService.alert(context, mailboxId);
+ }
+ }
+}
+
diff --git a/exchange2/src/com/android/exchange/MeetingResponseRequest.java b/exchange2/src/com/android/exchange/MeetingResponseRequest.java
new file mode 100644
index 0000000..ea769e2
--- /dev/null
+++ b/exchange2/src/com/android/exchange/MeetingResponseRequest.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+/**
+ * MeetingResponseRequest is the EAS wrapper for responding to meeting requests.
+ */
+public class MeetingResponseRequest extends Request {
+ public final int mResponse;
+
+ MeetingResponseRequest(long messageId, int response) {
+ super(messageId);
+ mResponse = response;
+ }
+
+ // MeetingResponseRequests are unique by their message id (i.e. there's only one response to
+ // a given message)
+ public boolean equals(Object o) {
+ if (!(o instanceof MeetingResponseRequest)) return false;
+ return ((MeetingResponseRequest)o).mMessageId == mMessageId;
+ }
+
+ public int hashCode() {
+ return (int)mMessageId;
+ }
+}
diff --git a/exchange2/src/com/android/exchange/MessageMoveRequest.java b/exchange2/src/com/android/exchange/MessageMoveRequest.java
new file mode 100644
index 0000000..a884bda
--- /dev/null
+++ b/exchange2/src/com/android/exchange/MessageMoveRequest.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+/**
+ * MessageMoveRequest is the EAS wrapper for requesting a "move to folder"
+ */
+public class MessageMoveRequest extends Request {
+ public final long mMailboxId;
+
+ public MessageMoveRequest(long messageId, long mailboxId) {
+ super(messageId);
+ mMailboxId = mailboxId;
+ }
+
+ // MessageMoveRequests are unique by their message id (i.e. it's meaningless to have two
+ // separate message moves queued at the same time)
+ public boolean equals(Object o) {
+ if (!(o instanceof MessageMoveRequest)) return false;
+ return ((MessageMoveRequest)o).mMessageId == mMessageId;
+ }
+
+ public int hashCode() {
+ return (int)mMessageId;
+ }
+}
diff --git a/exchange2/src/com/android/exchange/MockParserStream.java b/exchange2/src/com/android/exchange/MockParserStream.java
new file mode 100644
index 0000000..a518667
--- /dev/null
+++ b/exchange2/src/com/android/exchange/MockParserStream.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * MockParserStream is an InputStream that feeds pre-generated data into various EasParser
+ * subclasses.
+ *
+ * After parsing is done, the result can be obtained with getResult
+ *
+ */
+public class MockParserStream extends InputStream {
+ int[] array;
+ int pos = 0;
+ Object value;
+
+ MockParserStream (int[] _array) {
+ array = _array;
+ }
+
+ @Override
+ public int read() throws IOException {
+ try {
+ return array[pos++];
+ } catch (IndexOutOfBoundsException e) {
+ throw new IOException("End of stream");
+ }
+ }
+
+ public void setResult(Object _value) {
+ value = _value;
+ }
+
+ public Object getResult() {
+ return value;
+ }
+}
diff --git a/exchange2/src/com/android/exchange/PartRequest.java b/exchange2/src/com/android/exchange/PartRequest.java
new file mode 100644
index 0000000..23b4add
--- /dev/null
+++ b/exchange2/src/com/android/exchange/PartRequest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.emailcommon.provider.EmailContent.Attachment;
+
+/**
+ * PartRequest is the EAS wrapper for attachment loading requests. In addition to information about
+ * the attachment to be loaded, it also contains the callback to be used for status/progress
+ * updates to the UI.
+ */
+public class PartRequest extends Request {
+ public final Attachment mAttachment;
+ public final String mDestination;
+ public final String mContentUriString;
+ public final String mLocation;
+
+ public PartRequest(Attachment _att, String _destination, String _contentUriString) {
+ super(_att.mMessageKey);
+ mAttachment = _att;
+ mLocation = mAttachment.mLocation;
+ mDestination = _destination;
+ mContentUriString = _contentUriString;
+ }
+
+ // PartRequests are unique by their attachment id (i.e. multiple attachments might be queued
+ // for a particular message, but any individual attachment can only be loaded once)
+ public boolean equals(Object o) {
+ if (!(o instanceof PartRequest)) return false;
+ return ((PartRequest)o).mAttachment.mId == mAttachment.mId;
+ }
+
+ public int hashCode() {
+ return (int)mAttachment.mId;
+ }
+}
diff --git a/exchange2/src/com/android/exchange/Request.java b/exchange2/src/com/android/exchange/Request.java
new file mode 100644
index 0000000..280a084
--- /dev/null
+++ b/exchange2/src/com/android/exchange/Request.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/**
+ * Requests for mailbox actions are handled by subclasses of this abstract class.
+ * Three subclasses are now defined: PartRequest (attachment load), MeetingResponseRequest
+ * (respond to a meeting invitation), and MessageMoveRequest (move a message to another folder)
+ */
+public abstract class Request {
+ public final long mTimeStamp = System.currentTimeMillis();
+ public final long mMessageId;
+
+ public Request(long messageId) {
+ mMessageId = messageId;
+ }
+
+ // Subclasses of Request may have different semantics regarding equality; therefore,
+ // we force them to implement the equals method
+ public abstract boolean equals(Object o);
+ public abstract int hashCode();
+}
diff --git a/exchange2/src/com/android/exchange/SettingsRedirector.java b/exchange2/src/com/android/exchange/SettingsRedirector.java
new file mode 100644
index 0000000..5a50825
--- /dev/null
+++ b/exchange2/src/com/android/exchange/SettingsRedirector.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.exchange;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.emailcommon.utility.IntentUtilities;
+
+/**
+ * An empty {@link Activity} that simply redirects to the proper settings editor for the Email
+ * application.
+ * This is needed since the Exchange service runs as a separate UID and is therefore tracked as
+ * a separate entity in the framework for things such as data usage. Links from those places to
+ * Exchange should really go to Email where the real settings are.
+ */
+public class SettingsRedirector extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+ Intent redirect = new Intent(
+ Intent.ACTION_EDIT,
+ IntentUtilities.createActivityIntentUrlBuilder("settings").build());
+ redirect.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
+ startActivity(redirect);
+ finish();
+ }
+}
diff --git a/exchange2/src/com/android/exchange/StaleFolderListException.java b/exchange2/src/com/android/exchange/StaleFolderListException.java
new file mode 100644
index 0000000..70ac032
--- /dev/null
+++ b/exchange2/src/com/android/exchange/StaleFolderListException.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+public class StaleFolderListException extends EasException {
+ private static final long serialVersionUID = 1L;
+}
diff --git a/exchange2/src/com/android/exchange/adapter/AbstractSyncAdapter.java b/exchange2/src/com/android/exchange/adapter/AbstractSyncAdapter.java
new file mode 100644
index 0000000..52400c4
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/AbstractSyncAdapter.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.exchange.CommandStatusException;
+import com.android.exchange.Eas;
+import com.android.exchange.EasSyncService;
+import com.google.common.annotations.VisibleForTesting;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.os.TransactionTooLargeException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Parent class of all sync adapters (EasMailbox, EasCalendar, and EasContacts)
+ *
+ */
+public abstract class AbstractSyncAdapter {
+
+ public static final int SECONDS = 1000;
+ public static final int MINUTES = SECONDS*60;
+ public static final int HOURS = MINUTES*60;
+ public static final int DAYS = HOURS*24;
+ public static final int WEEKS = DAYS*7;
+
+ protected static final String PIM_WINDOW_SIZE = "4";
+
+ private static final long SEPARATOR_ID = Long.MAX_VALUE;
+
+ public Mailbox mMailbox;
+ public EasSyncService mService;
+ public Context mContext;
+ public Account mAccount;
+ public final ContentResolver mContentResolver;
+ public final android.accounts.Account mAccountManagerAccount;
+
+ // Create the data for local changes that need to be sent up to the server
+ public abstract boolean sendLocalChanges(Serializer s) throws IOException;
+ // Parse incoming data from the EAS server, creating, modifying, and deleting objects as
+ // required through the EmailProvider
+ public abstract boolean parse(InputStream is) throws IOException, CommandStatusException;
+ // The name used to specify the collection type of the target (Email, Calendar, or Contacts)
+ public abstract String getCollectionName();
+ public abstract void cleanup();
+ public abstract boolean isSyncable();
+ // Add sync options (filter, body type - html vs plain, and truncation)
+ public abstract void sendSyncOptions(Double protocolVersion, Serializer s) throws IOException;
+ /**
+ * Delete all records of this class in this account
+ */
+ public abstract void wipe();
+
+ public boolean isLooping() {
+ return false;
+ }
+
+ public AbstractSyncAdapter(EasSyncService service) {
+ mService = service;
+ mMailbox = service.mMailbox;
+ mContext = service.mContext;
+ mAccount = service.mAccount;
+ mAccountManagerAccount = new android.accounts.Account(mAccount.mEmailAddress,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ mContentResolver = mContext.getContentResolver();
+ }
+
+ public void userLog(String ...strings) {
+ mService.userLog(strings);
+ }
+
+ public void incrementChangeCount() {
+ mService.mChangeCount++;
+ }
+
+ /**
+ * Set sync options common to PIM's (contacts and calendar)
+ * @param protocolVersion the protocol version under which we're syncing
+ * @param the filter to use (or null)
+ * @param s the Serializer
+ * @throws IOException
+ */
+ protected void setPimSyncOptions(Double protocolVersion, String filter, Serializer s)
+ throws IOException {
+ s.tag(Tags.SYNC_DELETES_AS_MOVES);
+ s.tag(Tags.SYNC_GET_CHANGES);
+ s.data(Tags.SYNC_WINDOW_SIZE, PIM_WINDOW_SIZE);
+ s.start(Tags.SYNC_OPTIONS);
+ // Set the filter (lookback), if provided
+ if (filter != null) {
+ s.data(Tags.SYNC_FILTER_TYPE, filter);
+ }
+ // Set the truncation amount and body type
+ if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+ s.start(Tags.BASE_BODY_PREFERENCE);
+ // Plain text
+ s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT);
+ s.data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE);
+ s.end();
+ } else {
+ s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
+ }
+ s.end();
+ }
+
+ /**
+ * Returns the current SyncKey; override if the SyncKey is stored elsewhere (as for Contacts)
+ * @return the current SyncKey for the Mailbox
+ * @throws IOException
+ */
+ public String getSyncKey() throws IOException {
+ if (mMailbox.mSyncKey == null) {
+ userLog("Reset SyncKey to 0");
+ mMailbox.mSyncKey = "0";
+ }
+ return mMailbox.mSyncKey;
+ }
+
+ public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
+ mMailbox.mSyncKey = syncKey;
+ }
+
+ /**
+ * Operation is our binder-safe ContentProviderOperation (CPO) construct; an Operation can
+ * be created from a CPO, a CPO Builder, or a CPO Builder with a "back reference" column name
+ * and offset (that might be used in Builder.withValueBackReference). The CPO is not actually
+ * built until it is ready to be executed (with applyBatch); this allows us to recalculate
+ * back reference offsets if we are required to re-send a large batch in smaller chunks.
+ *
+ * NOTE: A failed binder transaction is something of an emergency case, and shouldn't happen
+ * with any frequency. When it does, and we are forced to re-send the data to the content
+ * provider in smaller chunks, we DO lose the sync-window atomicity, and thereby add another
+ * small risk to the data. Of course, this is far, far better than dropping the data on the
+ * floor, as was done before the framework implemented TransactionTooLargeException
+ */
+ protected static class Operation {
+ final ContentProviderOperation mOp;
+ final ContentProviderOperation.Builder mBuilder;
+ final String mColumnName;
+ final int mOffset;
+ // Is this Operation a separator? (a good place to break up a large transaction)
+ boolean mSeparator = false;
+
+ // For toString()
+ final String[] TYPES = new String[] {"???", "Ins", "Upd", "Del", "Assert"};
+
+ Operation(ContentProviderOperation.Builder builder, String columnName, int offset) {
+ mOp = null;
+ mBuilder = builder;
+ mColumnName = columnName;
+ mOffset = offset;
+ }
+
+ Operation(ContentProviderOperation.Builder builder) {
+ mOp = null;
+ mBuilder = builder;
+ mColumnName = null;
+ mOffset = 0;
+ }
+
+ Operation(ContentProviderOperation op) {
+ mOp = op;
+ mBuilder = null;
+ mColumnName = null;
+ mOffset = 0;
+ }
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder("Op: ");
+ ContentProviderOperation op = operationToContentProviderOperation(this, 0);
+ int type = 0;
+ //DO NOT SHIP WITH THE FOLLOWING LINE (the API is hidden!)
+ //type = op.getType();
+ sb.append(TYPES[type]);
+ Uri uri = op.getUri();
+ sb.append(' ');
+ sb.append(uri.getPath());
+ if (mColumnName != null) {
+ sb.append(" Back value of " + mColumnName + ": " + mOffset);
+ }
+ return sb.toString();
+ }
+ }
+
+ /**
+ * We apply the batch of CPO's here. We synchronize on the service to avoid thread-nasties,
+ * and we just return quickly if the service has already been stopped.
+ */
+ private ContentProviderResult[] execute(String authority,
+ ArrayList<ContentProviderOperation> ops)
+ throws RemoteException, OperationApplicationException {
+ synchronized (mService.getSynchronizer()) {
+ if (!mService.isStopped()) {
+ if (!ops.isEmpty()) {
+ ContentProviderResult[] result = mContentResolver.applyBatch(authority, ops);
+ mService.userLog("Results: " + result.length);
+ return result;
+ }
+ }
+ }
+ return new ContentProviderResult[0];
+ }
+
+ /**
+ * Convert an Operation to a CPO; if the Operation has a back reference, apply it with the
+ * passed-in offset
+ */
+ @VisibleForTesting
+ static ContentProviderOperation operationToContentProviderOperation(Operation op, int offset) {
+ if (op.mOp != null) {
+ return op.mOp;
+ } else if (op.mBuilder == null) {
+ throw new IllegalArgumentException("Operation must have CPO.Builder");
+ }
+ ContentProviderOperation.Builder builder = op.mBuilder;
+ if (op.mColumnName != null) {
+ builder.withValueBackReference(op.mColumnName, op.mOffset - offset);
+ }
+ return builder.build();
+ }
+
+ /**
+ * Create a list of CPOs from a list of Operations, and then apply them in a batch
+ */
+ private ContentProviderResult[] applyBatch(String authority, ArrayList<Operation> ops,
+ int offset) throws RemoteException, OperationApplicationException {
+ // Handle the empty case
+ if (ops.isEmpty()) {
+ return new ContentProviderResult[0];
+ }
+ ArrayList<ContentProviderOperation> cpos = new ArrayList<ContentProviderOperation>();
+ for (Operation op: ops) {
+ cpos.add(operationToContentProviderOperation(op, offset));
+ }
+ return execute(authority, cpos);
+ }
+
+ /**
+ * Apply the list of CPO's in the provider and copy the "mini" result into our full result array
+ */
+ private void applyAndCopyResults(String authority, ArrayList<Operation> mini,
+ ContentProviderResult[] result, int offset) throws RemoteException {
+ // Empty lists are ok; we just ignore them
+ if (mini.isEmpty()) return;
+ try {
+ ContentProviderResult[] miniResult = applyBatch(authority, mini, offset);
+ // Copy the results from this mini-batch into our results array
+ System.arraycopy(miniResult, 0, result, offset, miniResult.length);
+ } catch (OperationApplicationException e) {
+ // Not possible since we're building the ops ourselves
+ }
+ }
+
+ /**
+ * Called by a sync adapter to execute a list of Operations in the ContentProvider handling
+ * the passed-in authority. If the attempt to apply the batch fails due to a too-large
+ * binder transaction, we split the Operations as directed by separators. If any of the
+ * "mini" batches fails due to a too-large transaction, we're screwed, but this would be
+ * vanishingly rare. Other, possibly transient, errors are handled by throwing a
+ * RemoteException, which the caller will likely re-throw as an IOException so that the sync
+ * can be attempted again.
+ *
+ * Callers MAY leave a dangling separator at the end of the list; note that the separators
+ * themselves are only markers and are not sent to the provider.
+ */
+ protected ContentProviderResult[] safeExecute(String authority, ArrayList<Operation> ops)
+ throws RemoteException {
+ mService.userLog("Try to execute ", ops.size(), " CPO's for " + authority);
+ ContentProviderResult[] result = null;
+ try {
+ // Try to execute the whole thing
+ return applyBatch(authority, ops, 0);
+ } catch (TransactionTooLargeException e) {
+ // Nope; split into smaller chunks, demarcated by the separator operation
+ mService.userLog("Transaction too large; spliting!");
+ ArrayList<Operation> mini = new ArrayList<Operation>();
+ // Build a result array with the total size we're sending
+ result = new ContentProviderResult[ops.size()];
+ int count = 0;
+ int offset = 0;
+ for (Operation op: ops) {
+ if (op.mSeparator) {
+ try {
+ mService.userLog("Try mini-batch of ", mini.size(), " CPO's");
+ applyAndCopyResults(authority, mini, result, offset);
+ mini.clear();
+ // Save away the offset here; this will need to be subtracted out of the
+ // value originally set by the adapter
+ offset = count + 1; // Remember to add 1 for the separator!
+ } catch (TransactionTooLargeException e1) {
+ throw new RuntimeException("Can't send transaction; sync stopped.");
+ } catch (RemoteException e1) {
+ throw e1;
+ }
+ } else {
+ mini.add(op);
+ }
+ count++;
+ }
+ // Check out what's left; if it's more than just a separator, apply the batch
+ int miniSize = mini.size();
+ if ((miniSize > 0) && !(miniSize == 1 && mini.get(0).mSeparator)) {
+ applyAndCopyResults(authority, mini, result, offset);
+ }
+ } catch (RemoteException e) {
+ throw e;
+ } catch (OperationApplicationException e) {
+ // Not possible since we're building the ops ourselves
+ }
+ return result;
+ }
+
+ /**
+ * Called by a sync adapter to indicate a relatively safe place to split a batch of CPO's
+ */
+ protected void addSeparatorOperation(ArrayList<Operation> ops, Uri uri) {
+ Operation op = new Operation(
+ ContentProviderOperation.newDelete(ContentUris.withAppendedId(uri, SEPARATOR_ID)));
+ op.mSeparator = true;
+ ops.add(op);
+ }
+}
+
diff --git a/exchange2/src/com/android/exchange/adapter/AbstractSyncParser.java b/exchange2/src/com/android/exchange/adapter/AbstractSyncParser.java
new file mode 100644
index 0000000..1162b66
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/AbstractSyncParser.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.exchange.CommandStatusException;
+import com.android.exchange.CommandStatusException.CommandStatus;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.ExchangeService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Base class for the Email and PIM sync parsers
+ * Handles the basic flow of syncKeys, looping to get more data, handling errors, etc.
+ * Each subclass must implement a handful of methods that relate specifically to the data type
+ *
+ */
+public abstract class AbstractSyncParser extends Parser {
+
+ protected EasSyncService mService;
+ protected Mailbox mMailbox;
+ protected Account mAccount;
+ protected Context mContext;
+ protected ContentResolver mContentResolver;
+ protected AbstractSyncAdapter mAdapter;
+
+ private boolean mLooping;
+
+ public AbstractSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
+ super(in);
+ init(adapter);
+ }
+
+ public AbstractSyncParser(Parser p, AbstractSyncAdapter adapter) throws IOException {
+ super(p);
+ init(adapter);
+ }
+
+ private void init(AbstractSyncAdapter adapter) {
+ mAdapter = adapter;
+ mService = adapter.mService;
+ mContext = mService.mContext;
+ mContentResolver = mContext.getContentResolver();
+ mMailbox = mService.mMailbox;
+ mAccount = mService.mAccount;
+ }
+
+ /**
+ * Read, parse, and act on incoming commands from the Exchange server
+ * @throws IOException if the connection is broken
+ * @throws CommandStatusException
+ */
+ public abstract void commandsParser() throws IOException, CommandStatusException;
+
+ /**
+ * Read, parse, and act on server responses
+ * @throws IOException
+ */
+ public abstract void responsesParser() throws IOException;
+
+ /**
+ * Commit any changes found during parsing
+ * @throws IOException
+ */
+ public abstract void commit() throws IOException;
+
+ public boolean isLooping() {
+ return mLooping;
+ }
+
+ /**
+ * Skip through tags until we reach the specified end tag
+ * @param endTag the tag we end with
+ * @throws IOException
+ */
+ public void skipParser(int endTag) throws IOException {
+ while (nextTag(endTag) != END) {
+ skipTag();
+ }
+ }
+
+ /**
+ * Loop through the top-level structure coming from the Exchange server
+ * Sync keys and the more available flag are handled here, whereas specific data parsing
+ * is handled by abstract methods implemented for each data class (e.g. Email, Contacts, etc.)
+ * @throws CommandStatusException
+ */
+ @Override
+ public boolean parse() throws IOException, CommandStatusException {
+ int status;
+ boolean moreAvailable = false;
+ boolean newSyncKey = false;
+ int interval = mMailbox.mSyncInterval;
+ mLooping = false;
+ // If we're not at the top of the xml tree, throw an exception
+ if (nextTag(START_DOCUMENT) != Tags.SYNC_SYNC) {
+ throw new EasParserException();
+ }
+
+ boolean mailboxUpdated = false;
+ ContentValues cv = new ContentValues();
+
+ // Loop here through the remaining xml
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ if (tag == Tags.SYNC_COLLECTION || tag == Tags.SYNC_COLLECTIONS) {
+ // Ignore these tags, since we've only got one collection syncing in this loop
+ } else if (tag == Tags.SYNC_STATUS) {
+ // Status = 1 is success; everything else is a failure
+ status = getValueInt();
+ if (status != 1) {
+ mService.errorLog("Sync failed: " + CommandStatus.toString(status));
+ if (status == 3 || CommandStatus.isBadSyncKey(status)) {
+ // Must delete all of the data and start over with syncKey of "0"
+ mAdapter.setSyncKey("0", false);
+ // Make this a push box through the first sync
+ // TODO Make frequency conditional on user settings!
+ mMailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH;
+ mService.errorLog("Bad sync key; RESET and delete data");
+ mAdapter.wipe();
+ // Indicate there's more so that we'll start syncing again
+ moreAvailable = true;
+ } else if (status == 16 || status == 5) {
+ // Status 16 indicates a transient server error (indeterminate state)
+ // Status 5 indicates "server error"; this tends to loop for a while so
+ // throwing IOException will at least provide backoff behavior
+ throw new IOException();
+ } else if (status == 8 || status == 12) {
+ // Status 8 is Bad; it means the server doesn't recognize the serverId it
+ // sent us. 12 means that we're being asked to refresh the folder list.
+ // We'll do that with 8 also...
+ ExchangeService.reloadFolderList(mContext, mAccount.mId, true);
+ // We don't have any provision for telling the user "wait a minute while
+ // we sync folders"...
+ throw new IOException();
+ } else if (status == 7) {
+ mService.mUpsyncFailed = true;
+ moreAvailable = true;
+ } else {
+ // Access, provisioning, transient, etc.
+ throw new CommandStatusException(status);
+ }
+ }
+ } else if (tag == Tags.SYNC_COMMANDS) {
+ commandsParser();
+ } else if (tag == Tags.SYNC_RESPONSES) {
+ responsesParser();
+ } else if (tag == Tags.SYNC_MORE_AVAILABLE) {
+ moreAvailable = true;
+ } else if (tag == Tags.SYNC_SYNC_KEY) {
+ if (mAdapter.getSyncKey().equals("0")) {
+ moreAvailable = true;
+ }
+ String newKey = getValue();
+ userLog("Parsed key for ", mMailbox.mDisplayName, ": ", newKey);
+ if (!newKey.equals(mMailbox.mSyncKey)) {
+ mAdapter.setSyncKey(newKey, true);
+ cv.put(MailboxColumns.SYNC_KEY, newKey);
+ mailboxUpdated = true;
+ newSyncKey = true;
+ }
+ // If we were pushing (i.e. auto-start), now we'll become ping-triggered
+ if (mMailbox.mSyncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
+ mMailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_PING;
+ }
+ } else {
+ skipTag();
+ }
+ }
+
+ // If we don't have a new sync key, ignore moreAvailable (or we'll loop)
+ if (moreAvailable && !newSyncKey) {
+ mLooping = true;
+ }
+
+ // Commit any changes
+ commit();
+
+ boolean abortSyncs = false;
+
+ // If the sync interval has changed, we need to save it
+ if (mMailbox.mSyncInterval != interval) {
+ cv.put(MailboxColumns.SYNC_INTERVAL, mMailbox.mSyncInterval);
+ mailboxUpdated = true;
+ // If there are changes, and we were bounced from push/ping, try again
+ } else if (mService.mChangeCount > 0 &&
+ mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH &&
+ mMailbox.mSyncInterval > 0) {
+ userLog("Changes found to ping loop mailbox ", mMailbox.mDisplayName, ": will ping.");
+ cv.put(MailboxColumns.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PING);
+ mailboxUpdated = true;
+ abortSyncs = true;
+ }
+
+ if (mailboxUpdated) {
+ synchronized (mService.getSynchronizer()) {
+ if (!mService.isStopped()) {
+ mMailbox.update(mContext, cv);
+ }
+ }
+ }
+
+ if (abortSyncs) {
+ userLog("Aborting account syncs due to mailbox change to ping...");
+ ExchangeService.stopAccountSyncs(mAccount.mId);
+ }
+
+ // Let the caller know that there's more to do
+ if (moreAvailable) {
+ userLog("MoreAvailable");
+ }
+ return moreAvailable;
+ }
+
+ void userLog(String ...strings) {
+ mService.userLog(strings);
+ }
+
+ void userLog(String string, int num, String string2) {
+ mService.userLog(string, num, string2);
+ }
+}
diff --git a/exchange2/src/com/android/exchange/adapter/AccountSyncAdapter.java b/exchange2/src/com/android/exchange/adapter/AccountSyncAdapter.java
new file mode 100644
index 0000000..cf54ed5
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/AccountSyncAdapter.java
@@ -0,0 +1,45 @@
+package com.android.exchange.adapter;
+
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class AccountSyncAdapter extends AbstractSyncAdapter {
+
+ public AccountSyncAdapter(EasSyncService service) {
+ super(service);
+ }
+
+ @Override
+ public void cleanup() {
+ }
+
+ @Override
+ public void wipe() {
+ }
+
+ @Override
+ public String getCollectionName() {
+ return null;
+ }
+
+ @Override
+ public boolean parse(InputStream is) throws IOException {
+ return false;
+ }
+
+ @Override
+ public boolean sendLocalChanges(Serializer s) throws IOException {
+ return false;
+ }
+
+ @Override
+ public boolean isSyncable() {
+ return true;
+ }
+
+ @Override
+ public void sendSyncOptions(Double protocolVersion, Serializer s) throws IOException {
+ }
+}
diff --git a/exchange2/src/com/android/exchange/adapter/AttachmentLoader.java b/exchange2/src/com/android/exchange/adapter/AttachmentLoader.java
new file mode 100644
index 0000000..04cf555
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/AttachmentLoader.java
@@ -0,0 +1,248 @@
+/* Copyright (C) 2011 The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.os.RemoteException;
+
+import com.android.emailcommon.provider.EmailContent.Attachment;
+import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.service.EmailServiceStatus;
+import com.android.emailcommon.utility.AttachmentUtilities;
+import com.android.exchange.Eas;
+import com.android.exchange.EasResponse;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.ExchangeService;
+import com.android.exchange.PartRequest;
+import com.android.exchange.utility.UriCodec;
+import com.android.mail.providers.UIProvider;
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.http.HttpStatus;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Handle EAS attachment loading, regardless of protocol version
+ */
+public class AttachmentLoader {
+ static private final int CHUNK_SIZE = 16*1024;
+
+ private final EasSyncService mService;
+ private final Context mContext;
+ private final ContentResolver mResolver;
+ private final Attachment mAttachment;
+ private final long mAttachmentId;
+ private final int mAttachmentSize;
+ private final long mMessageId;
+ private final Message mMessage;
+ private final long mAccountId;
+ private final Uri mAttachmentUri;
+
+ public AttachmentLoader(EasSyncService service, PartRequest req) {
+ mService = service;
+ mContext = service.mContext;
+ mResolver = service.mContentResolver;
+ mAttachment = req.mAttachment;
+ mAttachmentId = mAttachment.mId;
+ mAttachmentSize = (int)mAttachment.mSize;
+ mAccountId = mAttachment.mAccountKey;
+ mMessageId = mAttachment.mMessageKey;
+ mMessage = Message.restoreMessageWithId(mContext, mMessageId);
+ mAttachmentUri = AttachmentUtilities.getAttachmentUri(mAccountId, mAttachmentId);
+ }
+
+ private void doStatusCallback(int status) {
+ try {
+ ExchangeService.callback().loadAttachmentStatus(mMessageId, mAttachmentId, status, 0);
+ } catch (RemoteException e) {
+ // No danger if the client is no longer around
+ }
+ }
+
+ private void doProgressCallback(int progress) {
+ try {
+ ExchangeService.callback().loadAttachmentStatus(mMessageId, mAttachmentId,
+ EmailServiceStatus.IN_PROGRESS, progress);
+ } catch (RemoteException e) {
+ // No danger if the client is no longer around
+ }
+ }
+
+ /**
+ * Save away the contentUri for this Attachment and notify listeners
+ */
+ private void finishLoadAttachment() {
+ ContentValues cv = new ContentValues();
+ cv.put(AttachmentColumns.CONTENT_URI, mAttachmentUri.toString());
+ cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.SAVED);
+ mAttachment.update(mContext, cv);
+ doStatusCallback(EmailServiceStatus.SUCCESS);
+ }
+
+ /**
+ * Read the attachment data in chunks and write the data back out to our attachment file
+ * @param inputStream the InputStream we're reading the attachment from
+ * @param outputStream the OutputStream the attachment will be written to
+ * @param len the number of expected bytes we're going to read
+ * @throws IOException
+ */
+ public void readChunked(InputStream inputStream, OutputStream outputStream, int len)
+ throws IOException {
+ byte[] bytes = new byte[CHUNK_SIZE];
+ int length = len;
+ // Loop terminates 1) when EOF is reached or 2) IOException occurs
+ // One of these is guaranteed to occur
+ int totalRead = 0;
+ int lastCallbackPct = -1;
+ int lastCallbackTotalRead = 0;
+ mService.userLog("Expected attachment length: ", len);
+ while (true) {
+ int read = inputStream.read(bytes, 0, CHUNK_SIZE);
+ if (read < 0) {
+ // -1 means EOF
+ mService.userLog("Attachment load reached EOF, totalRead: ", totalRead);
+ break;
+ }
+
+ // Keep track of how much we've read for progress callback
+ totalRead += read;
+ // Write these bytes out
+ outputStream.write(bytes, 0, read);
+
+ // We can't report percentage if data is chunked; the length of incoming data is unknown
+ if (length > 0) {
+ int pct = (totalRead * 100) / length;
+ // Callback only if we've read at least 1% more and have read more than CHUNK_SIZE
+ // We don't want to spam the Email app
+ if ((pct > lastCallbackPct) && (totalRead > (lastCallbackTotalRead + CHUNK_SIZE))) {
+ // Report progress back to the UI
+ doProgressCallback(pct);
+ lastCallbackTotalRead = totalRead;
+ lastCallbackPct = pct;
+ }
+ }
+ }
+ if (totalRead > length) {
+ // Apparently, the length, as reported by EAS, isn't always accurate; let's log it
+ mService.userLog("Read more than expected: ", totalRead);
+ }
+ }
+
+ @VisibleForTesting
+ static String encodeForExchange2003(String str) {
+ AttachmentNameEncoder enc = new AttachmentNameEncoder();
+ StringBuilder sb = new StringBuilder(str.length() + 16);
+ enc.appendPartiallyEncoded(sb, str);
+ return sb.toString();
+ }
+
+ /**
+ * Encoder for Exchange 2003 attachment names. They come from the server partially encoded,
+ * but there are still possible characters that need to be encoded (Why, MSFT, why?)
+ */
+ private static class AttachmentNameEncoder extends UriCodec {
+ @Override protected boolean isRetained(char c) {
+ // These four characters are commonly received in EAS 2.5 attachment names and are
+ // valid (verified by testing); we won't encode them
+ return c == '_' || c == ':' || c == '/' || c == '.';
+ }
+ }
+
+ /**
+ * Loads an attachment, based on the PartRequest passed in the constructor
+ * @throws IOException
+ */
+ public void loadAttachment() throws IOException {
+ if (mMessage == null) {
+ doStatusCallback(EmailServiceStatus.MESSAGE_NOT_FOUND);
+ return;
+ }
+ // Say we've started loading the attachment
+ doProgressCallback(0);
+
+ EasResponse resp;
+ boolean eas14 = mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE;
+ // The method of attachment loading is different in EAS 14.0 than in earlier versions
+ if (eas14) {
+ Serializer s = new Serializer();
+ s.start(Tags.ITEMS_ITEMS).start(Tags.ITEMS_FETCH);
+ s.data(Tags.ITEMS_STORE, "Mailbox");
+ s.data(Tags.BASE_FILE_REFERENCE, mAttachment.mLocation);
+ s.end().end().done(); // ITEMS_FETCH, ITEMS_ITEMS
+ resp = mService.sendHttpClientPost("ItemOperations", s.toByteArray());
+ } else {
+ String location = mAttachment.mLocation;
+ // For Exchange 2003 (EAS 2.5), we have to look for illegal characters in the file name
+ // that EAS sent to us!
+ if (mService.mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+ location = encodeForExchange2003(location);
+ }
+ String cmd = "GetAttachment&AttachmentName=" + location;
+ resp = mService.sendHttpClientPost(cmd, null, EasSyncService.COMMAND_TIMEOUT);
+ }
+
+ try {
+ int status = resp.getStatus();
+ if (status == HttpStatus.SC_OK) {
+ if (!resp.isEmpty()) {
+ InputStream is = resp.getInputStream();
+ OutputStream os = null;
+ try {
+ os = mResolver.openOutputStream(mAttachmentUri);
+ if (eas14) {
+ ItemOperationsParser p = new ItemOperationsParser(this, is, os,
+ mAttachmentSize);
+ p.parse();
+ if (p.getStatusCode() == 1 /* Success */) {
+ finishLoadAttachment();
+ return;
+ }
+ } else {
+ int len = resp.getLength();
+ if (len != 0) {
+ // len > 0 means that Content-Length was set in the headers
+ // len < 0 means "chunked" transfer-encoding
+ readChunked(is, os, (len < 0) ? mAttachmentSize : len);
+ finishLoadAttachment();
+ return;
+ }
+ }
+ } catch (FileNotFoundException e) {
+ mService.errorLog("Can't get attachment; write file not found?");
+ } finally {
+ if (os != null) {
+ os.flush();
+ os.close();
+ }
+ }
+ }
+ }
+ } finally {
+ resp.close();
+ }
+
+ // All errors lead here...
+ doStatusCallback(EmailServiceStatus.ATTACHMENT_NOT_FOUND);
+ }
+}
diff --git a/exchange2/src/com/android/exchange/adapter/Base64InputStream.java b/exchange2/src/com/android/exchange/adapter/Base64InputStream.java
new file mode 100644
index 0000000..5b78c4c
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/Base64InputStream.java
@@ -0,0 +1,175 @@
+/****************************************************************
+ * 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. *
+ ****************************************************************/
+
+/**
+ * Modified for Exchange attachment decoding 5/12/11. Changes are bracketed with START EAS CHANGES
+ * and END EAS CHANGES
+ *
+ * Without the included changes, the final bytes of the input stream will be read here and thrown
+ * away; in that case, the WBXML parser will lose information necessary to determine that the
+ * entire stream has been processed correctly. Since inline WBXML text is terminated with a zero
+ * byte, and since zero is not valid Base64, we terminate reading when we find a zero byte, leaving
+ * the remainder of the stream untouched (to be read by the Parser that created the
+ * Base64InputStream.
+ */
+
+package com.android.exchange.adapter;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Performs Base-64 decoding on an underlying stream.
+ *
+ *
+ * @version $Id: Base64InputStream.java,v 1.3 2004/11/29 13:15:47 ntherning Exp $
+ */
+public class Base64InputStream extends InputStream {
+ private final InputStream s;
+ private int outCount = 0;
+ private int outIndex = 0;
+ private final int[] outputBuffer = new int[3];
+ private final byte[] inputBuffer = new byte[4];
+ private boolean done = false;
+ // START EAS CHANGES
+ private boolean padSeen = false;
+ // END EAS CHANGES
+
+ public Base64InputStream(InputStream s) {
+ this.s = s;
+ }
+
+ /**
+ * Closes the underlying stream.
+ *
+ * @throws IOException on I/O errors.
+ */
+ @Override
+ public void close() throws IOException {
+ s.close();
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (outIndex == outCount) {
+ fillBuffer();
+ if (outIndex == outCount) {
+ return -1;
+ }
+ }
+
+ return outputBuffer[outIndex++];
+ }
+
+ /**
+ * Retrieve data from the underlying stream, decode it,
+ * and put the results in the byteq.
+ * @throws IOException
+ */
+ private void fillBuffer() throws IOException {
+ outCount = 0;
+ outIndex = 0;
+ int inCount = 0;
+
+ int i;
+ // "done" is needed for the two successive '=' at the end
+ while (!done) {
+ switch (i = s.read()) {
+ // START EAS CHANGES
+ case 0:
+ // In EAS, a zero will indicate the end of the (inline) base64 string
+ // Stop reading at this point, so that we can continue with WBXML
+ done = true;
+ return;
+ // END EAS CHANGES
+ case -1:
+ // No more input - just return, let outputBuffer drain out, and be done
+ return;
+ case '=':
+ // START EAS CHANGES
+ // Allow for a second '=' before we're really done (we should get a zero next)
+ if (padSeen) {
+ return;
+ }
+ // We've seen a (first) end padding character, flush what's in the buffer
+ padSeen = true;
+ // END EAS CHANGES
+ // PRE-EAS LINE COMMENTED OUT
+ //done = true;
+ decodeAndEnqueue(inCount);
+ return;
+ default:
+ byte sX = TRANSLATION[i];
+ if (sX < 0) continue;
+ inputBuffer[inCount++] = sX;
+ if (inCount == 4) {
+ decodeAndEnqueue(inCount);
+ return;
+ }
+ break;
+ }
+ }
+ }
+
+ private void decodeAndEnqueue(int len) {
+ int accum = 0;
+ accum |= inputBuffer[0] << 18;
+ accum |= inputBuffer[1] << 12;
+ accum |= inputBuffer[2] << 6;
+ accum |= inputBuffer[3];
+
+ // There's a bit of duplicated code here because we want to have straight-through operation
+ // for the most common case of len==4
+ if (len == 4) {
+ outputBuffer[0] = (accum >> 16) & 0xFF;
+ outputBuffer[1] = (accum >> 8) & 0xFF;
+ outputBuffer[2] = (accum) & 0xFF;
+ outCount = 3;
+ return;
+ } else if (len == 3) {
+ outputBuffer[0] = (accum >> 16) & 0xFF;
+ outputBuffer[1] = (accum >> 8) & 0xFF;
+ outCount = 2;
+ return;
+ } else { // len == 2
+ outputBuffer[0] = (accum >> 16) & 0xFF;
+ outCount = 1;
+ return;
+ }
+ }
+
+ private static byte[] TRANSLATION = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x00 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x10 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, /* 0x20 */
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, /* 0x30 */
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 0x40 */
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, /* 0x50 */
+ -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* 0x60 */
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, /* 0x70 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x80 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x90 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xA0 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xB0 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xC0 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xD0 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xE0 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 /* 0xF0 */
+ };
+}
diff --git a/exchange2/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/exchange2/src/com/android/exchange/adapter/CalendarSyncAdapter.java
new file mode 100644
index 0000000..4df45a9
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -0,0 +1,2163 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import 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.Entity.NamedContentValues;
+import android.content.EntityIterator;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.CalendarContract;
+import android.provider.CalendarContract.Attendees;
+import android.provider.CalendarContract.Calendars;
+import android.provider.CalendarContract.Events;
+import android.provider.CalendarContract.EventsEntity;
+import android.provider.CalendarContract.ExtendedProperties;
+import android.provider.CalendarContract.Reminders;
+import android.provider.CalendarContract.SyncState;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.SyncStateContract;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.calendarcommon.DateException;
+import com.android.calendarcommon.Duration;
+import com.android.emailcommon.AccountManagerTypes;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.utility.Utility;
+import com.android.exchange.CommandStatusException;
+import com.android.exchange.Eas;
+import com.android.exchange.EasOutboxService;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.ExchangeService;
+import com.android.exchange.utility.CalendarUtilities;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.GregorianCalendar;
+import java.util.Map.Entry;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
+import java.util.UUID;
+
+/**
+ * Sync adapter class for EAS calendars
+ *
+ */
+public class CalendarSyncAdapter extends AbstractSyncAdapter {
+
+ private static final String TAG = "EasCalendarSyncAdapter";
+
+ private static final String EVENT_SAVED_TIMEZONE_COLUMN = Events.SYNC_DATA1;
+ /**
+ * Used to keep track of exception vs parent event dirtiness.
+ */
+ private static final String EVENT_SYNC_MARK = Events.SYNC_DATA8;
+ private static final String EVENT_SYNC_VERSION = Events.SYNC_DATA4;
+ // 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_AND_CALENDAR_ID = Events._SYNC_ID + "=? AND " +
+ Events.ORIGINAL_SYNC_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?";
+ private static final String EVENT_ID_AND_CALENDAR_ID = Events._ID + "=? AND " +
+ Events.ORIGINAL_SYNC_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?";
+ private static final String DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR = "(" + Events.DIRTY
+ + "=1 OR " + EVENT_SYNC_MARK + "= 1) AND " +
+ Events.ORIGINAL_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?";
+ private static final String DIRTY_EXCEPTION_IN_CALENDAR =
+ Events.DIRTY + "=1 AND " + Events.ORIGINAL_ID + " NOTNULL AND " +
+ Events.CALENDAR_ID + "=?";
+ private static final String CLIENT_ID_SELECTION = Events.SYNC_DATA2 + "=?";
+ private static final String ORIGINAL_EVENT_AND_CALENDAR =
+ Events.ORIGINAL_SYNC_ID + "=? AND " + Events.CALENDAR_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_ID, Events._ID};
+ private static final String EVENT_ID_AND_NAME =
+ ExtendedProperties.EVENT_ID + "=? AND " + ExtendedProperties.NAME + "=?";
+
+ // Note that we use LIKE below for its case insensitivity
+ private static final String EVENT_AND_EMAIL =
+ Attendees.EVENT_ID + "=? AND "+ Attendees.ATTENDEE_EMAIL + " LIKE ?";
+ private static final int ATTENDEE_STATUS_COLUMN_STATUS = 0;
+ private static final String[] ATTENDEE_STATUS_PROJECTION =
+ new String[] {Attendees.ATTENDEE_STATUS};
+
+ public static final String CALENDAR_SELECTION =
+ Calendars.ACCOUNT_NAME + "=? AND " + Calendars.ACCOUNT_TYPE + "=?";
+ private static final int CALENDAR_SELECTION_ID = 0;
+
+ private static final String[] EXTENDED_PROPERTY_PROJECTION =
+ new String[] {ExtendedProperties._ID};
+ private static final int EXTENDED_PROPERTY_ID = 0;
+
+ private static final String CATEGORY_TOKENIZER_DELIMITER = "\\";
+ private static final String ATTENDEE_TOKENIZER_DELIMITER = CATEGORY_TOKENIZER_DELIMITER;
+
+ private static final String EXTENDED_PROPERTY_USER_ATTENDEE_STATUS = "userAttendeeStatus";
+ private static final String EXTENDED_PROPERTY_ATTENDEES = "attendees";
+ private static final String EXTENDED_PROPERTY_DTSTAMP = "dtstamp";
+ private static final String EXTENDED_PROPERTY_MEETING_STATUS = "meeting_status";
+ private static final String EXTENDED_PROPERTY_CATEGORIES = "categories";
+ // Used to indicate that we removed the attendee list because it was too large
+ private static final String EXTENDED_PROPERTY_ATTENDEES_REDACTED = "attendeesRedacted";
+ // Used to indicate that upsyncs aren't allowed (we catch this in sendLocalChanges)
+ private static final String EXTENDED_PROPERTY_UPSYNC_PROHIBITED = "upsyncProhibited";
+
+ private static final Operation PLACEHOLDER_OPERATION =
+ new Operation(ContentProviderOperation.newInsert(Uri.EMPTY));
+
+ private static final Object sSyncKeyLock = new Object();
+
+ private static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
+ private final TimeZone mLocalTimeZone = TimeZone.getDefault();
+
+
+ // Maximum number of allowed attendees; above this number, we mark the Event with the
+ // attendeesRedacted extended property and don't allow the event to be upsynced to the server
+ private static final int MAX_SYNCED_ATTENDEES = 50;
+ // We set the organizer to this when the user is the organizer and we've redacted the
+ // attendee list. By making the meeting organizer OTHER than the user, we cause the UI to
+ // prevent edits to this event (except local changes like reminder).
+ private static final String BOGUS_ORGANIZER_EMAIL = "upload_disallowed@uploadisdisallowed.aaa";
+ // Maximum number of CPO's before we start redacting attendees in exceptions
+ // The number 500 has been determined empirically; 1500 CPOs appears to be the limit before
+ // binder failures occur, but we need room at any point for additional events/exceptions so
+ // we set our limit at 1/3 of the apparent maximum for extra safety
+ // TODO Find a better solution to this workaround
+ private static final int MAX_OPS_BEFORE_EXCEPTION_ATTENDEE_REDACTION = 500;
+
+ private long mCalendarId = -1;
+ private String mCalendarIdString;
+ private String[] mCalendarIdArgument;
+ /*package*/ String mEmailAddress;
+
+ private ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
+ private ArrayList<Long> mUploadedIdList = new ArrayList<Long>();
+ private ArrayList<Long> mSendCancelIdList = new ArrayList<Long>();
+ private ArrayList<Message> mOutgoingMailList = new ArrayList<Message>();
+
+ private final Uri mAsSyncAdapterAttendees;
+ private final Uri mAsSyncAdapterEvents;
+ private final Uri mAsSyncAdapterReminders;
+ private final Uri mAsSyncAdapterExtendedProperties;
+
+ public CalendarSyncAdapter(EasSyncService service) {
+ super(service);
+ mEmailAddress = mAccount.mEmailAddress;
+
+ String amType = Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE;
+ mAsSyncAdapterAttendees =
+ asSyncAdapter(Attendees.CONTENT_URI, mEmailAddress, amType);
+ mAsSyncAdapterEvents =
+ asSyncAdapter(Events.CONTENT_URI, mEmailAddress, amType);
+ mAsSyncAdapterReminders =
+ asSyncAdapter(Reminders.CONTENT_URI, mEmailAddress, amType);
+ mAsSyncAdapterExtendedProperties =
+ asSyncAdapter(ExtendedProperties.CONTENT_URI, mEmailAddress, amType);
+
+ Cursor c = mService.mContentResolver.query(Calendars.CONTENT_URI,
+ new String[] {Calendars._ID}, CALENDAR_SELECTION,
+ new String[] {mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE}, null);
+ if (c == null) return;
+ try {
+ if (c.moveToFirst()) {
+ mCalendarId = c.getLong(CALENDAR_SELECTION_ID);
+ } else {
+ mCalendarId = CalendarUtilities.createCalendar(mService, mAccount, mMailbox);
+ }
+ mCalendarIdString = Long.toString(mCalendarId);
+ mCalendarIdArgument = new String[] {mCalendarIdString};
+ } finally {
+ c.close();
+ }
+ }
+
+ @Override
+ public String getCollectionName() {
+ return "Calendar";
+ }
+
+ @Override
+ public void cleanup() {
+ }
+
+ @Override
+ public void wipe() {
+ // Delete the calendar associated with this account
+ // CalendarProvider2 does NOT handle selection arguments in deletions
+ mContentResolver.delete(
+ asSyncAdapter(Calendars.CONTENT_URI, mEmailAddress,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
+ Calendars.ACCOUNT_NAME + "=" + DatabaseUtils.sqlEscapeString(mEmailAddress)
+ + " AND " + Calendars.ACCOUNT_TYPE + "="
+ + DatabaseUtils.sqlEscapeString(AccountManagerTypes.TYPE_EXCHANGE), null);
+ // Invalidate our calendar observers
+ ExchangeService.unregisterCalendarObservers();
+ }
+
+ @Override
+ public void sendSyncOptions(Double protocolVersion, Serializer s) throws IOException {
+ setPimSyncOptions(protocolVersion, Eas.FILTER_2_WEEKS, s);
+ }
+
+ @Override
+ public boolean isSyncable() {
+ return ContentResolver.getSyncAutomatically(mAccountManagerAccount,
+ CalendarContract.AUTHORITY);
+ }
+
+ @Override
+ public boolean parse(InputStream is) throws IOException, CommandStatusException {
+ EasCalendarSyncParser p = new EasCalendarSyncParser(is, this);
+ return p.parse();
+ }
+
+ public static Uri asSyncAdapter(Uri uri, String account, String accountType) {
+ return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
+ .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
+ .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).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 {
+ synchronized (sSyncKeyLock) {
+ ContentProviderClient client = mService.mContentResolver
+ .acquireContentProviderClient(CalendarContract.CONTENT_URI);
+ try {
+ byte[] data = SyncStateContract.Helpers.get(
+ client,
+ asSyncAdapter(SyncState.CONTENT_URI, mEmailAddress,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mAccountManagerAccount);
+ if (data == null || data.length == 0) {
+ // Initialize the SyncKey
+ setSyncKey("0", false);
+ return "0";
+ } else {
+ String syncKey = new String(data);
+ userLog("SyncKey retrieved as ", syncKey, " from CalendarProvider");
+ return syncKey;
+ }
+ } catch (RemoteException e) {
+ throw new IOException("Can't get SyncKey from CalendarProvider");
+ }
+ }
+ }
+
+ /**
+ * 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 {
+ synchronized (sSyncKeyLock) {
+ if ("0".equals(syncKey) || !inCommands) {
+ ContentProviderClient client = mService.mContentResolver
+ .acquireContentProviderClient(CalendarContract.CONTENT_URI);
+ try {
+ SyncStateContract.Helpers.set(
+ client,
+ asSyncAdapter(SyncState.CONTENT_URI, mEmailAddress,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mAccountManagerAccount,
+ 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 class EasCalendarSyncParser extends AbstractSyncParser {
+
+ String[] mBindArgument = new String[1];
+ Uri mAccountUri;
+ CalendarOperations mOps = new CalendarOperations();
+
+ public EasCalendarSyncParser(InputStream in, CalendarSyncAdapter adapter)
+ throws IOException {
+ super(in, adapter);
+ setLoggingTag("CalendarParser");
+ mAccountUri = Events.CONTENT_URI;
+ }
+
+ private void addOrganizerToAttendees(CalendarOperations ops, long eventId,
+ String organizerName, String organizerEmail) {
+ // Handle the organizer (who IS an attendee on device, but NOT in EAS)
+ if (organizerName != null || organizerEmail != null) {
+ ContentValues attendeeCv = new ContentValues();
+ if (organizerName != null) {
+ attendeeCv.put(Attendees.ATTENDEE_NAME, organizerName);
+ }
+ if (organizerEmail != null) {
+ attendeeCv.put(Attendees.ATTENDEE_EMAIL, organizerEmail);
+ }
+ attendeeCv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
+ attendeeCv.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
+ attendeeCv.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED);
+ if (eventId < 0) {
+ ops.newAttendee(attendeeCv);
+ } else {
+ ops.updatedAttendee(attendeeCv, eventId);
+ }
+ }
+ }
+
+ /**
+ * Set DTSTART, DTEND, DURATION and EVENT_TIMEZONE as appropriate for the given Event
+ * The follow rules are enforced by CalendarProvider2:
+ * Events that aren't exceptions MUST have either 1) a DTEND or 2) a DURATION
+ * Recurring events (i.e. events with RRULE) must have a DURATION
+ * All-day recurring events MUST have a DURATION that is in the form P<n>D
+ * Other events MAY have a DURATION in any valid form (we use P<n>M)
+ * All-day events MUST have hour, minute, and second = 0; in addition, they must have
+ * the EVENT_TIMEZONE set to UTC
+ * Also, exceptions to all-day events need to have an ORIGINAL_INSTANCE_TIME that has
+ * hour, minute, and second = 0 and be set in UTC
+ * @param cv the ContentValues for the Event
+ * @param startTime the start time for the Event
+ * @param endTime the end time for the Event
+ * @param allDayEvent whether this is an all day event (1) or not (0)
+ */
+ /*package*/ void setTimeRelatedValues(ContentValues cv, long startTime, long endTime,
+ int allDayEvent) {
+ // If there's no startTime, the event will be found to be invalid, so return
+ if (startTime < 0) return;
+ // EAS events can arrive without an end time, but CalendarProvider requires them
+ // so we'll default to 30 minutes; this will be superceded if this is an all-day event
+ if (endTime < 0) endTime = startTime + (30*MINUTES);
+
+ // If this is an all-day event, set hour, minute, and second to zero, and use UTC
+ if (allDayEvent != 0) {
+ startTime = CalendarUtilities.getUtcAllDayCalendarTime(startTime, mLocalTimeZone);
+ endTime = CalendarUtilities.getUtcAllDayCalendarTime(endTime, mLocalTimeZone);
+ String originalTimeZone = cv.getAsString(Events.EVENT_TIMEZONE);
+ cv.put(EVENT_SAVED_TIMEZONE_COLUMN, originalTimeZone);
+ cv.put(Events.EVENT_TIMEZONE, UTC_TIMEZONE.getID());
+ }
+
+ // If this is an exception, and the original was an all-day event, make sure the
+ // original instance time has hour, minute, and second set to zero, and is in UTC
+ if (cv.containsKey(Events.ORIGINAL_INSTANCE_TIME) &&
+ cv.containsKey(Events.ORIGINAL_ALL_DAY)) {
+ Integer ade = cv.getAsInteger(Events.ORIGINAL_ALL_DAY);
+ if (ade != null && ade != 0) {
+ long exceptionTime = cv.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
+ GregorianCalendar cal = new GregorianCalendar(UTC_TIMEZONE);
+ cal.setTimeInMillis(exceptionTime);
+ cal.set(GregorianCalendar.HOUR_OF_DAY, 0);
+ cal.set(GregorianCalendar.MINUTE, 0);
+ cal.set(GregorianCalendar.SECOND, 0);
+ cv.put(Events.ORIGINAL_INSTANCE_TIME, cal.getTimeInMillis());
+ }
+ }
+
+ // Always set DTSTART
+ cv.put(Events.DTSTART, startTime);
+ // For recurring events, set DURATION. Use P<n>D format for all day events
+ if (cv.containsKey(Events.RRULE)) {
+ if (allDayEvent != 0) {
+ cv.put(Events.DURATION, "P" + ((endTime - startTime) / DAYS) + "D");
+ }
+ else {
+ cv.put(Events.DURATION, "P" + ((endTime - startTime) / MINUTES) + "M");
+ }
+ // For other events, set DTEND and LAST_DATE
+ } else {
+ cv.put(Events.DTEND, endTime);
+ cv.put(Events.LAST_DATE, endTime);
+ }
+ }
+
+ 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_ID, serverId);
+ cv.put(Events.HAS_ATTENDEE_DATA, 1);
+ cv.put(Events.SYNC_DATA2, "0");
+
+ int allDayEvent = 0;
+ String organizerName = null;
+ String organizerEmail = null;
+ int eventOffset = -1;
+ int deleteOffset = -1;
+ int busyStatus = CalendarUtilities.BUSY_STATUS_TENTATIVE;
+ int responseType = CalendarUtilities.RESPONSE_TYPE_NONE;
+
+ boolean firstTag = true;
+ long eventId = -1;
+ long startTime = -1;
+ long endTime = -1;
+ TimeZone timeZone = null;
+
+ // Keep track of the attendees; exceptions will need them
+ ArrayList<ContentValues> attendeeValues = new ArrayList<ContentValues>();
+ int reminderMins = -1;
+ String dtStamp = null;
+ boolean organizerAdded = false;
+
+ 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 != null && c.moveToFirst()) {
+ id = c.getLong(0);
+ }
+ } finally {
+ if (c != null) c.close();
+ }
+ if (id > 0) {
+ // DTSTAMP can come first, and we simply need to track it
+ if (tag == Tags.CALENDAR_DTSTAMP) {
+ dtStamp = getValue();
+ continue;
+ } else if (tag == Tags.CALENDAR_ATTENDEES) {
+ // This is an attendees-only update; just
+ // delete/re-add attendees
+ mBindArgument[0] = Long.toString(id);
+ ops.add(new Operation(ContentProviderOperation
+ .newDelete(mAsSyncAdapterAttendees)
+ .withSelection(ATTENDEES_EXCEPT_ORGANIZER, mBindArgument)));
+ eventId = id;
+ } else {
+ // Otherwise, delete the original event and recreate it
+ userLog("Changing (delete/add) event ", serverId);
+ deleteOffset = ops.newDelete(id, serverId);
+ // 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 {
+ // The changed item isn't found. We'll treat this as a new item
+ eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
+ userLog(TAG, "Changed item not found; treating as new.");
+ }
+ } 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();
+ if (allDayEvent != 0 && timeZone != null) {
+ // If the event doesn't start at midnight local time, we won't consider
+ // this an all-day event in the local time zone (this is what OWA does)
+ GregorianCalendar cal = new GregorianCalendar(mLocalTimeZone);
+ cal.setTimeInMillis(startTime);
+ userLog("All-day event arrived in: " + timeZone.getID());
+ if (cal.get(GregorianCalendar.HOUR_OF_DAY) != 0 ||
+ cal.get(GregorianCalendar.MINUTE) != 0) {
+ allDayEvent = 0;
+ userLog("Not an all-day event locally: " + mLocalTimeZone.getID());
+ }
+ }
+ cv.put(Events.ALL_DAY, allDayEvent);
+ break;
+ case Tags.CALENDAR_ATTACHMENTS:
+ attachmentsParser();
+ break;
+ case Tags.CALENDAR_ATTENDEES:
+ // If eventId >= 0, this is an update; otherwise, a new Event
+ attendeeValues = attendeesParser(ops, 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 = CalendarUtilities.tziStringToTimeZone(getValue());
+ if (timeZone == null) {
+ timeZone = mLocalTimeZone;
+ }
+ cv.put(Events.EVENT_TIMEZONE, timeZone.getID());
+ break;
+ case Tags.CALENDAR_START_TIME:
+ startTime = Utility.parseDateTimeToMillis(getValue());
+ break;
+ case Tags.CALENDAR_END_TIME:
+ endTime = Utility.parseDateTimeToMillis(getValue());
+ break;
+ case Tags.CALENDAR_EXCEPTIONS:
+ // For exceptions to show the organizer, the organizer must be added before
+ // we call exceptionsParser
+ addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail);
+ organizerAdded = true;
+ exceptionsParser(ops, cv, attendeeValues, reminderMins, busyStatus,
+ startTime, endTime);
+ break;
+ case Tags.CALENDAR_LOCATION:
+ cv.put(Events.EVENT_LOCATION, getValue());
+ break;
+ case Tags.CALENDAR_RECURRENCE:
+ String rrule = recurrenceParser();
+ 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.ACCESS_LEVEL, encodeVisibility(getValueInt()));
+ break;
+ case Tags.CALENDAR_ORGANIZER_NAME:
+ organizerName = getValue();
+ break;
+ case Tags.CALENDAR_REMINDER_MINS_BEFORE:
+ // Save away whether this tag has content; Exchange 2010 sends an empty tag
+ // rather than not sending one (as with Ex07 and Ex03)
+ boolean hasContent = !noContent;
+ reminderMins = getValueInt();
+ if (hasContent) {
+ ops.newReminder(reminderMins);
+ 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:
+ cv.put(Events.SYNC_DATA2, getValue());
+ break;
+ case Tags.CALENDAR_DTSTAMP:
+ dtStamp = getValue();
+ break;
+ case Tags.CALENDAR_MEETING_STATUS:
+ ops.newExtendedProperty(EXTENDED_PROPERTY_MEETING_STATUS, getValue());
+ break;
+ case Tags.CALENDAR_BUSY_STATUS:
+ // We'll set the user's status in the Attendees table below
+ // Don't set selfAttendeeStatus or CalendarProvider will create a duplicate
+ // attendee!
+ busyStatus = getValueInt();
+ break;
+ case Tags.CALENDAR_RESPONSE_TYPE:
+ // EAS 14+ uses this for the user's response status; we'll use this instead
+ // of busy status, if it appears
+ responseType = getValueInt();
+ break;
+ case Tags.CALENDAR_CATEGORIES:
+ String categories = categoriesParser(ops);
+ if (categories.length() > 0) {
+ ops.newExtendedProperty(EXTENDED_PROPERTY_CATEGORIES, categories);
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+
+ // Enforce CalendarProvider required properties
+ setTimeRelatedValues(cv, startTime, endTime, allDayEvent);
+
+ // If we haven't added the organizer to attendees, do it now
+ if (!organizerAdded) {
+ addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail);
+ }
+
+ // Note that organizerEmail can be null with a DTSTAMP only change from the server
+ boolean selfOrganizer = (mEmailAddress.equals(organizerEmail));
+
+ // Store email addresses of attendees (in a tokenizable string) in ExtendedProperties
+ // If the user is an attendee, set the attendee status using busyStatus (note that the
+ // busyStatus is inherited from the parent unless it's specified in the exception)
+ // Add the insert/update operation for each attendee (based on whether it's add/change)
+ int numAttendees = attendeeValues.size();
+ if (numAttendees > MAX_SYNCED_ATTENDEES) {
+ // Indicate that we've redacted attendees. If we're the organizer, disable edit
+ // by setting organizerEmail to a bogus value and by setting the upsync prohibited
+ // extended properly.
+ // Note that we don't set ANY attendees if we're in this branch; however, the
+ // organizer has already been included above, and WILL show up (which is good)
+ if (eventId < 0) {
+ ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "1");
+ if (selfOrganizer) {
+ ops.newExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "1");
+ }
+ } else {
+ ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "1", eventId);
+ if (selfOrganizer) {
+ ops.updatedExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "1",
+ eventId);
+ }
+ }
+ if (selfOrganizer) {
+ organizerEmail = BOGUS_ORGANIZER_EMAIL;
+ cv.put(Events.ORGANIZER, organizerEmail);
+ }
+ // Tell UI that we don't have any attendees
+ cv.put(Events.HAS_ATTENDEE_DATA, "0");
+ mService.userLog("Maximum number of attendees exceeded; redacting");
+ } else if (numAttendees > 0) {
+ StringBuilder sb = new StringBuilder();
+ for (ContentValues attendee: attendeeValues) {
+ String attendeeEmail = attendee.getAsString(Attendees.ATTENDEE_EMAIL);
+ sb.append(attendeeEmail);
+ sb.append(ATTENDEE_TOKENIZER_DELIMITER);
+ if (mEmailAddress.equalsIgnoreCase(attendeeEmail)) {
+ int attendeeStatus;
+ // We'll use the response type (EAS 14), if we've got one; otherwise, we'll
+ // try to infer it from busy status
+ if (responseType != CalendarUtilities.RESPONSE_TYPE_NONE) {
+ attendeeStatus =
+ CalendarUtilities.attendeeStatusFromResponseType(responseType);
+ } else if (!update) {
+ // For new events in EAS < 14, we have no idea what the busy status
+ // means, so we show "none", allowing the user to select an option.
+ attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
+ } else {
+ // For updated events, we'll try to infer the attendee status from the
+ // busy status
+ attendeeStatus =
+ CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus);
+ }
+ attendee.put(Attendees.ATTENDEE_STATUS, attendeeStatus);
+ // If we're an attendee, save away our initial attendee status in the
+ // event's ExtendedProperties (we look for differences between this and
+ // the user's current attendee status to determine whether an email needs
+ // to be sent to the organizer)
+ // organizerEmail will be null in the case that this is an attendees-only
+ // change from the server
+ if (organizerEmail == null ||
+ !organizerEmail.equalsIgnoreCase(attendeeEmail)) {
+ if (eventId < 0) {
+ ops.newExtendedProperty(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS,
+ Integer.toString(attendeeStatus));
+ } else {
+ ops.updatedExtendedProperty(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS,
+ Integer.toString(attendeeStatus), eventId);
+
+ }
+ }
+ }
+ if (eventId < 0) {
+ ops.newAttendee(attendee);
+ } else {
+ ops.updatedAttendee(attendee, eventId);
+ }
+ }
+ if (eventId < 0) {
+ ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES, sb.toString());
+ ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "0");
+ ops.newExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "0");
+ } else {
+ ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES, sb.toString(),
+ eventId);
+ ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "0", eventId);
+ ops.updatedExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "0", eventId);
+ }
+ }
+
+ // Put the real event in the proper place in the ops ArrayList
+ if (eventOffset >= 0) {
+ // Store away the DTSTAMP here
+ if (dtStamp != null) {
+ ops.newExtendedProperty(EXTENDED_PROPERTY_DTSTAMP, dtStamp);
+ }
+
+ if (isValidEventValues(cv)) {
+ ops.set(eventOffset,
+ new Operation(ContentProviderOperation
+ .newInsert(mAsSyncAdapterEvents).withValues(cv)));
+ } else {
+ // If we can't add this event (it's invalid), remove all of the inserts
+ // we've built for it
+ int cnt = ops.mCount - eventOffset;
+ userLog(TAG, "Removing " + cnt + " inserts from mOps");
+ for (int i = 0; i < cnt; i++) {
+ ops.remove(eventOffset);
+ }
+ ops.mCount = eventOffset;
+ // If this is a change, we need to also remove the deletion that comes
+ // before the addition
+ if (deleteOffset >= 0) {
+ // Remove the deletion
+ ops.remove(deleteOffset);
+ // And the deletion of exceptions
+ ops.remove(deleteOffset);
+ userLog(TAG, "Removing deletion ops from mOps");
+ ops.mCount = deleteOffset;
+ }
+ }
+ }
+ // Mark the end of the event
+ addSeparatorOperation(ops, Events.CONTENT_URI);
+ }
+
+ private void logEventColumns(ContentValues cv, String reason) {
+ if (Eas.USER_LOG) {
+ StringBuilder sb =
+ new StringBuilder("Event invalid, " + reason + ", skipping: Columns = ");
+ for (Entry<String, Object> entry: cv.valueSet()) {
+ sb.append(entry.getKey());
+ sb.append('/');
+ }
+ userLog(TAG, sb.toString());
+ }
+ }
+
+ /*package*/ boolean isValidEventValues(ContentValues cv) {
+ boolean isException = cv.containsKey(Events.ORIGINAL_INSTANCE_TIME);
+ // All events require DTSTART
+ if (!cv.containsKey(Events.DTSTART)) {
+ logEventColumns(cv, "DTSTART missing");
+ return false;
+ // If we're a top-level event, we must have _SYNC_DATA (uid)
+ } else if (!isException && !cv.containsKey(Events.SYNC_DATA2)) {
+ logEventColumns(cv, "_SYNC_DATA missing");
+ return false;
+ // We must also have DTEND or DURATION if we're not an exception
+ } else if (!isException && !cv.containsKey(Events.DTEND) &&
+ !cv.containsKey(Events.DURATION)) {
+ logEventColumns(cv, "DTEND/DURATION missing");
+ return false;
+ // Exceptions require DTEND
+ } else if (isException && !cv.containsKey(Events.DTEND)) {
+ logEventColumns(cv, "Exception missing DTEND");
+ return false;
+ // If this is a recurrence, we need a DURATION (in days if an all-day event)
+ } else if (cv.containsKey(Events.RRULE)) {
+ String duration = cv.getAsString(Events.DURATION);
+ if (duration == null) return false;
+ if (cv.containsKey(Events.ALL_DAY)) {
+ Integer ade = cv.getAsInteger(Events.ALL_DAY);
+ if (ade != null && ade != 0 && !duration.endsWith("D")) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ public String recurrenceParser() 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,
+ ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus,
+ long startTime, long endTime) throws IOException {
+ ContentValues cv = new ContentValues();
+ cv.put(Events.CALENDAR_ID, mCalendarId);
+
+ // 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.getAsString(Events.DESCRIPTION));
+ cv.put(Events.ORIGINAL_ALL_DAY, parentCv.getAsInteger(Events.ALL_DAY));
+ cv.put(Events.EVENT_LOCATION, parentCv.getAsString(Events.EVENT_LOCATION));
+ cv.put(Events.ACCESS_LEVEL, parentCv.getAsString(Events.ACCESS_LEVEL));
+ cv.put(Events.EVENT_TIMEZONE, parentCv.getAsString(Events.EVENT_TIMEZONE));
+ // Exceptions should always have this set to zero, since EAS has no concept of
+ // separate attendee lists for exceptions; if we fail to do this, then the UI will
+ // allow the user to change attendee data, and this change would never get reflected
+ // on the server.
+ cv.put(Events.HAS_ATTENDEE_DATA, 0);
+
+ int allDayEvent = 0;
+
+ // This column is the key that links the exception to the serverId
+ cv.put(Events.ORIGINAL_SYNC_ID, parentCv.getAsString(Events._SYNC_ID));
+
+ String exceptionStartTime = "_noStartTime";
+ while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_ATTACHMENTS:
+ attachmentsParser();
+ break;
+ case Tags.CALENDAR_EXCEPTION_START_TIME:
+ exceptionStartTime = getValue();
+ cv.put(Events.ORIGINAL_INSTANCE_TIME,
+ Utility.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:
+ allDayEvent = getValueInt();
+ cv.put(Events.ALL_DAY, allDayEvent);
+ 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:
+ startTime = Utility.parseDateTimeToMillis(getValue());
+ break;
+ case Tags.CALENDAR_END_TIME:
+ endTime = Utility.parseDateTimeToMillis(getValue());
+ break;
+ case Tags.CALENDAR_LOCATION:
+ cv.put(Events.EVENT_LOCATION, getValue());
+ break;
+ case Tags.CALENDAR_RECURRENCE:
+ String rrule = recurrenceParser();
+ 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.ACCESS_LEVEL, encodeVisibility(getValueInt()));
+ break;
+ case Tags.CALENDAR_BUSY_STATUS:
+ busyStatus = getValueInt();
+ // Don't set selfAttendeeStatus or CalendarProvider will create a duplicate
+ // attendee!
+ 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_REMINDER_MINS_BEFORE:
+// ops.newReminder(getValueInt());
+// break;
+ 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);
+
+ // Enforce CalendarProvider required properties
+ setTimeRelatedValues(cv, startTime, endTime, allDayEvent);
+
+ // Don't insert an invalid exception event
+ if (!isValidEventValues(cv)) return;
+
+ // Add the exception insert
+ int exceptionStart = ops.mCount;
+ ops.newException(cv);
+ // Also add the attendees, because they need to be copied over from the parent event
+ boolean attendeesRedacted = false;
+ if (attendeeValues != null) {
+ for (ContentValues attValues: attendeeValues) {
+ // If this is the user, use his busy status for attendee status
+ String attendeeEmail = attValues.getAsString(Attendees.ATTENDEE_EMAIL);
+ // Note that the exception at which we surpass the redaction limit might have
+ // any number of attendees shown; since this is an edge case and a workaround,
+ // it seems to be an acceptable implementation
+ if (mEmailAddress.equalsIgnoreCase(attendeeEmail)) {
+ attValues.put(Attendees.ATTENDEE_STATUS,
+ CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus));
+ ops.newAttendee(attValues, exceptionStart);
+ } else if (ops.size() < MAX_OPS_BEFORE_EXCEPTION_ATTENDEE_REDACTION) {
+ ops.newAttendee(attValues, exceptionStart);
+ } else {
+ attendeesRedacted = true;
+ }
+ }
+ }
+ // And add the parent's reminder value
+ if (reminderMins > 0) {
+ ops.newReminder(reminderMins, exceptionStart);
+ }
+ if (attendeesRedacted) {
+ mService.userLog("Attendees redacted in this exception");
+ }
+ }
+
+ private int encodeVisibility(int easVisibility) {
+ int visibility = 0;
+ switch(easVisibility) {
+ case 0:
+ visibility = Events.ACCESS_DEFAULT;
+ break;
+ case 1:
+ visibility = Events.ACCESS_PUBLIC;
+ break;
+ case 2:
+ visibility = Events.ACCESS_PRIVATE;
+ break;
+ case 3:
+ visibility = Events.ACCESS_CONFIDENTIAL;
+ break;
+ }
+ return visibility;
+ }
+
+ private void exceptionsParser(CalendarOperations ops, ContentValues cv,
+ ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus,
+ long startTime, long endTime) throws IOException {
+ while (nextTag(Tags.CALENDAR_EXCEPTIONS) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_EXCEPTION:
+ exceptionParser(ops, cv, attendeeValues, reminderMins, busyStatus,
+ startTime, endTime);
+ 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);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ return categories.toString();
+ }
+
+ /**
+ * For now, we ignore (but still have to parse) event attachments; these are new in EAS 14
+ */
+ private void attachmentsParser() throws IOException {
+ while (nextTag(Tags.CALENDAR_ATTACHMENTS) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_ATTACHMENT:
+ skipParser(Tags.CALENDAR_ATTACHMENT);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private ArrayList<ContentValues> attendeesParser(CalendarOperations ops, long eventId)
+ throws IOException {
+ int attendeeCount = 0;
+ ArrayList<ContentValues> attendeeValues = new ArrayList<ContentValues>();
+ while (nextTag(Tags.CALENDAR_ATTENDEES) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_ATTENDEE:
+ ContentValues cv = attendeeParser(ops, eventId);
+ // If we're going to redact these attendees anyway, let's avoid unnecessary
+ // memory pressure, and not keep them around
+ // We still need to parse them all, however
+ attendeeCount++;
+ // Allow one more than MAX_ATTENDEES, so that the check for "too many" will
+ // succeed in addEvent
+ if (attendeeCount <= (MAX_SYNCED_ATTENDEES+1)) {
+ attendeeValues.add(cv);
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+ return attendeeValues;
+ }
+
+ private ContentValues 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);
+ return cv;
+ }
+
+ 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) {
+ return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_AND_CALENDAR_ID,
+ new String[] {serverId, mCalendarIdString}, 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), serverId);
+ }
+ } 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(new Operation(SyncStateContract.Helpers.newSetOperation(
+ asSyncAdapter(SyncState.CONTENT_URI, mEmailAddress,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
+ mAccountManagerAccount,
+ mMailbox.mSyncKey.getBytes())));
+
+ // We need to send cancellations now, because the Event won't exist after the commit
+ for (long eventId: mSendCancelIdList) {
+ EmailContent.Message msg;
+ try {
+ msg = CalendarUtilities.createMessageForEventId(mContext, eventId,
+ EmailContent.Message.FLAG_OUTGOING_MEETING_CANCEL, null,
+ mAccount);
+ } catch (RemoteException e) {
+ // Nothing to do here; the Event may no longer exist
+ continue;
+ }
+ if (msg != null) {
+ EasOutboxService.sendMessage(mContext, mAccount.mId, msg);
+ }
+ }
+
+ // Execute our CPO's safely
+ try {
+ mOps.mResults = safeExecute(CalendarContract.AUTHORITY, mOps);
+ } catch (RemoteException e) {
+ throw new IOException("Remote exception caught; will retry");
+ }
+
+ if (mOps.mResults != null) {
+ // Clear dirty and mark flags for updates sent to server
+ if (!mUploadedIdList.isEmpty()) {
+ ContentValues cv = new ContentValues();
+ cv.put(Events.DIRTY, 0);
+ cv.put(EVENT_SYNC_MARK, "0");
+ for (long eventId : mUploadedIdList) {
+ mContentResolver.update(
+ asSyncAdapter(
+ ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
+ mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv,
+ null, null);
+ }
+ }
+ // Delete events marked for deletion
+ if (!mDeletedIdList.isEmpty()) {
+ for (long eventId : mDeletedIdList) {
+ mContentResolver.delete(
+ asSyncAdapter(
+ ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
+ mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null,
+ null);
+ }
+ }
+ // Send any queued up email (invitations replies, etc.)
+ for (Message msg: mOutgoingMailList) {
+ EasOutboxService.sendMessage(mContext, mAccount.mId, msg);
+ }
+ }
+ }
+
+ 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);
+ cv.put(Events.SYNC_DATA2, clientId);
+ long id = c.getLong(0);
+ // Write the serverId into the Event
+ mOps.add(new Operation(ContentProviderOperation
+ .newUpdate(ContentUris.withAppendedId(mAsSyncAdapterEvents, id))
+ .withValues(cv)));
+ 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();
+ }
+ }
+ }
+
+ protected class CalendarOperations extends ArrayList<Operation> {
+ private static final long serialVersionUID = 1L;
+ public int mCount = 0;
+ private ContentProviderResult[] mResults = null;
+ private int mEventStart = 0;
+
+ @Override
+ public boolean add(Operation op) {
+ super.add(op);
+ mCount++;
+ return true;
+ }
+
+ public int newEvent(Operation op) {
+ mEventStart = mCount;
+ add(op);
+ return mEventStart;
+ }
+
+ public int newDelete(long id, String serverId) {
+ int offset = mCount;
+ delete(id, serverId);
+ return offset;
+ }
+
+ public void newAttendee(ContentValues cv) {
+ newAttendee(cv, mEventStart);
+ }
+
+ public void newAttendee(ContentValues cv, int eventStart) {
+ add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterAttendees)
+ .withValues(cv),
+ Attendees.EVENT_ID,
+ eventStart));
+ }
+
+ public void updatedAttendee(ContentValues cv, long id) {
+ cv.put(Attendees.EVENT_ID, id);
+ add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterAttendees)
+ .withValues(cv)));
+ }
+
+ public void newException(ContentValues cv) {
+ add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterEvents)
+ .withValues(cv)));
+ }
+
+ public void newExtendedProperty(String name, String value) {
+ add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterExtendedProperties)
+ .withValue(ExtendedProperties.NAME, name)
+ .withValue(ExtendedProperties.VALUE, value),
+ ExtendedProperties.EVENT_ID,
+ mEventStart));
+ }
+
+ public void updatedExtendedProperty(String name, String value, long id) {
+ // Find an existing ExtendedProperties row for this event and property name
+ Cursor c = mService.mContentResolver.query(ExtendedProperties.CONTENT_URI,
+ EXTENDED_PROPERTY_PROJECTION, EVENT_ID_AND_NAME,
+ new String[] {Long.toString(id), name}, null);
+ long extendedPropertyId = -1;
+ // If there is one, capture its _id
+ if (c != null) {
+ try {
+ if (c.moveToFirst()) {
+ extendedPropertyId = c.getLong(EXTENDED_PROPERTY_ID);
+ }
+ } finally {
+ c.close();
+ }
+ }
+ // Either do an update or an insert, depending on whether one
+ // already exists
+ if (extendedPropertyId >= 0) {
+ add(new Operation(ContentProviderOperation
+ .newUpdate(
+ ContentUris.withAppendedId(mAsSyncAdapterExtendedProperties,
+ extendedPropertyId))
+ .withValue(ExtendedProperties.VALUE, value)));
+ } else {
+ newExtendedProperty(name, value);
+ }
+ }
+
+ public void newReminder(int mins, int eventStart) {
+ add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterReminders)
+ .withValue(Reminders.MINUTES, mins)
+ .withValue(Reminders.METHOD, Reminders.METHOD_ALERT),
+ ExtendedProperties.EVENT_ID,
+ eventStart));
+ }
+
+ public void newReminder(int mins) {
+ newReminder(mins, mEventStart);
+ }
+
+ public void delete(long id, String syncId) {
+ add(new Operation(ContentProviderOperation.newDelete(
+ ContentUris.withAppendedId(mAsSyncAdapterEvents, id))));
+ // Delete the exceptions for this Event (CalendarProvider doesn't do this)
+ add(new Operation(ContentProviderOperation
+ .newDelete(mAsSyncAdapterEvents)
+ .withSelection(Events.ORIGINAL_SYNC_ID + "=?", new String[] {syncId})));
+ }
+ }
+
+ private String decodeVisibility(int visibility) {
+ int easVisibility = 0;
+ switch(visibility) {
+ case Events.ACCESS_DEFAULT:
+ easVisibility = 0;
+ break;
+ case Events.ACCESS_PUBLIC:
+ easVisibility = 1;
+ break;
+ case Events.ACCESS_PRIVATE:
+ easVisibility = 2;
+ break;
+ case Events.ACCESS_CONFIDENTIAL:
+ easVisibility = 3;
+ break;
+ }
+ return Integer.toString(easVisibility);
+ }
+
+ private int getInt(ContentValues cv, String column) {
+ Integer i = cv.getAsInteger(column);
+ if (i == null) return 0;
+ return i;
+ }
+
+ 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();
+ final boolean isException = (clientId == null);
+ boolean hasAttendees = false;
+ final boolean isChange = entityValues.containsKey(Events._SYNC_ID);
+ final Double version = mService.mProtocolVersionDouble;
+ final boolean allDay =
+ CalendarUtilities.getIntegerValueAsBoolean(entityValues, Events.ALL_DAY);
+
+ // NOTE: Exchange 2003 (EAS 2.5) seems to require the "exception deleted" and "exception
+ // start time" data before other data in exceptions. Failure to do so results in a
+ // status 6 error during sync
+ if (isException) {
+ // Send exception deleted flag if necessary
+ Integer deleted = entityValues.getAsInteger(Events.DELETED);
+ boolean isDeleted = deleted != null && deleted == 1;
+ Integer eventStatus = entityValues.getAsInteger(Events.STATUS);
+ boolean isCanceled = eventStatus != null && eventStatus.equals(Events.STATUS_CANCELED);
+ if (isDeleted || isCanceled) {
+ s.data(Tags.CALENDAR_EXCEPTION_IS_DELETED, "1");
+ // If we're deleted, the UI will continue to show this exception until we mark
+ // it canceled, so we'll do that here...
+ if (isDeleted && !isCanceled) {
+ final long eventId = entityValues.getAsLong(Events._ID);
+ ContentValues cv = new ContentValues();
+ cv.put(Events.STATUS, Events.STATUS_CANCELED);
+ mService.mContentResolver.update(
+ asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
+ mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv, null,
+ null);
+ }
+ } else {
+ s.data(Tags.CALENDAR_EXCEPTION_IS_DELETED, "0");
+ }
+
+ // TODO Add reminders to exceptions (allow them to be specified!)
+ Long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
+ if (originalTime != null) {
+ final boolean originalAllDay =
+ CalendarUtilities.getIntegerValueAsBoolean(entityValues,
+ Events.ORIGINAL_ALL_DAY);
+ if (originalAllDay) {
+ // For all day events, we need our local all-day time
+ originalTime =
+ CalendarUtilities.getLocalAllDayCalendarTime(originalTime, mLocalTimeZone);
+ }
+ s.data(Tags.CALENDAR_EXCEPTION_START_TIME,
+ CalendarUtilities.millisToEasDateTime(originalTime));
+ } else {
+ // Illegal; what should we do?
+ }
+ }
+
+ // Get the event's time zone
+ String timeZoneName =
+ entityValues.getAsString(allDay ? EVENT_SAVED_TIMEZONE_COLUMN : Events.EVENT_TIMEZONE);
+ if (timeZoneName == null) {
+ timeZoneName = mLocalTimeZone.getID();
+ }
+ TimeZone eventTimeZone = TimeZone.getTimeZone(timeZoneName);
+
+ if (!isException) {
+ // A time zone is required in all EAS events; we'll use the default if none is set
+ // Exchange 2003 seems to require this first... :-)
+ String timeZone = CalendarUtilities.timeZoneToTziString(eventTimeZone);
+ s.data(Tags.CALENDAR_TIME_ZONE, timeZone);
+ }
+
+ s.data(Tags.CALENDAR_ALL_DAY_EVENT, allDay ? "1" : "0");
+
+ // DTSTART is always supplied
+ long startTime = entityValues.getAsLong(Events.DTSTART);
+ // Determine endTime; it's either provided as DTEND or we calculate using DURATION
+ // If no DURATION is provided, we default to one hour
+ long endTime;
+ if (entityValues.containsKey(Events.DTEND)) {
+ endTime = entityValues.getAsLong(Events.DTEND);
+ } else {
+ long durationMillis = HOURS;
+ if (entityValues.containsKey(Events.DURATION)) {
+ Duration duration = new Duration();
+ try {
+ duration.parse(entityValues.getAsString(Events.DURATION));
+ durationMillis = duration.getMillis();
+ } catch (DateException e) {
+ // Can't do much about this; use the default (1 hour)
+ }
+ }
+ endTime = startTime + durationMillis;
+ }
+ if (allDay) {
+ TimeZone tz = mLocalTimeZone;
+ startTime = CalendarUtilities.getLocalAllDayCalendarTime(startTime, tz);
+ endTime = CalendarUtilities.getLocalAllDayCalendarTime(endTime, tz);
+ }
+ s.data(Tags.CALENDAR_START_TIME, CalendarUtilities.millisToEasDateTime(startTime));
+ s.data(Tags.CALENDAR_END_TIME, CalendarUtilities.millisToEasDateTime(endTime));
+
+ s.data(Tags.CALENDAR_DTSTAMP,
+ CalendarUtilities.millisToEasDateTime(System.currentTimeMillis()));
+
+ String loc = entityValues.getAsString(Events.EVENT_LOCATION);
+ if (!TextUtils.isEmpty(loc)) {
+ if (version < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+ // EAS 2.5 doesn't like bare line feeds
+ loc = Utility.replaceBareLfWithCrlf(loc);
+ }
+ s.data(Tags.CALENDAR_LOCATION, loc);
+ }
+ s.writeStringValue(entityValues, Events.TITLE, Tags.CALENDAR_SUBJECT);
+
+ if (version >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+ s.start(Tags.BASE_BODY);
+ s.data(Tags.BASE_TYPE, "1");
+ s.writeStringValue(entityValues, Events.DESCRIPTION, Tags.BASE_DATA);
+ s.end();
+ } else {
+ // EAS 2.5 doesn't like bare line feeds
+ s.writeStringValue(entityValues, Events.DESCRIPTION, Tags.CALENDAR_BODY);
+ }
+
+ if (!isException) {
+ // For Exchange 2003, only upsync if the event is new
+ if ((version >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) || !isChange) {
+ 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();
+ // The earliest of the reminders for this Event; we can only send one reminder...
+ int earliestReminder = -1;
+ for (NamedContentValues ncv: subValues) {
+ Uri ncvUri = ncv.uri;
+ ContentValues ncvValues = ncv.values;
+ if (ncvUri.equals(ExtendedProperties.CONTENT_URI)) {
+ String propertyName =
+ ncvValues.getAsString(ExtendedProperties.NAME);
+ String propertyValue =
+ ncvValues.getAsString(ExtendedProperties.VALUE);
+ if (TextUtils.isEmpty(propertyValue)) {
+ continue;
+ }
+ if (propertyName.equals(EXTENDED_PROPERTY_CATEGORIES)) {
+ // Send all the categories back to the server
+ // We've saved them as a String of delimited tokens
+ StringTokenizer st =
+ new StringTokenizer(propertyValue, 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)) {
+ Integer mins = ncvValues.getAsInteger(Reminders.MINUTES);
+ if (mins != null) {
+ // -1 means "default", which for Exchange, is 30
+ if (mins < 0) {
+ mins = 30;
+ }
+ // Save this away if it's the earliest reminder (greatest minutes)
+ if (mins > earliestReminder) {
+ earliestReminder = mins;
+ }
+ }
+ }
+ }
+
+ // If we have a reminder, send it to the server
+ if (earliestReminder >= 0) {
+ s.data(Tags.CALENDAR_REMINDER_MINS_BEFORE, Integer.toString(earliestReminder));
+ }
+
+ // We've got to send a UID, unless this is an exception. If the event is new, we've
+ // generated one; if not, we should have gotten one from extended properties.
+ if (clientId != null) {
+ s.data(Tags.CALENDAR_UID, clientId);
+ }
+
+ // Handle attendee data here; keep track of organizer and stream it afterward
+ String organizerName = null;
+ String organizerEmail = 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 there's no relationship, we can't create this for EAS
+ // Similarly, we need an attendee email for each invitee
+ if (relationship != null && ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
+ // Organizer isn't among attendees in EAS
+ if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
+ organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
+ organizerEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
+ continue;
+ }
+ if (!hasAttendees) {
+ s.start(Tags.CALENDAR_ATTENDEES);
+ hasAttendees = true;
+ }
+ s.start(Tags.CALENDAR_ATTENDEE);
+ String attendeeEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
+ String attendeeName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
+ if (attendeeName == null) {
+ attendeeName = attendeeEmail;
+ }
+ s.data(Tags.CALENDAR_ATTENDEE_NAME, attendeeName);
+ s.data(Tags.CALENDAR_ATTENDEE_EMAIL, attendeeEmail);
+ if (version >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+ s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1"); // Required
+ }
+ s.end(); // Attendee
+ }
+ }
+ }
+ if (hasAttendees) {
+ s.end(); // Attendees
+ }
+
+ // Get busy status from Attendees table
+ long eventId = entityValues.getAsLong(Events._ID);
+ int busyStatus = CalendarUtilities.BUSY_STATUS_TENTATIVE;
+ Cursor c = mService.mContentResolver.query(
+ asSyncAdapter(Attendees.CONTENT_URI, mEmailAddress,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
+ ATTENDEE_STATUS_PROJECTION, EVENT_AND_EMAIL,
+ new String[] {Long.toString(eventId), mEmailAddress}, null);
+ if (c != null) {
+ try {
+ if (c.moveToFirst()) {
+ busyStatus = CalendarUtilities.busyStatusFromAttendeeStatus(
+ c.getInt(ATTENDEE_STATUS_COLUMN_STATUS));
+ }
+ } finally {
+ c.close();
+ }
+ }
+ s.data(Tags.CALENDAR_BUSY_STATUS, Integer.toString(busyStatus));
+
+ // Meeting status, 0 = appointment, 1 = meeting, 3 = attendee
+ if (mEmailAddress.equalsIgnoreCase(organizerEmail)) {
+ s.data(Tags.CALENDAR_MEETING_STATUS, hasAttendees ? "1" : "0");
+ } else {
+ s.data(Tags.CALENDAR_MEETING_STATUS, "3");
+ }
+
+ // For Exchange 2003, only upsync if the event is new
+ if (((version >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) || !isChange) &&
+ organizerName != null) {
+ s.data(Tags.CALENDAR_ORGANIZER_NAME, organizerName);
+ }
+
+ // NOTE: Sensitivity must NOT be sent to the server for exceptions in Exchange 2003
+ // The result will be a status 6 failure during sync
+ Integer visibility = entityValues.getAsInteger(Events.ACCESS_LEVEL);
+ if (visibility != null) {
+ s.data(Tags.CALENDAR_SENSITIVITY, decodeVisibility(visibility));
+ } else {
+ // Default to private if not set
+ s.data(Tags.CALENDAR_SENSITIVITY, "1");
+ }
+ }
+ }
+
+ /**
+ * Convenience method for sending an email to the organizer declining the meeting
+ * @param entity
+ * @param clientId
+ */
+ private void sendDeclinedEmail(Entity entity, String clientId) {
+ Message msg =
+ CalendarUtilities.createMessageForEntity(mContext, entity,
+ Message.FLAG_OUTGOING_MEETING_DECLINE, clientId, mAccount);
+ if (msg != null) {
+ userLog("Queueing declined response to " + msg.mTo);
+ mOutgoingMailList.add(msg);
+ }
+ }
+
+ @Override
+ public boolean sendLocalChanges(Serializer s) throws IOException {
+ ContentResolver cr = mService.mContentResolver;
+
+ 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
+ ArrayList<Long> orphanedExceptions = new ArrayList<Long>();
+ Cursor c = cr.query(Events.CONTENT_URI, ORIGINAL_EVENT_PROJECTION,
+ DIRTY_EXCEPTION_IN_CALENDAR, mCalendarIdArgument, null);
+ try {
+ ContentValues cv = new ContentValues();
+ // We use _sync_mark here to distinguish dirty parents from parents with dirty
+ // exceptions
+ cv.put(EVENT_SYNC_MARK, "1");
+ while (c.moveToNext()) {
+ // Mark the parents of dirty exceptions
+ long parentId = c.getLong(0);
+ int cnt = cr.update(
+ asSyncAdapter(Events.CONTENT_URI, mEmailAddress,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv,
+ EVENT_ID_AND_CALENDAR_ID, new String[] {
+ Long.toString(parentId), mCalendarIdString
+ });
+ // Keep track of any orphaned exceptions
+ if (cnt == 0) {
+ orphanedExceptions.add(c.getLong(1));
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ // Delete any orphaned exceptions
+ for (long orphan : orphanedExceptions) {
+ userLog(TAG, "Deleted orphaned exception: " + orphan);
+ cr.delete(
+ asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, orphan),
+ mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null, null);
+ }
+ orphanedExceptions.clear();
+
+ // Now we can go through dirty/marked top-level events and send them
+ // back to the server
+ EntityIterator eventIterator = EventsEntity.newEntityIterator(cr.query(
+ asSyncAdapter(Events.CONTENT_URI, mEmailAddress,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null,
+ DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR, mCalendarIdArgument, null), cr);
+ ContentValues cidValues = new ContentValues();
+
+ try {
+ boolean first = true;
+ while (eventIterator.hasNext()) {
+ Entity entity = eventIterator.next();
+
+ // For each of these entities, create the change commands
+ ContentValues entityValues = entity.getEntityValues();
+ String serverId = entityValues.getAsString(Events._SYNC_ID);
+
+ // We first need to check whether we can upsync this event; our test for this
+ // is currently the value of EXTENDED_PROPERTY_ATTENDEES_REDACTED
+ // If this is set to "1", we can't upsync the event
+ for (NamedContentValues ncv: entity.getSubValues()) {
+ if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
+ ContentValues ncvValues = ncv.values;
+ if (ncvValues.getAsString(ExtendedProperties.NAME).equals(
+ EXTENDED_PROPERTY_UPSYNC_PROHIBITED)) {
+ if ("1".equals(ncvValues.getAsString(ExtendedProperties.VALUE))) {
+ // Make sure we mark this to clear the dirty flag
+ mUploadedIdList.add(entityValues.getAsLong(Events._ID));
+ continue;
+ }
+ }
+ }
+ }
+
+ // Find our uid in the entity; otherwise create one
+ String clientId = entityValues.getAsString(Events.SYNC_DATA2);
+ if (clientId == null) {
+ clientId = UUID.randomUUID().toString();
+ }
+
+ // EAS 2.5 needs: BusyStatus DtStamp EndTime Sensitivity StartTime TimeZone UID
+ // We can generate all but what we're testing for below
+ String organizerEmail = entityValues.getAsString(Events.ORGANIZER);
+ boolean selfOrganizer = organizerEmail.equalsIgnoreCase(mEmailAddress);
+
+ if (!entityValues.containsKey(Events.DTSTART)
+ || (!entityValues.containsKey(Events.DURATION) &&
+ !entityValues.containsKey(Events.DTEND))
+ || organizerEmail == null) {
+ continue;
+ }
+
+ if (first) {
+ s.start(Tags.SYNC_COMMANDS);
+ userLog("Sending Calendar changes to the server");
+ first = false;
+ }
+ long eventId = entityValues.getAsLong(Events._ID);
+ 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_DATA2, clientId);
+ cidValues.put(EVENT_SYNC_VERSION, "0");
+ cr.update(
+ asSyncAdapter(
+ ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
+ mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
+ 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(eventId);
+ if (selfOrganizer) {
+ mSendCancelIdList.add(eventId);
+ } else {
+ sendDeclinedEmail(entity, clientId);
+ }
+ continue;
+ }
+ userLog("Upsync change to event with serverId: " + serverId);
+ // Get the current version
+ String version = entityValues.getAsString(EVENT_SYNC_VERSION);
+ // This should never be null, but catch this error anyway
+ // Version should be "0" when we create the event, so use that
+ if (version == null) {
+ version = "0";
+ } else {
+ // Increment and save
+ try {
+ version = Integer.toString((Integer.parseInt(version) + 1));
+ } catch (Exception e) {
+ // Handle the case in which someone writes a non-integer here;
+ // shouldn't happen, but we don't want to kill the sync for his
+ version = "0";
+ }
+ }
+ cidValues.put(EVENT_SYNC_VERSION, version);
+ // Also save in entityValues so that we send it this time around
+ entityValues.put(EVENT_SYNC_VERSION, version);
+ cr.update(
+ asSyncAdapter(
+ ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
+ mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
+ cidValues, null, null);
+ 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 exIterator = EventsEntity.newEntityIterator(cr.query(
+ asSyncAdapter(Events.CONTENT_URI, mEmailAddress,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null,
+ ORIGINAL_EVENT_AND_CALENDAR, new String[] {
+ serverId, mCalendarIdString
+ }, null), cr);
+ boolean exFirst = true;
+ while (exIterator.hasNext()) {
+ Entity exEntity = exIterator.next();
+ if (exFirst) {
+ s.start(Tags.CALENDAR_EXCEPTIONS);
+ exFirst = false;
+ }
+ s.start(Tags.CALENDAR_EXCEPTION);
+ sendEvent(exEntity, null, s);
+ ContentValues exValues = exEntity.getEntityValues();
+ if (getInt(exValues, Events.DIRTY) == 1) {
+ // This is a new/updated exception, so we've got to notify our
+ // attendees about it
+ long exEventId = exValues.getAsLong(Events._ID);
+ int flag;
+
+ // Copy subvalues into the exception; otherwise, we won't see the
+ // attendees when preparing the message
+ for (NamedContentValues ncv: entity.getSubValues()) {
+ exEntity.addSubValue(ncv.uri, ncv.values);
+ }
+
+ if ((getInt(exValues, Events.DELETED) == 1) ||
+ (getInt(exValues, Events.STATUS) ==
+ Events.STATUS_CANCELED)) {
+ flag = Message.FLAG_OUTGOING_MEETING_CANCEL;
+ if (!selfOrganizer) {
+ // Send a cancellation notice to the organizer
+ // Since CalendarProvider2 sets the organizer of exceptions
+ // to the user, we have to reset it first to the original
+ // organizer
+ exValues.put(Events.ORGANIZER,
+ entityValues.getAsString(Events.ORGANIZER));
+ sendDeclinedEmail(exEntity, clientId);
+ }
+ } else {
+ flag = Message.FLAG_OUTGOING_MEETING_INVITE;
+ }
+ // Add the eventId of the exception to the uploaded id list, so that
+ // the dirty/mark bits are cleared
+ mUploadedIdList.add(exEventId);
+
+ // Copy version so the ics attachment shows the proper sequence #
+ exValues.put(EVENT_SYNC_VERSION,
+ entityValues.getAsString(EVENT_SYNC_VERSION));
+ // Copy location so that it's included in the outgoing email
+ if (entityValues.containsKey(Events.EVENT_LOCATION)) {
+ exValues.put(Events.EVENT_LOCATION,
+ entityValues.getAsString(Events.EVENT_LOCATION));
+ }
+
+ if (selfOrganizer) {
+ Message msg =
+ CalendarUtilities.createMessageForEntity(mContext,
+ exEntity, flag, clientId, mAccount);
+ if (msg != null) {
+ userLog("Queueing exception update to " + msg.mTo);
+ mOutgoingMailList.add(msg);
+ }
+ }
+ }
+ s.end(); // EXCEPTION
+ }
+ if (!exFirst) {
+ s.end(); // EXCEPTIONS
+ }
+ }
+
+ s.end().end(); // ApplicationData & Change
+ mUploadedIdList.add(eventId);
+
+ // Go through the extended properties of this Event and pull out our tokenized
+ // attendees list and the user attendee status; we will need them later
+ String attendeeString = null;
+ long attendeeStringId = -1;
+ String userAttendeeStatus = null;
+ long userAttendeeStatusId = -1;
+ for (NamedContentValues ncv: entity.getSubValues()) {
+ if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
+ ContentValues ncvValues = ncv.values;
+ String propertyName =
+ ncvValues.getAsString(ExtendedProperties.NAME);
+ if (propertyName.equals(EXTENDED_PROPERTY_ATTENDEES)) {
+ attendeeString =
+ ncvValues.getAsString(ExtendedProperties.VALUE);
+ attendeeStringId =
+ ncvValues.getAsLong(ExtendedProperties._ID);
+ } else if (propertyName.equals(
+ EXTENDED_PROPERTY_USER_ATTENDEE_STATUS)) {
+ userAttendeeStatus =
+ ncvValues.getAsString(ExtendedProperties.VALUE);
+ userAttendeeStatusId =
+ ncvValues.getAsLong(ExtendedProperties._ID);
+ }
+ }
+ }
+
+ // Send the meeting invite if there are attendees and we're the organizer AND
+ // if the Event itself is dirty (we might be syncing only because an exception
+ // is dirty, in which case we DON'T send email about the Event)
+ if (selfOrganizer &&
+ (getInt(entityValues, Events.DIRTY) == 1)) {
+ EmailContent.Message msg =
+ CalendarUtilities.createMessageForEventId(mContext, eventId,
+ EmailContent.Message.FLAG_OUTGOING_MEETING_INVITE, clientId,
+ mAccount);
+ if (msg != null) {
+ userLog("Queueing invitation to ", msg.mTo);
+ mOutgoingMailList.add(msg);
+ }
+ // Make a list out of our tokenized attendees, if we have any
+ ArrayList<String> originalAttendeeList = new ArrayList<String>();
+ if (attendeeString != null) {
+ StringTokenizer st =
+ new StringTokenizer(attendeeString, ATTENDEE_TOKENIZER_DELIMITER);
+ while (st.hasMoreTokens()) {
+ originalAttendeeList.add(st.nextToken());
+ }
+ }
+ StringBuilder newTokenizedAttendees = new StringBuilder();
+ // See if any attendees have been dropped and while we're at it, build
+ // an updated String with tokenized attendee addresses
+ for (NamedContentValues ncv: entity.getSubValues()) {
+ if (ncv.uri.equals(Attendees.CONTENT_URI)) {
+ String attendeeEmail =
+ ncv.values.getAsString(Attendees.ATTENDEE_EMAIL);
+ // Remove all found attendees
+ originalAttendeeList.remove(attendeeEmail);
+ newTokenizedAttendees.append(attendeeEmail);
+ newTokenizedAttendees.append(ATTENDEE_TOKENIZER_DELIMITER);
+ }
+ }
+ // Update extended properties with the new attendee list, if we have one
+ // Otherwise, create one (this would be the case for Events created on
+ // device or "legacy" events (before this code was added)
+ ContentValues cv = new ContentValues();
+ cv.put(ExtendedProperties.VALUE, newTokenizedAttendees.toString());
+ if (attendeeString != null) {
+ cr.update(asSyncAdapter(ContentUris.withAppendedId(
+ ExtendedProperties.CONTENT_URI, attendeeStringId),
+ mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
+ cv, null, null);
+ } else {
+ // If there wasn't an "attendees" property, insert one
+ cv.put(ExtendedProperties.NAME, EXTENDED_PROPERTY_ATTENDEES);
+ cv.put(ExtendedProperties.EVENT_ID, eventId);
+ cr.insert(asSyncAdapter(ExtendedProperties.CONTENT_URI,
+ mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv);
+ }
+ // Whoever is left has been removed from the attendee list; send them
+ // a cancellation
+ for (String removedAttendee: originalAttendeeList) {
+ // Send a cancellation message to each of them
+ msg = CalendarUtilities.createMessageForEventId(mContext, eventId,
+ Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, mAccount,
+ removedAttendee);
+ if (msg != null) {
+ // Just send it to the removed attendee
+ userLog("Queueing cancellation to removed attendee " + msg.mTo);
+ mOutgoingMailList.add(msg);
+ }
+ }
+ } else if (!selfOrganizer) {
+ // If we're not the organizer, see if we've changed our attendee status
+ // Our last synced attendee status is in ExtendedProperties, and we've
+ // retrieved it above as userAttendeeStatus
+ int currentStatus = entityValues.getAsInteger(Events.SELF_ATTENDEE_STATUS);
+ int syncStatus = Attendees.ATTENDEE_STATUS_NONE;
+ if (userAttendeeStatus != null) {
+ try {
+ syncStatus = Integer.parseInt(userAttendeeStatus);
+ } catch (NumberFormatException e) {
+ // Just in case somebody else mucked with this and it's not Integer
+ }
+ }
+ if ((currentStatus != syncStatus) &&
+ (currentStatus != Attendees.ATTENDEE_STATUS_NONE)) {
+ // If so, send a meeting reply
+ int messageFlag = 0;
+ switch (currentStatus) {
+ case Attendees.ATTENDEE_STATUS_ACCEPTED:
+ messageFlag = Message.FLAG_OUTGOING_MEETING_ACCEPT;
+ break;
+ case Attendees.ATTENDEE_STATUS_DECLINED:
+ messageFlag = Message.FLAG_OUTGOING_MEETING_DECLINE;
+ break;
+ case Attendees.ATTENDEE_STATUS_TENTATIVE:
+ messageFlag = Message.FLAG_OUTGOING_MEETING_TENTATIVE;
+ break;
+ }
+ // Make sure we have a valid status (messageFlag should never be zero)
+ if (messageFlag != 0 && userAttendeeStatusId >= 0) {
+ // Save away the new status
+ cidValues.clear();
+ cidValues.put(ExtendedProperties.VALUE,
+ Integer.toString(currentStatus));
+ cr.update(asSyncAdapter(ContentUris.withAppendedId(
+ ExtendedProperties.CONTENT_URI, userAttendeeStatusId),
+ mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
+ cidValues, null, null);
+ // Send mail to the organizer advising of the new status
+ EmailContent.Message msg =
+ CalendarUtilities.createMessageForEventId(mContext, eventId,
+ messageFlag, clientId, mAccount);
+ if (msg != null) {
+ userLog("Queueing invitation reply to " + msg.mTo);
+ mOutgoingMailList.add(msg);
+ }
+ }
+ }
+ }
+ }
+ if (!first) {
+ s.end(); // Commands
+ }
+ } finally {
+ eventIterator.close();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not read dirty events.");
+ }
+
+ return false;
+ }
+}
diff --git a/exchange2/src/com/android/exchange/adapter/ContactsSyncAdapter.java b/exchange2/src/com/android/exchange/adapter/ContactsSyncAdapter.java
new file mode 100644
index 0000000..1b21d11
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/ContactsSyncAdapter.java
@@ -0,0 +1,1935 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import android.content.ContentProviderClient;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderOperation.Builder;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Entity;
+import android.content.Entity.NamedContentValues;
+import android.content.EntityIterator;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.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.SyncStateContract;
+import android.text.TextUtils;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.exchange.CommandStatusException;
+import com.android.exchange.Eas;
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Sync adapter for EAS Contacts
+ *
+ */
+public class ContactsSyncAdapter extends AbstractSyncAdapter {
+
+ private static final String TAG = "EasContactsSyncAdapter";
+ private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?";
+ private static final String CLIENT_ID_SELECTION = RawContacts.SYNC1 + "=?";
+ private static final String[] ID_PROJECTION = new String[] {RawContacts._ID};
+ private static final String[] GROUP_TITLE_PROJECTION = new String[] {Groups.TITLE};
+ private static final String MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS = Data.MIMETYPE + "='" +
+ GroupMembership.CONTENT_ITEM_TYPE + "' AND " + GroupMembership.GROUP_ROW_ID + "=?";
+ private static final String[] GROUPS_ID_PROJECTION = new String[] {Groups._ID};
+
+ private static final ArrayList<NamedContentValues> EMPTY_ARRAY_NAMEDCONTENTVALUES
+ = new ArrayList<NamedContentValues>();
+
+ private static final String FOUND_DATA_ROW = "com.android.exchange.FOUND_ROW";
+
+ private static final int[] HOME_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
+ Tags.CONTACTS_HOME_ADDRESS_COUNTRY,
+ Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE,
+ Tags.CONTACTS_HOME_ADDRESS_STATE,
+ Tags.CONTACTS_HOME_ADDRESS_STREET};
+
+ private static final int[] WORK_ADDRESS_TAGS = new int[] {Tags.CONTACTS_BUSINESS_ADDRESS_CITY,
+ Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY,
+ Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE,
+ Tags.CONTACTS_BUSINESS_ADDRESS_STATE,
+ Tags.CONTACTS_BUSINESS_ADDRESS_STREET};
+
+ private static final int[] OTHER_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
+ Tags.CONTACTS_OTHER_ADDRESS_COUNTRY,
+ Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE,
+ Tags.CONTACTS_OTHER_ADDRESS_STATE,
+ Tags.CONTACTS_OTHER_ADDRESS_STREET};
+
+ private static final int MAX_IM_ROWS = 3;
+ private static final int MAX_EMAIL_ROWS = 3;
+ private static final int MAX_PHONE_ROWS = 2;
+ private static final String COMMON_DATA_ROW = Im.DATA; // Could have been Email.DATA, etc.
+ private static final String COMMON_TYPE_ROW = Phone.TYPE; // Could have been any typed row
+
+ private static final int[] IM_TAGS = new int[] {Tags.CONTACTS2_IM_ADDRESS,
+ Tags.CONTACTS2_IM_ADDRESS_2, Tags.CONTACTS2_IM_ADDRESS_3};
+
+ private static final int[] EMAIL_TAGS = new int[] {Tags.CONTACTS_EMAIL1_ADDRESS,
+ Tags.CONTACTS_EMAIL2_ADDRESS, Tags.CONTACTS_EMAIL3_ADDRESS};
+
+ private static final int[] WORK_PHONE_TAGS = new int[] {Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER,
+ Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER};
+
+ private static final int[] HOME_PHONE_TAGS = new int[] {Tags.CONTACTS_HOME_TELEPHONE_NUMBER,
+ Tags.CONTACTS_HOME2_TELEPHONE_NUMBER};
+
+ private static final Object sSyncKeyLock = new Object();
+
+ ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
+ ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
+
+ private final Uri mAccountUri;
+ private final ContentResolver mContentResolver;
+ private boolean mGroupsUsed = false;
+
+ public ContactsSyncAdapter(EasSyncService service) {
+ super(service);
+ mAccountUri = uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI);
+ mContentResolver = mContext.getContentResolver();
+ }
+
+ static Uri addCallerIsSyncAdapterParameter(Uri uri) {
+ return uri.buildUpon()
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build();
+ }
+
+ @Override
+ public void sendSyncOptions(Double protocolVersion, Serializer s) throws IOException {
+ setPimSyncOptions(protocolVersion, null, s);
+ }
+
+ @Override
+ public boolean isSyncable() {
+ return ContentResolver.getSyncAutomatically(
+ mAccountManagerAccount, ContactsContract.AUTHORITY);
+ }
+
+ @Override
+ public boolean parse(InputStream is) throws IOException, CommandStatusException {
+ EasContactsSyncParser p = new EasContactsSyncParser(is, this);
+ return p.parse();
+ }
+
+
+ @Override
+ public void wipe() {
+ mContentResolver.delete(mAccountUri, null, null);
+ }
+
+ interface UntypedRow {
+ public void addValues(RowBuilder builder);
+ public boolean isSameAs(int type, String value);
+ }
+
+ /**
+ * We get our SyncKey from ContactsProvider. If there's not one, we set it to "0" (the reset
+ * state) and save that away.
+ */
+ @Override
+ public String getSyncKey() throws IOException {
+ synchronized (sSyncKeyLock) {
+ ContentProviderClient client = mService.mContentResolver
+ .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
+ try {
+ byte[] data = SyncStateContract.Helpers.get(client,
+ ContactsContract.SyncState.CONTENT_URI, mAccountManagerAccount);
+ if (data == null || data.length == 0) {
+ // Initialize the SyncKey
+ setSyncKey("0", false);
+ // Make sure ungrouped contacts for Exchange are defaultly visible
+ ContentValues cv = new ContentValues();
+ cv.put(Groups.ACCOUNT_NAME, mAccount.mEmailAddress);
+ cv.put(Groups.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ cv.put(Settings.UNGROUPED_VISIBLE, true);
+ client.insert(addCallerIsSyncAdapterParameter(Settings.CONTENT_URI), cv);
+ return "0";
+ } else {
+ return new String(data);
+ }
+ } catch (RemoteException e) {
+ throw new IOException("Can't get SyncKey from ContactsProvider");
+ }
+ }
+ }
+
+ /**
+ * We only need to set this when we're forced to make the SyncKey "0" (a reset). In all other
+ * cases, the SyncKey is set within ContactOperations
+ */
+ @Override
+ public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
+ synchronized (sSyncKeyLock) {
+ if ("0".equals(syncKey) || !inCommands) {
+ ContentProviderClient client = mService.mContentResolver
+ .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
+ try {
+ SyncStateContract.Helpers.set(client, ContactsContract.SyncState.CONTENT_URI,
+ mAccountManagerAccount, syncKey.getBytes());
+ userLog("SyncKey set to ", syncKey, " in ContactsProvider");
+ } catch (RemoteException e) {
+ throw new IOException("Can't set SyncKey in ContactsProvider");
+ }
+ }
+ mMailbox.mSyncKey = syncKey;
+ }
+ }
+
+ public static final class EasChildren {
+ private EasChildren() {}
+
+ /** MIME type used when storing this in data table. */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children";
+ public static final int MAX_CHILDREN = 8;
+ public static final String[] ROWS =
+ new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"};
+ }
+
+ public static final class EasPersonal {
+ String anniversary;
+ String fileAs;
+
+ /** MIME type used when storing this in data table. */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal";
+ public static final String ANNIVERSARY = "data2";
+ public static final String FILE_AS = "data4";
+
+ boolean hasData() {
+ return anniversary != null || fileAs != null;
+ }
+ }
+
+ public static final class EasBusiness {
+ String customerId;
+ String governmentId;
+ String accountName;
+
+ /** MIME type used when storing this in data table. */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business";
+ public static final String CUSTOMER_ID = "data6";
+ public static final String GOVERNMENT_ID = "data7";
+ public static final String ACCOUNT_NAME = "data8";
+
+ boolean hasData() {
+ return customerId != null || governmentId != null || accountName != null;
+ }
+ }
+
+ public static final class Address {
+ String city;
+ String country;
+ String code;
+ String street;
+ String state;
+
+ boolean hasData() {
+ return city != null || country != null || code != null || state != null
+ || street != null;
+ }
+ }
+
+ class EmailRow implements UntypedRow {
+ String email;
+ String displayName;
+
+ public EmailRow(String _email) {
+ Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(_email);
+ // Can't happen, but belt & suspenders
+ if (tokens.length == 0) {
+ email = "";
+ displayName = "";
+ } else {
+ Rfc822Token token = tokens[0];
+ email = token.getAddress();
+ displayName = token.getName();
+ }
+ }
+
+ public void addValues(RowBuilder builder) {
+ builder.withValue(Email.DATA, email);
+ builder.withValue(Email.DISPLAY_NAME, displayName);
+ }
+
+ public boolean isSameAs(int type, String value) {
+ return email.equalsIgnoreCase(value);
+ }
+ }
+
+ class ImRow implements UntypedRow {
+ String im;
+
+ public ImRow(String _im) {
+ im = _im;
+ }
+
+ public void addValues(RowBuilder builder) {
+ builder.withValue(Im.DATA, im);
+ }
+
+ public boolean isSameAs(int type, String value) {
+ return im.equalsIgnoreCase(value);
+ }
+ }
+
+ class PhoneRow implements UntypedRow {
+ String phone;
+ int type;
+
+ public PhoneRow(String _phone, int _type) {
+ phone = _phone;
+ type = _type;
+ }
+
+ public void addValues(RowBuilder builder) {
+ builder.withValue(Im.DATA, phone);
+ builder.withValue(Phone.TYPE, type);
+ }
+
+ public boolean isSameAs(int _type, String value) {
+ return type == _type && phone.equalsIgnoreCase(value);
+ }
+ }
+
+ class EasContactsSyncParser extends AbstractSyncParser {
+
+ String[] mBindArgument = new String[1];
+ String mMailboxIdAsString;
+ ContactOperations ops = new ContactOperations();
+
+ public EasContactsSyncParser(InputStream in, ContactsSyncAdapter adapter)
+ throws IOException {
+ super(in, adapter);
+ }
+
+ public void addData(String serverId, ContactOperations ops, Entity entity)
+ throws IOException {
+ String fileAs = null;
+ String prefix = null;
+ String firstName = null;
+ String lastName = null;
+ String middleName = null;
+ String suffix = null;
+ String companyName = null;
+ String yomiFirstName = null;
+ String yomiLastName = null;
+ String yomiCompanyName = null;
+ String title = null;
+ String department = null;
+ String officeLocation = null;
+ Address home = new Address();
+ Address work = new Address();
+ Address other = new Address();
+ EasBusiness business = new EasBusiness();
+ EasPersonal personal = new EasPersonal();
+ ArrayList<String> children = new ArrayList<String>();
+ ArrayList<UntypedRow> emails = new ArrayList<UntypedRow>();
+ ArrayList<UntypedRow> ims = new ArrayList<UntypedRow>();
+ ArrayList<UntypedRow> homePhones = new ArrayList<UntypedRow>();
+ ArrayList<UntypedRow> workPhones = new ArrayList<UntypedRow>();
+ if (entity == null) {
+ ops.newContact(serverId);
+ }
+
+ while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+ switch (tag) {
+ case Tags.CONTACTS_FIRST_NAME:
+ firstName = getValue();
+ break;
+ case Tags.CONTACTS_LAST_NAME:
+ lastName = getValue();
+ break;
+ case Tags.CONTACTS_MIDDLE_NAME:
+ middleName = getValue();
+ break;
+ case Tags.CONTACTS_FILE_AS:
+ fileAs = getValue();
+ break;
+ case Tags.CONTACTS_SUFFIX:
+ suffix = getValue();
+ break;
+ case Tags.CONTACTS_COMPANY_NAME:
+ companyName = getValue();
+ break;
+ case Tags.CONTACTS_JOB_TITLE:
+ title = getValue();
+ break;
+ case Tags.CONTACTS_EMAIL1_ADDRESS:
+ case Tags.CONTACTS_EMAIL2_ADDRESS:
+ case Tags.CONTACTS_EMAIL3_ADDRESS:
+ emails.add(new EmailRow(getValue()));
+ break;
+ case Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER:
+ case Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER:
+ workPhones.add(new PhoneRow(getValue(), Phone.TYPE_WORK));
+ break;
+ case Tags.CONTACTS2_MMS:
+ ops.addPhone(entity, Phone.TYPE_MMS, getValue());
+ break;
+ case Tags.CONTACTS_BUSINESS_FAX_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_FAX_WORK, getValue());
+ break;
+ case Tags.CONTACTS2_COMPANY_MAIN_PHONE:
+ ops.addPhone(entity, Phone.TYPE_COMPANY_MAIN, getValue());
+ break;
+ case Tags.CONTACTS_HOME_FAX_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_FAX_HOME, getValue());
+ break;
+ case Tags.CONTACTS_HOME_TELEPHONE_NUMBER:
+ case Tags.CONTACTS_HOME2_TELEPHONE_NUMBER:
+ homePhones.add(new PhoneRow(getValue(), Phone.TYPE_HOME));
+ break;
+ case Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_MOBILE, getValue());
+ break;
+ case Tags.CONTACTS_CAR_TELEPHONE_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_CAR, getValue());
+ break;
+ case Tags.CONTACTS_RADIO_TELEPHONE_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_RADIO, getValue());
+ break;
+ case Tags.CONTACTS_PAGER_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_PAGER, getValue());
+ break;
+ case Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_ASSISTANT, getValue());
+ break;
+ case Tags.CONTACTS2_IM_ADDRESS:
+ case Tags.CONTACTS2_IM_ADDRESS_2:
+ case Tags.CONTACTS2_IM_ADDRESS_3:
+ ims.add(new ImRow(getValue()));
+ break;
+ case Tags.CONTACTS_BUSINESS_ADDRESS_CITY:
+ work.city = getValue();
+ break;
+ case Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY:
+ work.country = getValue();
+ break;
+ case Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE:
+ work.code = getValue();
+ break;
+ case Tags.CONTACTS_BUSINESS_ADDRESS_STATE:
+ work.state = getValue();
+ break;
+ case Tags.CONTACTS_BUSINESS_ADDRESS_STREET:
+ work.street = getValue();
+ break;
+ case Tags.CONTACTS_HOME_ADDRESS_CITY:
+ home.city = getValue();
+ break;
+ case Tags.CONTACTS_HOME_ADDRESS_COUNTRY:
+ home.country = getValue();
+ break;
+ case Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE:
+ home.code = getValue();
+ break;
+ case Tags.CONTACTS_HOME_ADDRESS_STATE:
+ home.state = getValue();
+ break;
+ case Tags.CONTACTS_HOME_ADDRESS_STREET:
+ home.street = getValue();
+ break;
+ case Tags.CONTACTS_OTHER_ADDRESS_CITY:
+ other.city = getValue();
+ break;
+ case Tags.CONTACTS_OTHER_ADDRESS_COUNTRY:
+ other.country = getValue();
+ break;
+ case Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE:
+ other.code = getValue();
+ break;
+ case Tags.CONTACTS_OTHER_ADDRESS_STATE:
+ other.state = getValue();
+ break;
+ case Tags.CONTACTS_OTHER_ADDRESS_STREET:
+ other.street = getValue();
+ break;
+
+ case Tags.CONTACTS_CHILDREN:
+ childrenParser(children);
+ break;
+
+ case Tags.CONTACTS_YOMI_COMPANY_NAME:
+ yomiCompanyName = getValue();
+ break;
+ case Tags.CONTACTS_YOMI_FIRST_NAME:
+ yomiFirstName = getValue();
+ break;
+ case Tags.CONTACTS_YOMI_LAST_NAME:
+ yomiLastName = getValue();
+ break;
+
+ case Tags.CONTACTS2_NICKNAME:
+ ops.addNickname(entity, getValue());
+ break;
+
+ case Tags.CONTACTS_ASSISTANT_NAME:
+ ops.addRelation(entity, Relation.TYPE_ASSISTANT, getValue());
+ break;
+ case Tags.CONTACTS2_MANAGER_NAME:
+ ops.addRelation(entity, Relation.TYPE_MANAGER, getValue());
+ break;
+ case Tags.CONTACTS_SPOUSE:
+ ops.addRelation(entity, Relation.TYPE_SPOUSE, getValue());
+ break;
+ case Tags.CONTACTS_DEPARTMENT:
+ department = getValue();
+ break;
+ case Tags.CONTACTS_TITLE:
+ prefix = getValue();
+ break;
+
+ // EAS Business
+ case Tags.CONTACTS_OFFICE_LOCATION:
+ officeLocation = getValue();
+ break;
+ case Tags.CONTACTS2_CUSTOMER_ID:
+ business.customerId = getValue();
+ break;
+ case Tags.CONTACTS2_GOVERNMENT_ID:
+ business.governmentId = getValue();
+ break;
+ case Tags.CONTACTS2_ACCOUNT_NAME:
+ business.accountName = getValue();
+ break;
+
+ // EAS Personal
+ case Tags.CONTACTS_ANNIVERSARY:
+ personal.anniversary = getValue();
+ break;
+ case Tags.CONTACTS_BIRTHDAY:
+ ops.addBirthday(entity, getValue());
+ break;
+ case Tags.CONTACTS_WEBPAGE:
+ ops.addWebpage(entity, getValue());
+ break;
+
+ case Tags.CONTACTS_PICTURE:
+ ops.addPhoto(entity, getValue());
+ break;
+
+ case Tags.BASE_BODY:
+ ops.addNote(entity, bodyParser());
+ break;
+ case Tags.CONTACTS_BODY:
+ ops.addNote(entity, getValue());
+ break;
+
+ case Tags.CONTACTS_CATEGORIES:
+ mGroupsUsed = true;
+ categoriesParser(ops, entity);
+ break;
+
+ case Tags.CONTACTS_COMPRESSED_RTF:
+ // We don't use this, and it isn't necessary to upload, so we'll ignore it
+ skipTag();
+ break;
+
+ default:
+ skipTag();
+ }
+ }
+
+ // We must have first name, last name, or company name
+ String name = null;
+ if (firstName != null || lastName != null) {
+ if (firstName == null) {
+ name = lastName;
+ } else if (lastName == null) {
+ name = firstName;
+ } else {
+ name = firstName + ' ' + lastName;
+ }
+ } else if (companyName != null) {
+ name = companyName;
+ }
+
+ ops.addName(entity, prefix, firstName, lastName, middleName, suffix, name,
+ yomiFirstName, yomiLastName, fileAs);
+ ops.addBusiness(entity, business);
+ ops.addPersonal(entity, personal);
+
+ ops.addUntyped(entity, emails, Email.CONTENT_ITEM_TYPE, -1, MAX_EMAIL_ROWS);
+ ops.addUntyped(entity, ims, Im.CONTENT_ITEM_TYPE, -1, MAX_IM_ROWS);
+ ops.addUntyped(entity, homePhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_HOME,
+ MAX_PHONE_ROWS);
+ ops.addUntyped(entity, workPhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_WORK,
+ MAX_PHONE_ROWS);
+
+ if (!children.isEmpty()) {
+ ops.addChildren(entity, children);
+ }
+
+ if (work.hasData()) {
+ ops.addPostal(entity, StructuredPostal.TYPE_WORK, work.street, work.city,
+ work.state, work.country, work.code);
+ }
+ if (home.hasData()) {
+ ops.addPostal(entity, StructuredPostal.TYPE_HOME, home.street, home.city,
+ home.state, home.country, home.code);
+ }
+ if (other.hasData()) {
+ ops.addPostal(entity, StructuredPostal.TYPE_OTHER, other.street, other.city,
+ other.state, other.country, other.code);
+ }
+
+ if (companyName != null) {
+ ops.addOrganization(entity, Organization.TYPE_WORK, companyName, title, department,
+ yomiCompanyName, officeLocation);
+ }
+
+ if (entity != null) {
+ // We've been removing rows from the list as they've been found in the xml
+ // Any that are left must have been deleted on the server
+ ArrayList<NamedContentValues> ncvList = entity.getSubValues();
+ for (NamedContentValues ncv: ncvList) {
+ // These rows need to be deleted...
+ Uri u = dataUriFromNamedContentValues(ncv);
+ ops.add(ContentProviderOperation.newDelete(addCallerIsSyncAdapterParameter(u))
+ .build());
+ }
+ }
+ }
+
+ private void categoriesParser(ContactOperations ops, Entity entity) throws IOException {
+ while (nextTag(Tags.CONTACTS_CATEGORIES) != END) {
+ switch (tag) {
+ case Tags.CONTACTS_CATEGORY:
+ ops.addGroup(entity, getValue());
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private void childrenParser(ArrayList<String> children) throws IOException {
+ while (nextTag(Tags.CONTACTS_CHILDREN) != END) {
+ switch (tag) {
+ case Tags.CONTACTS_CHILD:
+ if (children.size() < EasChildren.MAX_CHILDREN) {
+ children.add(getValue());
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private String bodyParser() throws IOException {
+ String body = null;
+ while (nextTag(Tags.BASE_BODY) != END) {
+ switch (tag) {
+ case Tags.BASE_DATA:
+ body = getValue();
+ break;
+ default:
+ skipTag();
+ }
+ }
+ return body;
+ }
+
+ public void addParser(ContactOperations ops) throws IOException {
+ String serverId = null;
+ while (nextTag(Tags.SYNC_ADD) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID: // same as
+ serverId = getValue();
+ break;
+ case Tags.SYNC_APPLICATION_DATA:
+ addData(serverId, ops, null);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private Cursor getServerIdCursor(String serverId) {
+ mBindArgument[0] = serverId;
+ return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION,
+ mBindArgument, null);
+ }
+
+ private Cursor getClientIdCursor(String clientId) {
+ mBindArgument[0] = clientId;
+ return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION,
+ mBindArgument, null);
+ }
+
+ public void deleteParser(ContactOperations ops) throws IOException {
+ while (nextTag(Tags.SYNC_DELETE) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ String serverId = getValue();
+ // Find the message in this mailbox with the given serverId
+ Cursor c = getServerIdCursor(serverId);
+ try {
+ if (c.moveToFirst()) {
+ userLog("Deleting ", serverId);
+ ops.delete(c.getLong(0));
+ }
+ } finally {
+ c.close();
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ class ServerChange {
+ long id;
+ boolean read;
+
+ ServerChange(long _id, boolean _read) {
+ id = _id;
+ read = _read;
+ }
+ }
+
+ /**
+ * Changes are handled row by row, and only changed/new rows are acted upon
+ * @param ops the array of pending ContactProviderOperations.
+ * @throws IOException
+ */
+ public void changeParser(ContactOperations ops) throws IOException {
+ String serverId = null;
+ Entity entity = null;
+ while (nextTag(Tags.SYNC_CHANGE) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ serverId = getValue();
+ Cursor c = getServerIdCursor(serverId);
+ try {
+ if (c.moveToFirst()) {
+ // TODO Handle deleted individual rows...
+ 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));
+ if (entityIterator.hasNext()) {
+ entity = entityIterator.next();
+ }
+ userLog("Changing contact ", serverId);
+ }
+ } finally {
+ c.close();
+ }
+ break;
+ case Tags.SYNC_APPLICATION_DATA:
+ addData(serverId, ops, entity);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ @Override
+ public void commandsParser() throws IOException {
+ while (nextTag(Tags.SYNC_COMMANDS) != END) {
+ if (tag == Tags.SYNC_ADD) {
+ addParser(ops);
+ incrementChangeCount();
+ } else if (tag == Tags.SYNC_DELETE) {
+ deleteParser(ops);
+ incrementChangeCount();
+ } else if (tag == Tags.SYNC_CHANGE) {
+ changeParser(ops);
+ incrementChangeCount();
+ } else
+ skipTag();
+ }
+ }
+
+ @Override
+ public void commit() throws IOException {
+ // Save the syncKey here, using the Helper provider by Contacts provider
+ userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey);
+ ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI,
+ mAccountManagerAccount, mMailbox.mSyncKey.getBytes()));
+
+ // Execute these all at once...
+ ops.execute();
+
+ if (ops.mResults != null) {
+ ContentValues cv = new ContentValues();
+ cv.put(RawContacts.DIRTY, 0);
+ for (int i = 0; i < ops.mContactIndexCount; i++) {
+ int index = ops.mContactIndexArray[i];
+ Uri u = ops.mResults[index].uri;
+ if (u != null) {
+ String idString = u.getLastPathSegment();
+ mContentResolver.update(
+ addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), cv,
+ RawContacts._ID + "=" + idString, null);
+ }
+ }
+ }
+ }
+
+ public void addResponsesParser() throws IOException {
+ String serverId = null;
+ String clientId = null;
+ ContentValues cv = new ContentValues();
+ while (nextTag(Tags.SYNC_ADD) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ serverId = getValue();
+ break;
+ case Tags.SYNC_CLIENT_ID:
+ clientId = getValue();
+ break;
+ case Tags.SYNC_STATUS:
+ getValue();
+ break;
+ default:
+ skipTag();
+ }
+ }
+
+ // This is theoretically impossible, but...
+ if (clientId == null || serverId == null) return;
+
+ Cursor c = getClientIdCursor(clientId);
+ try {
+ if (c.moveToFirst()) {
+ cv.put(RawContacts.SOURCE_ID, serverId);
+ cv.put(RawContacts.DIRTY, 0);
+ ops.add(ContentProviderOperation.newUpdate(
+ ContentUris.withAppendedId(
+ addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI),
+ c.getLong(0)))
+ .withValues(cv)
+ .build());
+ userLog("New contact " + clientId + " was given serverId: " + serverId);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ public void changeResponsesParser() throws IOException {
+ String serverId = null;
+ String status = null;
+ while (nextTag(Tags.SYNC_CHANGE) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ serverId = getValue();
+ break;
+ case Tags.SYNC_STATUS:
+ status = getValue();
+ break;
+ default:
+ skipTag();
+ }
+ }
+ if (serverId != null && status != null) {
+ userLog("Changed contact " + serverId + " failed with status: " + status);
+ }
+ }
+
+
+ @Override
+ public void responsesParser() throws IOException {
+ // Handle server responses here (for Add and Change)
+ while (nextTag(Tags.SYNC_RESPONSES) != END) {
+ if (tag == Tags.SYNC_ADD) {
+ addResponsesParser();
+ } else if (tag == Tags.SYNC_CHANGE) {
+ changeResponsesParser();
+ } else
+ skipTag();
+ }
+ }
+ }
+
+
+ private Uri uriWithAccountAndIsSyncAdapter(Uri uri) {
+ return uri.buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build();
+ }
+
+ /**
+ * SmartBuilder is a wrapper for the Builder class that is used to create/update rows for a
+ * ContentProvider. It has, in addition to the Builder, ContentValues which, if present,
+ * represent the current values of that row, that can be compared against current values to
+ * see whether an update is even necessary. The methods on SmartBuilder are delegated to
+ * the Builder.
+ */
+ private class RowBuilder {
+ Builder builder;
+ ContentValues cv;
+
+ public RowBuilder(Builder _builder) {
+ builder = _builder;
+ }
+
+ public RowBuilder(Builder _builder, NamedContentValues _ncv) {
+ builder = _builder;
+ cv = _ncv.values;
+ }
+
+ RowBuilder withValues(ContentValues values) {
+ builder.withValues(values);
+ return this;
+ }
+
+ RowBuilder withValueBackReference(String key, int previousResult) {
+ builder.withValueBackReference(key, previousResult);
+ return this;
+ }
+
+ ContentProviderOperation build() {
+ return builder.build();
+ }
+
+ RowBuilder withValue(String key, Object value) {
+ builder.withValue(key, value);
+ return this;
+ }
+ }
+
+ private class ContactOperations extends ArrayList<ContentProviderOperation> {
+ private static final long serialVersionUID = 1L;
+ private int mCount = 0;
+ private int mContactBackValue = mCount;
+ // Make an array big enough for the PIM window (max items we can get)
+ private int[] mContactIndexArray =
+ new int[Integer.parseInt(AbstractSyncAdapter.PIM_WINDOW_SIZE)];
+ private int mContactIndexCount = 0;
+ private ContentProviderResult[] mResults = null;
+
+ @Override
+ public boolean add(ContentProviderOperation op) {
+ super.add(op);
+ mCount++;
+ return true;
+ }
+
+ public void newContact(String serverId) {
+ Builder builder = ContentProviderOperation
+ .newInsert(uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI));
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.SOURCE_ID, serverId);
+ builder.withValues(values);
+ mContactBackValue = mCount;
+ mContactIndexArray[mContactIndexCount++] = mCount;
+ add(builder.build());
+ }
+
+ public void delete(long id) {
+ add(ContentProviderOperation
+ .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
+ .buildUpon()
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build())
+ .build());
+ }
+
+ public void execute() {
+ synchronized (mService.getSynchronizer()) {
+ if (!mService.isStopped()) {
+ try {
+ if (!isEmpty()) {
+ mService.userLog("Executing ", size(), " CPO's");
+ mResults = mContext.getContentResolver().applyBatch(
+ ContactsContract.AUTHORITY, this);
+ }
+ } catch (RemoteException e) {
+ // There is nothing sensible to be done here
+ Log.e(TAG, "problem inserting contact during server update", e);
+ } catch (OperationApplicationException e) {
+ // There is nothing sensible to be done here
+ Log.e(TAG, "problem inserting contact during server update", e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Given the list of NamedContentValues for an entity, a mime type, and a subtype,
+ * tries to find a match, returning it
+ * @param list the list of NCV's from the contact entity
+ * @param contentItemType the mime type we're looking for
+ * @param type the subtype (e.g. HOME, WORK, etc.)
+ * @return the matching NCV or null if not found
+ */
+ private NamedContentValues findTypedData(ArrayList<NamedContentValues> list,
+ String contentItemType, int type, String stringType) {
+ NamedContentValues result = null;
+
+ // Loop through the ncv's, looking for an existing row
+ for (NamedContentValues namedContentValues: list) {
+ Uri uri = namedContentValues.uri;
+ ContentValues cv = namedContentValues.values;
+ if (Data.CONTENT_URI.equals(uri)) {
+ String mimeType = cv.getAsString(Data.MIMETYPE);
+ if (mimeType.equals(contentItemType)) {
+ if (stringType != null) {
+ if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) {
+ result = namedContentValues;
+ }
+ // Note Email.TYPE could be ANY type column; they are all defined in
+ // the private CommonColumns class in ContactsContract
+ // We'll accept either type < 0 (don't care), cv doesn't have a type,
+ // or the types are equal
+ } else if (type < 0 || !cv.containsKey(Email.TYPE) ||
+ cv.getAsInteger(Email.TYPE) == type) {
+ result = namedContentValues;
+ }
+ }
+ }
+ }
+
+ // If we've found an existing data row, we'll delete it. Any rows left at the
+ // end should be deleted...
+ if (result != null) {
+ list.remove(result);
+ }
+
+ // Return the row found (or null)
+ return result;
+ }
+
+ /**
+ * Given the list of NamedContentValues for an entity and a mime type
+ * gather all of the matching NCV's, returning them
+ * @param list the list of NCV's from the contact entity
+ * @param contentItemType the mime type we're looking for
+ * @param type the subtype (e.g. HOME, WORK, etc.)
+ * @return the matching NCVs
+ */
+ private ArrayList<NamedContentValues> findUntypedData(ArrayList<NamedContentValues> list,
+ int type, String contentItemType) {
+ ArrayList<NamedContentValues> result = new ArrayList<NamedContentValues>();
+
+ // Loop through the ncv's, looking for an existing row
+ for (NamedContentValues namedContentValues: list) {
+ Uri uri = namedContentValues.uri;
+ ContentValues cv = namedContentValues.values;
+ if (Data.CONTENT_URI.equals(uri)) {
+ String mimeType = cv.getAsString(Data.MIMETYPE);
+ if (mimeType.equals(contentItemType)) {
+ if (type != -1) {
+ int subtype = cv.getAsInteger(Phone.TYPE);
+ if (type != subtype) {
+ continue;
+ }
+ }
+ result.add(namedContentValues);
+ }
+ }
+ }
+
+ // If we've found an existing data row, we'll delete it. Any rows left at the
+ // end should be deleted...
+ for (NamedContentValues values : result) {
+ list.remove(values);
+ }
+
+ // Return the row found (or null)
+ return result;
+ }
+
+ /**
+ * Create a wrapper for a builder (insert or update) that also includes the NCV for
+ * an existing row of this type. If the SmartBuilder's cv field is not null, then
+ * it represents the current (old) values of this field. The caller can then check
+ * whether the field is now different and needs to be updated; if it's not different,
+ * the caller will simply return and not generate a new CPO. Otherwise, the builder
+ * should have its content values set, and the built CPO should be added to the
+ * ContactOperations list.
+ *
+ * @param entity the contact entity (or null if this is a new contact)
+ * @param mimeType the mime type of this row
+ * @param type the subtype of this row
+ * @param stringType for groups, the name of the group (type will be ignored), or null
+ * @return the created SmartBuilder
+ */
+ public RowBuilder createBuilder(Entity entity, String mimeType, int type,
+ String stringType) {
+ RowBuilder builder = null;
+
+ if (entity != null) {
+ NamedContentValues ncv =
+ findTypedData(entity.getSubValues(), mimeType, type, stringType);
+ if (ncv != null) {
+ builder = new RowBuilder(
+ ContentProviderOperation
+ .newUpdate(addCallerIsSyncAdapterParameter(
+ dataUriFromNamedContentValues(ncv))),
+ ncv);
+ }
+ }
+
+ if (builder == null) {
+ builder = newRowBuilder(entity, mimeType);
+ }
+
+ // Return the appropriate builder (insert or update)
+ // Caller will fill in the appropriate values; 4 MIMETYPE is already set
+ return builder;
+ }
+
+ private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) {
+ return createBuilder(entity, mimeType, type, null);
+ }
+
+ private RowBuilder untypedRowBuilder(Entity entity, String mimeType) {
+ return createBuilder(entity, mimeType, -1, null);
+ }
+
+ private RowBuilder newRowBuilder(Entity entity, String mimeType) {
+ // This is a new row; first get the contactId
+ // If the Contact is new, use the saved back value; otherwise the value in the entity
+ int contactId = mContactBackValue;
+ if (entity != null) {
+ contactId = entity.getEntityValues().getAsInteger(RawContacts._ID);
+ }
+
+ // Create an insert operation with the proper contactId reference
+ RowBuilder builder =
+ new RowBuilder(ContentProviderOperation.newInsert(
+ addCallerIsSyncAdapterParameter(Data.CONTENT_URI)));
+ if (entity == null) {
+ builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId);
+ } else {
+ builder.withValue(Data.RAW_CONTACT_ID, contactId);
+ }
+
+ // Set the mime type of the row
+ builder.withValue(Data.MIMETYPE, mimeType);
+ return builder;
+ }
+
+ /**
+ * Compare a column in a ContentValues with an (old) value, and see if they are the
+ * same. For this purpose, null and an empty string are considered the same.
+ * @param cv a ContentValues object, from a NamedContentValues
+ * @param column a column that might be in the ContentValues
+ * @param oldValue an old value (or null) to check against
+ * @return whether the column's value in the ContentValues matches oldValue
+ */
+ private boolean cvCompareString(ContentValues cv, String column, String oldValue) {
+ if (cv.containsKey(column)) {
+ if (oldValue != null && cv.getAsString(column).equals(oldValue)) {
+ return true;
+ }
+ } else if (oldValue == null || oldValue.length() == 0) {
+ return true;
+ }
+ return false;
+ }
+
+ public void addChildren(Entity entity, ArrayList<String> children) {
+ RowBuilder builder = untypedRowBuilder(entity, EasChildren.CONTENT_ITEM_TYPE);
+ int i = 0;
+ for (String child: children) {
+ builder.withValue(EasChildren.ROWS[i++], child);
+ }
+ add(builder.build());
+ }
+
+ public void addGroup(Entity entity, String group) {
+ RowBuilder builder =
+ createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group);
+ builder.withValue(GroupMembership.GROUP_SOURCE_ID, group);
+ add(builder.build());
+ }
+
+ public void addBirthday(Entity entity, String birthday) {
+ RowBuilder builder =
+ typedRowBuilder(entity, Event.CONTENT_ITEM_TYPE, Event.TYPE_BIRTHDAY);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Event.START_DATE, birthday)) {
+ return;
+ }
+ builder.withValue(Event.START_DATE, birthday);
+ builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
+ add(builder.build());
+ }
+
+ public void addName(Entity entity, String prefix, String givenName, String familyName,
+ String middleName, String suffix, String displayName, String yomiFirstName,
+ String yomiLastName, String fileAs) {
+ RowBuilder builder = untypedRowBuilder(entity, StructuredName.CONTENT_ITEM_TYPE);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) &&
+ cvCompareString(cv, StructuredName.FAMILY_NAME, familyName) &&
+ cvCompareString(cv, StructuredName.MIDDLE_NAME, middleName) &&
+ cvCompareString(cv, StructuredName.PREFIX, prefix) &&
+ cvCompareString(cv, StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName) &&
+ cvCompareString(cv, StructuredName.PHONETIC_FAMILY_NAME, yomiLastName) &&
+ //cvCompareString(cv, StructuredName.DISPLAY_NAME, fileAs) &&
+ cvCompareString(cv, StructuredName.SUFFIX, suffix)) {
+ return;
+ }
+ builder.withValue(StructuredName.GIVEN_NAME, givenName);
+ builder.withValue(StructuredName.FAMILY_NAME, familyName);
+ builder.withValue(StructuredName.MIDDLE_NAME, middleName);
+ builder.withValue(StructuredName.SUFFIX, suffix);
+ builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName);
+ builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, yomiLastName);
+ builder.withValue(StructuredName.PREFIX, prefix);
+ //builder.withValue(StructuredName.DISPLAY_NAME, fileAs);
+ add(builder.build());
+ }
+
+ public void addPersonal(Entity entity, EasPersonal personal) {
+ RowBuilder builder = untypedRowBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, EasPersonal.ANNIVERSARY, personal.anniversary) &&
+ cvCompareString(cv, EasPersonal.FILE_AS , personal.fileAs)) {
+ return;
+ }
+ if (!personal.hasData()) {
+ return;
+ }
+ builder.withValue(EasPersonal.FILE_AS, personal.fileAs);
+ builder.withValue(EasPersonal.ANNIVERSARY, personal.anniversary);
+ add(builder.build());
+ }
+
+ public void addBusiness(Entity entity, EasBusiness business) {
+ RowBuilder builder = untypedRowBuilder(entity, EasBusiness.CONTENT_ITEM_TYPE);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, EasBusiness.ACCOUNT_NAME, business.accountName) &&
+ cvCompareString(cv, EasBusiness.CUSTOMER_ID, business.customerId) &&
+ cvCompareString(cv, EasBusiness.GOVERNMENT_ID, business.governmentId)) {
+ return;
+ }
+ if (!business.hasData()) {
+ return;
+ }
+ builder.withValue(EasBusiness.ACCOUNT_NAME, business.accountName);
+ builder.withValue(EasBusiness.CUSTOMER_ID, business.customerId);
+ builder.withValue(EasBusiness.GOVERNMENT_ID, business.governmentId);
+ add(builder.build());
+ }
+
+ public void addPhoto(Entity entity, String photo) {
+ RowBuilder builder = untypedRowBuilder(entity, Photo.CONTENT_ITEM_TYPE);
+ // We're always going to add this; it's not worth trying to figure out whether the
+ // picture is the same as the one stored.
+ byte[] pic = Base64.decode(photo, Base64.DEFAULT);
+ builder.withValue(Photo.PHOTO, pic);
+ add(builder.build());
+ }
+
+ public void addPhone(Entity entity, int type, String phone) {
+ RowBuilder builder = typedRowBuilder(entity, Phone.CONTENT_ITEM_TYPE, type);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) {
+ return;
+ }
+ builder.withValue(Phone.TYPE, type);
+ builder.withValue(Phone.NUMBER, phone);
+ add(builder.build());
+ }
+
+ public void addWebpage(Entity entity, String url) {
+ RowBuilder builder = untypedRowBuilder(entity, Website.CONTENT_ITEM_TYPE);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Website.URL, url)) {
+ return;
+ }
+ builder.withValue(Website.TYPE, Website.TYPE_WORK);
+ builder.withValue(Website.URL, url);
+ add(builder.build());
+ }
+
+ public void addRelation(Entity entity, int type, String value) {
+ RowBuilder builder = typedRowBuilder(entity, Relation.CONTENT_ITEM_TYPE, type);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Relation.DATA, value)) {
+ return;
+ }
+ builder.withValue(Relation.TYPE, type);
+ builder.withValue(Relation.DATA, value);
+ add(builder.build());
+ }
+
+ public void addNickname(Entity entity, String name) {
+ RowBuilder builder =
+ typedRowBuilder(entity, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE_DEFAULT);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Nickname.NAME, name)) {
+ return;
+ }
+ builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
+ builder.withValue(Nickname.NAME, name);
+ add(builder.build());
+ }
+
+ public void addPostal(Entity entity, int type, String street, String city, String state,
+ String country, String code) {
+ RowBuilder builder = typedRowBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE,
+ type);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) &&
+ cvCompareString(cv, StructuredPostal.STREET, street) &&
+ cvCompareString(cv, StructuredPostal.COUNTRY, country) &&
+ cvCompareString(cv, StructuredPostal.POSTCODE, code) &&
+ cvCompareString(cv, StructuredPostal.REGION, state)) {
+ return;
+ }
+ builder.withValue(StructuredPostal.TYPE, type);
+ builder.withValue(StructuredPostal.CITY, city);
+ builder.withValue(StructuredPostal.STREET, street);
+ builder.withValue(StructuredPostal.COUNTRY, country);
+ builder.withValue(StructuredPostal.POSTCODE, code);
+ builder.withValue(StructuredPostal.REGION, state);
+ add(builder.build());
+ }
+
+ /**
+ * We now are dealing with up to maxRows typeless rows of mimeType data. We need to try to
+ * match them with existing rows; if there's a match, everything's great. Otherwise, we
+ * either need to add a new row for the data, or we have to replace an existing one
+ * that no longer matches. This is similar to the way Emails are handled.
+ */
+ public void addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType,
+ int type, int maxRows) {
+ // Make a list of all same type rows in the existing entity
+ ArrayList<NamedContentValues> oldValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
+ ArrayList<NamedContentValues> entityValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
+ if (entity != null) {
+ oldValues = findUntypedData(entityValues, type, mimeType);
+ entityValues = entity.getSubValues();
+ }
+
+ // These will be rows needing replacement with new values
+ ArrayList<UntypedRow> rowsToReplace = new ArrayList<UntypedRow>();
+
+ // The count of existing rows
+ int numRows = oldValues.size();
+ for (UntypedRow row: rows) {
+ boolean found = false;
+ // If we already have this row, mark it
+ for (NamedContentValues ncv: oldValues) {
+ ContentValues cv = ncv.values;
+ String data = cv.getAsString(COMMON_DATA_ROW);
+ int rowType = -1;
+ if (cv.containsKey(COMMON_TYPE_ROW)) {
+ rowType = cv.getAsInteger(COMMON_TYPE_ROW);
+ }
+ if (row.isSameAs(rowType, data)) {
+ cv.put(FOUND_DATA_ROW, true);
+ // Remove this to indicate it's still being used
+ entityValues.remove(ncv);
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ // If we don't, there are two possibilities
+ if (numRows < maxRows) {
+ // If there are available rows, add a new one
+ RowBuilder builder = newRowBuilder(entity, mimeType);
+ row.addValues(builder);
+ add(builder.build());
+ numRows++;
+ } else {
+ // Otherwise, say we need to replace a row with this
+ rowsToReplace.add(row);
+ }
+ }
+ }
+
+ // Go through rows needing replacement
+ for (UntypedRow row: rowsToReplace) {
+ for (NamedContentValues ncv: oldValues) {
+ ContentValues cv = ncv.values;
+ // Find a row that hasn't been used (i.e. doesn't match current rows)
+ if (!cv.containsKey(FOUND_DATA_ROW)) {
+ // And update it
+ RowBuilder builder = new RowBuilder(
+ ContentProviderOperation
+ .newUpdate(addCallerIsSyncAdapterParameter(
+ dataUriFromNamedContentValues(ncv))),
+ ncv);
+ row.addValues(builder);
+ add(builder.build());
+ }
+ }
+ }
+ }
+
+ public void addOrganization(Entity entity, int type, String company, String title,
+ String department, String yomiCompanyName, String officeLocation) {
+ RowBuilder builder = typedRowBuilder(entity, Organization.CONTENT_ITEM_TYPE, type);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Organization.COMPANY, company) &&
+ cvCompareString(cv, Organization.PHONETIC_NAME, yomiCompanyName) &&
+ cvCompareString(cv, Organization.DEPARTMENT, department) &&
+ cvCompareString(cv, Organization.TITLE, title) &&
+ cvCompareString(cv, Organization.OFFICE_LOCATION, officeLocation)) {
+ return;
+ }
+ builder.withValue(Organization.TYPE, type);
+ builder.withValue(Organization.COMPANY, company);
+ builder.withValue(Organization.TITLE, title);
+ builder.withValue(Organization.DEPARTMENT, department);
+ builder.withValue(Organization.PHONETIC_NAME, yomiCompanyName);
+ builder.withValue(Organization.OFFICE_LOCATION, officeLocation);
+ add(builder.build());
+ }
+
+ public void addNote(Entity entity, String note) {
+ RowBuilder builder = typedRowBuilder(entity, Note.CONTENT_ITEM_TYPE, -1);
+ ContentValues cv = builder.cv;
+ if (note == null) return;
+ note = note.replaceAll("\r\n", "\n");
+ if (cv != null && cvCompareString(cv, Note.NOTE, note)) {
+ return;
+ }
+
+ // Reject notes with nothing in them. Often, we get something from Outlook when
+ // nothing was ever entered. Sigh.
+ int len = note.length();
+ int i = 0;
+ for (; i < len; i++) {
+ char c = note.charAt(i);
+ if (!Character.isWhitespace(c)) {
+ break;
+ }
+ }
+ if (i == len) return;
+
+ builder.withValue(Note.NOTE, note);
+ add(builder.build());
+ }
+ }
+
+ /**
+ * Generate the uri for the data row associated with this NamedContentValues object
+ * @param ncv the NamedContentValues object
+ * @return a uri that can be used to refer to this row
+ */
+ public Uri dataUriFromNamedContentValues(NamedContentValues ncv) {
+ long id = ncv.values.getAsLong(RawContacts._ID);
+ Uri dataUri = ContentUris.withAppendedId(ncv.uri, id);
+ return dataUri;
+ }
+
+ @Override
+ public void cleanup() {
+ // Mark the changed contacts dirty = 0
+ // Permanently delete the user deletions
+ ContactOperations ops = new ContactOperations();
+ for (Long id: mUpdatedIdList) {
+ ops.add(ContentProviderOperation
+ .newUpdate(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
+ .buildUpon()
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build())
+ .withValue(RawContacts.DIRTY, 0).build());
+ }
+ for (Long id: mDeletedIdList) {
+ ops.add(ContentProviderOperation
+ .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
+ .buildUpon()
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build())
+ .build());
+ }
+ ops.execute();
+ ContentResolver cr = mContext.getContentResolver();
+ if (mGroupsUsed) {
+ // Make sure the title column is set for all of our groups
+ // And that all of our groups are visible
+ // TODO Perhaps the visible part should only happen when the group is created, but
+ // this is fine for now.
+ Uri groupsUri = uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI);
+ Cursor c = cr.query(groupsUri, new String[] {Groups.SOURCE_ID, Groups.TITLE},
+ Groups.TITLE + " IS NULL", null, null);
+ ContentValues values = new ContentValues();
+ values.put(Groups.GROUP_VISIBLE, 1);
+ try {
+ while (c.moveToNext()) {
+ String sourceId = c.getString(0);
+ values.put(Groups.TITLE, sourceId);
+ cr.update(uriWithAccountAndIsSyncAdapter(groupsUri), values,
+ Groups.SOURCE_ID + "=?", new String[] {sourceId});
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ @Override
+ public String getCollectionName() {
+ return "Contacts";
+ }
+
+ private void sendEmail(Serializer s, ContentValues cv, int count, String displayName)
+ throws IOException {
+ // Get both parts of the email address (a newly created one in the UI won't have a name)
+ String addr = cv.getAsString(Email.DATA);
+ String name = cv.getAsString(Email.DISPLAY_NAME);
+ if (name == null) {
+ if (displayName != null) {
+ name = displayName;
+ } else {
+ name = addr;
+ }
+ }
+ // Compose address from name and addr
+ if (addr != null) {
+ String value;
+ // Only send the raw email address for EAS 2.5 (Hotmail, in particular, chokes on
+ // an RFC822 address)
+ if (mService.mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+ value = addr;
+ } else {
+ value = '\"' + name + "\" <" + addr + '>';
+ }
+ if (count < MAX_EMAIL_ROWS) {
+ s.data(EMAIL_TAGS[count], value);
+ }
+ }
+ }
+
+ private void sendIm(Serializer s, ContentValues cv, int count) throws IOException {
+ String value = cv.getAsString(Im.DATA);
+ if (value == null) return;
+ if (count < MAX_IM_ROWS) {
+ s.data(IM_TAGS[count], value);
+ }
+ }
+
+ private void sendOnePostal(Serializer s, ContentValues cv, int[] fieldNames)
+ throws IOException{
+ sendStringData(s, cv, StructuredPostal.CITY, fieldNames[0]);
+ sendStringData(s, cv, StructuredPostal.COUNTRY, fieldNames[1]);
+ sendStringData(s, cv, StructuredPostal.POSTCODE, fieldNames[2]);
+ sendStringData(s, cv, StructuredPostal.REGION, fieldNames[3]);
+ sendStringData(s, cv, StructuredPostal.STREET, fieldNames[4]);
+ }
+
+ private void sendStructuredPostal(Serializer s, ContentValues cv) throws IOException {
+ switch (cv.getAsInteger(StructuredPostal.TYPE)) {
+ case StructuredPostal.TYPE_HOME:
+ sendOnePostal(s, cv, HOME_ADDRESS_TAGS);
+ break;
+ case StructuredPostal.TYPE_WORK:
+ sendOnePostal(s, cv, WORK_ADDRESS_TAGS);
+ break;
+ case StructuredPostal.TYPE_OTHER:
+ sendOnePostal(s, cv, OTHER_ADDRESS_TAGS);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void sendStringData(Serializer s, ContentValues cv, String column, int tag)
+ throws IOException {
+ if (cv.containsKey(column)) {
+ String value = cv.getAsString(column);
+ if (!TextUtils.isEmpty(value)) {
+ s.data(tag, value);
+ }
+ }
+ }
+
+ private String sendStructuredName(Serializer s, ContentValues cv) throws IOException {
+ String displayName = null;
+ sendStringData(s, cv, StructuredName.FAMILY_NAME, Tags.CONTACTS_LAST_NAME);
+ sendStringData(s, cv, StructuredName.GIVEN_NAME, Tags.CONTACTS_FIRST_NAME);
+ sendStringData(s, cv, StructuredName.MIDDLE_NAME, Tags.CONTACTS_MIDDLE_NAME);
+ sendStringData(s, cv, StructuredName.SUFFIX, Tags.CONTACTS_SUFFIX);
+ sendStringData(s, cv, StructuredName.PHONETIC_GIVEN_NAME, Tags.CONTACTS_YOMI_FIRST_NAME);
+ sendStringData(s, cv, StructuredName.PHONETIC_FAMILY_NAME, Tags.CONTACTS_YOMI_LAST_NAME);
+ sendStringData(s, cv, StructuredName.PREFIX, Tags.CONTACTS_TITLE);
+ if (cv.containsKey(StructuredName.DISPLAY_NAME)) {
+ displayName = cv.getAsString(StructuredName.DISPLAY_NAME);
+ if (!TextUtils.isEmpty(displayName)) {
+ s.data(Tags.CONTACTS_FILE_AS, displayName);
+ }
+ }
+ return displayName;
+ }
+
+ private void sendBusiness(Serializer s, ContentValues cv) throws IOException {
+ sendStringData(s, cv, EasBusiness.ACCOUNT_NAME, Tags.CONTACTS2_ACCOUNT_NAME);
+ sendStringData(s, cv, EasBusiness.CUSTOMER_ID, Tags.CONTACTS2_CUSTOMER_ID);
+ sendStringData(s, cv, EasBusiness.GOVERNMENT_ID, Tags.CONTACTS2_GOVERNMENT_ID);
+ }
+
+ private void sendPersonal(Serializer s, ContentValues cv) throws IOException {
+ sendStringData(s, cv, EasPersonal.ANNIVERSARY, Tags.CONTACTS_ANNIVERSARY);
+ sendStringData(s, cv, EasPersonal.FILE_AS, Tags.CONTACTS_FILE_AS);
+ }
+
+ private void sendBirthday(Serializer s, ContentValues cv) throws IOException {
+ sendStringData(s, cv, Event.START_DATE, Tags.CONTACTS_BIRTHDAY);
+ }
+
+ private void sendPhoto(Serializer s, ContentValues cv) throws IOException {
+ if (cv.containsKey(Photo.PHOTO)) {
+ byte[] bytes = cv.getAsByteArray(Photo.PHOTO);
+ String pic = Base64.encodeToString(bytes, Base64.NO_WRAP);
+ s.data(Tags.CONTACTS_PICTURE, pic);
+ } else {
+ // Send an empty tag, which signals the server to delete any pre-existing photo
+ s.tag(Tags.CONTACTS_PICTURE);
+ }
+ }
+
+ private void sendOrganization(Serializer s, ContentValues cv) throws IOException {
+ sendStringData(s, cv, Organization.TITLE, Tags.CONTACTS_JOB_TITLE);
+ sendStringData(s, cv, Organization.COMPANY, Tags.CONTACTS_COMPANY_NAME);
+ sendStringData(s, cv, Organization.DEPARTMENT, Tags.CONTACTS_DEPARTMENT);
+ sendStringData(s, cv, Organization.OFFICE_LOCATION, Tags.CONTACTS_OFFICE_LOCATION);
+ }
+
+ private void sendNickname(Serializer s, ContentValues cv) throws IOException {
+ sendStringData(s, cv, Nickname.NAME, Tags.CONTACTS2_NICKNAME);
+ }
+
+ private void sendWebpage(Serializer s, ContentValues cv) throws IOException {
+ sendStringData(s, cv, Website.URL, Tags.CONTACTS_WEBPAGE);
+ }
+
+ private void sendNote(Serializer s, ContentValues cv) throws IOException {
+ // Even when there is no local note, we must explicitly upsync an empty note,
+ // which is the only way to force the server to delete any pre-existing note.
+ String note = "";
+ if (cv.containsKey(Note.NOTE)) {
+ // EAS won't accept note data with raw newline characters
+ note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n");
+ }
+ // Format of upsync data depends on protocol version
+ if (mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+ s.start(Tags.BASE_BODY);
+ s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note);
+ s.end();
+ } else {
+ s.data(Tags.CONTACTS_BODY, note);
+ }
+ }
+
+ private void sendChildren(Serializer s, ContentValues cv) throws IOException {
+ boolean first = true;
+ for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) {
+ String row = EasChildren.ROWS[i];
+ if (cv.containsKey(row)) {
+ if (first) {
+ s.start(Tags.CONTACTS_CHILDREN);
+ first = false;
+ }
+ s.data(Tags.CONTACTS_CHILD, cv.getAsString(row));
+ }
+ }
+ if (!first) {
+ s.end();
+ }
+ }
+
+ private void sendPhone(Serializer s, ContentValues cv, int workCount, int homeCount)
+ throws IOException {
+ String value = cv.getAsString(Phone.NUMBER);
+ if (value == null) return;
+ switch (cv.getAsInteger(Phone.TYPE)) {
+ case Phone.TYPE_WORK:
+ if (workCount < MAX_PHONE_ROWS) {
+ s.data(WORK_PHONE_TAGS[workCount], value);
+ }
+ break;
+ case Phone.TYPE_MMS:
+ s.data(Tags.CONTACTS2_MMS, value);
+ break;
+ case Phone.TYPE_ASSISTANT:
+ s.data(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER, value);
+ break;
+ case Phone.TYPE_FAX_WORK:
+ s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value);
+ break;
+ case Phone.TYPE_COMPANY_MAIN:
+ s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value);
+ break;
+ case Phone.TYPE_HOME:
+ if (homeCount < MAX_PHONE_ROWS) {
+ s.data(HOME_PHONE_TAGS[homeCount], value);
+ }
+ break;
+ case Phone.TYPE_MOBILE:
+ s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value);
+ break;
+ case Phone.TYPE_CAR:
+ s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value);
+ break;
+ case Phone.TYPE_PAGER:
+ s.data(Tags.CONTACTS_PAGER_NUMBER, value);
+ break;
+ case Phone.TYPE_RADIO:
+ s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value);
+ break;
+ case Phone.TYPE_FAX_HOME:
+ s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void sendRelation(Serializer s, ContentValues cv) throws IOException {
+ String value = cv.getAsString(Relation.DATA);
+ if (value == null) return;
+ switch (cv.getAsInteger(Relation.TYPE)) {
+ case Relation.TYPE_ASSISTANT:
+ s.data(Tags.CONTACTS_ASSISTANT_NAME, value);
+ break;
+ case Relation.TYPE_MANAGER:
+ s.data(Tags.CONTACTS2_MANAGER_NAME, value);
+ break;
+ case Relation.TYPE_SPOUSE:
+ s.data(Tags.CONTACTS_SPOUSE, value);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void dirtyContactsWithinDirtyGroups() {
+ ContentResolver cr = mService.mContentResolver;
+ Cursor c = cr.query(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI),
+ GROUPS_ID_PROJECTION, Groups.DIRTY + "=1", null, null);
+ try {
+ if (c.getCount() > 0) {
+ String[] updateArgs = new String[1];
+ ContentValues updateValues = new ContentValues();
+ while (c.moveToNext()) {
+ // For each, "touch" all data rows with this group id; this will mark contacts
+ // in this group as dirty (per ContactsContract). We will then know to upload
+ // them to the server with the modified group information
+ long id = c.getLong(0);
+ updateValues.put(GroupMembership.GROUP_ROW_ID, id);
+ updateArgs[0] = Long.toString(id);
+ cr.update(Data.CONTENT_URI, updateValues,
+ MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS, updateArgs);
+ }
+ // Really delete groups that are marked deleted
+ cr.delete(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI), Groups.DELETED + "=1",
+ null);
+ // Clear the dirty flag for all of our groups
+ updateValues.clear();
+ updateValues.put(Groups.DIRTY, 0);
+ cr.update(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI), updateValues, null,
+ null);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ @Override
+ public boolean sendLocalChanges(Serializer s) throws IOException {
+ ContentResolver cr = mService.mContentResolver;
+
+ // Find any groups of ours that are dirty and dirty those groups' members
+ dirtyContactsWithinDirtyGroups();
+
+ // First, let's find Contacts that have changed.
+ Uri uri = uriWithAccountAndIsSyncAdapter(RawContactsEntity.CONTENT_URI);
+ if (getSyncKey().equals("0")) {
+ return false;
+ }
+
+ // Get them all atomically
+ EntityIterator ei = RawContacts.newEntityIterator(
+ cr.query(uri, null, RawContacts.DIRTY + "=1", null, null));
+ ContentValues cidValues = new ContentValues();
+ try {
+ boolean first = true;
+ final Uri rawContactUri = addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI);
+ while (ei.hasNext()) {
+ Entity entity = ei.next();
+ // For each of these entities, create the change commands
+ ContentValues entityValues = entity.getEntityValues();
+ String serverId = entityValues.getAsString(RawContacts.SOURCE_ID);
+ ArrayList<Integer> groupIds = new ArrayList<Integer>();
+ if (first) {
+ s.start(Tags.SYNC_COMMANDS);
+ userLog("Sending Contacts changes to the server");
+ first = false;
+ }
+ if (serverId == null) {
+ // This is a new contact; create a clientId
+ String clientId = "new_" + mMailbox.mId + '_' + System.currentTimeMillis();
+ userLog("Creating new contact with clientId: ", clientId);
+ s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
+ // And save it in the raw contact
+ cidValues.put(RawContacts.SYNC1, clientId);
+ cr.update(ContentUris.
+ withAppendedId(rawContactUri,
+ entityValues.getAsLong(RawContacts._ID)),
+ cidValues, null, null);
+ } else {
+ if (entityValues.getAsInteger(RawContacts.DELETED) == 1) {
+ userLog("Deleting contact with serverId: ", serverId);
+ s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+ mDeletedIdList.add(entityValues.getAsLong(RawContacts._ID));
+ continue;
+ }
+ userLog("Upsync change to contact with serverId: " + serverId);
+ s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
+ }
+ s.start(Tags.SYNC_APPLICATION_DATA);
+ // Write out the data here
+ int imCount = 0;
+ int emailCount = 0;
+ int homePhoneCount = 0;
+ int workPhoneCount = 0;
+ String displayName = null;
+ ArrayList<ContentValues> emailValues = new ArrayList<ContentValues>();
+ for (NamedContentValues ncv: entity.getSubValues()) {
+ ContentValues cv = ncv.values;
+ String mimeType = cv.getAsString(Data.MIMETYPE);
+ if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
+ emailValues.add(cv);
+ } else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) {
+ sendNickname(s, cv);
+ } else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) {
+ sendChildren(s, cv);
+ } else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) {
+ sendBusiness(s, cv);
+ } else if (mimeType.equals(Website.CONTENT_ITEM_TYPE)) {
+ sendWebpage(s, cv);
+ } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) {
+ sendPersonal(s, cv);
+ } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
+ sendPhone(s, cv, workPhoneCount, homePhoneCount);
+ int type = cv.getAsInteger(Phone.TYPE);
+ if (type == Phone.TYPE_HOME) homePhoneCount++;
+ if (type == Phone.TYPE_WORK) workPhoneCount++;
+ } else if (mimeType.equals(Relation.CONTENT_ITEM_TYPE)) {
+ sendRelation(s, cv);
+ } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
+ displayName = sendStructuredName(s, cv);
+ } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
+ sendStructuredPostal(s, cv);
+ } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) {
+ sendOrganization(s, cv);
+ } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) {
+ sendIm(s, cv, imCount++);
+ } else if (mimeType.equals(Event.CONTENT_ITEM_TYPE)) {
+ Integer eventType = cv.getAsInteger(Event.TYPE);
+ if (eventType != null && eventType.equals(Event.TYPE_BIRTHDAY)) {
+ sendBirthday(s, cv);
+ }
+ } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) {
+ // We must gather these, and send them together (below)
+ groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID));
+ } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) {
+ sendNote(s, cv);
+ } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
+ sendPhoto(s, cv);
+ } else {
+ userLog("Contacts upsync, unknown data: ", mimeType);
+ }
+ }
+
+ // We do the email rows last, because we need to make sure we've found the
+ // displayName (if one exists); this would be in a StructuredName rnow
+ for (ContentValues cv: emailValues) {
+ sendEmail(s, cv, emailCount++, displayName);
+ }
+
+ // Now, we'll send up groups, if any
+ if (!groupIds.isEmpty()) {
+ boolean groupFirst = true;
+ for (int id: groupIds) {
+ // Since we get id's from the provider, we need to find their names
+ Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI, id),
+ GROUP_TITLE_PROJECTION, null, null, null);
+ try {
+ // Presumably, this should always succeed, but ...
+ if (c.moveToFirst()) {
+ if (groupFirst) {
+ s.start(Tags.CONTACTS_CATEGORIES);
+ groupFirst = false;
+ }
+ s.data(Tags.CONTACTS_CATEGORY, c.getString(0));
+ }
+ } finally {
+ c.close();
+ }
+ }
+ if (!groupFirst) {
+ s.end();
+ }
+ }
+ s.end().end(); // ApplicationData & Change
+ mUpdatedIdList.add(entityValues.getAsLong(RawContacts._ID));
+ }
+ if (!first) {
+ s.end(); // Commands
+ }
+ } finally {
+ ei.close();
+ }
+
+ return false;
+ }
+}
diff --git a/exchange2_src/com/android/exchange/adapter/EmailSyncAdapter.java b/exchange2/src/com/android/exchange/adapter/EmailSyncAdapter.java
similarity index 100%
rename from exchange2_src/com/android/exchange/adapter/EmailSyncAdapter.java
rename to exchange2/src/com/android/exchange/adapter/EmailSyncAdapter.java
diff --git a/exchange2/src/com/android/exchange/adapter/FolderSyncParser.java b/exchange2/src/com/android/exchange/adapter/FolderSyncParser.java
new file mode 100644
index 0000000..3b0bc7c
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/FolderSyncParser.java
@@ -0,0 +1,698 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.os.RemoteException;
+import android.text.TextUtils;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.AccountColumns;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.service.SyncWindow;
+import com.android.emailcommon.utility.AttachmentUtilities;
+import com.android.emailcommon.utility.EmailAsyncTask;
+import com.android.emailcommon.utility.Utility;
+import com.android.exchange.CommandStatusException;
+import com.android.exchange.CommandStatusException.CommandStatus;
+import com.android.exchange.Eas;
+import com.android.exchange.ExchangeService;
+import com.android.exchange.provider.MailboxUtilities;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Parse the result of a FolderSync command
+ *
+ * Handles the addition, deletion, and changes to folders in the user's Exchange account.
+ **/
+
+public class FolderSyncParser extends AbstractSyncParser {
+
+ public static final String TAG = "FolderSyncParser";
+
+ // These are defined by the EAS protocol
+ public static final int USER_GENERIC_TYPE = 1;
+ public static final int INBOX_TYPE = 2;
+ public static final int DRAFTS_TYPE = 3;
+ public static final int DELETED_TYPE = 4;
+ public static final int SENT_TYPE = 5;
+ public static final int OUTBOX_TYPE = 6;
+ public static final int TASKS_TYPE = 7;
+ public static final int CALENDAR_TYPE = 8;
+ public static final int CONTACTS_TYPE = 9;
+ public static final int NOTES_TYPE = 10;
+ public static final int JOURNAL_TYPE = 11;
+ public static final int USER_MAILBOX_TYPE = 12;
+
+ // Chunk size for our mailbox commits
+ public final static int MAILBOX_COMMIT_SIZE = 20;
+
+ // EAS types that we are willing to consider valid folders for EAS sync
+ public static final List<Integer> VALID_EAS_FOLDER_TYPES = Arrays.asList(INBOX_TYPE,
+ DRAFTS_TYPE, DELETED_TYPE, SENT_TYPE, OUTBOX_TYPE, USER_MAILBOX_TYPE, CALENDAR_TYPE,
+ CONTACTS_TYPE, USER_GENERIC_TYPE);
+
+ public static final String ALL_BUT_ACCOUNT_MAILBOX = MailboxColumns.ACCOUNT_KEY + "=? and " +
+ MailboxColumns.TYPE + "!=" + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
+
+ private static final String WHERE_SERVER_ID_AND_ACCOUNT = MailboxColumns.SERVER_ID + "=? and " +
+ MailboxColumns.ACCOUNT_KEY + "=?";
+
+ private static final String WHERE_DISPLAY_NAME_AND_ACCOUNT = MailboxColumns.DISPLAY_NAME +
+ "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
+
+ private static final String WHERE_PARENT_SERVER_ID_AND_ACCOUNT =
+ MailboxColumns.PARENT_SERVER_ID +"=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
+
+ private static final String[] MAILBOX_ID_COLUMNS_PROJECTION =
+ new String[] {MailboxColumns.ID, MailboxColumns.SERVER_ID, MailboxColumns.PARENT_SERVER_ID};
+ private static final int MAILBOX_ID_COLUMNS_ID = 0;
+ private static final int MAILBOX_ID_COLUMNS_SERVER_ID = 1;
+ private static final int MAILBOX_ID_COLUMNS_PARENT_SERVER_ID = 2;
+
+ @VisibleForTesting
+ long mAccountId;
+ @VisibleForTesting
+ String mAccountIdAsString;
+ @VisibleForTesting
+ boolean mInUnitTest = false;
+
+ private String[] mBindArguments = new String[2];
+ private ArrayList<ContentProviderOperation> mOperations =
+ new ArrayList<ContentProviderOperation>();
+ private boolean mInitialSync;
+ private ArrayList<String> mParentFixupsNeeded = new ArrayList<String>();
+ private boolean mFixupUninitializedNeeded = false;
+ // If true, we only care about status (this is true when validating an account) and ignore
+ // other data
+ private final boolean mStatusOnly;
+
+ private static final ContentValues UNINITIALIZED_PARENT_KEY = new ContentValues();
+
+ {
+ UNINITIALIZED_PARENT_KEY.put(MailboxColumns.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED);
+ }
+
+ public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
+ this(in, adapter, false);
+ }
+
+ public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter, boolean statusOnly)
+ throws IOException {
+ super(in, adapter);
+ mAccountId = mAccount.mId;
+ mAccountIdAsString = Long.toString(mAccountId);
+ mStatusOnly = statusOnly;
+ }
+
+ @Override
+ public boolean parse() throws IOException, CommandStatusException {
+ int status;
+ boolean res = false;
+ boolean resetFolders = false;
+ // Since we're now (potentially) committing mailboxes in chunks, ensure that we start with
+ // only the account mailbox
+ String key = mAccount.mSyncKey;
+ mInitialSync = (key == null) || "0".equals(key);
+ if (mInitialSync) {
+ mContentResolver.delete(Mailbox.CONTENT_URI, ALL_BUT_ACCOUNT_MAILBOX,
+ new String[] {Long.toString(mAccountId)});
+ }
+ if (nextTag(START_DOCUMENT) != Tags.FOLDER_FOLDER_SYNC)
+ throw new EasParserException();
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ if (tag == Tags.FOLDER_STATUS) {
+ status = getValueInt();
+ if (status != Eas.FOLDER_STATUS_OK) {
+ mService.errorLog("FolderSync failed: " + CommandStatus.toString(status));
+ // If the account hasn't been saved, this is a validation attempt, so we don't
+ // try reloading the folder list...
+ if (CommandStatus.isDeniedAccess(status) ||
+ CommandStatus.isNeedsProvisioning(status) ||
+ (mAccount.mId == Account.NOT_SAVED)) {
+ throw new CommandStatusException(status);
+ // Note that we need to catch both old-style (Eas.FOLDER_STATUS_INVALID_KEY)
+ // and EAS 14 style command status
+ } else if (status == Eas.FOLDER_STATUS_INVALID_KEY ||
+ CommandStatus.isBadSyncKey(status)) {
+ mService.errorLog("Bad sync key; RESET and delete all folders");
+ // Reset the sync key and save
+ mAccount.mSyncKey = "0";
+ ContentValues cv = new ContentValues();
+ cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
+ mContentResolver.update(ContentUris.withAppendedId(Account.CONTENT_URI,
+ mAccount.mId), cv, null, null);
+ // Delete PIM data
+ ExchangeService.deleteAccountPIMData(mAccountId);
+ // Save away any mailbox sync information that is NOT default
+ saveMailboxSyncOptions();
+ // And only then, delete mailboxes
+ mContentResolver.delete(Mailbox.CONTENT_URI, ALL_BUT_ACCOUNT_MAILBOX,
+ new String[] {Long.toString(mAccountId)});
+ // Stop existing syncs and reconstruct _main
+ ExchangeService.stopNonAccountMailboxSyncsForAccount(mAccountId);
+ res = true;
+ resetFolders = true;
+ } else {
+ // Other errors are at the server, so let's throw an error that will
+ // cause this sync to be retried at a later time
+ mService.errorLog("Throwing IOException; will retry later");
+ throw new EasParserException("Folder status error");
+ }
+ }
+ } else if (tag == Tags.FOLDER_SYNC_KEY) {
+ mAccount.mSyncKey = getValue();
+ userLog("New Account SyncKey: ", mAccount.mSyncKey);
+ } else if (tag == Tags.FOLDER_CHANGES) {
+ if (mStatusOnly) return res;
+ changesParser(mOperations, mInitialSync);
+ } else
+ skipTag();
+ }
+ if (mStatusOnly) return res;
+ synchronized (mService.getSynchronizer()) {
+ if (!mService.isStopped() || resetFolders) {
+ commit();
+ userLog("Leaving FolderSyncParser with Account syncKey=", mAccount.mSyncKey);
+ }
+ }
+ return res;
+ }
+
+ private Cursor getServerIdCursor(String serverId) {
+ mBindArguments[0] = serverId;
+ mBindArguments[1] = mAccountIdAsString;
+ return mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_ID_COLUMNS_PROJECTION,
+ WHERE_SERVER_ID_AND_ACCOUNT, mBindArguments, null);
+ }
+
+ public void deleteParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+ while (nextTag(Tags.FOLDER_DELETE) != END) {
+ switch (tag) {
+ case Tags.FOLDER_SERVER_ID:
+ String serverId = getValue();
+ // Find the mailbox in this account with the given serverId
+ Cursor c = getServerIdCursor(serverId);
+ try {
+ if (c.moveToFirst()) {
+ userLog("Deleting ", serverId);
+ ops.add(ContentProviderOperation.newDelete(
+ ContentUris.withAppendedId(Mailbox.CONTENT_URI,
+ c.getLong(MAILBOX_ID_COLUMNS_ID))).build());
+ AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext,
+ mAccountId, mMailbox.mId);
+ if (!mInitialSync) {
+ String parentId = c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
+ if (!TextUtils.isEmpty(parentId)) {
+ mParentFixupsNeeded.add(parentId);
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private static class SyncOptions {
+ private final int mInterval;
+ private final int mLookback;
+
+ private SyncOptions(int interval, int lookback) {
+ mInterval = interval;
+ mLookback = lookback;
+ }
+ }
+
+ private static final String MAILBOX_STATE_SELECTION =
+ MailboxColumns.ACCOUNT_KEY + "=? AND (" + MailboxColumns.SYNC_INTERVAL + "!=" +
+ Account.CHECK_INTERVAL_NEVER + " OR " + Mailbox.SYNC_LOOKBACK + "!=" +
+ SyncWindow.SYNC_WINDOW_UNKNOWN + ")";
+
+ private static final String[] MAILBOX_STATE_PROJECTION = new String[] {
+ MailboxColumns.SERVER_ID, MailboxColumns.SYNC_INTERVAL, MailboxColumns.SYNC_LOOKBACK};
+ private static final int MAILBOX_STATE_SERVER_ID = 0;
+ private static final int MAILBOX_STATE_INTERVAL = 1;
+ private static final int MAILBOX_STATE_LOOKBACK = 2;
+ @VisibleForTesting
+ final HashMap<String, SyncOptions> mSyncOptionsMap = new HashMap<String, SyncOptions>();
+
+ /**
+ * For every mailbox in this account that has a non-default interval or lookback, save those
+ * values.
+ */
+ @VisibleForTesting
+ void saveMailboxSyncOptions() {
+ // Shouldn't be necessary, but...
+ mSyncOptionsMap.clear();
+ Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_STATE_PROJECTION,
+ MAILBOX_STATE_SELECTION, new String[] {mAccountIdAsString}, null);
+ if (c != null) {
+ try {
+ while (c.moveToNext()) {
+ mSyncOptionsMap.put(c.getString(MAILBOX_STATE_SERVER_ID),
+ new SyncOptions(c.getInt(MAILBOX_STATE_INTERVAL),
+ c.getInt(MAILBOX_STATE_LOOKBACK)));
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ /**
+ * For every set of saved mailbox sync options, try to find and restore those values
+ */
+ @VisibleForTesting
+ void restoreMailboxSyncOptions() {
+ try {
+ ContentValues cv = new ContentValues();
+ mBindArguments[1] = mAccountIdAsString;
+ for (String serverId: mSyncOptionsMap.keySet()) {
+ SyncOptions options = mSyncOptionsMap.get(serverId);
+ cv.put(MailboxColumns.SYNC_INTERVAL, options.mInterval);
+ cv.put(MailboxColumns.SYNC_LOOKBACK, options.mLookback);
+ mBindArguments[0] = serverId;
+ // If we match account and server id, set the sync options
+ mContentResolver.update(Mailbox.CONTENT_URI, cv, WHERE_SERVER_ID_AND_ACCOUNT,
+ mBindArguments);
+ }
+ } finally {
+ mSyncOptionsMap.clear();
+ }
+ }
+
+ public Mailbox addParser() throws IOException {
+ String name = null;
+ String serverId = null;
+ String parentId = null;
+ int type = 0;
+
+ while (nextTag(Tags.FOLDER_ADD) != END) {
+ switch (tag) {
+ case Tags.FOLDER_DISPLAY_NAME: {
+ name = getValue();
+ break;
+ }
+ case Tags.FOLDER_TYPE: {
+ type = getValueInt();
+ break;
+ }
+ case Tags.FOLDER_PARENT_ID: {
+ parentId = getValue();
+ break;
+ }
+ case Tags.FOLDER_SERVER_ID: {
+ serverId = getValue();
+ break;
+ }
+ default:
+ skipTag();
+ }
+ }
+
+ if (VALID_EAS_FOLDER_TYPES.contains(type)) {
+ Mailbox mailbox = new Mailbox();
+ mailbox.mDisplayName = name;
+ mailbox.mServerId = serverId;
+ mailbox.mAccountKey = mAccountId;
+ mailbox.mType = Mailbox.TYPE_MAIL;
+ // Note that all mailboxes default to checking "never" (i.e. manual sync only)
+ // We set specific intervals for inbox, contacts, and (eventually) calendar
+ mailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
+ switch (type) {
+ case INBOX_TYPE:
+ mailbox.mType = Mailbox.TYPE_INBOX;
+ mailbox.mSyncInterval = mAccount.mSyncInterval;
+ break;
+ case CONTACTS_TYPE:
+ mailbox.mType = Mailbox.TYPE_CONTACTS;
+ mailbox.mSyncInterval = mAccount.mSyncInterval;
+ break;
+ case OUTBOX_TYPE:
+ // TYPE_OUTBOX mailboxes are known by ExchangeService to sync whenever they
+ // aren't empty. The value of mSyncFrequency is ignored for this kind of
+ // mailbox.
+ mailbox.mType = Mailbox.TYPE_OUTBOX;
+ break;
+ case SENT_TYPE:
+ mailbox.mType = Mailbox.TYPE_SENT;
+ break;
+ case DRAFTS_TYPE:
+ mailbox.mType = Mailbox.TYPE_DRAFTS;
+ break;
+ case DELETED_TYPE:
+ mailbox.mType = Mailbox.TYPE_TRASH;
+ break;
+ case CALENDAR_TYPE:
+ mailbox.mType = Mailbox.TYPE_CALENDAR;
+ mailbox.mSyncInterval = mAccount.mSyncInterval;
+ break;
+ case USER_GENERIC_TYPE:
+ mailbox.mType = Mailbox.TYPE_UNKNOWN;
+ break;
+ }
+
+ // Make boxes like Contacts and Calendar invisible in the folder list
+ mailbox.mFlagVisible = (mailbox.mType < Mailbox.TYPE_NOT_EMAIL);
+
+ if (!parentId.equals("0")) {
+ mailbox.mParentServerId = parentId;
+ if (!mInitialSync) {
+ mParentFixupsNeeded.add(parentId);
+ }
+ }
+ // At the least, we'll need to set flags
+ mFixupUninitializedNeeded = true;
+
+ return mailbox;
+ }
+ return null;
+ }
+
+ /**
+ * Determine whether a given mailbox holds mail, rather than other data. We do this by first
+ * checking the type of the mailbox (if it's a known good type, great; if it's a known bad
+ * type, return false). If it's unknown, we check the parent, first by trying to find it in
+ * the current set of newly synced items, and then by looking it up in EmailProvider. If
+ * we can find the parent, we use the same rules to determine if it holds mail; if it does,
+ * then its children do as well, so that's a go.
+ *
+ * @param mailbox the mailbox we're checking
+ * @param mailboxMap a HashMap relating server id's of mailboxes in the current sync set to
+ * the corresponding mailbox structures
+ * @return whether or not the mailbox contains email (rather than PIM or unknown data)
+ */
+ /*package*/ boolean isValidMailFolder(Mailbox mailbox, HashMap<String, Mailbox> mailboxMap) {
+ int folderType = mailbox.mType;
+ // Automatically accept our email types
+ if (folderType < Mailbox.TYPE_NOT_EMAIL) return true;
+ // Automatically reject everything else but "unknown"
+ if (folderType != Mailbox.TYPE_UNKNOWN) return false;
+ // If this is TYPE_UNKNOWN, check the parent
+ Mailbox parent = mailboxMap.get(mailbox.mParentServerId);
+ // If the parent is in the map, then check it out; if not, it could be an existing saved
+ // Mailbox, so we'll have to query the database
+ if (parent == null) {
+ mBindArguments[0] = Long.toString(mAccount.mId);
+ mBindArguments[1] = mailbox.mParentServerId;
+ long parentId = Utility.getFirstRowInt(mContext, Mailbox.CONTENT_URI,
+ EmailContent.ID_PROJECTION,
+ MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.SERVER_ID + "=?",
+ mBindArguments, null, EmailContent.ID_PROJECTION_COLUMN, -1);
+ if (parentId != -1) {
+ // Get the parent from the database
+ parent = Mailbox.restoreMailboxWithId(mContext, parentId);
+ if (parent == null) return false;
+ } else {
+ return false;
+ }
+ }
+ return isValidMailFolder(parent, mailboxMap);
+ }
+
+ public void updateParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+ String serverId = null;
+ String displayName = null;
+ String parentId = null;
+ while (nextTag(Tags.FOLDER_UPDATE) != END) {
+ switch (tag) {
+ case Tags.FOLDER_SERVER_ID:
+ serverId = getValue();
+ break;
+ case Tags.FOLDER_DISPLAY_NAME:
+ displayName = getValue();
+ break;
+ case Tags.FOLDER_PARENT_ID:
+ parentId = getValue();
+ break;
+ default:
+ skipTag();
+ break;
+ }
+ }
+ // We'll make a change if one of parentId or displayName are specified
+ // serverId is required, but let's be careful just the same
+ if (serverId != null && (displayName != null || parentId != null)) {
+ Cursor c = getServerIdCursor(serverId);
+ try {
+ // If we find the mailbox (using serverId), make the change
+ if (c.moveToFirst()) {
+ userLog("Updating ", serverId);
+ // Fix up old and new parents, as needed
+ if (!TextUtils.isEmpty(parentId)) {
+ mParentFixupsNeeded.add(parentId);
+ }
+ String oldParentId = c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
+ if (!TextUtils.isEmpty(oldParentId)) {
+ mParentFixupsNeeded.add(oldParentId);
+ }
+ // Set display name if we've got one
+ ContentValues cv = new ContentValues();
+ if (displayName != null) {
+ cv.put(Mailbox.DISPLAY_NAME, displayName);
+ }
+ // Save away the server id and uninitialize the parent key
+ cv.put(Mailbox.PARENT_SERVER_ID, parentId);
+ // Clear the parent key; it will be fixed up after the commit
+ cv.put(Mailbox.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED);
+ ops.add(ContentProviderOperation.newUpdate(
+ ContentUris.withAppendedId(Mailbox.CONTENT_URI,
+ c.getLong(MAILBOX_ID_COLUMNS_ID))).withValues(cv).build());
+ // Say we need to fixup uninitialized mailboxes
+ mFixupUninitializedNeeded = true;
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ private boolean commitMailboxes(ArrayList<Mailbox> validMailboxes,
+ ArrayList<Mailbox> userMailboxes, HashMap<String, Mailbox> mailboxMap,
+ ArrayList<ContentProviderOperation> ops) {
+
+ // Go through the generic user mailboxes; we'll call them valid if any parent is valid
+ for (Mailbox m: userMailboxes) {
+ if (isValidMailFolder(m, mailboxMap)) {
+ m.mType = Mailbox.TYPE_MAIL;
+ validMailboxes.add(m);
+ } else {
+ userLog("Rejecting unknown type mailbox: " + m.mDisplayName);
+ }
+ }
+
+ // Add operations for all valid mailboxes
+ for (Mailbox m: validMailboxes) {
+ userLog("Adding mailbox: ", m.mDisplayName);
+ ops.add(ContentProviderOperation
+ .newInsert(Mailbox.CONTENT_URI).withValues(m.toContentValues()).build());
+ }
+
+ // Commit the mailboxes
+ userLog("Applying ", mOperations.size(), " mailbox operations.");
+ // Execute the batch; throw IOExceptions if this fails, hoping the issue isn't repeatable
+ // If it IS repeatable, there's no good result, since the folder list will be invalid
+ try {
+ mContentResolver.applyBatch(EmailContent.AUTHORITY, mOperations);
+ return true;
+ } catch (RemoteException e) {
+ userLog("RemoteException in commitMailboxes");
+ return false;
+ } catch (OperationApplicationException e) {
+ userLog("OperationApplicationException in commitMailboxes");
+ return false;
+ }
+ }
+
+ public void changesParser(final ArrayList<ContentProviderOperation> ops,
+ final boolean initialSync) throws IOException {
+ // Array of added mailboxes
+ final ArrayList<Mailbox> addMailboxes = new ArrayList<Mailbox>();
+
+ // Indicate start of (potential) mailbox changes
+ MailboxUtilities.startMailboxChanges(mContext, mAccount.mId);
+
+ while (nextTag(Tags.FOLDER_CHANGES) != END) {
+ if (tag == Tags.FOLDER_ADD) {
+ Mailbox mailbox = addParser();
+ if (mailbox != null) {
+ addMailboxes.add(mailbox);
+ }
+ } else if (tag == Tags.FOLDER_DELETE) {
+ deleteParser(ops);
+ } else if (tag == Tags.FOLDER_UPDATE) {
+ updateParser(ops);
+ } else if (tag == Tags.FOLDER_COUNT) {
+ getValueInt();
+ } else
+ skipTag();
+ }
+
+ EmailAsyncTask<?, ?, ?> task =
+ EmailAsyncTask.runAsyncParallel(new Runnable() {
+ @Override
+ public void run() {
+ // Synchronize on the parser to prevent this being run concurrently
+ // (an extremely unlikely event, but nonetheless possible)
+ synchronized (FolderSyncParser.this) {
+ // Mailboxes that we known contain email
+ ArrayList<Mailbox> validMailboxes = new ArrayList<Mailbox>();
+ // Mailboxes that we're unsure about
+ ArrayList<Mailbox> userMailboxes = new ArrayList<Mailbox>();
+
+ // Maps folder serverId to mailbox (used to validate user mailboxes)
+ HashMap<String, Mailbox> mailboxMap = new HashMap<String, Mailbox>();
+ for (Mailbox mailbox : addMailboxes) {
+ mailboxMap.put(mailbox.mServerId, mailbox);
+ }
+
+ int mailboxCommitCount = 0;
+ for (Mailbox mailbox : addMailboxes) {
+ // And add the mailbox to the proper list
+ if (type == USER_MAILBOX_TYPE) {
+ userMailboxes.add(mailbox);
+ } else {
+ validMailboxes.add(mailbox);
+ }
+ // On initial sync, we commit what we have every 20 mailboxes
+ if (initialSync && (++mailboxCommitCount == MAILBOX_COMMIT_SIZE)) {
+ if (!commitMailboxes(validMailboxes, userMailboxes, mailboxMap,
+ ops)) {
+ mService.stop();
+ return;
+ }
+ // Clear our arrays to prepare for more
+ userMailboxes.clear();
+ validMailboxes.clear();
+ ops.clear();
+ mailboxCommitCount = 0;
+ }
+ }
+ // Commit the sync key and mailboxes
+ ContentValues cv = new ContentValues();
+ cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
+ ops.add(ContentProviderOperation
+ .newUpdate(
+ ContentUris.withAppendedId(Account.CONTENT_URI,
+ mAccount.mId))
+ .withValues(cv).build());
+ if (!commitMailboxes(validMailboxes, userMailboxes, mailboxMap, ops)) {
+ mService.stop();
+ return;
+ }
+ String accountSelector = Mailbox.ACCOUNT_KEY + "=" + mAccount.mId;
+ // For new boxes, setup the parent key and flags
+ if (mFixupUninitializedNeeded) {
+ MailboxUtilities.fixupUninitializedParentKeys(mContext,
+ accountSelector);
+ }
+ // For modified parents, reset the flags (and children's parent key)
+ for (String parentServerId: mParentFixupsNeeded) {
+ Cursor c = mContentResolver.query(Mailbox.CONTENT_URI,
+ Mailbox.CONTENT_PROJECTION, Mailbox.PARENT_SERVER_ID + "=?",
+ new String[] {parentServerId}, null);
+ try {
+ if (c.moveToFirst()) {
+ MailboxUtilities.setFlagsAndChildrensParentKey(mContext, c,
+ accountSelector);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ // Signal completion of mailbox changes
+ MailboxUtilities.endMailboxChanges(mContext, mAccount.mId);
+ }
+ }});
+ // Make this synchronous if in a unit test
+ if (mInUnitTest) {
+ try {
+ task.get();
+ } catch (Exception e) {
+ }
+ }
+ }
+
+ /**
+ * Not needed for FolderSync parsing; everything is done within changesParser
+ */
+ @Override
+ public void commandsParser() throws IOException {
+ }
+
+ /**
+ * Clean up after sync
+ */
+ @Override
+ public void commit() throws IOException {
+ // Look for sync issues and its children and delete them
+ // I'm not aware of any other way to deal with this properly
+ mBindArguments[0] = "Sync Issues";
+ mBindArguments[1] = mAccountIdAsString;
+ Cursor c = mContentResolver.query(Mailbox.CONTENT_URI,
+ MAILBOX_ID_COLUMNS_PROJECTION, WHERE_DISPLAY_NAME_AND_ACCOUNT,
+ mBindArguments, null);
+ String parentServerId = null;
+ long id = 0;
+ try {
+ if (c.moveToFirst()) {
+ id = c.getLong(MAILBOX_ID_COLUMNS_ID);
+ parentServerId = c.getString(MAILBOX_ID_COLUMNS_SERVER_ID);
+ }
+ } finally {
+ c.close();
+ }
+ if (parentServerId != null) {
+ mContentResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id),
+ null, null);
+ mBindArguments[0] = parentServerId;
+ mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_PARENT_SERVER_ID_AND_ACCOUNT,
+ mBindArguments);
+ }
+
+ // If we have saved options, restore them now
+ if (mInitialSync) {
+ restoreMailboxSyncOptions();
+ }
+ }
+
+ @Override
+ public void responsesParser() throws IOException {
+ }
+
+}
diff --git a/exchange2/src/com/android/exchange/adapter/GalParser.java b/exchange2/src/com/android/exchange/adapter/GalParser.java
new file mode 100644
index 0000000..94c8cdf
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/GalParser.java
@@ -0,0 +1,143 @@
+/* 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 com.android.exchange.provider.GalResult;
+import com.android.exchange.provider.GalResult.GalData;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parse the result of a GAL command.
+ */
+public class GalParser extends Parser {
+ private EasSyncService mService;
+ GalResult mGalResult = new GalResult();
+
+ public GalParser(InputStream in, EasSyncService service) throws IOException {
+ super(in);
+ mService = service;
+ }
+
+ public GalResult getGalResult() {
+ return mGalResult;
+ }
+
+ @Override
+ public boolean parse() throws IOException {
+ if (nextTag(START_DOCUMENT) != Tags.SEARCH_SEARCH) {
+ throw new IOException();
+ }
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ if (tag == Tags.SEARCH_RESPONSE) {
+ parseResponse(mGalResult);
+ } else {
+ skipTag();
+ }
+ }
+ return mGalResult.total > 0;
+ }
+
+ public void parseProperties(GalResult galResult) throws IOException {
+ GalData galData = new GalData();
+ while (nextTag(Tags.SEARCH_STORE) != END) {
+ switch(tag) {
+ // Display name and email address use both legacy and new code for galData
+ case Tags.GAL_DISPLAY_NAME:
+ String displayName = getValue();
+ galData.put(GalData.DISPLAY_NAME, displayName);
+ galData.displayName = displayName;
+ break;
+ case Tags.GAL_EMAIL_ADDRESS:
+ String emailAddress = getValue();
+ galData.put(GalData.EMAIL_ADDRESS, emailAddress);
+ galData.emailAddress = emailAddress;
+ break;
+ case Tags.GAL_PHONE:
+ galData.put(GalData.WORK_PHONE, getValue());
+ break;
+ case Tags.GAL_OFFICE:
+ galData.put(GalData.OFFICE, getValue());
+ break;
+ case Tags.GAL_TITLE:
+ galData.put(GalData.TITLE, getValue());
+ break;
+ case Tags.GAL_COMPANY:
+ galData.put(GalData.COMPANY, getValue());
+ break;
+ case Tags.GAL_ALIAS:
+ galData.put(GalData.ALIAS, getValue());
+ break;
+ case Tags.GAL_FIRST_NAME:
+ galData.put(GalData.FIRST_NAME, getValue());
+ break;
+ case Tags.GAL_LAST_NAME:
+ galData.put(GalData.LAST_NAME, getValue());
+ break;
+ case Tags.GAL_HOME_PHONE:
+ galData.put(GalData.HOME_PHONE, getValue());
+ break;
+ case Tags.GAL_MOBILE_PHONE:
+ galData.put(GalData.MOBILE_PHONE, getValue());
+ break;
+ default:
+ skipTag();
+ }
+ }
+ galResult.addGalData(galData);
+ }
+
+ public void parseResult(GalResult galResult) throws IOException {
+ while (nextTag(Tags.SEARCH_STORE) != END) {
+ if (tag == Tags.SEARCH_PROPERTIES) {
+ parseProperties(galResult);
+ } else {
+ skipTag();
+ }
+ }
+ }
+
+ public void parseResponse(GalResult galResult) throws IOException {
+ while (nextTag(Tags.SEARCH_RESPONSE) != END) {
+ if (tag == Tags.SEARCH_STORE) {
+ parseStore(galResult);
+ } else {
+ skipTag();
+ }
+ }
+ }
+
+ public void parseStore(GalResult galResult) throws IOException {
+ while (nextTag(Tags.SEARCH_STORE) != END) {
+ if (tag == Tags.SEARCH_RESULT) {
+ parseResult(galResult);
+ } else if (tag == Tags.SEARCH_RANGE) {
+ // Retrieve value, even if we're not using it for debug logging
+ String range = getValue();
+ if (EasSyncService.DEBUG_GAL_SERVICE) {
+ mService.userLog("GAL result range: " + range);
+ }
+ } else if (tag == Tags.SEARCH_TOTAL) {
+ galResult.total = getValueInt();
+ } else {
+ skipTag();
+ }
+ }
+ }
+}
+
diff --git a/exchange2/src/com/android/exchange/adapter/ItemOperationsParser.java b/exchange2/src/com/android/exchange/adapter/ItemOperationsParser.java
new file mode 100644
index 0000000..5f412ad
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/ItemOperationsParser.java
@@ -0,0 +1,94 @@
+/* Copyright (C) 2011 The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Parse the result of an ItemOperations command; we use this to load attachments in EAS 14.0
+ */
+public class ItemOperationsParser extends Parser {
+ private final AttachmentLoader mAttachmentLoader;
+ private int mStatusCode = 0;
+ private final OutputStream mAttachmentOutputStream;
+ private final int mAttachmentSize;
+
+ public ItemOperationsParser(AttachmentLoader loader, InputStream in, OutputStream out, int size)
+ throws IOException {
+ super(in);
+ mAttachmentLoader = loader;
+ mAttachmentOutputStream = out;
+ mAttachmentSize = size;
+ }
+
+ public int getStatusCode() {
+ return mStatusCode;
+ }
+
+ private void parseProperties() throws IOException {
+ while (nextTag(Tags.ITEMS_PROPERTIES) != END) {
+ if (tag == Tags.ITEMS_DATA) {
+ // Wrap the input stream in our custom base64 input stream
+ Base64InputStream bis = new Base64InputStream(getInput());
+ // Read the attachment
+ mAttachmentLoader.readChunked(bis, mAttachmentOutputStream, mAttachmentSize);
+ } else {
+ skipTag();
+ }
+ }
+ }
+
+ private void parseFetch() throws IOException {
+ while (nextTag(Tags.ITEMS_FETCH) != END) {
+ if (tag == Tags.ITEMS_PROPERTIES) {
+ parseProperties();
+ } else {
+ skipTag();
+ }
+ }
+ }
+
+ private void parseResponse() throws IOException {
+ while (nextTag(Tags.ITEMS_RESPONSE) != END) {
+ if (tag == Tags.ITEMS_FETCH) {
+ parseFetch();
+ } else {
+ skipTag();
+ }
+ }
+ }
+
+ @Override
+ public boolean parse() throws IOException {
+ boolean res = false;
+ if (nextTag(START_DOCUMENT) != Tags.ITEMS_ITEMS) {
+ throw new IOException();
+ }
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ if (tag == Tags.ITEMS_STATUS) {
+ // Save the status code
+ mStatusCode = getValueInt();
+ } else if (tag == Tags.ITEMS_RESPONSE) {
+ parseResponse();
+ } else {
+ skipTag();
+ }
+ }
+ return res;
+ }
+}
diff --git a/exchange2/src/com/android/exchange/adapter/MeetingResponseParser.java b/exchange2/src/com/android/exchange/adapter/MeetingResponseParser.java
new file mode 100644
index 0000000..142c419
--- /dev/null
+++ b/exchange2/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/exchange2/src/com/android/exchange/adapter/MoveItemsParser.java b/exchange2/src/com/android/exchange/adapter/MoveItemsParser.java
new file mode 100644
index 0000000..542331d
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/MoveItemsParser.java
@@ -0,0 +1,113 @@
+/* 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 MoveItems command.
+ */
+public class MoveItemsParser extends Parser {
+ private final EasSyncService mService;
+ private int mStatusCode = 0;
+ private String mNewServerId;
+
+ // These are the EAS status codes for MoveItems
+ private static final int STATUS_NO_SOURCE_FOLDER = 1;
+ private static final int STATUS_NO_DESTINATION_FOLDER = 2;
+ private static final int STATUS_SUCCESS = 3;
+ private static final int STATUS_SOURCE_DESTINATION_SAME = 4;
+ private static final int STATUS_INTERNAL_ERROR = 5;
+ private static final int STATUS_ALREADY_EXISTS = 6;
+ private static final int STATUS_LOCKED = 7;
+
+ // These are the status values we return to callers
+ public static final int STATUS_CODE_SUCCESS = 1;
+ public static final int STATUS_CODE_REVERT = 2;
+ public static final int STATUS_CODE_RETRY = 3;
+
+ public MoveItemsParser(InputStream in, EasSyncService service) throws IOException {
+ super(in);
+ mService = service;
+ }
+
+ public int getStatusCode() {
+ return mStatusCode;
+ }
+
+ public String getNewServerId() {
+ return mNewServerId;
+ }
+
+ public void parseResponse() throws IOException {
+ while (nextTag(Tags.MOVE_RESPONSE) != END) {
+ if (tag == Tags.MOVE_STATUS) {
+ int status = getValueInt();
+ // Convert the EAS status code with our external codes
+ switch(status) {
+ case STATUS_SUCCESS:
+ case STATUS_SOURCE_DESTINATION_SAME:
+ case STATUS_ALREADY_EXISTS:
+ // Same destination and already exists are ok with us; we'll continue as
+ // if the move succeeded
+ mStatusCode = STATUS_CODE_SUCCESS;
+ break;
+ case STATUS_LOCKED:
+ // This sounds like a transient error, so we can safely retry
+ mStatusCode = STATUS_CODE_RETRY;
+ break;
+ case STATUS_NO_SOURCE_FOLDER:
+ case STATUS_NO_DESTINATION_FOLDER:
+ case STATUS_INTERNAL_ERROR:
+ default:
+ // These are non-recoverable, so we'll revert the message to its original
+ // mailbox. If there's an unknown response, revert
+ mStatusCode = STATUS_CODE_REVERT;
+ break;
+ }
+ if (status != STATUS_SUCCESS) {
+ // There's not much to be done if this fails
+ mService.userLog("Error in MoveItems: " + status);
+ }
+ } else if (tag == Tags.MOVE_DSTMSGID) {
+ mNewServerId = getValue();
+ mService.userLog("Moved message id is now: " + mNewServerId);
+ } else {
+ skipTag();
+ }
+ }
+ }
+
+ @Override
+ public boolean parse() throws IOException {
+ boolean res = false;
+ if (nextTag(START_DOCUMENT) != Tags.MOVE_MOVE_ITEMS) {
+ throw new IOException();
+ }
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ if (tag == Tags.MOVE_RESPONSE) {
+ parseResponse();
+ } else {
+ skipTag();
+ }
+ }
+ return res;
+ }
+}
+
diff --git a/exchange2/src/com/android/exchange/adapter/Parser.java b/exchange2/src/com/android/exchange/adapter/Parser.java
new file mode 100644
index 0000000..d24e6c1
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/Parser.java
@@ -0,0 +1,606 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.exchange.Eas;
+import com.android.exchange.EasException;
+import com.android.exchange.utility.FileLogger;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Extremely fast and lightweight WBXML parser, implementing only the subset of WBXML that
+ * EAS uses (as defined in the EAS specification)
+ *
+ */
+public abstract class Parser {
+ private static final boolean LOG_VERBOSE = false;
+
+ // The following constants are Wbxml standard
+ public static final int START_DOCUMENT = 0;
+ public static final int DONE = 1;
+ public static final int START = 2;
+ public static final int END = 3;
+ public static final int TEXT = 4;
+ public static final int END_DOCUMENT = 3;
+ private static final int NOT_FETCHED = Integer.MIN_VALUE;
+ private static final int NOT_ENDED = Integer.MIN_VALUE;
+ private static final int EOF_BYTE = -1;
+ private boolean logging = false;
+ private boolean capture = false;
+ private String logTag = "EAS Parser";
+
+ // Where tags start in a page
+ private static final int TAG_BASE = 5;
+
+ private ArrayList<Integer> captureArray;
+
+ // The input stream for this parser
+ private InputStream in;
+
+ // The current tag depth
+ private int depth;
+
+ // The upcoming (saved) id from the stream
+ private int nextId = NOT_FETCHED;
+
+ // The current tag table (i.e. the tag table for the current page)
+ private String[] tagTable;
+
+ // An array of tag tables, as defined in EasTags
+ static private String[][] tagTables = new String[Tags.pages.length + 1][];
+
+ // The stack of names of tags being processed; used when debug = true
+ private String[] nameArray = new String[32];
+
+ // The stack of tags being processed
+ private int[] startTagArray = new int[32];
+
+ // The following vars are available to all to avoid method calls that represent the state of
+ // the parser at any given time
+ public int endTag = NOT_ENDED;
+
+ public int startTag;
+
+ // The type of the last token read
+ public int type;
+
+ // The current page
+ public int page;
+
+ // The current tag
+ public int tag;
+
+ // The name of the current tag
+ public String name;
+
+ // Whether the current tag is associated with content (a value)
+ public boolean noContent;
+
+ // The value read, as a String. Only one of text or num will be valid, depending on whether the
+ // value was requested as a String or an int (to avoid wasted effort in parsing)
+ public String text;
+
+ // The value read, as an int
+ public int num;
+
+ // The value read, as bytes
+ public byte[] bytes;
+
+ /**
+ * Generated when the parser comes to EOF prematurely during parsing (i.e. in error)
+ */
+ public class EofException extends IOException {
+ private static final long serialVersionUID = 1L;
+ }
+
+ /**
+ * An EmptyStreamException is an EofException that occurs reading the first byte in the parser's
+ * input stream; in other words, the stream had no content.
+ */
+ public class EmptyStreamException extends EofException {
+ private static final long serialVersionUID = 1L;
+ }
+
+ public class EodException extends IOException {
+ private static final long serialVersionUID = 1L;
+ }
+
+ public class EasParserException extends IOException {
+ private static final long serialVersionUID = 1L;
+
+ EasParserException() {
+ super("WBXML format error");
+ }
+
+ EasParserException(String reason) {
+ super(reason);
+ }
+ }
+
+ public boolean parse() throws IOException, EasException {
+ return false;
+ }
+
+ /**
+ * Initialize the tag tables; they are constant
+ *
+ */
+ {
+ String[][] pages = Tags.pages;
+ for (int i = 0; i < pages.length; i++) {
+ String[] page = pages[i];
+ if (page.length > 0) {
+ tagTables[i] = page;
+ }
+ }
+ }
+
+ public Parser(InputStream in) throws IOException {
+ setInput(in, true);
+ logging = Eas.PARSER_LOG;
+ }
+
+ /**
+ * Constructor for use when switching parsers within a input stream
+ * @param parser an existing, initialized parser
+ * @throws IOException
+ */
+ public Parser(Parser parser) throws IOException {
+ setInput(parser.in, false);
+ logging = Eas.PARSER_LOG;
+ }
+
+ /**
+ * Set the debug state of the parser. When debugging is on, every token is logged (Log.v) to
+ * the console.
+ *
+ * @param val the desired state for debug output
+ */
+ public void setDebug(boolean val) {
+ logging = val;
+ }
+
+ protected InputStream getInput() {
+ return in;
+ }
+
+ /**
+ * 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.
+ */
+ public void captureOn() {
+ capture = true;
+ captureArray = new ArrayList<Integer>();
+ }
+
+ /**
+ * Turns off data capture; writes the captured data to a specified file.
+ */
+ public void captureOff(Context context, String file) {
+ try {
+ FileOutputStream out = context.openFileOutput(file, Context.MODE_WORLD_WRITEABLE);
+ out.write(captureArray.toString().getBytes());
+ out.close();
+ } catch (FileNotFoundException e) {
+ // This is debug code; exceptions aren't interesting.
+ } catch (IOException e) {
+ // This is debug code; exceptions aren't interesting.
+ }
+ }
+
+ /**
+ * Return the value of the current tag, as a byte array. Note that the result of this call
+ * is indeterminate, and possibly null, if the value of the tag is not a byte array
+ *
+ * @return the byte array value of the current tag
+ * @throws IOException
+ */
+ public byte[] getValueBytes() throws IOException {
+ getValue();
+ return bytes;
+ }
+
+ /**
+ * Return the value of the current tag, as a String. Note that the result of this call is
+ * indeterminate, and possibly null, if the value of the tag is not an immediate string
+ *
+ * @return the String value of the current tag
+ * @throws IOException
+ */
+ public String getValue() throws IOException {
+ // The false argument tells getNext to return the value as a String
+ getNext(false);
+ // 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
+ getNext(false);
+ // If not, throw an exception
+ if (type != END) {
+ throw new IOException("No END found!");
+ }
+ return val;
+ }
+
+ /**
+ * Return the value of the current tag, as an integer. Note that the value of this call is
+ * indeterminate if the value of this tag is not an immediate string parsed as an integer
+ *
+ * @return the integer value of the current tag
+ * @throws IOException
+ */
+ public int getValueInt() throws IOException {
+ // The true argument to getNext indicates the desire for an integer return value
+ getNext(true);
+ 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
+ getNext(false);
+ // If not, throw an exception
+ if (type != END) {
+ throw new IOException("No END found!");
+ }
+ return val;
+ }
+
+ /**
+ * Return the next tag found in the stream; special tags END and END_DOCUMENT are used to
+ * mark the end of the current tag and end of document. If we hit end of document without
+ * looking for it, generate an EodException. The tag returned consists of the page number
+ * shifted PAGE_SHIFT bits OR'd with the tag retrieved from the stream. Thus, all tags returned
+ * are unique.
+ *
+ * @param endingTag the tag that would represent the end of the tag we're processing
+ * @return the next tag found
+ * @throws IOException
+ */
+ public int nextTag(int endingTag) throws IOException {
+ // Lose the page information
+ endTag = endingTag &= Tags.PAGE_MASK;
+ while (getNext(false) != DONE) {
+ // If we're a start, set tag to include the page and return it
+ if (type == START) {
+ tag = page | startTag;
+ return tag;
+ // If we're at the ending tag we're looking for, return the END signal
+ } else if (type == END && startTag == endTag) {
+ return END;
+ }
+ }
+ // We're at end of document here. If we're looking for it, return END_DOCUMENT
+ if (endTag == START_DOCUMENT) {
+ return END_DOCUMENT;
+ }
+ // Otherwise, we've prematurely hit end of document, so exception out
+ // EodException is a subclass of IOException; this will be treated as an IO error by
+ // ExchangeService
+ throw new EodException();
+ }
+
+ /**
+ * Skip anything found in the stream until the end of the current tag is reached. This can be
+ * used to ignore stretches of xml that aren't needed by the parser.
+ *
+ * @throws IOException
+ */
+ public void skipTag() throws IOException {
+ int thisTag = startTag;
+ // Just loop until we hit the end of the current tag
+ while (getNext(false) != DONE) {
+ if (type == END && startTag == thisTag) {
+ return;
+ }
+ }
+
+ // If we're at end of document, that's bad
+ throw new EofException();
+ }
+
+ /**
+ * Retrieve the next token from the input stream
+ *
+ * @return the token found
+ * @throws IOException
+ */
+ public int nextToken() throws IOException {
+ getNext(false);
+ return type;
+ }
+
+ /**
+ * Initializes the parser with an input stream; reads the first 4 bytes (which are always the
+ * same in EAS, and then sets the tag table to point to page 0 (by definition, the starting
+ * page).
+ *
+ * @param in the InputStream associated with this parser
+ * @throws IOException
+ */
+ public void setInput(InputStream in, boolean initialize) throws IOException {
+ this.in = in;
+ if ((in != null) && initialize) {
+ // If we fail on the very first byte, report an empty stream
+ try {
+ readByte(); // version
+ } catch (EofException e) {
+ throw new EmptyStreamException();
+ }
+ readInt(); // ?
+ readInt(); // 106 (UTF-8)
+ readInt(); // string table length
+ }
+ tagTable = tagTables[0];
+ }
+
+ @VisibleForTesting
+ void resetInput(InputStream in) {
+ this.in = in;
+ try {
+ // Read leading zero
+ read();
+ } catch (IOException e) {
+ }
+ }
+
+ void log(String str) {
+ int cr = str.indexOf('\n');
+ if (cr > 0) {
+ str = str.substring(0, cr);
+ }
+ Log.v(logTag, str);
+ if (Eas.FILE_LOG) {
+ FileLogger.log(logTag, str);
+ }
+ }
+
+ protected void pushTag(int id) {
+ page = id >> Tags.PAGE_SHIFT;
+ tagTable = tagTables[page];
+ push(id);
+ }
+
+ private void pop() {
+ if (logging) {
+ name = nameArray[depth];
+ log("</" + name + '>');
+ }
+ // Retrieve the now-current startTag from our stack
+ startTag = endTag = startTagArray[depth];
+ depth--;
+ }
+
+ private void push(int id) {
+ // The tag is in the low 6 bits
+ startTag = id & 0x3F;
+ // If the high bit is set, there is content (a value) to be read
+ noContent = (id & 0x40) == 0;
+ depth++;
+ if (logging) {
+ name = tagTable[startTag - TAG_BASE];
+ nameArray[depth] = name;
+ log("<" + name + (noContent ? '/' : "") + '>');
+ }
+ // Save the startTag to our stack
+ startTagArray[depth] = startTag;
+ }
+
+ /**
+ * Return the next piece of data from the stream. The return value indicates the type of data
+ * that has been retrieved - START (start of tag), END (end of tag), DONE (end of stream), or
+ * TEXT (the value of a tag)
+ *
+ * @param asInt whether a TEXT value should be parsed as a String or an int.
+ * @return the type of data retrieved
+ * @throws IOException
+ */
+ private final int getNext(boolean asInt) throws IOException {
+ if (noContent) {
+ nameArray[depth--] = null;
+ type = END;
+ noContent = false;
+ return type;
+ }
+
+ text = null;
+ name = null;
+
+ int id = nextId ();
+ while (id == Wbxml.SWITCH_PAGE) {
+ nextId = NOT_FETCHED;
+ // Get the new page number
+ int pg = readByte();
+ // Save the shifted page to add into the startTag in nextTag
+ page = pg << Tags.PAGE_SHIFT;
+ if (LOG_VERBOSE) {
+ log("Page: " + page);
+ }
+ // Retrieve the current tag table
+ tagTable = tagTables[pg];
+ id = nextId();
+ }
+ nextId = NOT_FETCHED;
+
+ switch (id) {
+ case EOF_BYTE:
+ // End of document
+ type = DONE;
+ break;
+
+ case Wbxml.END:
+ type = END;
+ pop();
+ break;
+
+ case Wbxml.STR_I:
+ // Inline string
+ type = TEXT;
+ if (asInt) {
+ num = readInlineInt();
+ } else {
+ text = readInlineString();
+ }
+ if (logging) {
+ name = tagTable[startTag - TAG_BASE];
+ log(name + ": " + (asInt ? Integer.toString(num) : text));
+ }
+ break;
+
+ case Wbxml.OPAQUE:
+ // Integer length + opaque data
+ int length = readInt();
+ bytes = new byte[length];
+ for (int i = 0; i < length; i++) {
+ bytes[i] = (byte)readByte();
+ }
+ if (logging) {
+ name = tagTable[startTag - TAG_BASE];
+ log(name + ": (opaque:" + length + ") ");
+ }
+ break;
+
+ default:
+ type = START;
+ push(id);
+ }
+
+ // Return the type of data we're dealing with
+ return type;
+ }
+
+ /**
+ * Read an int from the input stream, and capture it if necessary for debugging. Seems a small
+ * price to pay...
+ *
+ * @return the int read
+ * @throws IOException
+ */
+ private int read() throws IOException {
+ int i;
+ i = in.read();
+ if (capture) {
+ captureArray.add(i);
+ }
+ if (LOG_VERBOSE) {
+ log("Byte: " + i);
+ }
+ return i;
+ }
+
+ private int nextId() throws IOException {
+ if (nextId == NOT_FETCHED) {
+ nextId = read();
+ }
+ return nextId;
+ }
+
+ private int readByte() throws IOException {
+ int i = read();
+ if (i == EOF_BYTE) {
+ throw new EofException();
+ }
+ return i;
+ }
+
+ /**
+ * Read an integer from the stream; this is called when the parser knows that what follows is
+ * an inline string representing an integer (e.g. the Read tag in Email has a value known to
+ * be either "0" or "1")
+ *
+ * @return the integer as parsed from the stream
+ * @throws IOException
+ */
+ private int readInlineInt() throws IOException {
+ int result = 0;
+
+ while (true) {
+ int i = readByte();
+ // Inline strings are always terminated with a zero byte
+ if (i == 0) {
+ return result;
+ }
+ if (i >= '0' && i <= '9') {
+ result = (result * 10) + (i - '0');
+ } else {
+ throw new IOException("Non integer");
+ }
+ }
+ }
+
+ private int readInt() throws IOException {
+ int result = 0;
+ int i;
+
+ do {
+ i = readByte();
+ result = (result << 7) | (i & 0x7f);
+ } while ((i & 0x80) != 0);
+
+ return result;
+ }
+
+ /**
+ * Read an inline string from the stream
+ *
+ * @return the String as parsed from the stream
+ * @throws IOException
+ */
+ private String readInlineString() throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream(256);
+ while (true) {
+ int i = read();
+ if (i == 0) {
+ break;
+ } else if (i == EOF_BYTE) {
+ throw new EofException();
+ }
+ outputStream.write(i);
+ }
+ outputStream.flush();
+ String res = outputStream.toString("UTF-8");
+ outputStream.close();
+ return res;
+ }
+}
diff --git a/exchange2/src/com/android/exchange/adapter/PingParser.java b/exchange2/src/com/android/exchange/adapter/PingParser.java
new file mode 100644
index 0000000..f061472
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/PingParser.java
@@ -0,0 +1,97 @@
+/* Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import com.android.exchange.EasSyncService;
+import com.android.exchange.IllegalHeartbeatException;
+import com.android.exchange.StaleFolderListException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Parse the result of a Ping command.
+ *
+ * If there are folders with changes, add the serverId of those folders to the syncList array.
+ * If the folder list needs to be reloaded, throw a StaleFolderListException, which will be caught
+ * by the sync server, which will sync the updated folder list.
+ */
+public class PingParser extends Parser {
+ private ArrayList<String> syncList = new ArrayList<String>();
+ private EasSyncService mService;
+ private int mSyncStatus = 0;
+
+ public ArrayList<String> getSyncList() {
+ return syncList;
+ }
+
+ public int getSyncStatus() {
+ return mSyncStatus;
+ }
+
+ public PingParser(InputStream in, EasSyncService service) throws IOException {
+ super(in);
+ mService = service;
+ }
+
+ public void parsePingFolders(ArrayList<String> syncList) throws IOException {
+ while (nextTag(Tags.PING_FOLDERS) != END) {
+ if (tag == Tags.PING_FOLDER) {
+ // Here we'll keep track of which mailboxes need syncing
+ String serverId = getValue();
+ syncList.add(serverId);
+ mService.userLog("Changes found in: ", serverId);
+ } else {
+ skipTag();
+ }
+ }
+ }
+
+ @Override
+ public boolean parse() throws IOException, StaleFolderListException, IllegalHeartbeatException {
+ boolean res = false;
+ if (nextTag(START_DOCUMENT) != Tags.PING_PING) {
+ throw new IOException();
+ }
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ if (tag == Tags.PING_STATUS) {
+ int status = getValueInt();
+ mSyncStatus = status;
+ mService.userLog("Ping completed, status = ", status);
+ if (status == 2) {
+ res = true;
+ } else if (status == 7 || status == 4) {
+ // Status of 7 or 4 indicate a stale folder list
+ throw new StaleFolderListException();
+ } else if (status == 5) {
+ // Status 5 means our heartbeat is beyond allowable limits
+ // In this case, there will be a heartbeat interval set
+ }
+ } else if (tag == Tags.PING_FOLDERS) {
+ parsePingFolders(syncList);
+ } else if (tag == Tags.PING_HEARTBEAT_INTERVAL) {
+ // Throw an exception, saving away the legal heartbeat interval specified
+ throw new IllegalHeartbeatException(getValueInt());
+ } else {
+ skipTag();
+ }
+ }
+ return res;
+ }
+}
+
diff --git a/exchange2/src/com/android/exchange/adapter/ProvisionParser.java b/exchange2/src/com/android/exchange/adapter/ProvisionParser.java
new file mode 100644
index 0000000..de30111
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/ProvisionParser.java
@@ -0,0 +1,616 @@
+/* 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 android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
+
+import com.android.emailcommon.provider.Policy;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.R;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Parse the result of the Provision command
+ */
+public class ProvisionParser extends Parser {
+ private final EasSyncService mService;
+ private Policy mPolicy = null;
+ private String mSecuritySyncKey = null;
+ private boolean mRemoteWipe = false;
+ private boolean mIsSupportable = true;
+ private boolean smimeRequired = false;
+ private final Resources mResources;
+
+ public ProvisionParser(InputStream in, EasSyncService service) throws IOException {
+ super(in);
+ mService = service;
+ mResources = service.mContext.getResources();
+ }
+
+ public Policy getPolicy() {
+ return mPolicy;
+ }
+
+ public String getSecuritySyncKey() {
+ return mSecuritySyncKey;
+ }
+
+ public void setSecuritySyncKey(String securitySyncKey) {
+ mSecuritySyncKey = securitySyncKey;
+ }
+
+ public boolean getRemoteWipe() {
+ return mRemoteWipe;
+ }
+
+ public boolean hasSupportablePolicySet() {
+ return (mPolicy != null) && mIsSupportable;
+ }
+
+ public void clearUnsupportablePolicies() {
+ mIsSupportable = true;
+ mPolicy.mProtocolPoliciesUnsupported = null;
+ }
+
+ private void addPolicyString(StringBuilder sb, int res) {
+ sb.append(mResources.getString(res));
+ sb.append(Policy.POLICY_STRING_DELIMITER);
+ }
+
+ /**
+ * Complete setup of a Policy; we normalize it first (removing inconsistencies, etc.) and then
+ * generate the tokenized "protocol policies enforced" string. Note that unsupported policies
+ * must have been added prior to calling this method (this is only a possibility with wbxml
+ * policy documents, as all versions of the OS support the policies in xml documents).
+ */
+ private void setPolicy(Policy policy) {
+ policy.normalize();
+ StringBuilder sb = new StringBuilder();
+ if (policy.mDontAllowAttachments) {
+ addPolicyString(sb, R.string.policy_dont_allow_attachments);
+ }
+ if (policy.mRequireManualSyncWhenRoaming) {
+ addPolicyString(sb, R.string.policy_require_manual_sync_roaming);
+ }
+ policy.mProtocolPoliciesEnforced = sb.toString();
+ mPolicy = policy;
+ }
+
+ private boolean deviceSupportsEncryption() {
+ DevicePolicyManager dpm = (DevicePolicyManager)
+ mService.mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
+ int status = dpm.getStorageEncryptionStatus();
+ return status != DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED;
+ }
+
+ private void parseProvisionDocWbxml() throws IOException {
+ Policy policy = new Policy();
+ ArrayList<Integer> unsupportedList = new ArrayList<Integer>();
+ boolean passwordEnabled = false;
+
+ while (nextTag(Tags.PROVISION_EAS_PROVISION_DOC) != END) {
+ boolean tagIsSupported = true;
+ int res = 0;
+ switch (tag) {
+ case Tags.PROVISION_DEVICE_PASSWORD_ENABLED:
+ if (getValueInt() == 1) {
+ passwordEnabled = true;
+ if (policy.mPasswordMode == Policy.PASSWORD_MODE_NONE) {
+ policy.mPasswordMode = Policy.PASSWORD_MODE_SIMPLE;
+ }
+ }
+ break;
+ case Tags.PROVISION_MIN_DEVICE_PASSWORD_LENGTH:
+ policy.mPasswordMinLength = getValueInt();
+ break;
+ case Tags.PROVISION_ALPHA_DEVICE_PASSWORD_ENABLED:
+ if (getValueInt() == 1) {
+ policy.mPasswordMode = Policy.PASSWORD_MODE_STRONG;
+ }
+ break;
+ case Tags.PROVISION_MAX_INACTIVITY_TIME_DEVICE_LOCK:
+ // EAS gives us seconds, which is, happily, what the PolicySet requires
+ policy.mMaxScreenLockTime = getValueInt();
+ break;
+ case Tags.PROVISION_MAX_DEVICE_PASSWORD_FAILED_ATTEMPTS:
+ policy.mPasswordMaxFails = getValueInt();
+ break;
+ case Tags.PROVISION_DEVICE_PASSWORD_EXPIRATION:
+ policy.mPasswordExpirationDays = getValueInt();
+ break;
+ case Tags.PROVISION_DEVICE_PASSWORD_HISTORY:
+ policy.mPasswordHistory = getValueInt();
+ break;
+ case Tags.PROVISION_ALLOW_CAMERA:
+ policy.mDontAllowCamera = (getValueInt() == 0);
+ 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 policies, if false, can't be supported at the moment
+ case Tags.PROVISION_ALLOW_STORAGE_CARD:
+ case Tags.PROVISION_ALLOW_UNSIGNED_APPLICATIONS:
+ case Tags.PROVISION_ALLOW_UNSIGNED_INSTALLATION_PACKAGES:
+ case Tags.PROVISION_ALLOW_WIFI:
+ case Tags.PROVISION_ALLOW_TEXT_MESSAGING:
+ case Tags.PROVISION_ALLOW_POP_IMAP_EMAIL:
+ case Tags.PROVISION_ALLOW_IRDA:
+ case Tags.PROVISION_ALLOW_HTML_EMAIL:
+ case Tags.PROVISION_ALLOW_BROWSER:
+ case Tags.PROVISION_ALLOW_CONSUMER_EMAIL:
+ case Tags.PROVISION_ALLOW_INTERNET_SHARING:
+ if (getValueInt() == 0) {
+ tagIsSupported = false;
+ switch(tag) {
+ case Tags.PROVISION_ALLOW_STORAGE_CARD:
+ res = R.string.policy_dont_allow_storage_cards;
+ break;
+ case Tags.PROVISION_ALLOW_UNSIGNED_APPLICATIONS:
+ res = R.string.policy_dont_allow_unsigned_apps;
+ break;
+ case Tags.PROVISION_ALLOW_UNSIGNED_INSTALLATION_PACKAGES:
+ res = R.string.policy_dont_allow_unsigned_installers;
+ break;
+ case Tags.PROVISION_ALLOW_WIFI:
+ res = R.string.policy_dont_allow_wifi;
+ break;
+ case Tags.PROVISION_ALLOW_TEXT_MESSAGING:
+ res = R.string.policy_dont_allow_text_messaging;
+ break;
+ case Tags.PROVISION_ALLOW_POP_IMAP_EMAIL:
+ res = R.string.policy_dont_allow_pop_imap;
+ break;
+ case Tags.PROVISION_ALLOW_IRDA:
+ res = R.string.policy_dont_allow_irda;
+ break;
+ case Tags.PROVISION_ALLOW_HTML_EMAIL:
+ res = R.string.policy_dont_allow_html;
+ policy.mDontAllowHtml = true;
+ break;
+ case Tags.PROVISION_ALLOW_BROWSER:
+ res = R.string.policy_dont_allow_browser;
+ break;
+ case Tags.PROVISION_ALLOW_CONSUMER_EMAIL:
+ res = R.string.policy_dont_allow_consumer_email;
+ break;
+ case Tags.PROVISION_ALLOW_INTERNET_SHARING:
+ res = R.string.policy_dont_allow_internet_sharing;
+ break;
+ }
+ if (res > 0) {
+ unsupportedList.add(res);
+ }
+ }
+ break;
+ case Tags.PROVISION_ATTACHMENTS_ENABLED:
+ policy.mDontAllowAttachments = getValueInt() != 1;
+ break;
+ // Bluetooth: 0 = no bluetooth; 1 = only hands-free; 2 = allowed
+ case Tags.PROVISION_ALLOW_BLUETOOTH:
+ if (getValueInt() != 2) {
+ tagIsSupported = false;
+ unsupportedList.add(R.string.policy_bluetooth_restricted);
+ }
+ break;
+ // We may now support device (internal) encryption; we'll check this capability
+ // below with the call to SecurityPolicy.isSupported()
+ case Tags.PROVISION_REQUIRE_DEVICE_ENCRYPTION:
+ if (getValueInt() == 1) {
+ if (!deviceSupportsEncryption()) {
+ tagIsSupported = false;
+ unsupportedList.add(R.string.policy_require_encryption);
+ } else {
+ policy.mRequireEncryption = true;
+ }
+ }
+ break;
+ // Note that DEVICE_ENCRYPTION_ENABLED refers to SD card encryption, which the OS
+ // does not yet support.
+ case Tags.PROVISION_DEVICE_ENCRYPTION_ENABLED:
+ if (getValueInt() == 1) {
+ log("Policy requires SD card encryption");
+ // Let's see if this can be supported on our device...
+ if (deviceSupportsEncryption()) {
+ StorageManager sm = (StorageManager)mService.mContext.getSystemService(
+ Context.STORAGE_SERVICE);
+ // NOTE: Private API!
+ // Go through volumes; if ANY are removable, we can't support this
+ // policy.
+ StorageVolume[] volumeList = sm.getVolumeList();
+ for (StorageVolume volume: volumeList) {
+ if (volume.isRemovable()) {
+ tagIsSupported = false;
+ log("Removable: " + volume.getDescription());
+ break; // Break only from the storage volume loop
+ } else {
+ log("Not Removable: " + volume.getDescription());
+ }
+ }
+ if (tagIsSupported) {
+ // If this policy is requested, we MUST also require encryption
+ log("Device supports SD card encryption");
+ policy.mRequireEncryption = true;
+ break;
+ }
+ } else {
+ log("Device doesn't support encryption; failing");
+ tagIsSupported = false;
+ }
+ // If we fall through, we can't support the policy
+ unsupportedList.add(R.string.policy_require_sd_encryption);
+ }
+ break;
+ // Note this policy; we enforce it in ExchangeService
+ case Tags.PROVISION_REQUIRE_MANUAL_SYNC_WHEN_ROAMING:
+ policy.mRequireManualSyncWhenRoaming = getValueInt() == 1;
+ break;
+ // We are allowed to accept policies, regardless of value of this tag
+ // TODO: When we DO support a recovery password, we need to store the value in
+ // the account (so we know to utilize it)
+ case Tags.PROVISION_PASSWORD_RECOVERY_ENABLED:
+ // Read, but ignore, value
+ policy.mPasswordRecoveryEnabled = getValueInt() == 1;
+ break;
+ // The following policies, if true, can't be supported at the moment
+ case Tags.PROVISION_REQUIRE_SIGNED_SMIME_MESSAGES:
+ case Tags.PROVISION_REQUIRE_ENCRYPTED_SMIME_MESSAGES:
+ case Tags.PROVISION_REQUIRE_SIGNED_SMIME_ALGORITHM:
+ case Tags.PROVISION_REQUIRE_ENCRYPTION_SMIME_ALGORITHM:
+ if (getValueInt() == 1) {
+ tagIsSupported = false;
+ if (!smimeRequired) {
+ unsupportedList.add(R.string.policy_require_smime);
+ smimeRequired = true;
+ }
+ }
+ break;
+ case Tags.PROVISION_MAX_ATTACHMENT_SIZE:
+ int max = getValueInt();
+ if (max > 0) {
+ policy.mMaxAttachmentSize = max;
+ }
+ break;
+ // Complex characters are supported
+ case Tags.PROVISION_MIN_DEVICE_PASSWORD_COMPLEX_CHARS:
+ policy.mPasswordComplexChars = getValueInt();
+ break;
+ // The following policies are moot; they allow functionality that we don't support
+ case Tags.PROVISION_ALLOW_DESKTOP_SYNC:
+ case Tags.PROVISION_ALLOW_SMIME_ENCRYPTION_NEGOTIATION:
+ case Tags.PROVISION_ALLOW_SMIME_SOFT_CERTS:
+ case Tags.PROVISION_ALLOW_REMOTE_DESKTOP:
+ skipTag();
+ break;
+ // We don't handle approved/unapproved application lists
+ case Tags.PROVISION_UNAPPROVED_IN_ROM_APPLICATION_LIST:
+ case Tags.PROVISION_APPROVED_APPLICATION_LIST:
+ // Parse and throw away the content
+ if (specifiesApplications(tag)) {
+ tagIsSupported = false;
+ if (tag == Tags.PROVISION_UNAPPROVED_IN_ROM_APPLICATION_LIST) {
+ unsupportedList.add(R.string.policy_app_blacklist);
+ } else {
+ unsupportedList.add(R.string.policy_app_whitelist);
+ }
+ }
+ break;
+ // We accept calendar age, since we never ask for more than two weeks, and that's
+ // the most restrictive policy
+ case Tags.PROVISION_MAX_CALENDAR_AGE_FILTER:
+ policy.mMaxCalendarLookback = getValueInt();
+ break;
+ // We handle max email lookback
+ case Tags.PROVISION_MAX_EMAIL_AGE_FILTER:
+ policy.mMaxEmailLookback = getValueInt();
+ break;
+ // We currently reject these next two policies
+ case Tags.PROVISION_MAX_EMAIL_BODY_TRUNCATION_SIZE:
+ case Tags.PROVISION_MAX_EMAIL_HTML_BODY_TRUNCATION_SIZE:
+ String value = getValue();
+ // -1 indicates no required truncation
+ if (!value.equals("-1")) {
+ max = Integer.parseInt(value);
+ if (tag == Tags.PROVISION_MAX_EMAIL_BODY_TRUNCATION_SIZE) {
+ policy.mMaxTextTruncationSize = max;
+ unsupportedList.add(R.string.policy_text_truncation);
+ } else {
+ policy.mMaxHtmlTruncationSize = max;
+ unsupportedList.add(R.string.policy_html_truncation);
+ }
+ tagIsSupported = false;
+ }
+ break;
+ default:
+ skipTag();
+ }
+
+ if (!tagIsSupported) {
+ log("Policy not supported: " + tag);
+ mIsSupportable = false;
+ }
+ }
+
+ // Make sure policy settings are valid; password not enabled trumps other password settings
+ if (!passwordEnabled) {
+ policy.mPasswordMode = Policy.PASSWORD_MODE_NONE;
+ }
+
+ if (!unsupportedList.isEmpty()) {
+ StringBuilder sb = new StringBuilder();
+ for (int res: unsupportedList) {
+ addPolicyString(sb, res);
+ }
+ policy.mProtocolPoliciesUnsupported = sb.toString();
+ }
+
+ setPolicy(policy);
+ }
+
+ /**
+ * Return whether or not either of the application list tags specifies any applications
+ * @param endTag the tag whose children we're walking through
+ * @return whether any applications were specified (by name or by hash)
+ * @throws IOException
+ */
+ private boolean specifiesApplications(int endTag) throws IOException {
+ boolean specifiesApplications = false;
+ while (nextTag(endTag) != END) {
+ switch (tag) {
+ case Tags.PROVISION_APPLICATION_NAME:
+ case Tags.PROVISION_HASH:
+ specifiesApplications = true;
+ break;
+ default:
+ skipTag();
+ }
+ }
+ return specifiesApplications;
+ }
+
+ /*package*/ void parseProvisionDocXml(String doc) throws IOException {
+ Policy policy = new Policy();
+
+ try {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlPullParser parser = factory.newPullParser();
+ parser.setInput(new ByteArrayInputStream(doc.getBytes()), "UTF-8");
+ int type = parser.getEventType();
+ if (type == XmlPullParser.START_DOCUMENT) {
+ type = parser.next();
+ if (type == XmlPullParser.START_TAG) {
+ String tagName = parser.getName();
+ if (tagName.equals("wap-provisioningdoc")) {
+ parseWapProvisioningDoc(parser, policy);
+ }
+ }
+ }
+ } catch (XmlPullParserException e) {
+ throw new IOException();
+ }
+
+ setPolicy(policy);
+ }
+
+ /**
+ * Return true if password is required; otherwise false.
+ */
+ private boolean parseSecurityPolicy(XmlPullParser parser, Policy policy)
+ throws XmlPullParserException, IOException {
+ boolean passwordRequired = true;
+ while (true) {
+ int type = parser.nextTag();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("characteristic")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG) {
+ String tagName = parser.getName();
+ if (tagName.equals("parm")) {
+ String name = parser.getAttributeValue(null, "name");
+ if (name.equals("4131")) {
+ String value = parser.getAttributeValue(null, "value");
+ if (value.equals("1")) {
+ passwordRequired = false;
+ }
+ }
+ }
+ }
+ }
+ return passwordRequired;
+ }
+
+ private void parseCharacteristic(XmlPullParser parser, Policy policy)
+ throws XmlPullParserException, IOException {
+ boolean enforceInactivityTimer = true;
+ while (true) {
+ int type = parser.nextTag();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("characteristic")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("parm")) {
+ String name = parser.getAttributeValue(null, "name");
+ String value = parser.getAttributeValue(null, "value");
+ if (name.equals("AEFrequencyValue")) {
+ if (enforceInactivityTimer) {
+ if (value.equals("0")) {
+ policy.mMaxScreenLockTime = 1;
+ } else {
+ policy.mMaxScreenLockTime = 60*Integer.parseInt(value);
+ }
+ }
+ } else if (name.equals("AEFrequencyType")) {
+ // "0" here means we don't enforce an inactivity timeout
+ if (value.equals("0")) {
+ enforceInactivityTimer = false;
+ }
+ } else if (name.equals("DeviceWipeThreshold")) {
+ policy.mPasswordMaxFails = Integer.parseInt(value);
+ } else if (name.equals("CodewordFrequency")) {
+ // Ignore; has no meaning for us
+ } else if (name.equals("MinimumPasswordLength")) {
+ policy.mPasswordMinLength = Integer.parseInt(value);
+ } else if (name.equals("PasswordComplexity")) {
+ if (value.equals("0")) {
+ policy.mPasswordMode = Policy.PASSWORD_MODE_STRONG;
+ } else {
+ policy.mPasswordMode = Policy.PASSWORD_MODE_SIMPLE;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void parseRegistry(XmlPullParser parser, Policy policy)
+ throws XmlPullParserException, IOException {
+ while (true) {
+ int type = parser.nextTag();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("characteristic")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ if (name.equals("characteristic")) {
+ parseCharacteristic(parser, policy);
+ }
+ }
+ }
+ }
+
+ private void parseWapProvisioningDoc(XmlPullParser parser, Policy policy)
+ throws XmlPullParserException, IOException {
+ while (true) {
+ int type = parser.nextTag();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("wap-provisioningdoc")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ if (name.equals("characteristic")) {
+ String atype = parser.getAttributeValue(null, "type");
+ if (atype.equals("SecurityPolicy")) {
+ // If a password isn't required, stop here
+ if (!parseSecurityPolicy(parser, policy)) {
+ return;
+ }
+ } else if (atype.equals("Registry")) {
+ parseRegistry(parser, policy);
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ private void parseProvisionData() throws IOException {
+ while (nextTag(Tags.PROVISION_DATA) != END) {
+ if (tag == Tags.PROVISION_EAS_PROVISION_DOC) {
+ parseProvisionDocWbxml();
+ } else {
+ skipTag();
+ }
+ }
+ }
+
+ private void parsePolicy() throws IOException {
+ String policyType = null;
+ while (nextTag(Tags.PROVISION_POLICY) != END) {
+ switch (tag) {
+ case Tags.PROVISION_POLICY_TYPE:
+ policyType = getValue();
+ mService.userLog("Policy type: ", policyType);
+ break;
+ case Tags.PROVISION_POLICY_KEY:
+ mSecuritySyncKey = getValue();
+ break;
+ case Tags.PROVISION_STATUS:
+ mService.userLog("Policy status: ", getValue());
+ break;
+ case Tags.PROVISION_DATA:
+ if (policyType.equalsIgnoreCase(EasSyncService.EAS_2_POLICY_TYPE)) {
+ // Parse the old style XML document
+ parseProvisionDocXml(getValue());
+ } else {
+ // Parse the newer WBXML data
+ parseProvisionData();
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private void parsePolicies() throws IOException {
+ while (nextTag(Tags.PROVISION_POLICIES) != END) {
+ if (tag == Tags.PROVISION_POLICY) {
+ parsePolicy();
+ } else {
+ skipTag();
+ }
+ }
+ }
+
+ private void parseDeviceInformation() throws IOException {
+ while (nextTag(Tags.SETTINGS_DEVICE_INFORMATION) != END) {
+ if (tag == Tags.SETTINGS_STATUS) {
+ mService.userLog("DeviceInformation status: " + getValue());
+ } 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);
+ res = (status == 1);
+ break;
+ case Tags.SETTINGS_DEVICE_INFORMATION:
+ parseDeviceInformation();
+ break;
+ case Tags.PROVISION_POLICIES:
+ parsePolicies();
+ break;
+ case Tags.PROVISION_REMOTE_WIPE:
+ // Indicate remote wipe command received
+ mRemoteWipe = true;
+ break;
+ default:
+ skipTag();
+ }
+ }
+ return res;
+ }
+}
diff --git a/exchange2/src/com/android/exchange/adapter/Search.java b/exchange2/src/com/android/exchange/adapter/Search.java
new file mode 100644
index 0000000..62cda19
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/Search.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.service.EmailServiceStatus;
+import com.android.emailcommon.service.SearchParams;
+import com.android.emailcommon.utility.TextUtilities;
+import com.android.exchange.Eas;
+import com.android.exchange.EasResponse;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.ExchangeService;
+import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
+import com.android.mail.providers.UIProvider;
+
+import org.apache.http.HttpStatus;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Implementation of server-side search for EAS using the EmailService API
+ */
+public class Search {
+ // The shortest search query we'll accept
+ // TODO Check with UX whether this is correct
+ private static final int MIN_QUERY_LENGTH = 3;
+ // The largest number of results we'll ask for per server request
+ private static final int MAX_SEARCH_RESULTS = 100;
+
+ public static int searchMessages(Context context, long accountId, SearchParams searchParams,
+ long destMailboxId) {
+ // Sanity check for arguments
+ int offset = searchParams.mOffset;
+ int limit = searchParams.mLimit;
+ String filter = searchParams.mFilter;
+ if (limit < 0 || limit > MAX_SEARCH_RESULTS || offset < 0) return 0;
+ // TODO Should this be checked in UI? Are there guidelines for minimums?
+ if (filter == null || filter.length() < MIN_QUERY_LENGTH) return 0;
+
+ int res = 0;
+ Account account = Account.restoreAccountWithId(context, accountId);
+ if (account == null) return res;
+ EasSyncService svc = EasSyncService.setupServiceForAccount(context, account);
+ if (svc == null) return res;
+ Mailbox searchMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId);
+ // Sanity check; account might have been deleted?
+ if (searchMailbox == null) return res;
+ ContentValues statusValues = new ContentValues();
+ try {
+ // Set the status of this mailbox to indicate query
+ statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.USER_QUERY);
+ searchMailbox.update(context, statusValues);
+
+ svc.mMailbox = searchMailbox;
+ svc.mAccount = account;
+ Serializer s = new Serializer();
+ s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE);
+ s.data(Tags.SEARCH_NAME, "Mailbox");
+ s.start(Tags.SEARCH_QUERY).start(Tags.SEARCH_AND);
+ s.data(Tags.SYNC_CLASS, "Email");
+
+ // If this isn't an inbox search, then include the collection id
+ Mailbox inbox = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_INBOX);
+ if (inbox == null) return 0;
+ if (searchParams.mMailboxId != inbox.mId) {
+ s.data(Tags.SYNC_COLLECTION_ID, inbox.mServerId);
+ }
+
+ s.data(Tags.SEARCH_FREE_TEXT, filter);
+ s.end().end(); // SEARCH_AND, SEARCH_QUERY
+ s.start(Tags.SEARCH_OPTIONS);
+ if (offset == 0) {
+ s.tag(Tags.SEARCH_REBUILD_RESULTS);
+ }
+ if (searchParams.mIncludeChildren) {
+ s.tag(Tags.SEARCH_DEEP_TRAVERSAL);
+ }
+ // Range is sent in the form first-last (e.g. 0-9)
+ s.data(Tags.SEARCH_RANGE, offset + "-" + (offset + limit - 1));
+ s.start(Tags.BASE_BODY_PREFERENCE);
+ s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_HTML);
+ s.data(Tags.BASE_TRUNCATION_SIZE, "20000");
+ s.end(); // BASE_BODY_PREFERENCE
+ s.end().end().end().done(); // SEARCH_OPTIONS, SEARCH_STORE, SEARCH_SEARCH
+ EasResponse resp = svc.sendHttpClientPost("Search", s.toByteArray());
+ try {
+ int code = resp.getStatus();
+ if (code == HttpStatus.SC_OK) {
+ InputStream is = resp.getInputStream();
+ try {
+ SearchParser sp = new SearchParser(is, svc, filter);
+ sp.parse();
+ res = sp.getTotalResults();
+ } finally {
+ is.close();
+ }
+ } else {
+ svc.userLog("Search returned " + code);
+ }
+ } finally {
+ resp.close();
+ }
+ } catch (IOException e) {
+ svc.userLog("Search exception " + e);
+ } finally {
+ try {
+ // TODO: Handle error states
+ // Set the status of this mailbox to indicate query over
+ statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC);
+ searchMailbox.update(context, statusValues);
+ ExchangeService.callback().syncMailboxStatus(destMailboxId,
+ EmailServiceStatus.SUCCESS, 100);
+ } catch (RemoteException e) {
+ }
+ }
+ // Return the total count
+ return res;
+ }
+
+ /**
+ * Parse the result of a Search command
+ */
+ static class SearchParser extends Parser {
+ private final EasSyncService mService;
+ private final String mQuery;
+ private int mTotalResults;
+
+ private SearchParser(InputStream in, EasSyncService service, String query)
+ throws IOException {
+ super(in);
+ mService = service;
+ mQuery = query;
+ }
+
+ protected int getTotalResults() {
+ return mTotalResults;
+ }
+
+ @Override
+ public boolean parse() throws IOException {
+ boolean res = false;
+ if (nextTag(START_DOCUMENT) != Tags.SEARCH_SEARCH) {
+ throw new IOException();
+ }
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ if (tag == Tags.SEARCH_STATUS) {
+ String status = getValue();
+ if (Eas.USER_LOG) {
+ Log.d(Logging.LOG_TAG, "Search status: " + status);
+ }
+ } else if (tag == Tags.SEARCH_RESPONSE) {
+ parseResponse();
+ } else {
+ skipTag();
+ }
+ }
+ return res;
+ }
+
+ private boolean parseResponse() throws IOException {
+ boolean res = false;
+ while (nextTag(Tags.SEARCH_RESPONSE) != END) {
+ if (tag == Tags.SEARCH_STORE) {
+ parseStore();
+ } else {
+ skipTag();
+ }
+ }
+ return res;
+ }
+
+ private boolean parseStore() throws IOException {
+ EmailSyncAdapter adapter = new EmailSyncAdapter(mService);
+ EasEmailSyncParser parser = adapter.new EasEmailSyncParser(this, adapter);
+ ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+ boolean res = false;
+
+ while (nextTag(Tags.SEARCH_STORE) != END) {
+ if (tag == Tags.SEARCH_STATUS) {
+ String status = getValue();
+ } else if (tag == Tags.SEARCH_TOTAL) {
+ mTotalResults = getValueInt();
+ } else if (tag == Tags.SEARCH_RESULT) {
+ parseResult(parser, ops);
+ } else {
+ skipTag();
+ }
+ }
+
+ try {
+ adapter.mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
+ if (Eas.USER_LOG) {
+ mService.userLog("Saved " + ops.size() + " search results");
+ }
+ } catch (RemoteException e) {
+ Log.d(Logging.LOG_TAG, "RemoteException while saving search results.");
+ } catch (OperationApplicationException e) {
+ }
+
+ return res;
+ }
+
+ private boolean parseResult(EasEmailSyncParser parser,
+ ArrayList<ContentProviderOperation> ops) throws IOException {
+ // Get an email sync parser for our incoming message data
+ boolean res = false;
+ Message msg = new Message();
+ while (nextTag(Tags.SEARCH_RESULT) != END) {
+ if (tag == Tags.SYNC_CLASS) {
+ getValue();
+ } else if (tag == Tags.SYNC_COLLECTION_ID) {
+ getValue();
+ } else if (tag == Tags.SEARCH_LONG_ID) {
+ msg.mProtocolSearchInfo = getValue();
+ } else if (tag == Tags.SEARCH_PROPERTIES) {
+ msg.mAccountKey = mService.mAccount.mId;
+ msg.mMailboxKey = mService.mMailbox.mId;
+ msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
+ parser.pushTag(tag);
+ parser.addData(msg, tag);
+ if (msg.mHtml != null) {
+ msg.mHtml = TextUtilities.highlightTermsInHtml(msg.mHtml, mQuery);
+ }
+ msg.addSaveOps(ops);
+ } else {
+ skipTag();
+ }
+ }
+ return res;
+ }
+ }
+}
diff --git a/exchange2/src/com/android/exchange/adapter/Serializer.java b/exchange2/src/com/android/exchange/adapter/Serializer.java
new file mode 100644
index 0000000..bc79603
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/Serializer.java
@@ -0,0 +1,250 @@
+/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE. */
+
+//Contributors: Jonathan Cox, Bogdan Onoiu, Jerry Tian
+// Greatly simplified for Google, Inc. by Marc Blank
+
+package com.android.exchange.adapter;
+
+import android.content.ContentValues;
+import android.util.Log;
+
+import com.android.exchange.Eas;
+import com.android.exchange.utility.FileLogger;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class Serializer {
+ private static final String TAG = "Serializer";
+ private static final int BUFFER_SIZE = 16*1024;
+ private static final int NOT_PENDING = -1;
+
+ private final OutputStream mOutput;
+ private int mPendingTag = NOT_PENDING;
+ private int mDepth;
+ private String[] mNameStack = new String[20];
+ private int mTagPage = 0;
+ private boolean mLogging = Log.isLoggable(TAG, Log.VERBOSE);
+
+ public Serializer() throws IOException {
+ this(new ByteArrayOutputStream(), true);
+ }
+
+ public Serializer(OutputStream os) throws IOException {
+ this(os, true);
+ }
+
+ @VisibleForTesting
+ public Serializer(boolean startDocument) throws IOException {
+ this(new ByteArrayOutputStream(), startDocument);
+ }
+
+ /**
+ * Base constructor
+ * @param outputStream the stream we're serializing to
+ * @param startDocument whether or not to start a document
+ * @param _logging whether or not to log our output
+ * @throws IOException
+ */
+ public Serializer(OutputStream outputStream, boolean startDocument) throws IOException {
+ super();
+ mOutput = outputStream;
+ if (startDocument) {
+ startDocument();
+ } else {
+ mOutput.write(0);
+ }
+ }
+
+ void log(String str) {
+ int cr = str.indexOf('\n');
+ if (cr > 0) {
+ str = str.substring(0, cr);
+ }
+ Log.v(TAG, str);
+ if (Eas.FILE_LOG) {
+ FileLogger.log(TAG, str);
+ }
+ }
+
+ public void done() throws IOException {
+ if (mDepth != 0) {
+ throw new IOException("Done received with unclosed tags");
+ }
+ mOutput.flush();
+ }
+
+ public void startDocument() throws IOException{
+ mOutput.write(0x03); // version 1.3
+ mOutput.write(0x01); // unknown or missing public identifier
+ mOutput.write(106); // UTF-8
+ mOutput.write(0); // 0 length string array
+ }
+
+ public void checkPendingTag(boolean degenerated) throws IOException {
+ if (mPendingTag == NOT_PENDING)
+ return;
+
+ int page = mPendingTag >> Tags.PAGE_SHIFT;
+ int tag = mPendingTag & Tags.PAGE_MASK;
+ if (page != mTagPage) {
+ mTagPage = page;
+ mOutput.write(Wbxml.SWITCH_PAGE);
+ mOutput.write(page);
+ }
+
+ mOutput.write(degenerated ? tag : tag | Wbxml.WITH_CONTENT);
+ if (mLogging) {
+ String name = Tags.pages[page][tag - 5];
+ mNameStack[mDepth] = name;
+ log("<" + name + '>');
+ }
+ mPendingTag = NOT_PENDING;
+ }
+
+ public Serializer start(int tag) throws IOException {
+ checkPendingTag(false);
+ mPendingTag = tag;
+ mDepth++;
+ return this;
+ }
+
+ public Serializer end() throws IOException {
+ if (mPendingTag >= 0) {
+ checkPendingTag(true);
+ } else {
+ mOutput.write(Wbxml.END);
+ if (mLogging) {
+ log("</" + mNameStack[mDepth] + '>');
+ }
+ }
+ mDepth--;
+ return this;
+ }
+
+ public Serializer tag(int t) throws IOException {
+ start(t);
+ end();
+ return this;
+ }
+
+ public Serializer data(int tag, String value) throws IOException {
+ if (value == null) {
+ Log.e(TAG, "Writing null data for tag: " + tag);
+ }
+ start(tag);
+ text(value);
+ end();
+ return this;
+ }
+
+ public Serializer text(String text) throws IOException {
+ if (text == null) {
+ Log.e(TAG, "Writing null text for pending tag: " + mPendingTag);
+ }
+ checkPendingTag(false);
+ mOutput.write(Wbxml.STR_I);
+ writeLiteralString(mOutput, text);
+ if (mLogging) {
+ log(text);
+ }
+ return this;
+ }
+
+ public Serializer opaque(InputStream is, int length) throws IOException {
+ checkPendingTag(false);
+ mOutput.write(Wbxml.OPAQUE);
+ writeInteger(mOutput, length);
+ if (mLogging) {
+ log("Opaque, length: " + length);
+ }
+ // Now write out the opaque data in batches
+ byte[] buffer = new byte[BUFFER_SIZE];
+ while (length > 0) {
+ int bytesRead = is.read(buffer, 0, (int)Math.min(BUFFER_SIZE, length));
+ if (bytesRead == -1) {
+ break;
+ }
+ mOutput.write(buffer, 0, bytesRead);
+ length -= bytesRead;
+ }
+ return this;
+ }
+
+ public Serializer opaqueWithoutData(int length) throws IOException {
+ checkPendingTag(false);
+ mOutput.write(Wbxml.OPAQUE);
+ writeInteger(mOutput, length);
+ return this;
+ }
+
+ void writeInteger(OutputStream out, int i) throws IOException {
+ byte[] buf = new byte[5];
+ int idx = 0;
+
+ do {
+ buf[idx++] = (byte) (i & 0x7f);
+ i = i >> 7;
+ } while (i != 0);
+
+ while (idx > 1) {
+ out.write(buf[--idx] | 0x80);
+ }
+ out.write(buf[0]);
+ if (mLogging) {
+ log(Integer.toString(i));
+ }
+ }
+
+ void writeLiteralString(OutputStream out, String s) throws IOException {
+ byte[] data = s.getBytes("UTF-8");
+ out.write(data);
+ out.write(0);
+ }
+
+ void writeStringValue (ContentValues cv, String key, int tag) throws IOException {
+ String value = cv.getAsString(key);
+ if (value != null && value.length() > 0) {
+ data(tag, value);
+ } else {
+ tag(tag);
+ }
+ }
+
+ @Override
+ public String toString() {
+ if (mOutput instanceof ByteArrayOutputStream) {
+ return ((ByteArrayOutputStream)mOutput).toString();
+ }
+ throw new IllegalStateException();
+ }
+
+ public byte[] toByteArray() {
+ if (mOutput instanceof ByteArrayOutputStream) {
+ return ((ByteArrayOutputStream)mOutput).toByteArray();
+ }
+ throw new IllegalStateException();
+ }
+
+}
diff --git a/exchange2/src/com/android/exchange/adapter/SettingsParser.java b/exchange2/src/com/android/exchange/adapter/SettingsParser.java
new file mode 100644
index 0000000..2cbc753
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/SettingsParser.java
@@ -0,0 +1,82 @@
+/* Copyright (C) 2011 The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parse the result of a Settings command.
+ *
+ * We only send the Settings command in EAS 14.0 after sending a Provision command for the first
+ * time. parse() returns true in the normal case; false if access to the account is denied due
+ * to the actual settings (e.g. if a particular device type isn't allowed by the server)
+ */
+public class SettingsParser extends Parser {
+ private final EasSyncService mService;
+
+ public SettingsParser(InputStream in, EasSyncService service) throws IOException {
+ super(in);
+ mService = service;
+ }
+
+ @Override
+ public boolean parse() throws IOException {
+ boolean res = false;
+ if (nextTag(START_DOCUMENT) != Tags.SETTINGS_SETTINGS) {
+ throw new IOException();
+ }
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ if (tag == Tags.SETTINGS_STATUS) {
+ int status = getValueInt();
+ mService.userLog("Settings status = ", status);
+ if (status == 1) {
+ res = true;
+ } else {
+ // Access denied = 3; others should never be seen
+ res = false;
+ }
+ } else if (tag == Tags.SETTINGS_DEVICE_INFORMATION) {
+ parseDeviceInformation();
+ } else {
+ skipTag();
+ }
+ }
+ return res;
+ }
+
+ public void parseDeviceInformation() throws IOException {
+ while (nextTag(Tags.SETTINGS_DEVICE_INFORMATION) != END) {
+ if (tag == Tags.SETTINGS_SET) {
+ parseSet();
+ } else {
+ skipTag();
+ }
+ }
+ }
+
+ public void parseSet() throws IOException {
+ while (nextTag(Tags.SETTINGS_SET) != END) {
+ if (tag == Tags.SETTINGS_STATUS) {
+ mService.userLog("Set status = ", getValueInt());
+ } else {
+ skipTag();
+ }
+ }
+ }
+}
diff --git a/exchange2/src/com/android/exchange/adapter/Tags.java b/exchange2/src/com/android/exchange/adapter/Tags.java
new file mode 100644
index 0000000..183a103
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/Tags.java
@@ -0,0 +1,813 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+/**
+ * The wbxml tags for EAS are all defined here.
+ *
+ * The static final int's, of the form <page>_<tag> = <constant> are used in parsing incoming
+ * responses from the server (i.e. EasParser and its subclasses) and sending requests to the
+ * server (via Serializer)
+ *
+ * The array of string arrays is used only for generating logging output
+ */
+public class Tags {
+
+ // Wbxml page definitions for EAS
+ public static final int AIRSYNC = 0x00;
+ public static final int CONTACTS = 0x01;
+ public static final int EMAIL = 0x02;
+ public static final int CALENDAR = 0x04;
+ public static final int MOVE = 0x05;
+ public static final int GIE = 0x06;
+ public static final int FOLDER = 0x07;
+ public static final int 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 SEARCH = 0x0F;
+ public static final int GAL = 0x10;
+ public static final int BASE = 0x11;
+ public static final int SETTINGS = 0x12;
+ public static final int DOCS = 0x13;
+ public static final int ITEMS = 0x14;
+ // 14.0
+ public static final int COMPOSE = 0x15;
+ public static final int EMAIL2 = 0x16;
+ // 14.1
+ public static final int NOTES = 0x17;
+ public static final int RIGHTS = 0x18;
+
+ // Shift applied to page numbers to generate tag
+ public static final int PAGE_SHIFT = 6;
+ public static final int PAGE_MASK = 0x3F; // 6 bits
+
+ public static final int SYNC_PAGE = 0 << PAGE_SHIFT;
+ public static final int SYNC_SYNC = SYNC_PAGE + 5;
+ public static final int SYNC_RESPONSES = SYNC_PAGE + 6;
+ public static final int SYNC_ADD = SYNC_PAGE + 7;
+ public static final int SYNC_CHANGE = SYNC_PAGE + 8;
+ public static final int SYNC_DELETE = SYNC_PAGE + 9;
+ public static final int SYNC_FETCH = SYNC_PAGE + 0xA;
+ public static final int SYNC_SYNC_KEY = SYNC_PAGE + 0xB;
+ public static final int SYNC_CLIENT_ID = SYNC_PAGE + 0xC;
+ public static final int SYNC_SERVER_ID = SYNC_PAGE + 0xD;
+ public static final int SYNC_STATUS = SYNC_PAGE + 0xE;
+ public static final int SYNC_COLLECTION = SYNC_PAGE + 0xF;
+ public static final int SYNC_CLASS = SYNC_PAGE + 0x10;
+ public static final int SYNC_VERSION = SYNC_PAGE + 0x11;
+ public static final int SYNC_COLLECTION_ID = SYNC_PAGE + 0x12;
+ public static final int SYNC_GET_CHANGES = SYNC_PAGE + 0x13;
+ public static final int SYNC_MORE_AVAILABLE = SYNC_PAGE + 0x14;
+ public static final int SYNC_WINDOW_SIZE = SYNC_PAGE + 0x15;
+ public static final int SYNC_COMMANDS = SYNC_PAGE + 0x16;
+ public static final int SYNC_OPTIONS = SYNC_PAGE + 0x17;
+ public static final int SYNC_FILTER_TYPE = SYNC_PAGE + 0x18;
+ public static final int SYNC_TRUNCATION = SYNC_PAGE + 0x19;
+ public static final int SYNC_RTF_TRUNCATION = SYNC_PAGE + 0x1A;
+ public static final int SYNC_CONFLICT = SYNC_PAGE + 0x1B;
+ public static final int SYNC_COLLECTIONS = SYNC_PAGE + 0x1C;
+ public static final int SYNC_APPLICATION_DATA = SYNC_PAGE + 0x1D;
+ public static final int SYNC_DELETES_AS_MOVES = SYNC_PAGE + 0x1E;
+ public static final int SYNC_NOTIFY_GUID = SYNC_PAGE + 0x1F;
+ public static final int SYNC_SUPPORTED = SYNC_PAGE + 0x20;
+ public static final int SYNC_SOFT_DELETE = SYNC_PAGE + 0x21;
+ public static final int SYNC_MIME_SUPPORT = SYNC_PAGE + 0x22;
+ public static final int SYNC_MIME_TRUNCATION = SYNC_PAGE + 0x23;
+ public static final int SYNC_WAIT = SYNC_PAGE + 0x24;
+ public static final int SYNC_LIMIT = SYNC_PAGE + 0x25;
+ public static final int SYNC_PARTIAL = SYNC_PAGE + 0x26;
+
+ public static final int GIE_PAGE = GIE << PAGE_SHIFT;
+ public static final int GIE_GET_ITEM_ESTIMATE = GIE_PAGE + 5;
+ public static final int GIE_VERSION = GIE_PAGE + 6;
+ public static final int GIE_COLLECTIONS = GIE_PAGE + 7;
+ public static final int GIE_COLLECTION = GIE_PAGE + 8;
+ public static final int GIE_CLASS = GIE_PAGE + 9;
+ public static final int GIE_COLLECTION_ID = GIE_PAGE + 0xA;
+ public static final int GIE_DATE_TIME = GIE_PAGE + 0xB;
+ public static final int GIE_ESTIMATE = GIE_PAGE + 0xC;
+ public static final int GIE_RESPONSE = GIE_PAGE + 0xD;
+ public static final int GIE_STATUS = GIE_PAGE + 0xE;
+
+ public static final int CONTACTS_PAGE = CONTACTS << PAGE_SHIFT;
+ public static final int CONTACTS_ANNIVERSARY = CONTACTS_PAGE + 5;
+ public static final int CONTACTS_ASSISTANT_NAME = CONTACTS_PAGE + 6;
+ public static final int CONTACTS_ASSISTANT_TELEPHONE_NUMBER = CONTACTS_PAGE + 7;
+ public static final int CONTACTS_BIRTHDAY = CONTACTS_PAGE + 8;
+ public static final int CONTACTS_BODY = CONTACTS_PAGE + 9;
+ public static final int CONTACTS_BODY_SIZE = CONTACTS_PAGE + 0xA;
+ public static final int CONTACTS_BODY_TRUNCATED = CONTACTS_PAGE + 0xB;
+ public static final int CONTACTS_BUSINESS2_TELEPHONE_NUMBER = CONTACTS_PAGE + 0xC;
+ public static final int CONTACTS_BUSINESS_ADDRESS_CITY = CONTACTS_PAGE + 0xD;
+ public static final int CONTACTS_BUSINESS_ADDRESS_COUNTRY = CONTACTS_PAGE + 0xE;
+ public static final int CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE = CONTACTS_PAGE + 0xF;
+ public static final int CONTACTS_BUSINESS_ADDRESS_STATE = CONTACTS_PAGE + 0x10;
+ public static final int CONTACTS_BUSINESS_ADDRESS_STREET = CONTACTS_PAGE + 0x11;
+ public static final int CONTACTS_BUSINESS_FAX_NUMBER = CONTACTS_PAGE + 0x12;
+ public static final int CONTACTS_BUSINESS_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x13;
+ public static final int CONTACTS_CAR_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x14;
+ public static final int CONTACTS_CATEGORIES = CONTACTS_PAGE + 0x15;
+ public static final int CONTACTS_CATEGORY = CONTACTS_PAGE + 0x16;
+ public static final int CONTACTS_CHILDREN = CONTACTS_PAGE + 0x17;
+ public static final int CONTACTS_CHILD = CONTACTS_PAGE + 0x18;
+ public static final int CONTACTS_COMPANY_NAME = CONTACTS_PAGE + 0x19;
+ public static final int CONTACTS_DEPARTMENT = CONTACTS_PAGE + 0x1A;
+ public static final int CONTACTS_EMAIL1_ADDRESS = CONTACTS_PAGE + 0x1B;
+ public static final int CONTACTS_EMAIL2_ADDRESS = CONTACTS_PAGE + 0x1C;
+ public static final int CONTACTS_EMAIL3_ADDRESS = CONTACTS_PAGE + 0x1D;
+ public static final int CONTACTS_FILE_AS = CONTACTS_PAGE + 0x1E;
+ public static final int CONTACTS_FIRST_NAME = CONTACTS_PAGE + 0x1F;
+ public static final int CONTACTS_HOME2_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x20;
+ public static final int CONTACTS_HOME_ADDRESS_CITY = CONTACTS_PAGE + 0x21;
+ public static final int CONTACTS_HOME_ADDRESS_COUNTRY = CONTACTS_PAGE + 0x22;
+ public static final int CONTACTS_HOME_ADDRESS_POSTAL_CODE = CONTACTS_PAGE + 0x23;
+ public static final int CONTACTS_HOME_ADDRESS_STATE = CONTACTS_PAGE + 0x24;
+ public static final int CONTACTS_HOME_ADDRESS_STREET = CONTACTS_PAGE + 0x25;
+ public static final int CONTACTS_HOME_FAX_NUMBER = CONTACTS_PAGE + 0x26;
+ public static final int CONTACTS_HOME_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x27;
+ public static final int CONTACTS_JOB_TITLE = CONTACTS_PAGE + 0x28;
+ public static final int CONTACTS_LAST_NAME = CONTACTS_PAGE + 0x29;
+ public static final int CONTACTS_MIDDLE_NAME = CONTACTS_PAGE + 0x2A;
+ public static final int CONTACTS_MOBILE_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x2B;
+ public static final int CONTACTS_OFFICE_LOCATION = CONTACTS_PAGE + 0x2C;
+ public static final int CONTACTS_OTHER_ADDRESS_CITY = CONTACTS_PAGE + 0x2D;
+ public static final int CONTACTS_OTHER_ADDRESS_COUNTRY = CONTACTS_PAGE + 0x2E;
+ public static final int CONTACTS_OTHER_ADDRESS_POSTAL_CODE = CONTACTS_PAGE + 0x2F;
+ public static final int CONTACTS_OTHER_ADDRESS_STATE = CONTACTS_PAGE + 0x30;
+ public static final int CONTACTS_OTHER_ADDRESS_STREET = CONTACTS_PAGE + 0x31;
+ public static final int CONTACTS_PAGER_NUMBER = CONTACTS_PAGE + 0x32;
+ public static final int CONTACTS_RADIO_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x33;
+ public static final int CONTACTS_SPOUSE = CONTACTS_PAGE + 0x34;
+ public static final int CONTACTS_SUFFIX = CONTACTS_PAGE + 0x35;
+ public static final int CONTACTS_TITLE = CONTACTS_PAGE + 0x36;
+ public static final int CONTACTS_WEBPAGE = CONTACTS_PAGE + 0x37;
+ public static final int CONTACTS_YOMI_COMPANY_NAME = CONTACTS_PAGE + 0x38;
+ public static final int CONTACTS_YOMI_FIRST_NAME = CONTACTS_PAGE + 0x39;
+ public static final int CONTACTS_YOMI_LAST_NAME = CONTACTS_PAGE + 0x3A;
+ public static final int CONTACTS_COMPRESSED_RTF = CONTACTS_PAGE + 0x3B;
+ public static final int CONTACTS_PICTURE = CONTACTS_PAGE + 0x3C;
+
+ public static final int CALENDAR_PAGE = CALENDAR << PAGE_SHIFT;
+ public static final int CALENDAR_TIME_ZONE = CALENDAR_PAGE + 5;
+ public static final int CALENDAR_ALL_DAY_EVENT = CALENDAR_PAGE + 6;
+ public static final int CALENDAR_ATTENDEES = CALENDAR_PAGE + 7;
+ public static final int CALENDAR_ATTENDEE = CALENDAR_PAGE + 8;
+ public static final int CALENDAR_ATTENDEE_EMAIL = CALENDAR_PAGE + 9;
+ public static final int CALENDAR_ATTENDEE_NAME = CALENDAR_PAGE + 0xA;
+ public static final int CALENDAR_BODY = CALENDAR_PAGE + 0xB;
+ public static final int CALENDAR_BODY_TRUNCATED = CALENDAR_PAGE + 0xC;
+ public static final int CALENDAR_BUSY_STATUS = CALENDAR_PAGE + 0xD;
+ public static final int CALENDAR_CATEGORIES = CALENDAR_PAGE + 0xE;
+ public static final int CALENDAR_CATEGORY = CALENDAR_PAGE + 0xF;
+ public static final int CALENDAR_COMPRESSED_RTF = CALENDAR_PAGE + 0x10;
+ public static final int CALENDAR_DTSTAMP = CALENDAR_PAGE + 0x11;
+ public static final int CALENDAR_END_TIME = CALENDAR_PAGE + 0x12;
+ public static final int CALENDAR_EXCEPTION = CALENDAR_PAGE + 0x13;
+ public static final int CALENDAR_EXCEPTIONS = CALENDAR_PAGE + 0x14;
+ public static final int CALENDAR_EXCEPTION_IS_DELETED = CALENDAR_PAGE + 0x15;
+ public static final int CALENDAR_EXCEPTION_START_TIME = CALENDAR_PAGE + 0x16;
+ public static final int CALENDAR_LOCATION = CALENDAR_PAGE + 0x17;
+ public static final int CALENDAR_MEETING_STATUS = CALENDAR_PAGE + 0x18;
+ public static final int CALENDAR_ORGANIZER_EMAIL = CALENDAR_PAGE + 0x19;
+ public static final int CALENDAR_ORGANIZER_NAME = CALENDAR_PAGE + 0x1A;
+ public static final int CALENDAR_RECURRENCE = CALENDAR_PAGE + 0x1B;
+ public static final int CALENDAR_RECURRENCE_TYPE = CALENDAR_PAGE + 0x1C;
+ public static final int CALENDAR_RECURRENCE_UNTIL = CALENDAR_PAGE + 0x1D;
+ public static final int CALENDAR_RECURRENCE_OCCURRENCES = CALENDAR_PAGE + 0x1E;
+ public static final int CALENDAR_RECURRENCE_INTERVAL = CALENDAR_PAGE + 0x1F;
+ public static final int CALENDAR_RECURRENCE_DAYOFWEEK = CALENDAR_PAGE + 0x20;
+ public static final int CALENDAR_RECURRENCE_DAYOFMONTH = CALENDAR_PAGE + 0x21;
+ public static final int CALENDAR_RECURRENCE_WEEKOFMONTH = CALENDAR_PAGE + 0x22;
+ public static final int CALENDAR_RECURRENCE_MONTHOFYEAR = CALENDAR_PAGE + 0x23;
+ public static final int CALENDAR_REMINDER_MINS_BEFORE = CALENDAR_PAGE + 0x24;
+ public static final int CALENDAR_SENSITIVITY = CALENDAR_PAGE + 0x25;
+ public static final int CALENDAR_SUBJECT = CALENDAR_PAGE + 0x26;
+ public static final int CALENDAR_START_TIME = CALENDAR_PAGE + 0x27;
+ public static final int CALENDAR_UID = CALENDAR_PAGE + 0x28;
+ public static final int CALENDAR_ATTENDEE_STATUS = CALENDAR_PAGE + 0x29;
+ public static final int CALENDAR_ATTENDEE_TYPE = CALENDAR_PAGE + 0x2A;
+ public static final int CALENDAR_ATTACHMENT = CALENDAR_PAGE + 0x2B;
+ public static final int CALENDAR_ATTACHMENTS = CALENDAR_PAGE + 0x2C;
+ public static final int CALENDAR_ATT_NAME = CALENDAR_PAGE + 0x2D;
+ public static final int CALENDAR_ATT_SIZE = CALENDAR_PAGE + 0x2E;
+ public static final int CALENDAR_ATT_OID = CALENDAR_PAGE + 0x2F;
+ public static final int CALENDAR_ATT_METHOD = CALENDAR_PAGE + 0x30;
+ public static final int CALENDAR_ATT_REMOVED = CALENDAR_PAGE + 0x31;
+ public static final int CALENDAR_DISPLAY_NAME = CALENDAR_PAGE + 0x32;
+ public static final int CALENDAR_DISALLOW_NEW_TIME_PROPOSAL = CALENDAR_PAGE + 0x33;
+ public static final int CALENDAR_RESPONSE_REQUESTED = CALENDAR_PAGE + 0x34;
+ public static final int CALENDAR_APPOINTMENT_REPLY_TIME = CALENDAR_PAGE + 0x35;
+ public static final int CALENDAR_RESPONSE_TYPE = CALENDAR_PAGE + 0x36;
+ public static final int CALENDAR_CALENDAR_TYPE = CALENDAR_PAGE + 0x37;
+ public static final int CALENDAR_IS_LEAP_MONTH = CALENDAR_PAGE + 0x38;
+ public static final int CALENDAR_FIRST_DAY_OF_WEEK = CALENDAR_PAGE + 0x39;
+ public static final int CALENDAR_ONLINE_MEETING_CONFLINK = CALENDAR_PAGE + 0x3A;
+ public static final int CALENDAR_ONLINE_MEETING_EXTERNAL_LINK = CALENDAR_PAGE + 0x3B;
+
+ public static final int FOLDER_PAGE = FOLDER << PAGE_SHIFT;
+ public static final int FOLDER_FOLDERS = FOLDER_PAGE + 5;
+ public static final int FOLDER_FOLDER = FOLDER_PAGE + 6;
+ public static final int FOLDER_DISPLAY_NAME = FOLDER_PAGE + 7;
+ public static final int FOLDER_SERVER_ID = FOLDER_PAGE + 8;
+ public static final int FOLDER_PARENT_ID = FOLDER_PAGE + 9;
+ public static final int FOLDER_TYPE = FOLDER_PAGE + 0xA;
+ public static final int FOLDER_RESPONSE = FOLDER_PAGE + 0xB;
+ public static final int FOLDER_STATUS = FOLDER_PAGE + 0xC;
+ public static final int FOLDER_CONTENT_CLASS = FOLDER_PAGE + 0xD;
+ public static final int FOLDER_CHANGES = FOLDER_PAGE + 0xE;
+ public static final int FOLDER_ADD = FOLDER_PAGE + 0xF;
+ public static final int FOLDER_DELETE = FOLDER_PAGE + 0x10;
+ public static final int FOLDER_UPDATE = FOLDER_PAGE + 0x11;
+ public static final int FOLDER_SYNC_KEY = FOLDER_PAGE + 0x12;
+ public static final int FOLDER_FOLDER_CREATE = FOLDER_PAGE + 0x13;
+ public static final int FOLDER_FOLDER_DELETE= FOLDER_PAGE + 0x14;
+ public static final int FOLDER_FOLDER_UPDATE = FOLDER_PAGE + 0x15;
+ public static final int FOLDER_FOLDER_SYNC = FOLDER_PAGE + 0x16;
+ public static final int FOLDER_COUNT = FOLDER_PAGE + 0x17;
+ public static final int FOLDER_VERSION = FOLDER_PAGE + 0x18;
+
+ public static final int 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;
+ public static final int EMAIL_ATT_NAME = EMAIL_PAGE + 7;
+ public static final int EMAIL_ATT_SIZE = EMAIL_PAGE + 8;
+ public static final int EMAIL_ATT0ID = EMAIL_PAGE + 9;
+ public static final int EMAIL_ATT_METHOD = EMAIL_PAGE + 0xA;
+ public static final int EMAIL_ATT_REMOVED = EMAIL_PAGE + 0xB;
+ public static final int EMAIL_BODY = EMAIL_PAGE + 0xC;
+ public static final int EMAIL_BODY_SIZE = EMAIL_PAGE + 0xD;
+ public static final int EMAIL_BODY_TRUNCATED = EMAIL_PAGE + 0xE;
+ public static final int EMAIL_DATE_RECEIVED = EMAIL_PAGE + 0xF;
+ public static final int EMAIL_DISPLAY_NAME = EMAIL_PAGE + 0x10;
+ public static final int EMAIL_DISPLAY_TO = EMAIL_PAGE + 0x11;
+ public static final int EMAIL_IMPORTANCE = EMAIL_PAGE + 0x12;
+ public static final int EMAIL_MESSAGE_CLASS = EMAIL_PAGE + 0x13;
+ public static final int EMAIL_SUBJECT = EMAIL_PAGE + 0x14;
+ public static final int EMAIL_READ = EMAIL_PAGE + 0x15;
+ public static final int EMAIL_TO = EMAIL_PAGE + 0x16;
+ public static final int EMAIL_CC = EMAIL_PAGE + 0x17;
+ public static final int EMAIL_FROM = EMAIL_PAGE + 0x18;
+ public static final int EMAIL_REPLY_TO = EMAIL_PAGE + 0x19;
+ public static final int EMAIL_ALL_DAY_EVENT = EMAIL_PAGE + 0x1A;
+ public static final int EMAIL_CATEGORIES = EMAIL_PAGE + 0x1B;
+ public static final int EMAIL_CATEGORY = EMAIL_PAGE + 0x1C;
+ public static final int EMAIL_DTSTAMP = EMAIL_PAGE + 0x1D;
+ public static final int EMAIL_END_TIME = EMAIL_PAGE + 0x1E;
+ public static final int EMAIL_INSTANCE_TYPE = EMAIL_PAGE + 0x1F;
+ public static final int EMAIL_INTD_BUSY_STATUS = EMAIL_PAGE + 0x20;
+ public static final int EMAIL_LOCATION = EMAIL_PAGE + 0x21;
+ public static final int EMAIL_MEETING_REQUEST = EMAIL_PAGE + 0x22;
+ public static final int EMAIL_ORGANIZER = EMAIL_PAGE + 0x23;
+ public static final int EMAIL_RECURRENCE_ID = EMAIL_PAGE + 0x24;
+ public static final int EMAIL_REMINDER = EMAIL_PAGE + 0x25;
+ public static final int EMAIL_RESPONSE_REQUESTED = EMAIL_PAGE + 0x26;
+ public static final int EMAIL_RECURRENCES = EMAIL_PAGE + 0x27;
+ public static final int EMAIL_RECURRENCE = EMAIL_PAGE + 0x28;
+ public static final int EMAIL_RECURRENCE_TYPE = EMAIL_PAGE + 0x29;
+ public static final int EMAIL_RECURRENCE_UNTIL = EMAIL_PAGE + 0x2A;
+ public static final int EMAIL_RECURRENCE_OCCURRENCES = EMAIL_PAGE + 0x2B;
+ public static final int EMAIL_RECURRENCE_INTERVAL = EMAIL_PAGE + 0x2C;
+ public static final int EMAIL_RECURRENCE_DAYOFWEEK = EMAIL_PAGE + 0x2D;
+ public static final int EMAIL_RECURRENCE_DAYOFMONTH = EMAIL_PAGE + 0x2E;
+ public static final int EMAIL_RECURRENCE_WEEKOFMONTH = EMAIL_PAGE + 0x2F;
+ public static final int EMAIL_RECURRENCE_MONTHOFYEAR = EMAIL_PAGE + 0x30;
+ public static final int EMAIL_START_TIME = EMAIL_PAGE + 0x31;
+ public static final int EMAIL_SENSITIVITY = EMAIL_PAGE + 0x32;
+ public static final int EMAIL_TIME_ZONE = EMAIL_PAGE + 0x33;
+ public static final int EMAIL_GLOBAL_OBJID = EMAIL_PAGE + 0x34;
+ public static final int EMAIL_THREAD_TOPIC = EMAIL_PAGE + 0x35;
+ public static final int EMAIL_MIME_DATA = EMAIL_PAGE + 0x36;
+ public static final int EMAIL_MIME_TRUNCATED = EMAIL_PAGE + 0x37;
+ public static final int EMAIL_MIME_SIZE = EMAIL_PAGE + 0x38;
+ public static final int EMAIL_INTERNET_CPID = EMAIL_PAGE + 0x39;
+ public static final int EMAIL_FLAG = EMAIL_PAGE + 0x3A;
+ public static final int EMAIL_FLAG_STATUS = EMAIL_PAGE + 0x3B;
+ public static final int EMAIL_CONTENT_CLASS = EMAIL_PAGE + 0x3C;
+ public static final int EMAIL_FLAG_TYPE = EMAIL_PAGE + 0x3D;
+ public static final int EMAIL_COMPLETE_TIME = EMAIL_PAGE + 0x3E;
+ public static final int EMAIL_DISALLOW_NEW_TIME_PROPOSAL = EMAIL_PAGE + 0x3F;
+
+ public static final int TASK_PAGE = TASK << PAGE_SHIFT;
+ public static final int TASK_BODY = TASK_PAGE + 5;
+ public static final int TASK_BODY_SIZE = TASK_PAGE + 6;
+ public static final int TASK_BODY_TRUNCATED = TASK_PAGE + 7;
+ public static final int TASK_CATEGORIES = TASK_PAGE + 8;
+ public static final int TASK_CATEGORY = TASK_PAGE + 9;
+ public static final int TASK_COMPLETE = TASK_PAGE + 0xA;
+ public static final int TASK_DATE_COMPLETED = TASK_PAGE + 0xB;
+ public static final int TASK_DUE_DATE = TASK_PAGE + 0xC;
+ public static final int TASK_UTC_DUE_DATE = TASK_PAGE + 0xD;
+ public static final int TASK_IMPORTANCE = TASK_PAGE + 0xE;
+ public static final int TASK_RECURRENCE = TASK_PAGE + 0xF;
+ public static final int TASK_RECURRENCE_TYPE = TASK_PAGE + 0x10;
+ public static final int TASK_RECURRENCE_START = TASK_PAGE + 0x11;
+ public static final int TASK_RECURRENCE_UNTIL = TASK_PAGE + 0x12;
+ public static final int TASK_RECURRENCE_OCCURRENCES = TASK_PAGE + 0x13;
+ public static final int TASK_RECURRENCE_INTERVAL = TASK_PAGE + 0x14;
+ public static final int TASK_RECURRENCE_DAY_OF_MONTH = TASK_PAGE + 0x15;
+ public static final int TASK_RECURRENCE_DAY_OF_WEEK = TASK_PAGE + 0x16;
+ public static final int TASK_RECURRENCE_WEEK_OF_MONTH = TASK_PAGE + 0x17;
+ public static final int TASK_RECURRENCE_MONTH_OF_YEAR = TASK_PAGE + 0x18;
+ public static final int TASK_RECURRENCE_REGENERATE = TASK_PAGE + 0x19;
+ public static final int TASK_RECURRENCE_DEAD_OCCUR = TASK_PAGE + 0x1A;
+ public static final int TASK_REMINDER_SET = TASK_PAGE + 0x1B;
+ public static final int TASK_REMINDER_TIME = TASK_PAGE + 0x1C;
+ public static final int TASK_SENSITIVITY = TASK_PAGE + 0x1D;
+ public static final int TASK_START_DATE = TASK_PAGE + 0x1E;
+ public static final int TASK_UTC_START_DATE = TASK_PAGE + 0x1F;
+ public static final int TASK_SUBJECT = TASK_PAGE + 0x20;
+ public static final int COMPRESSED_RTF = TASK_PAGE + 0x21;
+ public static final int ORDINAL_DATE = TASK_PAGE + 0x22;
+ public static final int SUBORDINAL_DATE = TASK_PAGE + 0x23;
+
+ public static final int MOVE_PAGE = MOVE << PAGE_SHIFT;
+ public static final int MOVE_MOVE_ITEMS = MOVE_PAGE + 5;
+ public static final int MOVE_MOVE = MOVE_PAGE + 6;
+ public static final int MOVE_SRCMSGID = MOVE_PAGE + 7;
+ public static final int MOVE_SRCFLDID = MOVE_PAGE + 8;
+ public static final int MOVE_DSTFLDID = MOVE_PAGE + 9;
+ public static final int MOVE_RESPONSE = MOVE_PAGE + 0xA;
+ public static final int MOVE_STATUS = MOVE_PAGE + 0xB;
+ public static final int MOVE_DSTMSGID = MOVE_PAGE + 0xC;
+
+ public static final int CONTACTS2_PAGE = CONTACTS2 << PAGE_SHIFT;
+ public static final int CONTACTS2_CUSTOMER_ID = CONTACTS2_PAGE + 5;
+ public static final int CONTACTS2_GOVERNMENT_ID = CONTACTS2_PAGE + 6;
+ public static final int CONTACTS2_IM_ADDRESS = CONTACTS2_PAGE + 7;
+ public static final int CONTACTS2_IM_ADDRESS_2 = CONTACTS2_PAGE + 8;
+ public static final int CONTACTS2_IM_ADDRESS_3 = CONTACTS2_PAGE + 9;
+ public static final int CONTACTS2_MANAGER_NAME = CONTACTS2_PAGE + 0xA;
+ public static final int CONTACTS2_COMPANY_MAIN_PHONE = CONTACTS2_PAGE + 0xB;
+ public static final int CONTACTS2_ACCOUNT_NAME = CONTACTS2_PAGE + 0xC;
+ public static final int CONTACTS2_NICKNAME = CONTACTS2_PAGE + 0xD;
+ public static final int CONTACTS2_MMS = CONTACTS2_PAGE + 0xE;
+
+ public static final int PING_PAGE = PING << PAGE_SHIFT;
+ public static final int PING_PING = PING_PAGE + 5;
+ public static final int PING_AUTD_STATE = PING_PAGE + 6;
+ public static final int PING_STATUS = PING_PAGE + 7;
+ public static final int PING_HEARTBEAT_INTERVAL = PING_PAGE + 8;
+ public static final int PING_FOLDERS = PING_PAGE + 9;
+ public static final int PING_FOLDER = PING_PAGE + 0xA;
+ public static final int PING_ID = PING_PAGE + 0xB;
+ public static final int PING_CLASS = PING_PAGE + 0xC;
+ public static final int PING_MAX_FOLDERS = PING_PAGE + 0xD;
+
+ public static final int SEARCH_PAGE = SEARCH << PAGE_SHIFT;
+ public static final int SEARCH_SEARCH = SEARCH_PAGE + 5;
+ public static final int SEARCH_STORES = SEARCH_PAGE + 6;
+ public static final int SEARCH_STORE = SEARCH_PAGE + 7;
+ public static final int SEARCH_NAME = SEARCH_PAGE + 8;
+ public static final int SEARCH_QUERY = SEARCH_PAGE + 9;
+ public static final int SEARCH_OPTIONS = SEARCH_PAGE + 0xA;
+ public static final int SEARCH_RANGE = SEARCH_PAGE + 0xB;
+ public static final int SEARCH_STATUS = SEARCH_PAGE + 0xC;
+ public static final int SEARCH_RESPONSE = SEARCH_PAGE + 0xD;
+ public static final int SEARCH_RESULT = SEARCH_PAGE + 0xE;
+ public static final int SEARCH_PROPERTIES = SEARCH_PAGE + 0xF;
+ public static final int SEARCH_TOTAL = SEARCH_PAGE + 0x10;
+ public static final int SEARCH_EQUAL_TO = SEARCH_PAGE + 0x11;
+ public static final int SEARCH_VALUE = SEARCH_PAGE + 0x12;
+ public static final int SEARCH_AND = SEARCH_PAGE + 0x13;
+ public static final int SEARCH_OR = SEARCH_PAGE + 0x14;
+ public static final int SEARCH_FREE_TEXT = SEARCH_PAGE + 0x15;
+ public static final int SEARCH_SUBSTRING_OP = SEARCH_PAGE + 0x16;
+ public static final int SEARCH_DEEP_TRAVERSAL = SEARCH_PAGE + 0x17;
+ public static final int SEARCH_LONG_ID = SEARCH_PAGE + 0x18;
+ public static final int SEARCH_REBUILD_RESULTS = SEARCH_PAGE + 0x19;
+ public static final int SEARCH_LESS_THAN = SEARCH_PAGE + 0x1A;
+ public static final int SEARCH_GREATER_THAN = SEARCH_PAGE + 0x1B;
+ public static final int SEARCH_SCHEMA = SEARCH_PAGE + 0x1C;
+ public static final int SEARCH_SUPPORTED = SEARCH_PAGE + 0x1D;
+
+ public static final int GAL_PAGE = GAL << PAGE_SHIFT;
+ public static final int GAL_DISPLAY_NAME = GAL_PAGE + 5;
+ public static final int GAL_PHONE = GAL_PAGE + 6;
+ public static final int GAL_OFFICE = GAL_PAGE + 7;
+ public static final int GAL_TITLE = GAL_PAGE + 8;
+ public static final int GAL_COMPANY = GAL_PAGE + 9;
+ public static final int GAL_ALIAS = GAL_PAGE + 0xA;
+ public static final int GAL_FIRST_NAME = GAL_PAGE + 0xB;
+ public static final int GAL_LAST_NAME = GAL_PAGE + 0xC;
+ public static final int GAL_HOME_PHONE = GAL_PAGE + 0xD;
+ public static final int GAL_MOBILE_PHONE = GAL_PAGE + 0xE;
+ public static final int GAL_EMAIL_ADDRESS = GAL_PAGE + 0xF;
+
+ 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;
+ public static final int BASE_TRUNCATION_SIZE = BASE_PAGE + 7;
+ public static final int BASE_ALL_OR_NONE = BASE_PAGE + 8;
+ public static final int BASE_RESERVED = BASE_PAGE + 9;
+ public static final int BASE_BODY = BASE_PAGE + 0xA;
+ public static final int BASE_DATA = BASE_PAGE + 0xB;
+ public static final int BASE_ESTIMATED_DATA_SIZE = BASE_PAGE + 0xC;
+ public static final int BASE_TRUNCATED = BASE_PAGE + 0xD;
+ public static final int BASE_ATTACHMENTS = BASE_PAGE + 0xE;
+ public static final int BASE_ATTACHMENT = BASE_PAGE + 0xF;
+ public static final int BASE_DISPLAY_NAME = BASE_PAGE + 0x10;
+ public static final int BASE_FILE_REFERENCE = BASE_PAGE + 0x11;
+ public static final int BASE_METHOD = BASE_PAGE + 0x12;
+ public static final int BASE_CONTENT_ID = BASE_PAGE + 0x13;
+ public static final int BASE_CONTENT_LOCATION = BASE_PAGE + 0x14;
+ public static final int BASE_IS_INLINE = BASE_PAGE + 0x15;
+ public static final int BASE_NATIVE_BODY_TYPE = BASE_PAGE + 0x16;
+ public static final int BASE_CONTENT_TYPE = BASE_PAGE + 0x17;
+
+ public static final int SETTINGS_PAGE = SETTINGS << PAGE_SHIFT;
+ public static final int SETTINGS_SETTINGS = SETTINGS_PAGE + 5;
+ public static final int SETTINGS_STATUS = SETTINGS_PAGE + 6;
+ public static final int SETTINGS_GET = SETTINGS_PAGE + 7;
+ public static final int SETTINGS_SET = SETTINGS_PAGE + 8;
+ public static final int SETTINGS_OOF = SETTINGS_PAGE + 9;
+ public static final int SETTINGS_OOF_STATE = SETTINGS_PAGE + 0xA;
+ public static final int SETTINGS_START_TIME = SETTINGS_PAGE + 0xB;
+ public static final int SETTINGS_END_TIME = SETTINGS_PAGE + 0xC;
+ public static final int SETTINGS_OOF_MESSAGE = SETTINGS_PAGE + 0xD;
+ public static final int SETTINGS_APPLIES_TO_INTERNAL = SETTINGS_PAGE + 0xE;
+ public static final int SETTINGS_APPLIES_TO_EXTERNAL_KNOWN = SETTINGS_PAGE + 0xF;
+ public static final int SETTINGS_APPLIES_TO_EXTERNAL_UNKNOWN = SETTINGS_PAGE + 0x10;
+ public static final int SETTINGS_ENABLED = SETTINGS_PAGE + 0x11;
+ public static final int SETTINGS_REPLY_MESSAGE = SETTINGS_PAGE + 0x12;
+ public static final int SETTINGS_BODY_TYPE = SETTINGS_PAGE + 0x13;
+ public static final int SETTINGS_DEVICE_PASSWORD = SETTINGS_PAGE + 0x14;
+ public static final int SETTINGS_PASSWORD = SETTINGS_PAGE + 0x15;
+ public static final int SETTINGS_DEVICE_INFORMATION = SETTINGS_PAGE + 0x16;
+ public static final int SETTINGS_MODEL = SETTINGS_PAGE + 0x17;
+ public static final int SETTINGS_IMEI = SETTINGS_PAGE + 0x18;
+ public static final int SETTINGS_FRIENDLY_NAME = SETTINGS_PAGE + 0x19;
+ public static final int SETTINGS_OS = SETTINGS_PAGE + 0x1A;
+ public static final int SETTINGS_OS_LANGUAGE = SETTINGS_PAGE + 0x1B;
+ public static final int SETTINGS_PHONE_NUMBER = SETTINGS_PAGE + 0x1C;
+ public static final int SETTINGS_USER_INFORMATION = SETTINGS_PAGE + 0x1D;
+ public static final int SETTINGS_EMAIL_ADDRESS = SETTINGS_PAGE + 0x1E;
+ public static final int SETTINGS_SMTP_ADDRESS = SETTINGS_PAGE + 0x1F;
+ public static final int SETTINGS_USER_AGENT = SETTINGS_PAGE + 0x20;
+ public static final int SETTINGS_ENABLE_OUTGOING_SMS = SETTINGS_PAGE + 0x21;
+ public static final int SETTINGS_MOBILE_OPERATOR = SETTINGS_PAGE + 0x22;
+
+ public static final int ITEMS_PAGE = ITEMS << PAGE_SHIFT;
+ public static final int ITEMS_ITEMS = ITEMS_PAGE + 5;
+ public static final int ITEMS_FETCH = ITEMS_PAGE + 6;
+ public static final int ITEMS_STORE = ITEMS_PAGE + 7;
+ public static final int ITEMS_OPTIONS = ITEMS_PAGE + 8;
+ public static final int ITEMS_RANGE = ITEMS_PAGE + 9;
+ public static final int ITEMS_TOTAL = ITEMS_PAGE + 0xA;
+ public static final int ITEMS_PROPERTIES = ITEMS_PAGE + 0xB;
+ public static final int ITEMS_DATA = ITEMS_PAGE + 0xC;
+ public static final int ITEMS_STATUS = ITEMS_PAGE + 0xD;
+ public static final int ITEMS_RESPONSE = ITEMS_PAGE + 0xE;
+ public static final int ITEMS_VERSION = ITEMS_PAGE + 0xF;
+ public static final int ITEMS_SCHEMA = ITEMS_PAGE + 0x10;
+ public static final int ITEMS_PART = ITEMS_PAGE + 0x11;
+ public static final int ITEMS_EMPTY_FOLDER = ITEMS_PAGE + 0x12;
+ public static final int ITEMS_DELETE_SUB_FOLDERS = ITEMS_PAGE + 0x13;
+ public static final int ITEMS_USERNAME = ITEMS_PAGE + 0x14;
+ public static final int ITEMS_PASSWORD = ITEMS_PAGE + 0x15;
+ public static final int ITEMS_MOVE = ITEMS_PAGE + 0x16;
+ public static final int ITEMS_DSTFLDID = ITEMS_PAGE + 0x17;
+ public static final int ITEMS_CONVERSATION_ID = ITEMS_PAGE + 0x18;
+ public static final int ITEMS_MOVE_ALWAYS = ITEMS_PAGE + 0x19;
+
+ public static final int COMPOSE_PAGE = COMPOSE << PAGE_SHIFT;
+ public static final int COMPOSE_SEND_MAIL = COMPOSE_PAGE + 5;
+ public static final int COMPOSE_SMART_FORWARD = COMPOSE_PAGE + 6;
+ public static final int COMPOSE_SMART_REPLY = COMPOSE_PAGE + 7;
+ public static final int COMPOSE_SAVE_IN_SENT_ITEMS = COMPOSE_PAGE + 8;
+ public static final int COMPOSE_REPLACE_MIME = COMPOSE_PAGE + 9;
+ // There no tag for COMPOSE_PAGE + 0xA
+ public static final int COMPOSE_SOURCE = COMPOSE_PAGE + 0xB;
+ public static final int COMPOSE_FOLDER_ID = COMPOSE_PAGE + 0xC;
+ public static final int COMPOSE_ITEM_ID = COMPOSE_PAGE + 0xD;
+ public static final int COMPOSE_LONG_ID = COMPOSE_PAGE + 0xE;
+ public static final int COMPOSE_INSTANCE_ID = COMPOSE_PAGE + 0xF;
+ public static final int COMPOSE_MIME = COMPOSE_PAGE + 0x10;
+ public static final int COMPOSE_CLIENT_ID = COMPOSE_PAGE + 0x11;
+ public static final int COMPOSE_STATUS = COMPOSE_PAGE + 0x12;
+ public static final int COMPOSE_ACCOUNT_ID = COMPOSE_PAGE + 0x13;
+
+ public static final int EMAIL2_PAGE = EMAIL2 << PAGE_SHIFT;
+ public static final int EMAIL2_UM_CALLER_ID = EMAIL2_PAGE + 5;
+ public static final int EMAIL2_UM_USER_NOTES = EMAIL2_PAGE + 6;
+ public static final int EMAIL2_UM_ATT_DURATION = EMAIL2_PAGE + 7;
+ public static final int EMAIL2_UM_ATT_ORDER = EMAIL2_PAGE + 8;
+ public static final int EMAIL2_CONVERSATION_ID = EMAIL2_PAGE + 9;
+ public static final int EMAIL2_CONVERSATION_INDEX = EMAIL2_PAGE + 0xA;
+ public static final int EMAIL2_LAST_VERB_EXECUTED = EMAIL2_PAGE + 0xB;
+ public static final int EMAIL2_LAST_VERB_EXECUTION_TIME = EMAIL2_PAGE + 0xC;
+ public static final int EMAIL2_RECEIVED_AS_BCC = EMAIL2_PAGE + 0xD;
+ public static final int EMAIL2_SENDER = EMAIL2_PAGE + 0xE;
+ public static final int EMAIL2_CALENDAR_TYPE = EMAIL2_PAGE + 0xF;
+ public static final int EMAIL2_IS_LEAP_MONTH = EMAIL2_PAGE + 0x10;
+ public static final int EMAIL2_ACCOUNT_ID = EMAIL2_PAGE + 0x11;
+ public static final int EMAIL2_FIRST_DAY_OF_WEEK = EMAIL2_PAGE + 0x12;
+ public static final int EMAIL2_MEETING_MESSAGE_TYPE = EMAIL2_PAGE + 0x13;
+
+ public static final int RIGHTS_PAGE = RIGHTS << PAGE_SHIFT;
+ public static final int RIGHTS_SUPPORT = RIGHTS_PAGE + 5;
+ public static final int RIGHTS_TEMPLATES = RIGHTS_PAGE + 6;
+ public static final int RIGHTS_TEMPLATE = RIGHTS_PAGE + 7;
+ public static final int RIGHTS_LICENSE = RIGHTS_PAGE + 8;
+ public static final int RIGHTS_EDIT_ALLOWED = RIGHTS_PAGE + 9;
+ public static final int RIGHTS_REPLY_ALLOWED = RIGHTS_PAGE + 0xA;
+ public static final int RIGHTS_REPLY_ALL_ALLOWED = RIGHTS_PAGE + 0xB;
+ public static final int RIGHTS_FORWARD_ALLOWED = RIGHTS_PAGE + 0xC;
+ public static final int RIGHTS_MODIFY_RECIPIENTS_ALLOWED = RIGHTS_PAGE + 0xD;
+ public static final int RIGHTS_EXTRACT_ALLOWED = RIGHTS_PAGE + 0xE;
+ public static final int RIGHTS_PRINT_ALLOWED = RIGHTS_PAGE + 0xF;
+ public static final int RIGHTS_EXPORT_ALLOWED = RIGHTS_PAGE + 0x10;
+ public static final int RIGHTS_PROGRAMMATIC_ACCESS_ALLOWED = RIGHTS_PAGE + 0x11;
+ public static final int RIGHTS_OWNER = RIGHTS_PAGE + 0x12;
+ public static final int RIGHTS_CONTENT_EXPIRY_DATE = RIGHTS_PAGE + 0x13;
+ public static final int RIGHTS_TEMPLATE_ID = RIGHTS_PAGE + 0x14;
+ public static final int RIGHTS_TEMPLATE_NAME = RIGHTS_PAGE + 0x15;
+ public static final int RIGHTS_TEMPLATE_DESCRIPTION = RIGHTS_PAGE + 0x16;
+ public static final int RIGHTS_CONTENT_OWNER = RIGHTS_PAGE + 0x17;
+ public static final int RIGHTS_REMOVE_RM_DISTRIBUTION = RIGHTS_PAGE + 0x18;
+
+ static public String[][] pages = {
+ { // 0x00 AirSync
+ "Sync", "Responses", "Add", "Change", "Delete", "Fetch", "SyncKey", "ClientId",
+ "ServerId", "Status", "Collection", "Class", "Version", "CollectionId", "GetChanges",
+ "MoreAvailable", "WindowSize", "Commands", "Options", "FilterType", "Truncation",
+ "RTFTruncation", "Conflict", "Collections", "ApplicationData", "DeletesAsMoves",
+ "NotifyGUID", "Supported", "SoftDelete", "MIMESupport", "MIMETruncation", "Wait",
+ "Limit", "Partial"
+ },
+ {
+ // 0x01 Contacts
+ "Anniversary", "AssistantName", "AssistantTelephoneNumber", "Birthday", "ContactsBody",
+ "ContactsBodySize", "ContactsBodyTruncated", "Business2TelephoneNumber",
+ "BusinessAddressCity",
+ "BusinessAddressCountry", "BusinessAddressPostalCode", "BusinessAddressState",
+ "BusinessAddressStreet", "BusinessFaxNumber", "BusinessTelephoneNumber",
+ "CarTelephoneNumber", "ContactsCategories", "ContactsCategory", "Children", "Child",
+ "CompanyName", "Department", "Email1Address", "Email2Address", "Email3Address",
+ "FileAs", "FirstName", "Home2TelephoneNumber", "HomeAddressCity", "HomeAddressCountry",
+ "HomeAddressPostalCode", "HomeAddressState", "HomeAddressStreet", "HomeFaxNumber",
+ "HomeTelephoneNumber", "JobTitle", "LastName", "MiddleName", "MobileTelephoneNumber",
+ "OfficeLocation", "OtherAddressCity", "OtherAddressCountry",
+ "OtherAddressPostalCode", "OtherAddressState", "OtherAddressStreet", "PagerNumber",
+ "RadioTelephoneNumber", "Spouse", "Suffix", "Title", "Webpage", "YomiCompanyName",
+ "YomiFirstName", "YomiLastName", "CompressedRTF", "Picture"
+ },
+ {
+ // 0x02 Email
+ "Attachment", "Attachments", "AttName", "AttSize", "Add0Id", "AttMethod", "AttRemoved",
+ "Body", "BodySize", "BodyTruncated", "DateReceived", "DisplayName", "DisplayTo",
+ "Importance", "MessageClass", "Subject", "Read", "To", "CC", "From", "ReplyTo",
+ "AllDayEvent", "Categories", "Category", "DTStamp", "EndTime", "InstanceType",
+ "IntDBusyStatus", "Location", "MeetingRequest", "Organizer", "RecurrenceId", "Reminder",
+ "ResponseRequested", "Recurrences", "Recurence", "Recurrence_Type", "Recurrence_Until",
+ "Recurrence_Occurrences", "Recurrence_Interval", "Recurrence_DayOfWeek",
+ "Recurrence_DayOfMonth", "Recurrence_WeekOfMonth", "Recurrence_MonthOfYear",
+ "StartTime", "Sensitivity", "TimeZone", "GlobalObjId", "ThreadTopic", "MIMEData",
+ "MIMETruncated", "MIMESize", "InternetCPID", "Flag", "FlagStatus", "EmailContentClass",
+ "FlagType", "CompleteTime", "DisallowNewTimeProposal"
+ },
+ {
+ // 0x03 AirNotify
+ },
+ {
+ // 0x04 Calendar
+ "CalTimeZone", "CalAllDayEvent", "CalAttendees", "CalAttendee", "CalAttendee_Email",
+ "CalAttendee_Name", "CalBody", "CalBodyTruncated", "CalBusyStatus", "CalCategories",
+ "CalCategory", "CalCompressed_RTF", "CalDTStamp", "CalEndTime", "CalExeption",
+ "CalExceptions", "CalException_IsDeleted", "CalException_StartTime", "CalLocation",
+ "CalMeetingStatus", "CalOrganizer_Email", "CalOrganizer_Name", "CalRecurrence",
+ "CalRecurrence_Type", "CalRecurrence_Until", "CalRecurrence_Occurrences",
+ "CalRecurrence_Interval", "CalRecurrence_DayOfWeek", "CalRecurrence_DayOfMonth",
+ "CalRecurrence_WeekOfMonth", "CalRecurrence_MonthOfYear", "CalReminder_MinsBefore",
+ "CalSensitivity", "CalSubject", "CalStartTime", "CalUID", "CalAttendee_Status",
+ "CalAttendee_Type", "CalAttachment", "CalAttachments", "CalAttName", "CalAttSize",
+ "CalAttOid", "CalAttMethod", "CalAttRemoved", "CalDisplayName",
+ "CalDisallowNewTimeProposal", "CalResponseRequested", "CalAppointmentReplyTime",
+ "CalResponseType", "CalCalendarType", "CalIsLeapMonth", "CalFirstDayOfWeek",
+ "CalOnlineMeetingConfLink", "CalOnlineMeetingExternalLink"
+ },
+ {
+ // 0x05 Move
+ "MoveItems", "Move", "SrcMsgId", "SrcFldId", "DstFldId", "MoveResponse", "MoveStatus",
+ "DstMsgId"
+ },
+ {
+ // 0x06 ItemEstimate
+ "GetItemEstimate", "Version", "IECollections", "IECollection", "IEClass",
+ "IECollectionId", "DateTime", "Estimate", "IEResponse", "ItemEstimateStatus"
+ },
+ {
+ // 0x07 FolderHierarchy
+ "Folders", "Folder", "FolderDisplayName", "FolderServerId", "FolderParentId", "Type",
+ "FolderResponse", "FolderStatus", "FolderContentClass", "Changes", "FolderAdd",
+ "FolderDelete", "FolderUpdate", "FolderSyncKey", "FolderFolderCreate",
+ "FolderFolderDelete", "FolderFolderUpdate", "FolderSync", "Count", "FolderVersion"
+ },
+ {
+ // 0x08 MeetingResponse
+ "CalId", "CollectionId", "MeetingResponse", "ReqId", "Request",
+ "MeetingResponseResult", "MeetingResponseStatus", "UserResponse", "Version"
+ },
+ {
+ // 0x09 Tasks
+ "TasksBody", "TasksBodySize", "TasksBodyTruncated", "TasksCategories", "TasksCategory",
+ "Complete", "DateCompleted", "DueDate", "UTCDueDate", "TasksImportance", "Recurrence",
+ "RecurrenceType", "RecurrenceStart", "RecurrenceUntil", "RecurrenceOccurrences",
+ "RecurrenceInterval", "RecurrenceDOM", "RecurrenceDOW", "RecurrenceWOM",
+ "RecurrenceMOY", "RecurrenceRegenerate", "RecurrenceDeadOccur", "ReminderSet",
+ "ReminderTime", "TasksSensitivity", "StartDate", "UTCStartDate", "TasksSubject",
+ "TasksCompressedRTF", "OrdinalDate", "SubordinalDate"
+ },
+ {
+ // 0x0A ResolveRecipients
+ },
+ {
+ // 0x0B ValidateCert
+ },
+ {
+ // 0x0C Contacts2
+ "CustomerId", "GovernmentId", "IMAddress", "IMAddress2", "IMAddress3", "ManagerName",
+ "CompanyMainPhone", "AccountName", "NickName", "MMS"
+ },
+ {
+ // 0x0D Ping
+ "Ping", "AutdState", "PingStatus", "HeartbeatInterval", "PingFolders", "PingFolder",
+ "PingId", "PingClass", "MaxFolders"
+ },
+ {
+ // 0x0E Provision
+ "Provision", "Policies", "Policy", "PolicyType", "PolicyKey", "Data", "ProvisionStatus",
+ "RemoteWipe", "EASProvidionDoc", "DevicePasswordEnabled",
+ "AlphanumericDevicePasswordRequired",
+ "DeviceEncryptionEnabled", "PasswordRecoveryEnabled", "-unused-", "AttachmentsEnabled",
+ "MinDevicePasswordLength",
+ "MaxInactivityTimeDeviceLock", "MaxDevicePasswordFailedAttempts", "MaxAttachmentSize",
+ "AllowSimpleDevicePassword", "DevicePasswordExpiration", "DevicePasswordHistory",
+ "AllowStorageCard", "AllowCamera", "RequireDeviceEncryption",
+ "AllowUnsignedApplications", "AllowUnsignedInstallationPackages",
+ "MinDevicePasswordComplexCharacters", "AllowWiFi", "AllowTextMessaging",
+ "AllowPOPIMAPEmail", "AllowBluetooth", "AllowIrDA", "RequireManualSyncWhenRoaming",
+ "AllowDesktopSync",
+ "MaxCalendarAgeFilder", "AllowHTMLEmail", "MaxEmailAgeFilter",
+ "MaxEmailBodyTruncationSize", "MaxEmailHTMLBodyTruncationSize",
+ "RequireSignedSMIMEMessages", "RequireEncryptedSMIMEMessages",
+ "RequireSignedSMIMEAlgorithm", "RequireEncryptionSMIMEAlgorithm",
+ "AllowSMIMEEncryptionAlgorithmNegotiation", "AllowSMIMESoftCerts", "AllowBrowser",
+ "AllowConsumerEmail", "AllowRemoteDesktop", "AllowInternetSharing",
+ "UnapprovedInROMApplicationList", "ApplicationName", "ApprovedApplicationList", "Hash"
+ },
+ {
+ // 0x0F Search
+ "Search", "Stores", "Store", "Name", "Query",
+ "SearchOptions", "Range", "SearchStatus", "Response", "Result",
+ "Properties", "Total", "EqualTo", "Value", "And",
+ "Or", "FreeText", "SubstringOp", "DeepTraversal", "LongId",
+ "RebuildResults", "LessThan", "GreateerThan", "Schema", "SearchSupported"
+ },
+ {
+ // 0x10 Gal
+ "GalDisplayName", "GalPhone", "GalOffice", "GalTitle", "GalCompany", "GalAlias",
+ "GalFirstName", "GalLastName", "GalHomePhone", "GalMobilePhone", "GalEmailAddress"
+ },
+ {
+ // 0x11 AirSyncBase
+ "BodyPreference", "BodyPreferenceType", "BodyPreferenceTruncationSize", "AllOrNone",
+ "--unused1--", "BaseBody", "BaseData", "BaseEstimatedDataSize", "BaseTruncated",
+ "BaseAttachments", "BaseAttachment", "BaseDisplayName", "FileReference", "BaseMethod",
+ "BaseContentId", "BaseContentLocation", "BaseIsInline", "BaseNativeBodyType",
+ "BaseContentType"
+ },
+ {
+ // 0x12 Settings
+ "Settings", "SettingsStatus", "Get", "Set", "Oof", "OofState", "SettingsStartTime",
+ "SettingsEndTime", "OofMessage", "AppliesToInternal", "AppliesToExternalKnown",
+ "AppliesToExternalUnknown", "Enabled", "ReplyMessage", "BodyType", "DevicePassword",
+ "Password", "DeviceInformation", "Model", "IMEI", "FriendlyName", "OS", "OSLanguage",
+ "PhoneNumber", "UserInformation", "EmailAddress", "StmpAddress", "UserAgent",
+ "EnableOutboundSMS", "MobileOperator"
+ },
+ {
+ // 0x13 DocumentLibrary
+ },
+ {
+ // 0x14 ItemOperations
+ "Items", "ItemsFetch", "ItemsStore", "ItemsOptions", "ItemsRange",
+ "ItemsTotal", "ItemsProperties", "ItemsData", "ItemsStatus", "ItemsResponse",
+ "ItemsVersion", "ItemsSchema", "ItemsPart", "ItemsEmptyFolder", "ItemsDeleteSubFolders",
+ "ItemsUserName", "ItemsPassword", "ItemsMove", "ItemsDstFldId", "ItemsConversationId",
+ "ItemsMoveAlways"
+ },
+ {
+ // 0x15 ComposeMail
+ "SendMail", "SmartForward", "SmartReply", "SaveInSentItems", "ReplaceMime",
+ "--unused2--", "ComposeSource", "ComposeFolderId", "ComposeItemId", "ComposeLongId",
+ "ComposeInstanceId", "ComposeMime", "ComposeClientId", "ComposeStatus",
+ "ComposeAccountId"
+ },
+ {
+ // 0x16 Email2
+ "UmCallerId", "UmUserNotes", "UmAttDuration", "UmAttOrder", "ConversationId",
+ "ConversationIndex", "LastVerbExecuted", "LastVerbExecutionTime", "ReceivedAsBcc",
+ "Sender", "CalendarType", "IsLeapMonth", "AccountId", "FirstDayOfWeek",
+ "MeetingMessageType"
+ },
+ {
+ // 0x17 Notes
+ },
+ {
+ // 0x18 Rights Management
+ "RMSupport", "RMTemplates", "RMTemplate", "RMLicense", "EditAllowed", "ReplyAllowed",
+ "ReplyAllAllowed", "ForwardAllowed", "ModifyRecipientsAllowed", "ExtractAllowed",
+ "PrintAllowed", "ExportAllowed", "ProgrammaticAccessAllowed", "RMOwner",
+ "ContentExpiryDate", "TemplateID", "TemplateName", "TemplateDescription",
+ "ContentOwner", "RemoveRMDistribution"
+ }
+ };
+}
diff --git a/exchange2/src/com/android/exchange/adapter/Wbxml.java b/exchange2/src/com/android/exchange/adapter/Wbxml.java
new file mode 100644
index 0000000..b8ae789
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/Wbxml.java
@@ -0,0 +1,51 @@
+/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE. */
+
+package com.android.exchange.adapter;
+
+
+/** contains the WBXML constants */
+
+
+public interface Wbxml {
+
+ static public final int SWITCH_PAGE = 0;
+ static public final int END = 1;
+ static public final int ENTITY = 2;
+ static public final int STR_I = 3;
+ static public final int LITERAL = 4;
+ static public final int EXT_I_0 = 0x40;
+ static public final int EXT_I_1 = 0x41;
+ static public final int EXT_I_2 = 0x42;
+ static public final int PI = 0x43;
+ static public final int LITERAL_C = 0x44;
+ static public final int EXT_T_0 = 0x80;
+ static public final int EXT_T_1 = 0x81;
+ static public final int EXT_T_2 = 0x82;
+ static public final int STR_T = 0x83;
+ static public final int LITERAL_A = 0x084;
+ static public final int EXT_0 = 0x0c0;
+ static public final int EXT_1 = 0x0c1;
+ static public final int EXT_2 = 0x0c2;
+ static public final int OPAQUE = 0x0c3;
+ static public final int LITERAL_AC = 0x0c4;
+
+ static public final int WITH_CONTENT = 0x40;
+}
diff --git a/exchange2/src/com/android/exchange/adapter/patent_disclaimer.txt b/exchange2/src/com/android/exchange/adapter/patent_disclaimer.txt
new file mode 100644
index 0000000..8a715a3
--- /dev/null
+++ b/exchange2/src/com/android/exchange/adapter/patent_disclaimer.txt
@@ -0,0 +1,9 @@
+-----------------------------
+THIS IS NOT A GRANT OF PATENT RIGHTS.
+
+Google makes no representation or warranty that the source code made available hereunder is
+unencumbered by third-party patents. Those intending to use this source code in hardware or
+software products are advised that implementations of this code, including in open source software
+or shareware, may require patent licenses from the relevant patent holders.
+
+-----------------------------
diff --git a/exchange2/src/com/android/exchange/patent_disclaimer.txt b/exchange2/src/com/android/exchange/patent_disclaimer.txt
new file mode 100644
index 0000000..8a715a3
--- /dev/null
+++ b/exchange2/src/com/android/exchange/patent_disclaimer.txt
@@ -0,0 +1,9 @@
+-----------------------------
+THIS IS NOT A GRANT OF PATENT RIGHTS.
+
+Google makes no representation or warranty that the source code made available hereunder is
+unencumbered by third-party patents. Those intending to use this source code in hardware or
+software products are advised that implementations of this code, including in open source software
+or shareware, may require patent licenses from the relevant patent holders.
+
+-----------------------------
diff --git a/exchange2/src/com/android/exchange/provider/ExchangeDirectoryProvider.java b/exchange2/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
new file mode 100644
index 0000000..6d725ff
--- /dev/null
+++ b/exchange2/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
@@ -0,0 +1,441 @@
+/*
+ * 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.provider;
+
+import com.android.emailcommon.Configuration;
+import com.android.emailcommon.mail.PackedString;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.AccountColumns;
+import com.android.emailcommon.service.AccountServiceProxy;
+import com.android.emailcommon.utility.Utility;
+import com.android.exchange.Eas;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.R;
+import com.android.exchange.provider.GalResult.GalData;
+
+import android.accounts.AccountManager;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * ExchangeDirectoryProvider provides real-time data from the Exchange server; at the moment, it is
+ * used solely to provide GAL (Global Address Lookup) service to email address adapters
+ */
+public class ExchangeDirectoryProvider extends ContentProvider {
+ public static final String EXCHANGE_GAL_AUTHORITY = "com.android.exchange.directory.provider";
+
+ private static final int DEFAULT_CONTACT_ID = 1;
+ private static final int DEFAULT_LOOKUP_LIMIT = 20;
+
+ private static final int GAL_BASE = 0;
+ private static final int GAL_DIRECTORIES = GAL_BASE;
+ private static final int GAL_FILTER = GAL_BASE + 1;
+ private static final int GAL_CONTACT = GAL_BASE + 2;
+ private static final int GAL_CONTACT_WITH_ID = GAL_BASE + 3;
+ private static final int GAL_EMAIL_FILTER = GAL_BASE + 4;
+
+ private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ /*package*/ final HashMap<String, Long> mAccountIdMap = new HashMap<String, Long>();
+
+ static {
+ sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "directories", GAL_DIRECTORIES);
+ sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/filter/*", GAL_FILTER);
+ sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/entities", GAL_CONTACT);
+ sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/#/entities",
+ GAL_CONTACT_WITH_ID);
+ sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/emails/filter/*", GAL_EMAIL_FILTER);
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ static class GalProjection {
+ final int size;
+ final HashMap<String, Integer> columnMap = new HashMap<String, Integer>();
+
+ GalProjection(String[] projection) {
+ size = projection.length;
+ for (int i = 0; i < projection.length; i++) {
+ columnMap.put(projection[i], i);
+ }
+ }
+ }
+
+ static class GalContactRow {
+ private final GalProjection mProjection;
+ private Object[] row;
+ static long dataId = 1;
+
+ GalContactRow(GalProjection projection, long contactId, String lookupKey,
+ String accountName, String displayName) {
+ this.mProjection = projection;
+ row = new Object[projection.size];
+
+ put(Contacts.Entity.CONTACT_ID, contactId);
+
+ // We only have one raw contact per aggregate, so they can have the same ID
+ put(Contacts.Entity.RAW_CONTACT_ID, contactId);
+ put(Contacts.Entity.DATA_ID, dataId++);
+
+ put(Contacts.DISPLAY_NAME, displayName);
+
+ // TODO alternative display name
+ put(Contacts.DISPLAY_NAME_ALTERNATIVE, displayName);
+
+ put(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ put(RawContacts.ACCOUNT_NAME, accountName);
+ put(RawContacts.RAW_CONTACT_IS_READ_ONLY, 1);
+ put(Data.IS_READ_ONLY, 1);
+ }
+
+ Object[] getRow () {
+ return row;
+ }
+
+ void put(String columnName, Object value) {
+ Integer integer = mProjection.columnMap.get(columnName);
+ if (integer != null) {
+ row[integer] = value;
+ } else {
+ System.out.println("Unsupported column: " + columnName);
+ }
+ }
+
+ static void addEmailAddress(MatrixCursor cursor, GalProjection galProjection,
+ long contactId, String lookupKey, String accountName, String displayName,
+ String address) {
+ if (!TextUtils.isEmpty(address)) {
+ GalContactRow r = new GalContactRow(
+ galProjection, contactId, lookupKey, accountName, displayName);
+ r.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+ r.put(Email.TYPE, Email.TYPE_WORK);
+ r.put(Email.ADDRESS, address);
+ cursor.addRow(r.getRow());
+ }
+ }
+
+ static void addPhoneRow(MatrixCursor cursor, GalProjection projection, long contactId,
+ String lookupKey, String accountName, String displayName, int type, String number) {
+ if (!TextUtils.isEmpty(number)) {
+ GalContactRow r = new GalContactRow(
+ projection, contactId, lookupKey, accountName, displayName);
+ r.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ r.put(Phone.TYPE, type);
+ r.put(Phone.NUMBER, number);
+ cursor.addRow(r.getRow());
+ }
+ }
+
+ public static void addNameRow(MatrixCursor cursor, GalProjection galProjection,
+ long contactId, String lookupKey, String accountName, String displayName,
+ String firstName, String lastName) {
+ GalContactRow r = new GalContactRow(
+ galProjection, contactId, lookupKey, accountName, displayName);
+ r.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+ r.put(StructuredName.GIVEN_NAME, firstName);
+ r.put(StructuredName.FAMILY_NAME, lastName);
+ r.put(StructuredName.DISPLAY_NAME, displayName);
+ cursor.addRow(r.getRow());
+ }
+ }
+
+ /**
+ * Find the record id of an Account, given its name (email address)
+ * @param accountName the name of the account
+ * @return the record id of the Account, or -1 if not found
+ */
+ /*package*/ long getAccountIdByName(Context context, String accountName) {
+ Long accountId = mAccountIdMap.get(accountName);
+ if (accountId == null) {
+ accountId = Utility.getFirstRowLong(context, Account.CONTENT_URI,
+ EmailContent.ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?",
+ new String[] {accountName}, null, EmailContent.ID_PROJECTION_COLUMN , -1L);
+ if (accountId != -1) {
+ mAccountIdMap.put(accountName, accountId);
+ }
+ }
+ return accountId;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ int match = sURIMatcher.match(uri);
+ MatrixCursor cursor;
+ Object[] row;
+ PackedString ps;
+ String lookupKey;
+
+ switch (match) {
+ case GAL_DIRECTORIES: {
+ // Assuming that GAL can be used with all exchange accounts
+ android.accounts.Account[] accounts = AccountManager.get(getContext())
+ .getAccountsByType(Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ cursor = new MatrixCursor(projection);
+ if (accounts != null) {
+ for (android.accounts.Account account : accounts) {
+ row = new Object[projection.length];
+
+ for (int i = 0; i < projection.length; i++) {
+ String column = projection[i];
+ if (column.equals(Directory.ACCOUNT_NAME)) {
+ row[i] = account.name;
+ } else if (column.equals(Directory.ACCOUNT_TYPE)) {
+ row[i] = account.type;
+ } else if (column.equals(Directory.TYPE_RESOURCE_ID)) {
+ Bundle bundle = null;
+ String accountType = Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE;
+ bundle = new AccountServiceProxy(getContext())
+ .getConfigurationData(accountType);
+ // Default to the alternative name, erring on the conservative side
+ int exchangeName = R.string.exchange_name_alternate;
+ if (bundle != null && !bundle.getBoolean(
+ Configuration.EXCHANGE_CONFIGURATION_USE_ALTERNATE_STRINGS,
+ true)) {
+ exchangeName = R.string.exchange_name;
+ }
+ row[i] = exchangeName;
+ } else if (column.equals(Directory.DISPLAY_NAME)) {
+ // If the account name is an email address, extract
+ // the domain name and use it as the directory display name
+ final String accountName = account.name;
+ int atIndex = accountName.indexOf('@');
+ if (atIndex != -1 && atIndex < accountName.length() - 2) {
+ final char firstLetter = Character.toUpperCase(
+ accountName.charAt(atIndex + 1));
+ row[i] = firstLetter + accountName.substring(atIndex + 2);
+ } else {
+ row[i] = account.name;
+ }
+ } else if (column.equals(Directory.EXPORT_SUPPORT)) {
+ row[i] = Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY;
+ } else if (column.equals(Directory.SHORTCUT_SUPPORT)) {
+ row[i] = Directory.SHORTCUT_SUPPORT_NONE;
+ }
+ }
+ cursor.addRow(row);
+ }
+ }
+ return cursor;
+ }
+
+ case GAL_FILTER:
+ case GAL_EMAIL_FILTER: {
+ String filter = uri.getLastPathSegment();
+ // We should have at least two characters before doing a GAL search
+ if (filter == null || filter.length() < 2) {
+ return null;
+ }
+
+ String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
+ if (accountName == null) {
+ return null;
+ }
+
+ // Enforce a limit on the number of lookup responses
+ String limitString = uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY);
+ int limit = DEFAULT_LOOKUP_LIMIT;
+ if (limitString != null) {
+ try {
+ limit = Integer.parseInt(limitString);
+ } catch (NumberFormatException e) {
+ limit = 0;
+ }
+ if (limit <= 0) {
+ throw new IllegalArgumentException("Limit not valid: " + limitString);
+ }
+ }
+
+ long callingId = Binder.clearCallingIdentity();
+ try {
+ // Find the account id to pass along to EasSyncService
+ long accountId = getAccountIdByName(getContext(), accountName);
+ if (accountId == -1) {
+ // The account was deleted?
+ return null;
+ }
+
+ // Get results from the Exchange account
+ GalResult galResult = EasSyncService.searchGal(getContext(), accountId,
+ filter, limit);
+ if (galResult != null) {
+ return buildGalResultCursor(projection, galResult);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
+ }
+ break;
+ }
+
+ case GAL_CONTACT:
+ case GAL_CONTACT_WITH_ID: {
+ String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
+ if (accountName == null) {
+ return null;
+ }
+
+ GalProjection galProjection = new GalProjection(projection);
+ cursor = new MatrixCursor(projection);
+ // Handle the decomposition of the key into rows suitable for CP2
+ List<String> pathSegments = uri.getPathSegments();
+ lookupKey = pathSegments.get(2);
+ long contactId = (match == GAL_CONTACT_WITH_ID)
+ ? Long.parseLong(pathSegments.get(3))
+ : DEFAULT_CONTACT_ID;
+ ps = new PackedString(lookupKey);
+ String displayName = ps.get(GalData.DISPLAY_NAME);
+ GalContactRow.addEmailAddress(cursor, galProjection, contactId, lookupKey,
+ accountName, displayName, ps.get(GalData.EMAIL_ADDRESS));
+ GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName,
+ displayName, displayName, Phone.TYPE_HOME, ps.get(GalData.HOME_PHONE));
+ GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName,
+ displayName, displayName, Phone.TYPE_WORK, ps.get(GalData.WORK_PHONE));
+ GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName,
+ displayName, displayName, Phone.TYPE_MOBILE, ps.get(GalData.MOBILE_PHONE));
+ GalContactRow.addNameRow(cursor, galProjection, contactId, accountName, displayName,
+ ps.get(GalData.FIRST_NAME), ps.get(GalData.LAST_NAME), displayName);
+ return cursor;
+ }
+ }
+
+ return null;
+ }
+
+ /*package*/ Cursor buildGalResultCursor(String[] projection, GalResult galResult) {
+ int displayNameIndex = -1;
+ int alternateDisplayNameIndex = -1;;
+ int emailIndex = -1;
+ int idIndex = -1;
+ int lookupIndex = -1;
+
+ for (int i = 0; i < projection.length; i++) {
+ String column = projection[i];
+ if (Contacts.DISPLAY_NAME.equals(column) ||
+ Contacts.DISPLAY_NAME_PRIMARY.equals(column)) {
+ displayNameIndex = i;
+ } else if (Contacts.DISPLAY_NAME_ALTERNATIVE.equals(column)) {
+ alternateDisplayNameIndex = i;
+ } else if (CommonDataKinds.Email.ADDRESS.equals(column)) {
+ emailIndex = i;
+ } else if (Contacts._ID.equals(column)) {
+ idIndex = i;
+ } else if (Contacts.LOOKUP_KEY.equals(column)) {
+ lookupIndex = i;
+ }
+ }
+
+ Object[] row = new Object[projection.length];
+
+ /*
+ * ContactsProvider will ensure that every request has a non-null projection.
+ */
+ MatrixCursor cursor = new MatrixCursor(projection);
+ int count = galResult.galData.size();
+ for (int i = 0; i < count; i++) {
+ GalData galDataRow = galResult.galData.get(i);
+ String firstName = galDataRow.get(GalData.FIRST_NAME);
+ String lastName = galDataRow.get(GalData.LAST_NAME);
+ String displayName = galDataRow.get(GalData.DISPLAY_NAME);
+ // If we don't have a display name, try to create one using first and last name
+ if (displayName == null) {
+ if (firstName != null && lastName != null) {
+ displayName = firstName + " " + lastName;
+ } else if (firstName != null) {
+ displayName = firstName;
+ } else if (lastName != null) {
+ displayName = lastName;
+ }
+ }
+ galDataRow.put(GalData.DISPLAY_NAME, displayName);
+
+ if (displayNameIndex != -1) {
+ row[displayNameIndex] = displayName;
+ }
+ if (alternateDisplayNameIndex != -1) {
+ // Try to create an alternate display name, using first and last name
+ // TODO: Check with Contacts team to make sure we're using this properly
+ if (firstName != null && lastName != null) {
+ row[alternateDisplayNameIndex] = lastName + " " + firstName;
+ } else {
+ row[alternateDisplayNameIndex] = displayName;
+ }
+ }
+ if (emailIndex != -1) {
+ row[emailIndex] = galDataRow.get(GalData.EMAIL_ADDRESS);
+ }
+ if (idIndex != -1) {
+ row[idIndex] = i + 1; // Let's be 1 based
+ }
+ if (lookupIndex != -1) {
+ // We use the packed string as our lookup key; it contains ALL of the gal data
+ // We do this because we are not able to provide a stable id to ContactsProvider
+ row[lookupIndex] = Uri.encode(galDataRow.toPackedString());
+ }
+ cursor.addRow(row);
+ }
+ return cursor;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ int match = sURIMatcher.match(uri);
+ switch (match) {
+ case GAL_FILTER:
+ return Contacts.CONTENT_ITEM_TYPE;
+ }
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/exchange2/src/com/android/exchange/provider/GalResult.java b/exchange2/src/com/android/exchange/provider/GalResult.java
new file mode 100644
index 0000000..4d0d908
--- /dev/null
+++ b/exchange2/src/com/android/exchange/provider/GalResult.java
@@ -0,0 +1,94 @@
+/* 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.provider;
+
+import com.android.emailcommon.mail.PackedString;
+
+import java.util.ArrayList;
+
+/**
+ * A container for GAL results from EAS
+ * Each element of the galData array becomes an element of the list used by autocomplete
+ */
+public class GalResult {
+ // Total number of matches in this result
+ public int total;
+ public ArrayList<GalData> galData = new ArrayList<GalData>();
+
+ public GalResult() {
+ }
+
+ /**
+ * Legacy method for email address autocomplete
+ */
+ public void addGalData(long id, String displayName, String emailAddress) {
+ galData.add(new GalData(id, displayName, emailAddress));
+ }
+
+ public void addGalData(GalData data) {
+ galData.add(data);
+ }
+
+ public static class GalData {
+ // PackedString constants for GalData
+ public static final String ID = "_id";
+ public static final String DISPLAY_NAME = "displayName";
+ public static final String EMAIL_ADDRESS = "emailAddress";
+ public static final String WORK_PHONE = "workPhone";
+ public static final String HOME_PHONE = "homePhone";
+ public static final String MOBILE_PHONE = "mobilePhone";
+ public static final String FIRST_NAME = "firstName";
+ public static final String LAST_NAME = "lastName";
+ public static final String COMPANY = "company";
+ public static final String TITLE = "title";
+ public static final String OFFICE = "office";
+ public static final String ALIAS = "alias";
+ // The Builder we use to construct the PackedString
+ PackedString.Builder builder = new PackedString.Builder();
+
+ // The following three fields are for legacy email autocomplete
+ public long _id = 0;
+ public String displayName;
+ public String emailAddress;
+
+ /**
+ * Legacy constructor for email address autocomplete
+ */
+ private GalData(long id, String _displayName, String _emailAddress) {
+ put(ID, Long.toString(id));
+ _id = id;
+ put(DISPLAY_NAME, _displayName);
+ displayName = _displayName;
+ put(EMAIL_ADDRESS, _emailAddress);
+ emailAddress = _emailAddress;
+ }
+
+ public GalData() {
+ }
+
+ public String get(String field) {
+ return builder.get(field);
+ }
+
+ public void put(String field, String value) {
+ builder.put(field, value);
+ }
+
+ public String toPackedString() {
+ return builder.toString();
+ }
+ }
+}
diff --git a/exchange2_src/com/android/exchange/provider/MailboxUtilities.java b/exchange2/src/com/android/exchange/provider/MailboxUtilities.java
similarity index 100%
rename from exchange2_src/com/android/exchange/provider/MailboxUtilities.java
rename to exchange2/src/com/android/exchange/provider/MailboxUtilities.java
diff --git a/exchange2/src/com/android/exchange/service/ExchangeBroadcastProcessorService.java b/exchange2/src/com/android/exchange/service/ExchangeBroadcastProcessorService.java
new file mode 100644
index 0000000..8f88910
--- /dev/null
+++ b/exchange2/src/com/android/exchange/service/ExchangeBroadcastProcessorService.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.service;
+
+import android.accounts.AccountManager;
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import com.android.emailcommon.Logging;
+import com.android.exchange.Eas;
+import com.android.exchange.ExchangeService;
+
+/**
+ * The service that really handles broadcast intents on a worker thread.
+ *
+ * We make it a service, because:
+ * <ul>
+ * <li>So that it's less likely for the process to get killed.
+ * <li>Even if it does, the Intent that have started it will be re-delivered by the system,
+ * and we can start the process again. (Using {@link #setIntentRedelivery}).
+ * </ul>
+ */
+public class ExchangeBroadcastProcessorService extends IntentService {
+ // Action used for BroadcastReceiver entry point
+ private static final String ACTION_BROADCAST = "broadcast_receiver";
+
+ public ExchangeBroadcastProcessorService() {
+ // Class name will be the thread name.
+ super(ExchangeBroadcastProcessorService.class.getName());
+ // Intent should be redelivered if the process gets killed before completing the job.
+ setIntentRedelivery(true);
+ }
+
+ /**
+ * Entry point for {@link ExchangeBroadcastReceiver}.
+ */
+ public static void processBroadcastIntent(Context context, Intent broadcastIntent) {
+ Intent i = new Intent(context, ExchangeBroadcastProcessorService.class);
+ i.setAction(ACTION_BROADCAST);
+ i.putExtra(Intent.EXTRA_INTENT, broadcastIntent);
+ context.startService(i);
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ // Dispatch from entry point
+ final String action = intent.getAction();
+ if (ACTION_BROADCAST.equals(action)) {
+ final Intent broadcastIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT);
+ final String broadcastAction = broadcastIntent.getAction();
+
+ if (Intent.ACTION_BOOT_COMPLETED.equals(broadcastAction)) {
+ onBootCompleted();
+ } else if (AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(broadcastAction)) {
+ if (Eas.USER_LOG) {
+ Log.d(Logging.LOG_TAG, "Login accounts changed; reconciling...");
+ }
+ ExchangeService.runAccountReconcilerSync(this);
+ }
+ }
+ }
+
+ /**
+ * Handles {@link Intent#ACTION_BOOT_COMPLETED}. Called on a worker thread.
+ */
+ private void onBootCompleted() {
+ startService(new Intent(this, ExchangeService.class));
+ }
+}
diff --git a/exchange2/src/com/android/exchange/service/ExchangeBroadcastReceiver.java b/exchange2/src/com/android/exchange/service/ExchangeBroadcastReceiver.java
new file mode 100644
index 0000000..e79eec0
--- /dev/null
+++ b/exchange2/src/com/android/exchange/service/ExchangeBroadcastReceiver.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.service;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * The broadcast receiver. The actual job is done in EmailBroadcastProcessor on a worker thread.
+ */
+public class ExchangeBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ ExchangeBroadcastProcessorService.processBroadcastIntent(context, intent);
+ }
+}
diff --git a/exchange2/src/com/android/exchange/utility/CalendarUtilities.java b/exchange2/src/com/android/exchange/utility/CalendarUtilities.java
new file mode 100644
index 0000000..2302b7a
--- /dev/null
+++ b/exchange2/src/com/android/exchange/utility/CalendarUtilities.java
@@ -0,0 +1,1949 @@
+/*
+ * 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.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Entity;
+import android.content.Entity.NamedContentValues;
+import android.content.EntityIterator;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.CalendarContract.Attendees;
+import android.provider.CalendarContract.Calendars;
+import android.provider.CalendarContract.Events;
+import android.provider.CalendarContract.EventsEntity;
+import android.text.TextUtils;
+import android.text.format.Time;
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.calendarcommon.DateException;
+import com.android.calendarcommon.Duration;
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.mail.Address;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.Attachment;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.service.AccountServiceProxy;
+import com.android.emailcommon.utility.Utility;
+import com.android.exchange.Eas;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.ExchangeService;
+import com.android.exchange.R;
+import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.Tags;
+import com.android.internal.util.ArrayUtils;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.text.DateFormat;
+import java.util.ArrayList;
+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;
+
+ // We want to find a time zone whose DST info is accurate to one minute
+ static final int STANDARD_DST_PRECISION = MINUTES;
+ // If we can't find one, we'll try a more lenient standard (this is better than guessing a
+ // time zone, which is what we otherwise do). Note that this specifically addresses an issue
+ // seen in some time zones sent by MS Exchange in which the start and end hour differ
+ // for no apparent reason
+ static final int LENIENT_DST_PRECISION = 4*HOURS;
+
+ private static final String SYNC_VERSION = Events.SYNC_DATA4;
+ // 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>();
+
+ private static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
+ // Default, Popup
+ private static final String ALLOWED_REMINDER_TYPES = "0,1";
+ // None, required, optional
+ private static final String ALLOWED_ATTENDEE_TYPES = "0,1,2";
+ // Busy, free, tentative
+ private static final String ALLOWED_AVAILABILITIES = "0,1,2";
+
+ // 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"};
+
+ // Bits used in EAS recurrences for days of the week
+ protected static final int EAS_SUNDAY = 1<<0;
+ protected static final int EAS_MONDAY = 1<<1;
+ protected static final int EAS_TUESDAY = 1<<2;
+ protected static final int EAS_WEDNESDAY = 1<<3;
+ protected static final int EAS_THURSDAY = 1<<4;
+ protected static final int EAS_FRIDAY = 1<<5;
+ protected static final int EAS_SATURDAY = 1<<6;
+ protected static final int EAS_WEEKDAYS =
+ EAS_MONDAY | EAS_TUESDAY | EAS_WEDNESDAY | EAS_THURSDAY | EAS_FRIDAY;
+ protected static final int EAS_WEEKENDS = EAS_SATURDAY | EAS_SUNDAY;
+
+ static final int sCurrentYear = new GregorianCalendar().get(Calendar.YEAR);
+ static final TimeZone sGmtTimeZone = TimeZone.getTimeZone("GMT");
+
+ private static final String ICALENDAR_ATTENDEE = "ATTENDEE;ROLE=REQ-PARTICIPANT";
+ static final String ICALENDAR_ATTENDEE_CANCEL = ICALENDAR_ATTENDEE;
+ static final String ICALENDAR_ATTENDEE_INVITE =
+ ICALENDAR_ATTENDEE + ";PARTSTAT=NEEDS-ACTION;RSVP=TRUE";
+ static final String ICALENDAR_ATTENDEE_ACCEPT =
+ ICALENDAR_ATTENDEE + ";PARTSTAT=ACCEPTED";
+ static final String ICALENDAR_ATTENDEE_DECLINE =
+ ICALENDAR_ATTENDEE + ";PARTSTAT=DECLINED";
+ static final String ICALENDAR_ATTENDEE_TENTATIVE =
+ ICALENDAR_ATTENDEE + ";PARTSTAT=TENTATIVE";
+
+ // Note that these constants apply to Calendar items
+ // For future reference: MeetingRequest data can also include free/busy information, but the
+ // constants for these four options in MeetingRequest data have different values!
+ // See [MS-ASCAL] 2.2.2.8 for Calendar BusyStatus
+ // See [MS-EMAIL] 2.2.2.34 for MeetingRequest BusyStatus
+ public static final int BUSY_STATUS_FREE = 0;
+ public static final int BUSY_STATUS_TENTATIVE = 1;
+ public static final int BUSY_STATUS_BUSY = 2;
+ public static final int BUSY_STATUS_OUT_OF_OFFICE = 3;
+
+ // Note that these constants apply to Calendar items, and are used in EAS 14+
+ // See [MS-ASCAL] 2.2.2.22 for Calendar ResponseType
+ public static final int RESPONSE_TYPE_NONE = 0;
+ public static final int RESPONSE_TYPE_ORGANIZER = 1;
+ public static final int RESPONSE_TYPE_TENTATIVE = 2;
+ public static final int RESPONSE_TYPE_ACCEPTED = 3;
+ public static final int RESPONSE_TYPE_DECLINED = 4;
+ public static final int RESPONSE_TYPE_NOT_RESPONDED = 5;
+
+ // 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;
+ }
+
+ @VisibleForTesting
+ static void clearTimeZoneCache() {
+ sTimeZoneCache.clear();
+ }
+
+ static void putRuleIntoTimeZoneInformation(byte[] bytes, int offset, RRule rrule, int hour,
+ int minute) {
+ // MSFT months are 1 based, same as RRule
+ setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, rrule.month);
+ // MSFT day of week starts w/ Sunday = 0; RRule starts w/ Sunday = 1
+ setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, rrule.dayOfWeek - 1);
+ // 5 means "last" in MSFT land; for RRule, it's -1
+ setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, rrule.week < 0 ? 5 : rrule.week);
+ // Turn hours/minutes into ms from midnight (per TimeZone)
+ setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, hour);
+ setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, minute);
+ }
+
+ // Write a transition time into SYSTEMTIME data (via an offset into a byte array)
+ static void putTransitionMillisIntoSystemTime(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, getTrueTransitionHour(cal));
+ setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, getTrueTransitionMinute(cal));
+ }
+
+ // 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;
+ }
+
+ /**
+ * 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 long getMillisAtTimeZoneDateTransition(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);
+ testCalendar.set(GregorianCalendar.SECOND, 0);
+ return testCalendar.getTimeInMillis();
+ }
+
+ /**
+ * Return a GregorianCalendar representing the first standard/daylight transition between a
+ * start time and an end time in the given time zone
+ * @param tz a TimeZone the time zone in which we're looking for transitions
+ * @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 a GregorianCalendar representing the transition or null if none
+ */
+ static GregorianCalendar findTransitionDate(TimeZone tz, long startTime,
+ long endTime, boolean startInDaylightTime) {
+ long startingEndTime = endTime;
+ Date date = null;
+
+ // We'll keep splitting the difference until we're within a minute
+ while ((endTime - startTime) > MINUTES) {
+ long checkTime = ((startTime + endTime) / 2) + 1;
+ date = new Date(checkTime);
+ boolean inDaylightTime = tz.inDaylightTime(date);
+ if (inDaylightTime != startInDaylightTime) {
+ endTime = checkTime;
+ } else {
+ startTime = checkTime;
+ }
+ }
+
+ // If these are the same, we're really messed up; return null
+ if (endTime == startingEndTime) {
+ return null;
+ }
+
+ // Set up our calendar and return it
+ GregorianCalendar calendar = new GregorianCalendar(tz);
+ calendar.setTimeInMillis(startTime);
+ return calendar;
+ }
+
+ /**
+ * 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) {
+ ExchangeService.log(TAG, "TZI string for " + tz.getDisplayName() +
+ " found in cache.");
+ }
+ return tziString;
+ }
+ tziString = timeZoneToTziStringImpl(tz);
+ sTziStringCache.put(tz, tziString);
+ return tziString;
+ }
+
+ /**
+ * A class for storing RRULE information. The RRULE members can be accessed individually or
+ * an RRULE string can be created with toString()
+ */
+ static class RRule {
+ static final int RRULE_NONE = 0;
+ static final int RRULE_DAY_WEEK = 1;
+ static final int RRULE_DATE = 2;
+
+ int type;
+ int dayOfWeek;
+ int week;
+ int month;
+ int date;
+
+ /**
+ * Create an RRULE based on month and date
+ * @param _month the month (1 = JAN, 12 = DEC)
+ * @param _date the date in the month (1-31)
+ */
+ RRule(int _month, int _date) {
+ type = RRULE_DATE;
+ month = _month;
+ date = _date;
+ }
+
+ /**
+ * Create an RRULE based on month, day of week, and week #
+ * @param _month the month (1 = JAN, 12 = DEC)
+ * @param _dayOfWeek the day of the week (1 = SU, 7 = SA)
+ * @param _week the week in the month (1-5 or -1 for last)
+ */
+ RRule(int _month, int _dayOfWeek, int _week) {
+ type = RRULE_DAY_WEEK;
+ month = _month;
+ dayOfWeek = _dayOfWeek;
+ week = _week;
+ }
+
+ @Override
+ public String toString() {
+ if (type == RRULE_DAY_WEEK) {
+ return "FREQ=YEARLY;BYMONTH=" + month + ";BYDAY=" + week +
+ sDayTokens[dayOfWeek - 1];
+ } else {
+ return "FREQ=YEARLY;BYMONTH=" + month + ";BYMONTHDAY=" + date;
+ }
+ }
+ }
+
+ /**
+ * Generate an RRULE string for an array of GregorianCalendars, if possible. For now, we are
+ * only looking for rules based on the same date in a month or a specific instance of a day of
+ * the week in a month (e.g. 2nd Tuesday or last Friday). Indeed, these are the only kinds of
+ * rules used in the current tzinfo database.
+ * @param calendars an array of GregorianCalendar, set to a series of transition times in
+ * consecutive years starting with the current year
+ * @return an RRULE or null if none could be inferred from the calendars
+ */
+ static RRule inferRRuleFromCalendars(GregorianCalendar[] calendars) {
+ // Let's see if we can make a rule about these
+ GregorianCalendar calendar = calendars[0];
+ if (calendar == null) return null;
+ int month = calendar.get(Calendar.MONTH);
+ int date = calendar.get(Calendar.DAY_OF_MONTH);
+ int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
+ int week = calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH);
+ int maxWeek = calendar.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH);
+ boolean dateRule = false;
+ boolean dayOfWeekRule = false;
+ for (int i = 1; i < calendars.length; i++) {
+ GregorianCalendar cal = calendars[i];
+ if (cal == null) return null;
+ // If it's not the same month, there's no rule
+ if (cal.get(Calendar.MONTH) != month) {
+ return null;
+ } else if (dayOfWeek == cal.get(Calendar.DAY_OF_WEEK)) {
+ // Ok, it seems to be the same day of the week
+ if (dateRule) {
+ return null;
+ }
+ dayOfWeekRule = true;
+ int thisWeek = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
+ if (week != thisWeek) {
+ if (week < 0 || week == maxWeek) {
+ int thisMaxWeek = cal.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH);
+ if (thisWeek == thisMaxWeek) {
+ // We'll use -1 (i.e. last) week
+ week = -1;
+ continue;
+ }
+ }
+ return null;
+ }
+ } else if (date == cal.get(Calendar.DAY_OF_MONTH)) {
+ // Maybe the same day of the month?
+ if (dayOfWeekRule) {
+ return null;
+ }
+ dateRule = true;
+ } else {
+ return null;
+ }
+ }
+
+ if (dateRule) {
+ return new RRule(month + 1, date);
+ }
+ // sDayTokens is 0 based (SU = 0); Calendar days of week are 1 based (SU = 1)
+ // iCalendar months are 1 based; Calendar months are 0 based
+ // So we adjust these when building the string
+ return new RRule(month + 1, dayOfWeek, week);
+ }
+
+ /**
+ * Generate an rfc2445 utcOffset from minutes offset from GMT
+ * These look like +0800 or -0100
+ * @param offsetMinutes minutes offset from GMT (east is positive, west is negative
+ * @return a utcOffset
+ */
+ static String utcOffsetString(int offsetMinutes) {
+ StringBuilder sb = new StringBuilder();
+ int hours = offsetMinutes / 60;
+ if (hours < 0) {
+ sb.append('-');
+ hours = 0 - hours;
+ } else {
+ sb.append('+');
+ }
+ int minutes = offsetMinutes % 60;
+ if (hours < 10) {
+ sb.append('0');
+ }
+ sb.append(hours);
+ if (minutes < 10) {
+ sb.append('0');
+ }
+ sb.append(minutes);
+ return sb.toString();
+ }
+
+ /**
+ * Fill the passed in GregorianCalendars arrays with DST transition information for this and
+ * the following years (based on the length of the arrays)
+ * @param tz the time zone
+ * @param toDaylightCalendars an array of GregorianCalendars, one for each year, representing
+ * the transition to daylight time
+ * @param toStandardCalendars an array of GregorianCalendars, one for each year, representing
+ * the transition to standard time
+ * @return true if transitions could be found for all years, false otherwise
+ */
+ static boolean getDSTCalendars(TimeZone tz, GregorianCalendar[] toDaylightCalendars,
+ GregorianCalendar[] toStandardCalendars) {
+ // We'll use the length of the arrays to determine how many years to check
+ int maxYears = toDaylightCalendars.length;
+ if (toStandardCalendars.length != maxYears) {
+ return false;
+ }
+ // Get the transitions for this year and the next few years
+ for (int i = 0; i < maxYears; i++) {
+ GregorianCalendar cal = new GregorianCalendar(tz);
+ cal.set(sCurrentYear + i, Calendar.JANUARY, 1, 0, 0, 0);
+ long startTime = cal.getTimeInMillis();
+ // Calculate end of year; no need to be insanely precise
+ long endOfYearTime = startTime + (365*DAYS) + (DAYS>>2);
+ Date date = new Date(startTime);
+ boolean startInDaylightTime = tz.inDaylightTime(date);
+ // Find the first transition, and store
+ cal = findTransitionDate(tz, startTime, endOfYearTime, startInDaylightTime);
+ if (cal == null) {
+ return false;
+ } else if (startInDaylightTime) {
+ toStandardCalendars[i] = cal;
+ } else {
+ toDaylightCalendars[i] = cal;
+ }
+ // Find the second transition, and store
+ cal = findTransitionDate(tz, startTime, endOfYearTime, !startInDaylightTime);
+ if (cal == null) {
+ return false;
+ } else if (startInDaylightTime) {
+ toDaylightCalendars[i] = cal;
+ } else {
+ toStandardCalendars[i] = cal;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Write out the STANDARD block of VTIMEZONE and end the VTIMEZONE
+ * @param writer the SimpleIcsWriter we're using
+ * @param tz the time zone
+ * @param offsetString the offset string in VTIMEZONE format (e.g. +0800)
+ * @throws IOException
+ */
+ static private void writeNoDST(SimpleIcsWriter writer, TimeZone tz, String offsetString)
+ throws IOException {
+ writer.writeTag("BEGIN", "STANDARD");
+ writer.writeTag("TZOFFSETFROM", offsetString);
+ writer.writeTag("TZOFFSETTO", offsetString);
+ // Might as well use start of epoch for start date
+ writer.writeTag("DTSTART", millisToEasDateTime(0L));
+ writer.writeTag("END", "STANDARD");
+ writer.writeTag("END", "VTIMEZONE");
+ }
+
+ /** Write a VTIMEZONE block for a given TimeZone into a SimpleIcsWriter
+ * @param tz the TimeZone to be used in the conversion
+ * @param writer the SimpleIcsWriter to be used
+ * @throws IOException
+ */
+ static void timeZoneToVTimezone(TimeZone tz, SimpleIcsWriter writer)
+ throws IOException {
+ // We'll use these regardless of whether there's DST in this time zone or not
+ int rawOffsetMinutes = tz.getRawOffset() / MINUTES;
+ String standardOffsetString = utcOffsetString(rawOffsetMinutes);
+
+ // Preamble for all of our VTIMEZONEs
+ writer.writeTag("BEGIN", "VTIMEZONE");
+ writer.writeTag("TZID", tz.getID());
+ writer.writeTag("X-LIC-LOCATION", tz.getDisplayName());
+
+ // Simplest case is no daylight time
+ if (!tz.useDaylightTime()) {
+ writeNoDST(writer, tz, standardOffsetString);
+ return;
+ }
+
+ int maxYears = 3;
+ GregorianCalendar[] toDaylightCalendars = new GregorianCalendar[maxYears];
+ GregorianCalendar[] toStandardCalendars = new GregorianCalendar[maxYears];
+ if (!getDSTCalendars(tz, toDaylightCalendars, toStandardCalendars)) {
+ writeNoDST(writer, tz, standardOffsetString);
+ return;
+ }
+ // Try to find a rule to cover these yeras
+ RRule daylightRule = inferRRuleFromCalendars(toDaylightCalendars);
+ RRule standardRule = inferRRuleFromCalendars(toStandardCalendars);
+ String daylightOffsetString =
+ utcOffsetString(rawOffsetMinutes + (tz.getDSTSavings() / MINUTES));
+ // We'll use RRULE's if we found both
+ // Otherwise we write the first as DTSTART and the others as RDATE
+ boolean hasRule = daylightRule != null && standardRule != null;
+
+ // Write the DAYLIGHT block
+ writer.writeTag("BEGIN", "DAYLIGHT");
+ writer.writeTag("TZOFFSETFROM", standardOffsetString);
+ writer.writeTag("TZOFFSETTO", daylightOffsetString);
+ writer.writeTag("DTSTART",
+ transitionMillisToVCalendarTime(
+ toDaylightCalendars[0].getTimeInMillis(), tz, true));
+ if (hasRule) {
+ writer.writeTag("RRULE", daylightRule.toString());
+ } else {
+ for (int i = 1; i < maxYears; i++) {
+ writer.writeTag("RDATE", transitionMillisToVCalendarTime(
+ toDaylightCalendars[i].getTimeInMillis(), tz, true));
+ }
+ }
+ writer.writeTag("END", "DAYLIGHT");
+ // Write the STANDARD block
+ writer.writeTag("BEGIN", "STANDARD");
+ writer.writeTag("TZOFFSETFROM", daylightOffsetString);
+ writer.writeTag("TZOFFSETTO", standardOffsetString);
+ writer.writeTag("DTSTART",
+ transitionMillisToVCalendarTime(
+ toStandardCalendars[0].getTimeInMillis(), tz, false));
+ if (hasRule) {
+ writer.writeTag("RRULE", standardRule.toString());
+ } else {
+ for (int i = 1; i < maxYears; i++) {
+ writer.writeTag("RDATE", transitionMillisToVCalendarTime(
+ toStandardCalendars[i].getTimeInMillis(), tz, true));
+ }
+ }
+ writer.writeTag("END", "STANDARD");
+ // And we're done
+ writer.writeTag("END", "VTIMEZONE");
+ }
+
+ /**
+ * Find the next transition to occur (i.e. after the current date/time)
+ * @param transitions calendars representing transitions to/from DST
+ * @return millis for the first transition after the current date/time
+ */
+ static long findNextTransition(long startingMillis, GregorianCalendar[] transitions) {
+ for (GregorianCalendar transition: transitions) {
+ long transitionMillis = transition.getTimeInMillis();
+ if (transitionMillis > startingMillis) {
+ return transitionMillis;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * 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 String timeZoneToTziStringImpl(TimeZone tz) {
+ String tziString;
+ 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 more work
+ if (tz.useDaylightTime()) {
+ GregorianCalendar[] toDaylightCalendars = new GregorianCalendar[3];
+ GregorianCalendar[] toStandardCalendars = new GregorianCalendar[3];
+ // See if we can get transitions for a few years; if not, we can't generate DST info
+ // for this time zone
+ if (getDSTCalendars(tz, toDaylightCalendars, toStandardCalendars)) {
+ // Try to find a rule to cover these years
+ RRule daylightRule = inferRRuleFromCalendars(toDaylightCalendars);
+ RRule standardRule = inferRRuleFromCalendars(toStandardCalendars);
+ if ((daylightRule != null) && (daylightRule.type == RRule.RRULE_DAY_WEEK) &&
+ (standardRule != null) && (standardRule.type == RRule.RRULE_DAY_WEEK)) {
+ // We need both rules and they have to be DAY/WEEK type
+ // Write month, day of week, week, hour, minute
+ putRuleIntoTimeZoneInformation(tziBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET,
+ standardRule,
+ getTrueTransitionHour(toStandardCalendars[0]),
+ getTrueTransitionMinute(toStandardCalendars[0]));
+ putRuleIntoTimeZoneInformation(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET,
+ daylightRule,
+ getTrueTransitionHour(toDaylightCalendars[0]),
+ getTrueTransitionMinute(toDaylightCalendars[0]));
+ } else {
+ // If there's no rule, we'll use the first transition to standard/to daylight
+ // And indicate that it's just for this year...
+ long now = System.currentTimeMillis();
+ long standardTransition = findNextTransition(now, toStandardCalendars);
+ long daylightTransition = findNextTransition(now, toDaylightCalendars);
+ // If we can't find transitions, we can't do DST
+ if (standardTransition != 0 && daylightTransition != 0) {
+ putTransitionMillisIntoSystemTime(tziBytes,
+ MSFT_TIME_ZONE_STANDARD_DATE_OFFSET, standardTransition);
+ putTransitionMillisIntoSystemTime(tziBytes,
+ MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET, daylightTransition);
+ }
+ }
+ }
+ int dstOffset = tz.getDSTSavings();
+ setLong(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET, - dstOffset / MINUTES);
+ }
+ byte[] tziEncodedBytes = Base64.encode(tziBytes, Base64.NO_WRAP);
+ tziString = new String(tziEncodedBytes);
+ 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
+ * @param precision the number of milliseconds of precision in TimeZone determination
+ * @return the TimeZone, or TimeZone.getDefault() if not found
+ */
+ @VisibleForTesting
+ static TimeZone tziStringToTimeZone(String timeZoneString, int precision) {
+ // If we have this time zone cached, use that value and return
+ TimeZone timeZone = sTimeZoneCache.get(timeZoneString);
+ if (timeZone != null) {
+ if (Eas.USER_LOG) {
+ ExchangeService.log(TAG, " Using cached TimeZone " + timeZone.getID());
+ }
+ } else {
+ timeZone = tziStringToTimeZoneImpl(timeZoneString, precision);
+ if (timeZone == null) {
+ // If we don't find a match, we just return the current TimeZone. In theory, this
+ // shouldn't be happening...
+ ExchangeService.alwaysLog("TimeZone not found using default: " + timeZoneString);
+ timeZone = TimeZone.getDefault();
+ }
+ sTimeZoneCache.put(timeZoneString, timeZone);
+ }
+ return timeZone;
+ }
+
+ /**
+ * The standard entry to EAS time zone conversion, using one minute as the precision
+ */
+ static public TimeZone tziStringToTimeZone(String timeZoneString) {
+ return tziStringToTimeZone(timeZoneString, MINUTES);
+ }
+
+ /**
+ * 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. If the test time zone string includes DST and
+ * we don't find a match, and we're using standard precision, we try again with lenient
+ * precision, which is a bit better than guessing
+ * @param timeZoneString the String read from the server
+ * @return the TimeZone, or null if not found
+ */
+ static TimeZone tziStringToTimeZoneImpl(String timeZoneString, int precision) {
+ TimeZone timeZone = null;
+ // First, we need to decode the base64 string
+ byte[] timeZoneBytes = Base64.decode(timeZoneString, Base64.DEFAULT);
+
+ // 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) {
+ // If the default time zone is a match
+ TimeZone defaultTimeZone = TimeZone.getDefault();
+ if (!defaultTimeZone.useDaylightTime() &&
+ ArrayUtils.contains(zoneIds, defaultTimeZone.getID())) {
+ if (Eas.USER_LOG) {
+ ExchangeService.log(TAG, "TimeZone without DST found to be default: " +
+ defaultTimeZone.getID());
+ }
+ return defaultTimeZone;
+ }
+ // In this case, there is no daylight savings time, so the only interesting data
+ // for possible matches is the offset and DST availability; we'll take the first
+ // match for those
+ for (String zoneId: zoneIds) {
+ timeZone = TimeZone.getTimeZone(zoneId);
+ if (!timeZone.useDaylightTime()) {
+ if (Eas.USER_LOG) {
+ ExchangeService.log(TAG, "TimeZone without DST found by offset: " +
+ timeZone.getID());
+ }
+ return timeZone;
+ }
+ }
+ // None found, return null
+ return null;
+ } 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.)
+
+ // Check one minute before and after DST start transition
+ long millisAtTransition = getMillisAtTimeZoneDateTransition(timeZone, dstStart);
+ Date before = new Date(millisAtTransition - precision);
+ Date after = new Date(millisAtTransition + precision);
+ if (timeZone.inDaylightTime(before)) continue;
+ if (!timeZone.inDaylightTime(after)) continue;
+
+ // Check one minute before and after DST end transition
+ millisAtTransition = getMillisAtTimeZoneDateTransition(timeZone, dstEnd);
+ // Note that we need to subtract an extra hour here, because we end up with
+ // gaining an hour in the transition BACK to standard time
+ before = new Date(millisAtTransition - (dstSavings + precision));
+ after = new Date(millisAtTransition + precision);
+ if (!timeZone.inDaylightTime(before)) continue;
+ if (timeZone.inDaylightTime(after)) continue;
+
+ // Check that the savings are the same
+ if (dstSavings != timeZone.getDSTSavings()) continue;
+ return timeZone;
+ }
+ // 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
+ boolean lenient = false;
+ if ((dstStart.hour != dstEnd.hour) && (precision == STANDARD_DST_PRECISION)) {
+ timeZone = tziStringToTimeZoneImpl(timeZoneString, LENIENT_DST_PRECISION);
+ lenient = true;
+ } else {
+ timeZone = TimeZone.getTimeZone(zoneIds[0]);
+ }
+ if (Eas.USER_LOG) {
+ ExchangeService.log(TAG,
+ "No TimeZone with correct DST settings; using " +
+ (lenient ? "lenient" : "first") + ": " + timeZone.getID());
+ }
+ return timeZone;
+ }
+ }
+ return null;
+ }
+
+ static public String convertEmailDateTimeToCalendarDateTime(String date) {
+ // Format for email date strings is 2010-02-23T16:00:00.000Z
+ // Format for calendar date strings is 20100223T160000Z
+ return date.substring(0, 4) + date.substring(5, 7) + date.substring(8, 13) +
+ date.substring(14, 16) + date.substring(17, 19) + 'Z';
+ }
+
+ 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, true);
+ }
+
+ /**
+ * Generate an EAS formatted local date/time string from a time and a time zone. If the final
+ * argument is false, only a date will be returned (e.g. 20100331)
+ * @param millis a time in milliseconds
+ * @param tz a time zone
+ * @param withTime if the time is to be included in the string
+ * @return an EAS formatted string indicating the date (and time) in the given time zone
+ */
+ static public String millisToEasDateTime(long millis, TimeZone tz, boolean withTime) {
+ 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)));
+ if (withTime) {
+ 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)));
+ if (tz == sGmtTimeZone) {
+ sb.append('Z');
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Return the true minute at which a transition occurs
+ * Our transition time should be the in the minute BEFORE the transition
+ * If this minute is 59, set minute to 0 and increment the hour
+ * NOTE: We don't want to add a minute and retrieve minute/hour from the Calendar, because
+ * Calendar time will itself be influenced by the transition! So adding 1 minute to
+ * 01:59 (assume PST->PDT) will become 03:00, which isn't what we want (we want 02:00)
+ *
+ * @param calendar the calendar holding the transition date/time
+ * @return the true minute of the transition
+ */
+ static int getTrueTransitionMinute(GregorianCalendar calendar) {
+ int minute = calendar.get(Calendar.MINUTE);
+ if (minute == 59) {
+ minute = 0;
+ }
+ return minute;
+ }
+
+ /**
+ * Return the true hour at which a transition occurs
+ * See description for getTrueTransitionMinute, above
+ * @param calendar the calendar holding the transition date/time
+ * @return the true hour of the transition
+ */
+ static int getTrueTransitionHour(GregorianCalendar calendar) {
+ int hour = calendar.get(Calendar.HOUR_OF_DAY);
+ hour++;
+ if (hour == 24) {
+ hour = 0;
+ }
+ return hour;
+ }
+
+ /**
+ * Generate a date/time string suitable for VTIMEZONE from a transition time in millis
+ * The format is YYYYMMDDTHHMMSS
+ * @param millis a transition time in milliseconds
+ * @param tz a time zone
+ * @param dst whether we're entering daylight time
+ */
+ static String transitionMillisToVCalendarTime(long millis, TimeZone tz, boolean dst) {
+ 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(getTrueTransitionHour(cal)));
+ sb.append(formatTwo(getTrueTransitionMinute(cal)));
+ sb.append(formatTwo(0));
+ return sb.toString();
+ }
+
+ /**
+ * Returns a UTC calendar with year/month/day from local calendar and h/m/s/ms = 0
+ * @param time the time in seconds of an all-day event in local time
+ * @return the time in seconds in UTC
+ */
+ static public long getUtcAllDayCalendarTime(long time, TimeZone localTimeZone) {
+ return transposeAllDayTime(time, localTimeZone, UTC_TIMEZONE);
+ }
+
+ /**
+ * Returns a local calendar with year/month/day from UTC calendar and h/m/s/ms = 0
+ * @param time the time in seconds of an all-day event in UTC
+ * @return the time in seconds in local time
+ */
+ static public long getLocalAllDayCalendarTime(long time, TimeZone localTimeZone) {
+ return transposeAllDayTime(time, UTC_TIMEZONE, localTimeZone);
+ }
+
+ static private long transposeAllDayTime(long time, TimeZone fromTimeZone,
+ TimeZone toTimeZone) {
+ GregorianCalendar fromCalendar = new GregorianCalendar(fromTimeZone);
+ fromCalendar.setTimeInMillis(time);
+ GregorianCalendar toCalendar = new GregorianCalendar(toTimeZone);
+ // Set this calendar with correct year, month, and day, but zero hour, minute, and seconds
+ toCalendar.set(fromCalendar.get(GregorianCalendar.YEAR),
+ fromCalendar.get(GregorianCalendar.MONTH),
+ fromCalendar.get(GregorianCalendar.DATE), 0, 0, 0);
+ toCalendar.set(GregorianCalendar.MILLISECOND, 0);
+ return toCalendar.getTimeInMillis();
+ }
+
+ 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 addBySetpos(StringBuilder rrule, int dow, int wom) {
+ // Indicate the days, but don't use wom in this case (it's used in the BYSETPOS);
+ addByDay(rrule, dow, 0);
+ rrule.append(";BYSETPOS=");
+ rrule.append(wom == 5 ? "-1" : wom);
+ }
+
+ 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 ((c == ';') || (end == len)) {
+ if (end == len) end++;
+ return rrule.substring(start, end -1);
+ }
+ } while (true);
+ }
+
+ /**
+ * Reformat an RRULE style UNTIL to an EAS style until
+ */
+ @VisibleForTesting
+ static String recurrenceUntilToEasUntil(String until) {
+ // Get a calendar in our local time zone
+ GregorianCalendar localCalendar = new GregorianCalendar(TimeZone.getDefault());
+ // Set the time per GMT time in the 'until'
+ localCalendar.setTimeInMillis(Utility.parseDateTimeToMillis(until));
+ StringBuilder sb = new StringBuilder();
+ // Build a string with local year/month/date
+ sb.append(localCalendar.get(Calendar.YEAR));
+ sb.append(formatTwo(localCalendar.get(Calendar.MONTH) + 1));
+ sb.append(formatTwo(localCalendar.get(Calendar.DAY_OF_MONTH)));
+ // EAS ignores the time in 'until'; go figure
+ sb.append("T000000Z");
+ return sb.toString();
+ }
+
+ /**
+ * Convenience method to add "count", "interval", and "until" to an EAS calendar stream
+ * According to EAS docs, OCCURRENCES must always come before INTERVAL
+ */
+ static private void addCountIntervalAndUntil(String rrule, Serializer s) throws IOException {
+ String count = tokenFromRrule(rrule, "COUNT=");
+ if (count != null) {
+ s.data(Tags.CALENDAR_RECURRENCE_OCCURRENCES, count);
+ }
+ String interval = tokenFromRrule(rrule, "INTERVAL=");
+ if (interval != null) {
+ s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, interval);
+ }
+ String until = tokenFromRrule(rrule, "UNTIL=");
+ if (until != null) {
+ s.data(Tags.CALENDAR_RECURRENCE_UNTIL, recurrenceUntilToEasUntil(until));
+ }
+ }
+
+ static private void addByDay(String byDay, Serializer s) throws IOException {
+ // This can be 1WE (1st Wednesday) or -1FR (last Friday)
+ int weekOfMonth = byDay.charAt(0);
+ String bareByDay;
+ if (weekOfMonth == '-') {
+ // -1 is the only legal case (last week) Use "5" for EAS
+ weekOfMonth = 5;
+ bareByDay = byDay.substring(2);
+ } else {
+ weekOfMonth = weekOfMonth - '0';
+ bareByDay = byDay.substring(1);
+ }
+ s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(weekOfMonth));
+ s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(bareByDay));
+ }
+
+ static private void addByDaySetpos(String byDay, String bySetpos, Serializer s)
+ throws IOException {
+ int weekOfMonth = bySetpos.charAt(0);
+ if (weekOfMonth == '-') {
+ // -1 is the only legal case (last week) Use "5" for EAS
+ weekOfMonth = 5;
+ } else {
+ weekOfMonth = weekOfMonth - '0';
+ }
+ s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(weekOfMonth));
+ s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay));
+ }
+
+ /**
+ * 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 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 {
+ if (Eas.USER_LOG) {
+ ExchangeService.log(TAG, "RRULE: " + 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");
+ addCountIntervalAndUntil(rrule, s);
+ s.end();
+ } else if (freq.equals("WEEKLY")) {
+ s.start(Tags.CALENDAR_RECURRENCE);
+ s.data(Tags.CALENDAR_RECURRENCE_TYPE, "1");
+ // Requires a day of week (whereas RRULE does not)
+ addCountIntervalAndUntil(rrule, s);
+ String byDay = tokenFromRrule(rrule, "BYDAY=");
+ if (byDay != null) {
+ s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay));
+ // Find week number (1-4 and 5 for last)
+ if (byDay.startsWith("-1")) {
+ s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, "5");
+ } else {
+ char c = byDay.charAt(0);
+ if (c >= '1' && c <= '4') {
+ s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, byDay.substring(0, 1));
+ }
+ }
+ }
+ s.end();
+ } else if (freq.equals("MONTHLY")) {
+ String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
+ if (byMonthDay != null) {
+ s.start(Tags.CALENDAR_RECURRENCE);
+ // Special case for last day of month
+ if (byMonthDay == "-1") {
+ s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3");
+ addCountIntervalAndUntil(rrule, s);
+ s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, "127");
+ } else {
+ // The nth day of the month
+ s.data(Tags.CALENDAR_RECURRENCE_TYPE, "2");
+ addCountIntervalAndUntil(rrule, s);
+ s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
+ }
+ s.end();
+ } else {
+ String byDay = tokenFromRrule(rrule, "BYDAY=");
+ String bySetpos = tokenFromRrule(rrule, "BYSETPOS=");
+ if (byDay != null) {
+ s.start(Tags.CALENDAR_RECURRENCE);
+ s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3");
+ addCountIntervalAndUntil(rrule, s);
+ if (bySetpos != null) {
+ addByDaySetpos(byDay, bySetpos, s);
+ } else {
+ addByDay(byDay, s);
+ }
+ s.end();
+ }
+ }
+ } else if (freq.equals("YEARLY")) {
+ String byMonth = tokenFromRrule(rrule, "BYMONTH=");
+ String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
+ String byDay = tokenFromRrule(rrule, "BYDAY=");
+ 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));
+ }
+ if (byMonth != null && (byMonthDay != null || byDay != null)) {
+ s.start(Tags.CALENDAR_RECURRENCE);
+ s.data(Tags.CALENDAR_RECURRENCE_TYPE, byDay == null ? "5" : "6");
+ addCountIntervalAndUntil(rrule, s);
+ s.data(Tags.CALENDAR_RECURRENCE_MONTHOFYEAR, byMonth);
+ // Note that both byMonthDay and byDay can't be true in a valid RRULE
+ if (byMonthDay != null) {
+ s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
+ } else {
+ addByDay(byDay, s);
+ }
+ 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 (occurrences > 0) {
+ rrule.append(";COUNT=" + occurrences);
+ }
+ if (interval > 0) {
+ rrule.append(";INTERVAL=" + interval);
+ }
+
+ // Days, weeks, months, etc.
+ switch(type) {
+ case 0: // DAILY
+ case 1: // WEEKLY
+ if (dow > 0) addByDay(rrule, dow, wom);
+ break;
+ case 2: // MONTHLY
+ if (dom > 0) addByMonthDay(rrule, dom);
+ break;
+ case 3: // MONTHLY (on the nth day)
+ // 127 is a special case meaning "last day of the month"
+ if (dow == 127) {
+ rrule.append(";BYMONTHDAY=-1");
+ // week 5 and dow = weekdays -> last weekday (need BYSETPOS)
+ } else if (wom == 5 && (dow == EAS_WEEKDAYS || dow == EAS_WEEKENDS)) {
+ addBySetpos(rrule, dow, wom);
+ } else if (dow > 0) addByDay(rrule, dow, wom);
+ break;
+ case 5: // YEARLY (specific day)
+ if (dom > 0) addByMonthDay(rrule, dom);
+ if (moy > 0) {
+ rrule.append(";BYMONTH=" + moy);
+ }
+ break;
+ case 6: // YEARLY
+ if (dow > 0) addByDay(rrule, dow, wom);
+ if (dom > 0) addByMonthDay(rrule, dom);
+ if (moy > 0) {
+ rrule.append(";BYMONTH=" + moy);
+ }
+ break;
+ default:
+ break;
+ }
+
+ // UNTIL comes last
+ if (until != null) {
+ rrule.append(";UNTIL=" + until);
+ }
+
+ if (Eas.USER_LOG) {
+ Log.d(Logging.LOG_TAG, "Created rrule: " + rrule);
+ }
+ return rrule.toString();
+ }
+
+ /**
+ * Create a Calendar in CalendarProvider to which synced Events will be linked
+ * @param service the sync service requesting Calendar creation
+ * @param account the account being synced
+ * @param mailbox the Exchange mailbox for the calendar
+ * @return the unique id of the Calendar
+ */
+ static public long createCalendar(EasSyncService service, Account account, Mailbox mailbox) {
+ // Create a Calendar object
+ ContentValues cv = new ContentValues();
+ // TODO How will this change if the user changes his account display name?
+ cv.put(Calendars.CALENDAR_DISPLAY_NAME, account.mDisplayName);
+ cv.put(Calendars.ACCOUNT_NAME, account.mEmailAddress);
+ cv.put(Calendars.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ cv.put(Calendars.SYNC_EVENTS, 1);
+ cv.put(Calendars.VISIBLE, 1);
+ // Don't show attendee status if we're the organizer
+ cv.put(Calendars.CAN_ORGANIZER_RESPOND, 0);
+ cv.put(Calendars.CAN_MODIFY_TIME_ZONE, 0);
+ cv.put(Calendars.MAX_REMINDERS, 1);
+ cv.put(Calendars.ALLOWED_REMINDERS, ALLOWED_REMINDER_TYPES);
+ cv.put(Calendars.ALLOWED_ATTENDEE_TYPES, ALLOWED_ATTENDEE_TYPES);
+ cv.put(Calendars.ALLOWED_AVAILABILITY, ALLOWED_AVAILABILITIES);
+
+ // TODO Coordinate account colors w/ Calendar, if possible
+ int color = new AccountServiceProxy(service.mContext).getAccountColor(account.mId);
+ cv.put(Calendars.CALENDAR_COLOR, color);
+ cv.put(Calendars.CALENDAR_TIME_ZONE, Time.getCurrentTimezone());
+ cv.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
+ cv.put(Calendars.OWNER_ACCOUNT, account.mEmailAddress);
+
+ Uri uri = service.mContentResolver.insert(
+ asSyncAdapter(Calendars.CONTENT_URI, account.mEmailAddress,
+ Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv);
+ // We save the id of the calendar into mSyncStatus
+ if (uri != null) {
+ String stringId = uri.getPathSegments().get(1);
+ mailbox.mSyncStatus = stringId;
+ return Long.parseLong(stringId);
+ }
+ return -1;
+ }
+
+ static Uri asSyncAdapter(Uri uri, String account, String accountType) {
+ return uri.buildUpon()
+ .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER,
+ "true")
+ .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
+ .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
+ }
+
+ /**
+ * Return the uid for an event based on its globalObjId
+ * @param globalObjId the base64 encoded String provided by EAS
+ * @return the uid for the calendar event
+ */
+ static public String getUidFromGlobalObjId(String globalObjId) {
+ StringBuilder sb = new StringBuilder();
+ // First get the decoded base64
+ try {
+ byte[] idBytes = Base64.decode(globalObjId, Base64.DEFAULT);
+ String idString = new String(idBytes);
+ // If the base64 decoded string contains the magic substring: "vCal-Uid", then
+ // the actual uid is hidden within; the magic substring is never at the start of the
+ // decoded base64
+ int index = idString.indexOf("vCal-Uid");
+ if (index > 0) {
+ // The uid starts after "vCal-Uidxxxx", where xxxx are padding
+ // characters. And it ends before the last character, which is ascii 0
+ return idString.substring(index + 12, idString.length() - 1);
+ } else {
+ // This is an EAS uid. Go through the bytes and write out the hex
+ // values as characters; this is what we'll need to pass back to EAS
+ // when responding to the invitation
+ for (byte b: idBytes) {
+ Utility.byteToHex(sb, b);
+ }
+ return sb.toString();
+ }
+ } catch (RuntimeException e) {
+ // In the worst of cases (bad format, etc.), we can always return the input
+ return globalObjId;
+ }
+ }
+
+ /**
+ * Get a selfAttendeeStatus from a busy status
+ * The default here is NONE (i.e. we don't know the status)
+ * Note that a busy status of FREE must mean NONE as well, since it can't mean declined
+ * (there would be no event)
+ * @param busyStatus the busy status, from EAS
+ * @return the corresponding value for selfAttendeeStatus
+ */
+ static public int attendeeStatusFromBusyStatus(int busyStatus) {
+ int attendeeStatus;
+ switch (busyStatus) {
+ case BUSY_STATUS_BUSY:
+ attendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED;
+ break;
+ case BUSY_STATUS_TENTATIVE:
+ attendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE;
+ break;
+ case BUSY_STATUS_FREE:
+ case BUSY_STATUS_OUT_OF_OFFICE:
+ default:
+ attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
+ }
+ return attendeeStatus;
+ }
+
+ /**
+ * Get a selfAttendeeStatus from a response type (EAS 14+)
+ * The default here is NONE (i.e. we don't know the status), though in theory this can't happen
+ * @param busyStatus the response status, from EAS
+ * @return the corresponding value for selfAttendeeStatus
+ */
+ static public int attendeeStatusFromResponseType(int responseType) {
+ int attendeeStatus;
+ switch (responseType) {
+ case RESPONSE_TYPE_NOT_RESPONDED:
+ attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
+ break;
+ case RESPONSE_TYPE_ACCEPTED:
+ attendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED;
+ break;
+ case RESPONSE_TYPE_TENTATIVE:
+ attendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE;
+ break;
+ case RESPONSE_TYPE_DECLINED:
+ attendeeStatus = Attendees.ATTENDEE_STATUS_DECLINED;
+ break;
+ default:
+ attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
+ }
+ return attendeeStatus;
+ }
+
+ /** Get a busy status from a selfAttendeeStatus
+ * The default here is BUSY
+ * @param selfAttendeeStatus from CalendarProvider2
+ * @return the corresponding value of busy status
+ */
+ static public int busyStatusFromAttendeeStatus(int selfAttendeeStatus) {
+ int busyStatus;
+ switch (selfAttendeeStatus) {
+ case Attendees.ATTENDEE_STATUS_DECLINED:
+ case Attendees.ATTENDEE_STATUS_NONE:
+ case Attendees.ATTENDEE_STATUS_INVITED:
+ busyStatus = BUSY_STATUS_FREE;
+ break;
+ case Attendees.ATTENDEE_STATUS_TENTATIVE:
+ busyStatus = BUSY_STATUS_TENTATIVE;
+ break;
+ case Attendees.ATTENDEE_STATUS_ACCEPTED:
+ default:
+ busyStatus = BUSY_STATUS_BUSY;
+ break;
+ }
+ return busyStatus;
+ }
+
+ static public String buildMessageTextFromEntityValues(Context context,
+ ContentValues entityValues, StringBuilder sb) {
+ if (sb == null) {
+ sb = new StringBuilder();
+ }
+ Resources resources = context.getResources();
+ // TODO: Add more detail to message text
+ // Right now, we're using.. When: Tuesday, March 5th at 2:00pm
+ // What we're missing is the duration and any recurrence information. So this should be
+ // more like... When: Tuesdays, starting March 5th from 2:00pm - 3:00pm
+ // This would require code to build complex strings, and it will have to wait
+ // For now, we'll just use the meeting_recurring string
+
+ boolean allDayEvent = false;
+ if (entityValues.containsKey(Events.ALL_DAY)) {
+ Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
+ allDayEvent = (ade != null) && (ade == 1);
+ }
+ boolean recurringEvent = !entityValues.containsKey(Events.ORIGINAL_SYNC_ID) &&
+ entityValues.containsKey(Events.RRULE);
+
+ String dateTimeString;
+ int res;
+ long startTime = entityValues.getAsLong(Events.DTSTART);
+ if (allDayEvent) {
+ Date date = new Date(getLocalAllDayCalendarTime(startTime, TimeZone.getDefault()));
+ dateTimeString = DateFormat.getDateInstance().format(date);
+ res = recurringEvent ? R.string.meeting_allday_recurring : R.string.meeting_allday;
+ } else {
+ dateTimeString = DateFormat.getDateTimeInstance().format(new Date(startTime));
+ res = recurringEvent ? R.string.meeting_recurring : R.string.meeting_when;
+ }
+ sb.append(resources.getString(res, dateTimeString));
+
+ String location = null;
+ if (entityValues.containsKey(Events.EVENT_LOCATION)) {
+ location = entityValues.getAsString(Events.EVENT_LOCATION);
+ if (!TextUtils.isEmpty(location)) {
+ sb.append("\n");
+ sb.append(resources.getString(R.string.meeting_where, location));
+ }
+ }
+ // If there's a description for this event, append it
+ String desc = entityValues.getAsString(Events.DESCRIPTION);
+ if (desc != null) {
+ sb.append("\n--\n");
+ sb.append(desc);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Add an attendee to the ics attachment and the to list of the Message being composed
+ * @param ics the ics attachment writer
+ * @param toList the list of addressees for this email
+ * @param attendeeName the name of the attendee
+ * @param attendeeEmail the email address of the attendee
+ * @param messageFlag the flag indicating the action to be indicated by the message
+ * @param account the sending account of the email
+ */
+ static private void addAttendeeToMessage(SimpleIcsWriter ics, ArrayList<Address> toList,
+ String attendeeName, String attendeeEmail, int messageFlag, Account account) {
+ if ((messageFlag & Message.FLAG_OUTGOING_MEETING_REQUEST_MASK) != 0) {
+ String icalTag = ICALENDAR_ATTENDEE_INVITE;
+ if ((messageFlag & Message.FLAG_OUTGOING_MEETING_CANCEL) != 0) {
+ icalTag = ICALENDAR_ATTENDEE_CANCEL;
+ }
+ if (attendeeName != null) {
+ icalTag += ";CN=" + SimpleIcsWriter.quoteParamValue(attendeeName);
+ }
+ ics.writeTag(icalTag, "MAILTO:" + attendeeEmail);
+ toList.add(attendeeName == null ? new Address(attendeeEmail) :
+ new Address(attendeeEmail, attendeeName));
+ } else if (attendeeEmail.equalsIgnoreCase(account.mEmailAddress)) {
+ String icalTag = null;
+ switch (messageFlag) {
+ case Message.FLAG_OUTGOING_MEETING_ACCEPT:
+ icalTag = ICALENDAR_ATTENDEE_ACCEPT;
+ break;
+ case Message.FLAG_OUTGOING_MEETING_DECLINE:
+ icalTag = ICALENDAR_ATTENDEE_DECLINE;
+ break;
+ case Message.FLAG_OUTGOING_MEETING_TENTATIVE:
+ icalTag = ICALENDAR_ATTENDEE_TENTATIVE;
+ break;
+ }
+ if (icalTag != null) {
+ if (attendeeName != null) {
+ icalTag += ";CN="
+ + SimpleIcsWriter.quoteParamValue(attendeeName);
+ }
+ ics.writeTag(icalTag, "MAILTO:" + attendeeEmail);
+ }
+ }
+ }
+
+ /**
+ * Create a Message for an (Event) Entity
+ * @param entity the Entity for the Event (as might be retrieved by CalendarProvider)
+ * @param messageFlag the Message.FLAG_XXX constant indicating the type of email to be sent
+ * @param the unique id of this Event, or null if it can be retrieved from the Event
+ * @param the user's account
+ * @return a Message with many fields pre-filled (more later)
+ */
+ static public Message createMessageForEntity(Context context, Entity entity,
+ int messageFlag, String uid, Account account) {
+ return createMessageForEntity(context, entity, messageFlag, uid, account,
+ null /*specifiedAttendee*/);
+ }
+
+ static public EmailContent.Message createMessageForEntity(Context context, Entity entity,
+ int messageFlag, String uid, Account account, String specifiedAttendee) {
+ ContentValues entityValues = entity.getEntityValues();
+ ArrayList<NamedContentValues> subValues = entity.getSubValues();
+ boolean isException = entityValues.containsKey(Events.ORIGINAL_SYNC_ID);
+ boolean isReply = false;
+
+ EmailContent.Message msg = new EmailContent.Message();
+ msg.mFlags = messageFlag;
+ msg.mTimeStamp = System.currentTimeMillis();
+
+ String method;
+ if ((messageFlag & EmailContent.Message.FLAG_OUTGOING_MEETING_INVITE) != 0) {
+ method = "REQUEST";
+ } else if ((messageFlag & EmailContent.Message.FLAG_OUTGOING_MEETING_CANCEL) != 0) {
+ method = "CANCEL";
+ } else {
+ method = "REPLY";
+ isReply = true;
+ }
+
+ try {
+ // Create our iCalendar writer and start generating tags
+ SimpleIcsWriter ics = new SimpleIcsWriter();
+ ics.writeTag("BEGIN", "VCALENDAR");
+ ics.writeTag("METHOD", method);
+ ics.writeTag("PRODID", "AndroidEmail");
+ ics.writeTag("VERSION", "2.0");
+
+ // Our default vcalendar time zone is UTC, but this will change (below) if we're
+ // sending a recurring event, in which case we use local time
+ TimeZone vCalendarTimeZone = sGmtTimeZone;
+ String vCalendarDateSuffix = "";
+
+ // Check for all day event
+ boolean allDayEvent = false;
+ if (entityValues.containsKey(Events.ALL_DAY)) {
+ Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
+ allDayEvent = (ade != null) && (ade == 1);
+ if (allDayEvent) {
+ // Example: DTSTART;VALUE=DATE:20100331 (all day event)
+ vCalendarDateSuffix = ";VALUE=DATE";
+ }
+ }
+
+ // If we're inviting people and the meeting is recurring, we need to send our time zone
+ // information and make sure to send DTSTART/DTEND in local time (unless, of course,
+ // this is an all-day event). Recurring, for this purpose, includes exceptions to
+ // recurring events
+ if (!isReply && !allDayEvent &&
+ (entityValues.containsKey(Events.RRULE) ||
+ entityValues.containsKey(Events.ORIGINAL_SYNC_ID))) {
+ vCalendarTimeZone = TimeZone.getDefault();
+ // Write the VTIMEZONE block to the writer
+ timeZoneToVTimezone(vCalendarTimeZone, ics);
+ // Example: DTSTART;TZID=US/Pacific:20100331T124500
+ vCalendarDateSuffix = ";TZID=" + vCalendarTimeZone.getID();
+ }
+
+ ics.writeTag("BEGIN", "VEVENT");
+ if (uid == null) {
+ uid = entityValues.getAsString(Events.SYNC_DATA2);
+ }
+ if (uid != null) {
+ ics.writeTag("UID", uid);
+ }
+
+ if (entityValues.containsKey("DTSTAMP")) {
+ ics.writeTag("DTSTAMP", entityValues.getAsString("DTSTAMP"));
+ } else {
+ ics.writeTag("DTSTAMP", millisToEasDateTime(System.currentTimeMillis()));
+ }
+
+ long startTime = entityValues.getAsLong(Events.DTSTART);
+ if (startTime != 0) {
+ ics.writeTag("DTSTART" + vCalendarDateSuffix,
+ millisToEasDateTime(startTime, vCalendarTimeZone, !allDayEvent));
+ }
+
+ // If this is an Exception, we send the recurrence-id, which is just the original
+ // instance time
+ if (isException) {
+ long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
+ ics.writeTag("RECURRENCE-ID" + vCalendarDateSuffix,
+ millisToEasDateTime(originalTime, vCalendarTimeZone, !allDayEvent));
+ }
+
+ if (!entityValues.containsKey(Events.DURATION)) {
+ if (entityValues.containsKey(Events.DTEND)) {
+ ics.writeTag("DTEND" + vCalendarDateSuffix,
+ millisToEasDateTime(
+ entityValues.getAsLong(Events.DTEND), vCalendarTimeZone,
+ !allDayEvent));
+ }
+ } 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));
+ durationMillis = duration.getMillis();
+ } catch (DateException e) {
+ // We'll use the default in this case
+ }
+ ics.writeTag("DTEND" + vCalendarDateSuffix,
+ millisToEasDateTime(
+ startTime + durationMillis, vCalendarTimeZone, !allDayEvent));
+ }
+
+ String location = null;
+ if (entityValues.containsKey(Events.EVENT_LOCATION)) {
+ location = entityValues.getAsString(Events.EVENT_LOCATION);
+ ics.writeTag("LOCATION", location);
+ }
+
+ String sequence = entityValues.getAsString(SYNC_VERSION);
+ if (sequence == null) {
+ sequence = "0";
+ }
+
+ // We'll use 0 to mean a meeting invitation
+ int titleId = 0;
+ switch (messageFlag) {
+ case Message.FLAG_OUTGOING_MEETING_INVITE:
+ if (!sequence.equals("0")) {
+ titleId = R.string.meeting_updated;
+ }
+ break;
+ case Message.FLAG_OUTGOING_MEETING_ACCEPT:
+ titleId = R.string.meeting_accepted;
+ break;
+ case Message.FLAG_OUTGOING_MEETING_DECLINE:
+ titleId = R.string.meeting_declined;
+ break;
+ case Message.FLAG_OUTGOING_MEETING_TENTATIVE:
+ titleId = R.string.meeting_tentative;
+ break;
+ case Message.FLAG_OUTGOING_MEETING_CANCEL:
+ titleId = R.string.meeting_canceled;
+ break;
+ }
+ Resources resources = context.getResources();
+ String title = entityValues.getAsString(Events.TITLE);
+ if (title == null) {
+ title = "";
+ }
+ ics.writeTag("SUMMARY", title);
+ // For meeting invitations just use the title
+ if (titleId == 0) {
+ msg.mSubject = title;
+ } else {
+ // Otherwise, use the additional text
+ msg.mSubject = resources.getString(titleId, title);
+ }
+
+ // Build the text for the message, starting with an initial line describing the
+ // exception (if this is one)
+ StringBuilder sb = new StringBuilder();
+ if (isException && !isReply) {
+ // Add the line, depending on whether this is a cancellation or update
+ Date date = new Date(entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME));
+ String dateString = DateFormat.getDateInstance().format(date);
+ if (titleId == R.string.meeting_canceled) {
+ sb.append(resources.getString(R.string.exception_cancel, dateString));
+ } else {
+ sb.append(resources.getString(R.string.exception_updated, dateString));
+ }
+ sb.append("\n\n");
+ }
+ String text =
+ CalendarUtilities.buildMessageTextFromEntityValues(context, entityValues, sb);
+
+ if (text.length() > 0) {
+ ics.writeTag("DESCRIPTION", text);
+ }
+ // And store the message text
+ msg.mText = text;
+ if (!isReply) {
+ if (entityValues.containsKey(Events.ALL_DAY)) {
+ Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
+ ics.writeTag("X-MICROSOFT-CDO-ALLDAYEVENT", ade == 0 ? "FALSE" : "TRUE");
+ }
+
+ String rrule = entityValues.getAsString(Events.RRULE);
+ if (rrule != null) {
+ ics.writeTag("RRULE", rrule);
+ }
+
+ // If we decide to send alarm information in the meeting request ics file,
+ // handle it here by looping through the subvalues
+ }
+
+ // Handle attendee data here; determine "to" list and add ATTENDEE tags to ics
+ String organizerName = null;
+ String organizerEmail = null;
+ ArrayList<Address> toList = new ArrayList<Address>();
+ 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 there's no relationship, we can't create this for EAS
+ // Similarly, we need an attendee email for each invitee
+ if (relationship != null &&
+ ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
+ // Organizer isn't among attendees in EAS
+ if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
+ organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
+ organizerEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
+ continue;
+ }
+ String attendeeEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
+ String attendeeName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
+
+ // This shouldn't be possible, but allow for it
+ if (attendeeEmail == null) continue;
+ // If we only want to send to the specifiedAttendee, eliminate others here
+ if ((specifiedAttendee != null) &&
+ !attendeeEmail.equalsIgnoreCase(specifiedAttendee)) {
+ continue;
+ }
+
+ addAttendeeToMessage(ics, toList, attendeeName, attendeeEmail, messageFlag,
+ account);
+ }
+ }
+ }
+
+ // Manually add the specifiedAttendee if he wasn't added in the Attendees loop
+ if (toList.isEmpty() && (specifiedAttendee != null)) {
+ addAttendeeToMessage(ics, toList, null, specifiedAttendee, messageFlag, account);
+ }
+
+ // Create the organizer tag for ical
+ if (organizerEmail != null) {
+ String icalTag = "ORGANIZER";
+ // We should be able to find this, assuming the Email is the user's email
+ // TODO Find this in the account
+ if (organizerName != null) {
+ icalTag += ";CN=" + SimpleIcsWriter.quoteParamValue(organizerName);
+ }
+ ics.writeTag(icalTag, "MAILTO:" + organizerEmail);
+ if (isReply) {
+ toList.add(organizerName == null ? new Address(organizerEmail) :
+ new Address(organizerEmail, organizerName));
+ }
+ }
+
+ // If we have no "to" list, we're done
+ if (toList.isEmpty()) return null;
+
+ // Write out the "to" list
+ Address[] toArray = new Address[toList.size()];
+ int i = 0;
+ for (Address address: toList) {
+ toArray[i++] = address;
+ }
+ msg.mTo = Address.pack(toArray);
+
+ ics.writeTag("CLASS", "PUBLIC");
+ ics.writeTag("STATUS", (messageFlag == Message.FLAG_OUTGOING_MEETING_CANCEL) ?
+ "CANCELLED" : "CONFIRMED");
+ ics.writeTag("TRANSP", "OPAQUE"); // What Exchange uses
+ ics.writeTag("PRIORITY", "5"); // 1 to 9, 5 = medium
+ ics.writeTag("SEQUENCE", sequence);
+ ics.writeTag("END", "VEVENT");
+ ics.writeTag("END", "VCALENDAR");
+
+ // Create the ics attachment using the "content" field
+ Attachment att = new Attachment();
+ att.mContentBytes = ics.getBytes();
+ att.mMimeType = "text/calendar; method=" + method;
+ att.mFileName = "invite.ics";
+ att.mSize = att.mContentBytes.length;
+ // We don't send content-disposition with this attachment
+ att.mFlags = Attachment.FLAG_ICS_ALTERNATIVE_PART;
+
+ // Add the attachment to the message
+ msg.mAttachments = new ArrayList<Attachment>();
+ msg.mAttachments.add(att);
+ } catch (IOException e) {
+ Log.w(TAG, "IOException in createMessageForEntity");
+ return null;
+ }
+
+ // Return the new Message to caller
+ return msg;
+ }
+
+ /**
+ * Create a Message for an Event that can be retrieved from CalendarProvider
+ * by its unique id
+ *
+ * @param cr a content resolver that can be used to query for the Event
+ * @param eventId the unique id of the Event
+ * @param messageFlag the Message.FLAG_XXX constant indicating the type of
+ * email to be sent
+ * @param the unique id of this Event, or null if it can be retrieved from
+ * the Event
+ * @param the user's account
+ * @param requireAddressees if true (the default), no Message is returned if
+ * there aren't any addressees; if false, return the Message
+ * regardless (addressees will be filled in later)
+ * @return a Message with many fields pre-filled (more later)
+ * @throws RemoteException if there is an issue retrieving the Event from
+ * CalendarProvider
+ */
+ static public EmailContent.Message createMessageForEventId(Context context, long eventId,
+ int messageFlag, String uid, Account account) throws RemoteException {
+ return createMessageForEventId(context, eventId, messageFlag, uid, account,
+ null /* specifiedAttendee */);
+ }
+
+ static public EmailContent.Message createMessageForEventId(Context context, long eventId,
+ int messageFlag, String uid, Account account, String specifiedAttendee)
+ throws RemoteException {
+ ContentResolver cr = context.getContentResolver();
+ EntityIterator eventIterator = EventsEntity.newEntityIterator(cr.query(
+ ContentUris.withAppendedId(Events.CONTENT_URI, eventId), null, null, null, null),
+ cr);
+ try {
+ while (eventIterator.hasNext()) {
+ Entity entity = eventIterator.next();
+ return createMessageForEntity(context, entity, messageFlag, uid, account,
+ specifiedAttendee);
+ }
+ } finally {
+ eventIterator.close();
+ }
+ return null;
+ }
+
+ /**
+ * Return a boolean value for an integer ContentValues column
+ * @param values a ContentValues object
+ * @param columnName the name of a column to be found in the ContentValues
+ * @return a boolean representation of the value of columnName in values; null and 0 = false,
+ * other integers = true
+ */
+ static public boolean getIntegerValueAsBoolean(ContentValues values, String columnName) {
+ Integer intValue = values.getAsInteger(columnName);
+ return (intValue != null && intValue != 0);
+ }
+}
diff --git a/exchange2/src/com/android/exchange/utility/FileLogger.java b/exchange2/src/com/android/exchange/utility/FileLogger.java
new file mode 100644
index 0000000..bb17151
--- /dev/null
+++ b/exchange2/src/com/android/exchange/utility/FileLogger.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.utility;
+
+import android.content.Context;
+import android.os.Environment;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Date;
+
+public class FileLogger {
+ private static FileLogger LOGGER = null;
+ private static FileWriter sLogWriter = null;
+ public static String LOG_FILE_NAME =
+ Environment.getExternalStorageDirectory() + "/emaillog.txt";
+
+ public synchronized static FileLogger getLogger (Context c) {
+ LOGGER = new FileLogger();
+ return LOGGER;
+ }
+
+ private FileLogger() {
+ try {
+ sLogWriter = new FileWriter(LOG_FILE_NAME, true);
+ } catch (IOException e) {
+ // Doesn't matter
+ }
+ }
+
+ static public synchronized void close() {
+ if (sLogWriter != null) {
+ try {
+ sLogWriter.close();
+ } catch (IOException e) {
+ // Doesn't matter
+ }
+ sLogWriter = null;
+ }
+ }
+
+ static public synchronized void log(Exception e) {
+ if (sLogWriter != null) {
+ log("Exception", "Stack trace follows...");
+ PrintWriter pw = new PrintWriter(sLogWriter);
+ e.printStackTrace(pw);
+ pw.flush();
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ static public synchronized void log(String prefix, String str) {
+ if (LOGGER == null) {
+ LOGGER = new FileLogger();
+ log("Logger", "\r\n\r\n --- New Log ---");
+ }
+ Date d = new Date();
+ int hr = d.getHours();
+ int min = d.getMinutes();
+ int sec = d.getSeconds();
+
+ // I don't use DateFormat here because (in my experience), it's much slower
+ StringBuffer sb = new StringBuffer(256);
+ sb.append('[');
+ sb.append(hr);
+ sb.append(':');
+ if (min < 10)
+ sb.append('0');
+ sb.append(min);
+ sb.append(':');
+ if (sec < 10) {
+ sb.append('0');
+ }
+ sb.append(sec);
+ sb.append("] ");
+ if (prefix != null) {
+ sb.append(prefix);
+ sb.append("| ");
+ }
+ sb.append(str);
+ sb.append("\r\n");
+ String s = sb.toString();
+
+ if (sLogWriter != null) {
+ try {
+ sLogWriter.write(s);
+ sLogWriter.flush();
+ } catch (IOException e) {
+ // Something might have happened to the sdcard
+ if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
+ // If the card is mounted and we can create the writer, retry
+ LOGGER = new FileLogger();
+ if (sLogWriter != null) {
+ try {
+ log("FileLogger", "Exception writing log; recreating...");
+ log(prefix, str);
+ } catch (Exception e1) {
+ // Nothing to do at this point
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/exchange2/src/com/android/exchange/utility/SimpleIcsWriter.java b/exchange2/src/com/android/exchange/utility/SimpleIcsWriter.java
new file mode 100644
index 0000000..1bb8463
--- /dev/null
+++ b/exchange2/src/com/android/exchange/utility/SimpleIcsWriter.java
@@ -0,0 +1,156 @@
+/* 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 com.android.emailcommon.utility.Utility;
+
+import android.text.TextUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Class to generate iCalender object (*.ics) per RFC 5545.
+ */
+public class SimpleIcsWriter {
+ private static final int MAX_LINE_LENGTH = 75; // In bytes, excluding CRLF
+ private static final int CHAR_MAX_BYTES_IN_UTF8 = 4; // Used to be 6, but RFC3629 limited it.
+ private final ByteArrayOutputStream mOut = new ByteArrayOutputStream();
+
+ public SimpleIcsWriter() {
+ }
+
+ /**
+ * Low level method to write a line, performing line-folding if necessary.
+ */
+ /* package for testing */ void writeLine(String string) {
+ int numBytes = 0;
+ for (byte b : Utility.toUtf8(string)) {
+ // Fold it when necessary.
+ // To make it simple, we assume all chars are 4 bytes.
+ // If not (and usually it's not), we end up wrapping earlier than necessary, but that's
+ // completely fine.
+ if (numBytes > (MAX_LINE_LENGTH - CHAR_MAX_BYTES_IN_UTF8)
+ && Utility.isFirstUtf8Byte(b)) { // Only wrappable if it's before the first byte
+ mOut.write((byte) '\r');
+ mOut.write((byte) '\n');
+ mOut.write((byte) '\t');
+ numBytes = 1; // for TAB
+ }
+ mOut.write(b);
+ numBytes++;
+ }
+ mOut.write((byte) '\r');
+ mOut.write((byte) '\n');
+ }
+
+ /**
+ * Write a tag with a value.
+ */
+ public void writeTag(String name, String value) {
+ // Belt and suspenders here; don't crash on null value; just return
+ if (TextUtils.isEmpty(value)) {
+ return;
+ }
+
+ // The following properties take a TEXT value, which need to be escaped.
+ // (These property names should be all interned, so using equals() should be faster than
+ // using a hash table.)
+
+ // TODO make constants for these literals
+ if ("CALSCALE".equals(name)
+ || "METHOD".equals(name)
+ || "PRODID".equals(name)
+ || "VERSION".equals(name)
+ || "CATEGORIES".equals(name)
+ || "CLASS".equals(name)
+ || "COMMENT".equals(name)
+ || "DESCRIPTION".equals(name)
+ || "LOCATION".equals(name)
+ || "RESOURCES".equals(name)
+ || "STATUS".equals(name)
+ || "SUMMARY".equals(name)
+ || "TRANSP".equals(name)
+ || "TZID".equals(name)
+ || "TZNAME".equals(name)
+ || "CONTACT".equals(name)
+ || "RELATED-TO".equals(name)
+ || "UID".equals(name)
+ || "ACTION".equals(name)
+ || "REQUEST-STATUS".equals(name)
+ || "X-LIC-LOCATION".equals(name)
+ ) {
+ value = escapeTextValue(value);
+ }
+ writeLine(name + ":" + value);
+ }
+
+ /**
+ * For debugging
+ */
+ @Override
+ public String toString() {
+ return Utility.fromUtf8(getBytes());
+ }
+
+ /**
+ * @return the entire iCalendar invitation object.
+ */
+ public byte[] getBytes() {
+ try {
+ mOut.flush();
+ } catch (IOException wonthappen) {
+ }
+ return mOut.toByteArray();
+ }
+
+ /**
+ * Quote a param-value string, according to RFC 5545, section 3.1
+ */
+ public static String quoteParamValue(String paramValue) {
+ if (paramValue == null) {
+ return null;
+ }
+ // Wrap with double quotes.
+ // The spec doesn't allow putting double-quotes in a param value, so let's use single quotes
+ // as a substitute.
+ // It's not the smartest implementation. e.g. we don't have to wrap an empty string with
+ // double quotes. But it works.
+ return "\"" + paramValue.replace("\"", "'") + "\"";
+ }
+
+ /**
+ * Escape a TEXT value per RFC 5545 section 3.3.11
+ */
+ /* package for testing */ static String escapeTextValue(String s) {
+ StringBuilder sb = new StringBuilder(s.length());
+ for (int i = 0; i < s.length(); i++) {
+ char ch = s.charAt(i);
+ if (ch == '\n') {
+ sb.append("\\n");
+ } else if (ch == '\r') {
+ // Remove CR
+ } else if (ch == ',' || ch == ';' || ch == '\\') {
+ sb.append('\\');
+ sb.append(ch);
+ } else {
+ sb.append(ch);
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/exchange2/src/com/android/exchange/utility/UriCodec.java b/exchange2/src/com/android/exchange/utility/UriCodec.java
new file mode 100644
index 0000000..d8811c6
--- /dev/null
+++ b/exchange2/src/com/android/exchange/utility/UriCodec.java
@@ -0,0 +1,216 @@
+/*
+ * 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.
+ */
+
+package com.android.exchange.utility;
+
+import java.io.ByteArrayOutputStream;
+import java.net.URISyntaxException;
+import java.nio.charset.Charset;
+import java.nio.charset.Charsets;
+
+// Note: This class copied verbatim from libcore.net
+
+/**
+ * Encodes and decodes {@code application/x-www-form-urlencoded} content.
+ * Subclasses define exactly which characters are legal.
+ *
+ * <p>By default, UTF-8 is used to encode escaped characters. A single input
+ * character like "\u0080" may be encoded to multiple octets like %C2%80.
+ */
+public abstract class UriCodec {
+
+ /**
+ * Returns true if {@code c} does not need to be escaped.
+ */
+ protected abstract boolean isRetained(char c);
+
+ /**
+ * Throws if {@code s} is invalid according to this encoder.
+ */
+ public final String validate(String uri, int start, int end, String name)
+ throws URISyntaxException {
+ for (int i = start; i < end; ) {
+ char ch = uri.charAt(i);
+ if ((ch >= 'a' && ch <= 'z')
+ || (ch >= 'A' && ch <= 'Z')
+ || (ch >= '0' && ch <= '9')
+ || isRetained(ch)) {
+ i++;
+ } else if (ch == '%') {
+ if (i + 2 >= end) {
+ throw new URISyntaxException(uri, "Incomplete % sequence in " + name, i);
+ }
+ int d1 = hexToInt(uri.charAt(i + 1));
+ int d2 = hexToInt(uri.charAt(i + 2));
+ if (d1 == -1 || d2 == -1) {
+ throw new URISyntaxException(uri, "Invalid % sequence: "
+ + uri.substring(i, i + 3) + " in " + name, i);
+ }
+ i += 3;
+ } else {
+ throw new URISyntaxException(uri, "Illegal character in " + name, i);
+ }
+ }
+ return uri.substring(start, end);
+ }
+
+ /**
+ * Throws if {@code s} contains characters that are not letters, digits or
+ * in {@code legal}.
+ */
+ public static void validateSimple(String s, String legal)
+ throws URISyntaxException {
+ for (int i = 0; i < s.length(); i++) {
+ char ch = s.charAt(i);
+ if (!((ch >= 'a' && ch <= 'z')
+ || (ch >= 'A' && ch <= 'Z')
+ || (ch >= '0' && ch <= '9')
+ || legal.indexOf(ch) > -1)) {
+ throw new URISyntaxException(s, "Illegal character", i);
+ }
+ }
+ }
+
+ /**
+ * Encodes {@code s} and appends the result to {@code builder}.
+ *
+ * @param isPartiallyEncoded true to fix input that has already been
+ * partially or fully encoded. For example, input of "hello%20world" is
+ * unchanged with isPartiallyEncoded=true but would be double-escaped to
+ * "hello%2520world" otherwise.
+ */
+ private void appendEncoded(StringBuilder builder, String s, Charset charset,
+ boolean isPartiallyEncoded) {
+ if (s == null) {
+ throw new NullPointerException();
+ }
+
+ int escapeStart = -1;
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if ((c >= 'a' && c <= 'z')
+ || (c >= 'A' && c <= 'Z')
+ || (c >= '0' && c <= '9')
+ || isRetained(c)
+ || (c == '%' && isPartiallyEncoded)) {
+ if (escapeStart != -1) {
+ appendHex(builder, s.substring(escapeStart, i), charset);
+ escapeStart = -1;
+ }
+ if (c == '%' && isPartiallyEncoded) {
+ // this is an encoded 3-character sequence like "%20"
+ builder.append(s, i, i + 3);
+ i += 2;
+ } else if (c == ' ') {
+ builder.append('+');
+ } else {
+ builder.append(c);
+ }
+ } else if (escapeStart == -1) {
+ escapeStart = i;
+ }
+ }
+ if (escapeStart != -1) {
+ appendHex(builder, s.substring(escapeStart, s.length()), charset);
+ }
+ }
+
+ public final String encode(String s, Charset charset) {
+ // Guess a bit larger for encoded form
+ StringBuilder builder = new StringBuilder(s.length() + 16);
+ appendEncoded(builder, s, charset, false);
+ return builder.toString();
+ }
+
+ public final void appendEncoded(StringBuilder builder, String s) {
+ appendEncoded(builder, s, Charsets.UTF_8, false);
+ }
+
+ public final void appendPartiallyEncoded(StringBuilder builder, String s) {
+ appendEncoded(builder, s, Charsets.UTF_8, true);
+ }
+
+ /**
+ * @param convertPlus true to convert '+' to ' '.
+ */
+ public static String decode(String s, boolean convertPlus, Charset charset) {
+ if (s.indexOf('%') == -1 && (!convertPlus || s.indexOf('+') == -1)) {
+ return s;
+ }
+
+ StringBuilder result = new StringBuilder(s.length());
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ for (int i = 0; i < s.length();) {
+ char c = s.charAt(i);
+ if (c == '%') {
+ do {
+ if (i + 2 >= s.length()) {
+ throw new IllegalArgumentException("Incomplete % sequence at: " + i);
+ }
+ int d1 = hexToInt(s.charAt(i + 1));
+ int d2 = hexToInt(s.charAt(i + 2));
+ if (d1 == -1 || d2 == -1) {
+ throw new IllegalArgumentException("Invalid % sequence " +
+ s.substring(i, i + 3) + " at " + i);
+ }
+ out.write((byte) ((d1 << 4) + d2));
+ i += 3;
+ } while (i < s.length() && s.charAt(i) == '%');
+ result.append(new String(out.toByteArray(), charset));
+ out.reset();
+ } else {
+ if (convertPlus && c == '+') {
+ c = ' ';
+ }
+ result.append(c);
+ i++;
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * Like {@link Character#digit}, but without support for non-ASCII
+ * characters.
+ */
+ private static int hexToInt(char c) {
+ if ('0' <= c && c <= '9') {
+ return c - '0';
+ } else if ('a' <= c && c <= 'f') {
+ return 10 + (c - 'a');
+ } else if ('A' <= c && c <= 'F') {
+ return 10 + (c - 'A');
+ } else {
+ return -1;
+ }
+ }
+
+ public static String decode(String s) {
+ return decode(s, false, Charsets.UTF_8);
+ }
+
+ private static void appendHex(StringBuilder builder, String s, Charset charset) {
+ for (byte b : s.getBytes(charset)) {
+ appendHex(builder, b);
+ }
+ }
+
+ private static void appendHex(StringBuilder sb, byte b) {
+ sb.append('%');
+ sb.append(Byte.toHexString(b, true));
+ }
+}
diff --git a/exchange2/src/com/android/exchange/utility/patent_disclaimer.txt b/exchange2/src/com/android/exchange/utility/patent_disclaimer.txt
new file mode 100644
index 0000000..8a715a3
--- /dev/null
+++ b/exchange2/src/com/android/exchange/utility/patent_disclaimer.txt
@@ -0,0 +1,9 @@
+-----------------------------
+THIS IS NOT A GRANT OF PATENT RIGHTS.
+
+Google makes no representation or warranty that the source code made available hereunder is
+unencumbered by third-party patents. Those intending to use this source code in hardware or
+software products are advised that implementations of this code, including in open source software
+or shareware, may require patent licenses from the relevant patent holders.
+
+-----------------------------
diff --git a/exchange2/tests/Android.mk b/exchange2/tests/Android.mk
new file mode 100644
index 0000000..0f36cd2
--- /dev/null
+++ b/exchange2/tests/Android.mk
@@ -0,0 +1,34 @@
+# Copyright 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.
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+# Notice that we don't have to include the src files of Exchange because, by
+# running the tests using an instrumentation targeting Exchange, we
+# automatically get all of its classes loaded into our environment.
+
+LOCAL_PACKAGE_NAME := Exchange2Tests
+
+LOCAL_INSTRUMENTATION_FOR := Exchange2
+
+include $(BUILD_PACKAGE)
diff --git a/exchange2/tests/AndroidManifest.xml b/exchange2/tests/AndroidManifest.xml
new file mode 100644
index 0000000..6511938
--- /dev/null
+++ b/exchange2/tests/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?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.
+-->
+
+<!-- package name must be unique so suffix with "tests" so package loader doesn't ignore us -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.exchange.tests"
+ >
+
+ <!-- We add an application tag here just so that we can indicate that
+ this package needs to link against the android.test library,
+ which is needed when building test cases. -->
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <!--
+ This declares that this app uses the instrumentation test runner targeting
+ the package of com.android.email. To run the tests use the command:
+ "adb shell am instrument -w com.android.exchange.tests/android.test.InstrumentationTestRunner"
+ -->
+ <instrumentation android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.android.exchange"
+ android:label="Tests for Exchange."/>
+
+</manifest>
diff --git a/exchange2/tests/src/com/android/exchange/CalendarSyncEnablerTest.java b/exchange2/tests/src/com/android/exchange/CalendarSyncEnablerTest.java
new file mode 100644
index 0000000..4198c08
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/CalendarSyncEnablerTest.java
@@ -0,0 +1,297 @@
+/*
+ * 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 android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.app.NotificationManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.CalendarContract;
+import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.emailcommon.AccountManagerTypes;
+import com.android.emailcommon.Logging;
+import com.android.exchange.utility.ExchangeTestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+@MediumTest
+public class CalendarSyncEnablerTest extends ExchangeTestCase {
+
+ protected static final String TEST_ACCOUNT_PREFIX = "__test";
+ protected static final String TEST_ACCOUNT_SUFFIX = "@android.com";
+
+ private HashMap<Account, Boolean> origCalendarSyncStates = new HashMap<Account, Boolean>();
+
+ // To make the rest of the code shorter thus more readable...
+ private static final String EAT = AccountManagerTypes.TYPE_EXCHANGE;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ // Delete any test accounts we might have created earlier
+ deleteTemporaryAccountManagerAccounts();
+
+ // Save the original calendar sync states.
+ for (Account account : AccountManager.get(getContext()).getAccounts()) {
+ origCalendarSyncStates.put(account,
+ ContentResolver.getSyncAutomatically(account, CalendarContract.AUTHORITY));
+ }
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ // Delete any test accounts we might have created earlier
+ deleteTemporaryAccountManagerAccounts();
+
+ // Restore the original calendar sync states.
+ // Note we restore only for Exchange accounts.
+ // Other accounts should remain intact throughout the tests. Plus we don't know if the
+ // Calendar.AUTHORITY is supported by other types of accounts.
+ for (Account account : getExchangeAccounts()) {
+ Boolean state = origCalendarSyncStates.get(account);
+ if (state == null) continue; // Shouldn't happen, but just in case.
+
+ ContentResolver.setSyncAutomatically(account, CalendarContract.AUTHORITY, state);
+ }
+ }
+
+ public void testEnableEasCalendarSync() {
+ final Account[] baseAccounts = getExchangeAccounts();
+
+ String a1 = getTestAccountEmailAddress("1");
+ String a2 = getTestAccountEmailAddress("2");
+
+ // 1. Test with 1 account
+
+ CalendarSyncEnabler enabler = new CalendarSyncEnabler(getContext());
+
+ // Add exchange accounts
+ createAccountManagerAccount(a1);
+
+ String emailAddresses = enabler.enableEasCalendarSyncInternalForTest();
+
+ // Verify
+ verifyCalendarSyncState();
+
+ // There seems to be no good way to examine the contents of Notification, so let's verify
+ // we at least (tried to) show the correct email addresses.
+ checkNotificationEmailAddresses(emailAddresses, baseAccounts, a1);
+
+ // Delete added account.
+ deleteTemporaryAccountManagerAccounts();
+
+ // 2. Test with 2 accounts
+ enabler = new CalendarSyncEnabler(getContext());
+
+ // Add exchange accounts
+ createAccountManagerAccount(a1);
+ createAccountManagerAccount(a2);
+
+ emailAddresses = enabler.enableEasCalendarSyncInternalForTest();
+
+ // Verify
+ verifyCalendarSyncState();
+
+ // Check
+ checkNotificationEmailAddresses(emailAddresses, baseAccounts, a1, a2);
+ }
+
+ private static void checkNotificationEmailAddresses(String actual, Account[] baseAccounts,
+ String... addedAddresses) {
+ // Build and sort actual string array.
+ final String[] actualArray = TextUtils.split(actual, " ");
+ Arrays.sort(actualArray);
+
+ // Build and sort expected string array.
+ ArrayList<String> expected = new ArrayList<String>();
+ for (Account account : baseAccounts) {
+ expected.add(account.name);
+ }
+ for (String address : addedAddresses) {
+ expected.add(address);
+ }
+ final String[] expectedArray = new String[expected.size()];
+ expected.toArray(expectedArray);
+ Arrays.sort(expectedArray);
+
+ // Check!
+ MoreAsserts.assertEquals(expectedArray, actualArray);
+ }
+
+ /**
+ * For all {@link Account}, confirm that:
+ * <ol>
+ * <li>Calendar sync is enabled if it's an Exchange account.<br>
+ * Unfortunately setSyncAutomatically() doesn't take effect immediately, so we skip this
+ * check for now.
+ TODO Find a stable way to check this.
+ * <li>Otherwise, calendar sync state isn't changed.
+ * </ol>
+ */
+ private void verifyCalendarSyncState() {
+ // It's very unfortunate that setSyncAutomatically doesn't take effect immediately.
+ for (Account account : AccountManager.get(getContext()).getAccounts()) {
+ String message = "account=" + account.name + "(" + account.type + ")";
+ boolean enabled = ContentResolver.getSyncAutomatically(account,
+ CalendarContract.AUTHORITY);
+ int syncable = ContentResolver.getIsSyncable(account, CalendarContract.AUTHORITY);
+
+ if (EAT.equals(account.type)) {
+ // Should be enabled.
+ // assertEquals(message, Boolean.TRUE, (Boolean) enabled);
+ // assertEquals(message, 1, syncable);
+ } else {
+ // Shouldn't change.
+ assertEquals(message, origCalendarSyncStates.get(account), (Boolean) enabled);
+ }
+ }
+ }
+
+ public void testEnableEasCalendarSyncWithNoExchangeAccounts() {
+ // This test can only meaningfully run when there's no exchange accounts
+ // set up on the device. Otherwise there'll be no difference from
+ // testEnableEasCalendarSync.
+ if (AccountManager.get(getContext()).getAccountsByType(EAT).length > 0) {
+ Log.w(Logging.LOG_TAG, "testEnableEasCalendarSyncWithNoExchangeAccounts skipped:"
+ + " It only runs when there's no Exchange account on the device.");
+ return;
+ }
+ CalendarSyncEnabler enabler = new CalendarSyncEnabler(getContext());
+ String emailAddresses = enabler.enableEasCalendarSyncInternalForTest();
+
+ // Verify (nothing should change)
+ verifyCalendarSyncState();
+
+ // No exchange accounts found.
+ assertEquals(0, emailAddresses.length());
+ }
+
+ public void testShowNotification() {
+ CalendarSyncEnabler enabler = new CalendarSyncEnabler(getContext());
+
+ // We can't really check the result, but at least we can make sure it won't crash....
+ enabler.showNotificationForTest("a@b.com");
+
+ // Remove the notification. Comment it out when you want to know how it looks like.
+ // TODO If NotificationController supports this notification, we can just mock it out
+ // and remove this code.
+ ((NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE))
+ .cancel(CalendarSyncEnabler.NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED);
+ }
+
+ protected Account[] getExchangeAccounts() {
+ return AccountManager.get(getContext()).getAccountsByType(
+ AccountManagerTypes.TYPE_EXCHANGE);
+ }
+
+ protected Account makeAccountManagerAccount(String username) {
+ return new Account(username, AccountManagerTypes.TYPE_EXCHANGE);
+ }
+
+ protected void createAccountManagerAccount(String username) {
+ final Account account = makeAccountManagerAccount(username);
+ AccountManager.get(getContext()).addAccountExplicitly(account, "password", null);
+ }
+
+ protected com.android.emailcommon.provider.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 + TEST_ACCOUNT_SUFFIX);
+ return setupTestAccount(username, true);
+ }
+
+ protected ArrayList<com.android.emailcommon.provider.Account> makeExchangeServiceAccountList() {
+ ArrayList<com.android.emailcommon.provider.Account> accountList =
+ new ArrayList<com.android.emailcommon.provider.Account>();
+ Cursor c = mProviderContext.getContentResolver().query(
+ com.android.emailcommon.provider.Account.CONTENT_URI,
+ com.android.emailcommon.provider.Account.CONTENT_PROJECTION, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ com.android.emailcommon.provider.Account account =
+ new com.android.emailcommon.provider.Account();
+ account.restore(c);
+ accountList.add(account);
+ }
+ } finally {
+ c.close();
+ }
+ return accountList;
+ }
+
+ protected void deleteAccountManagerAccount(Account account) {
+ AccountManagerFuture<Boolean> future =
+ AccountManager.get(getContext()).removeAccount(account, null, null);
+ try {
+ future.getResult();
+ } catch (OperationCanceledException e) {
+ } catch (AuthenticatorException e) {
+ } catch (IOException e) {
+ }
+ }
+
+ protected void deleteTemporaryAccountManagerAccounts() {
+ for (Account accountManagerAccount: getExchangeAccounts()) {
+ if (accountManagerAccount.name.startsWith(TEST_ACCOUNT_PREFIX) &&
+ accountManagerAccount.name.endsWith(TEST_ACCOUNT_SUFFIX)) {
+ deleteAccountManagerAccount(accountManagerAccount);
+ }
+ }
+ }
+
+ protected String getTestAccountName(String name) {
+ return TEST_ACCOUNT_PREFIX + name;
+ }
+
+ protected String getTestAccountEmailAddress(String name) {
+ return TEST_ACCOUNT_PREFIX + name + TEST_ACCOUNT_SUFFIX;
+ }
+
+
+ /**
+ * Helper to retrieve account manager accounts *and* remove any preexisting accounts
+ * from the list, to "hide" them from the reconciler.
+ */
+ protected Account[] getAccountManagerAccounts(Account[] baseline) {
+ Account[] rawList = getExchangeAccounts();
+ if (baseline.length == 0) {
+ return rawList;
+ }
+ HashSet<Account> set = new HashSet<Account>();
+ for (Account addAccount : rawList) {
+ set.add(addAccount);
+ }
+ for (Account removeAccount : baseline) {
+ set.remove(removeAccount);
+ }
+ return set.toArray(new Account[0]);
+ }
+}
diff --git a/tests/src/com/android/exchange/EasAccountServiceTests.java b/exchange2/tests/src/com/android/exchange/EasAccountServiceTests.java
similarity index 100%
rename from tests/src/com/android/exchange/EasAccountServiceTests.java
rename to exchange2/tests/src/com/android/exchange/EasAccountServiceTests.java
diff --git a/exchange2/tests/src/com/android/exchange/EasOutboxServiceTests.java b/exchange2/tests/src/com/android/exchange/EasOutboxServiceTests.java
new file mode 100644
index 0000000..da995d1
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/EasOutboxServiceTests.java
@@ -0,0 +1,47 @@
+/*
+ * 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 android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.emailcommon.provider.Mailbox;
+import com.android.exchange.EasOutboxService.OriginalMessageInfo;
+import com.android.exchange.utility.ExchangeTestCase;
+
+/**
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.EasOutboxServiceTests exchange
+ */
+@MediumTest
+public class EasOutboxServiceTests extends ExchangeTestCase {
+
+ public void testGenerateSmartSendCmd() {
+ EasOutboxService svc = new EasOutboxService(mProviderContext, new Mailbox());
+ // Test encoding of collection id; colon should be preserved
+ OriginalMessageInfo info = new OriginalMessageInfo("1339085683659694034", "Mail:^f", null);
+ String cmd = svc.generateSmartSendCmd(true, info);
+ assertEquals("SmartReply&ItemId=1339085683659694034&CollectionId=Mail:%5Ef", cmd);
+ // Test encoding of item id
+ info = new OriginalMessageInfo("14:&3", "6", null);
+ cmd = svc.generateSmartSendCmd(false, info);
+ assertEquals("SmartForward&ItemId=14:%263&CollectionId=6", cmd);
+ // Test use of long id
+ info = new OriginalMessageInfo("1339085683659694034", "Mail:^f", "3232323AAA");
+ cmd = svc.generateSmartSendCmd(false, info);
+ assertEquals("SmartForward&LongId=3232323AAA", cmd);
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/EasSyncServiceTests.java b/exchange2/tests/src/com/android/exchange/EasSyncServiceTests.java
new file mode 100644
index 0000000..150b1f1
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/EasSyncServiceTests.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Base64;
+
+import com.android.emailcommon.provider.Account;
+
+import org.apache.http.Header;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpRequestBase;
+
+import java.io.IOException;
+
+/**
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.EasSyncServiceTests exchange
+ */
+@SmallTest
+public class EasSyncServiceTests extends AndroidTestCase {
+ static private final String USER = "user";
+ static private final String PASSWORD = "password";
+ static private final String HOST = "xxx.host.zzz";
+ static private final String ID = "id";
+
+ Context mMockContext;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mMockContext = getContext();
+ }
+
+ public void testAddHeaders() {
+ HttpRequestBase method = new HttpPost();
+ EasSyncService svc = new EasSyncService();
+ svc.mAuthString = "auth";
+ svc.mProtocolVersion = "12.1";
+ svc.mAccount = null;
+ // With second argument false, there should be no header
+ svc.setHeaders(method, false);
+ Header[] headers = method.getHeaders("X-MS-PolicyKey");
+ assertEquals(0, headers.length);
+ // With second argument true, there should always be a header
+ // The value will be "0" without an account
+ method.removeHeaders("X-MS-PolicyKey");
+ svc.setHeaders(method, true);
+ headers = method.getHeaders("X-MS-PolicyKey");
+ assertEquals(1, headers.length);
+ assertEquals("0", headers[0].getValue());
+ // With an account, but null security key, the header's value should be "0"
+ Account account = new Account();
+ account.mSecuritySyncKey = null;
+ svc.mAccount = account;
+ method.removeHeaders("X-MS-PolicyKey");
+ svc.setHeaders(method, true);
+ headers = method.getHeaders("X-MS-PolicyKey");
+ assertEquals(1, headers.length);
+ assertEquals("0", headers[0].getValue());
+ // With an account and security key, the header's value should be the security key
+ account.mSecuritySyncKey = "key";
+ svc.mAccount = account;
+ method.removeHeaders("X-MS-PolicyKey");
+ svc.setHeaders(method, true);
+ headers = method.getHeaders("X-MS-PolicyKey");
+ assertEquals(1, headers.length);
+ assertEquals("key", headers[0].getValue());
+ }
+
+ public void testGetProtocolVersionDouble() {
+ assertEquals(Eas.SUPPORTED_PROTOCOL_EX2003_DOUBLE,
+ Eas.getProtocolVersionDouble(Eas.SUPPORTED_PROTOCOL_EX2003));
+ assertEquals(Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE,
+ Eas.getProtocolVersionDouble(Eas.SUPPORTED_PROTOCOL_EX2007));
+ assertEquals(Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE,
+ Eas.getProtocolVersionDouble(Eas.SUPPORTED_PROTOCOL_EX2007_SP1));
+ }
+
+ private EasSyncService setupService(String user) {
+ EasSyncService svc = new EasSyncService();
+ svc.mUserName = user;
+ svc.mPassword = PASSWORD;
+ svc.mDeviceId = ID;
+ svc.mHostAddress = HOST;
+ return svc;
+ }
+
+ public void testMakeUriString() throws IOException {
+ // Simple user name and command
+ EasSyncService svc = setupService(USER);
+ String uriString = svc.makeUriString("Sync", null);
+ // These next two should now be cached
+ assertNotNull(svc.mAuthString);
+ assertNotNull(svc.mUserString);
+ assertEquals("Basic " + Base64.encodeToString((USER+":"+PASSWORD).getBytes(),
+ Base64.NO_WRAP), svc.mAuthString);
+ assertEquals("&User=" + USER + "&DeviceId=" + ID + "&DeviceType=" +
+ EasSyncService.DEVICE_TYPE, svc.mUserString);
+ assertEquals("https://" + HOST + "/Microsoft-Server-ActiveSync?Cmd=Sync" +
+ svc.mUserString, uriString);
+ // User name that requires encoding
+ String user = "name_with_underscore@foo%bar.com";
+ svc = setupService(user);
+ uriString = svc.makeUriString("Sync", null);
+ assertEquals("Basic " + Base64.encodeToString((user+":"+PASSWORD).getBytes(),
+ Base64.NO_WRAP), svc.mAuthString);
+ String safeUserName = "name_with_underscore%40foo%25bar.com";
+ assertEquals("&User=" + safeUserName + "&DeviceId=" + ID + "&DeviceType=" +
+ EasSyncService.DEVICE_TYPE, svc.mUserString);
+ assertEquals("https://" + HOST + "/Microsoft-Server-ActiveSync?Cmd=Sync" +
+ svc.mUserString, uriString);
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/ExchangeServiceAccountTests.java b/exchange2/tests/src/com/android/exchange/ExchangeServiceAccountTests.java
new file mode 100644
index 0000000..6b30b1d
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/ExchangeServiceAccountTests.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.exchange.ExchangeService.SyncError;
+import com.android.exchange.provider.EmailContentSetupUtils;
+import com.android.exchange.utility.ExchangeTestCase;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.ExchangeServiceAccountTests exchange
+ */
+@MediumTest
+public class ExchangeServiceAccountTests extends ExchangeTestCase {
+
+ public ExchangeServiceAccountTests() {
+ super();
+ }
+
+ public void testReleaseSyncHolds() {
+ ExchangeService exchangeService = new ExchangeService();
+ SyncError securityErrorAccount1 =
+ exchangeService.new SyncError(AbstractSyncService.EXIT_SECURITY_FAILURE, false);
+ SyncError ioError =
+ exchangeService.new SyncError(AbstractSyncService.EXIT_IO_ERROR, false);
+ SyncError securityErrorAccount2 =
+ exchangeService.new SyncError(AbstractSyncService.EXIT_SECURITY_FAILURE, false);
+ // Create account and two mailboxes
+ Account acct1 = setupTestAccount("acct1", true);
+ Mailbox box1 = EmailContentSetupUtils.setupMailbox("box1", acct1.mId, true,
+ mProviderContext);
+ Mailbox box2 = EmailContentSetupUtils.setupMailbox("box2", acct1.mId, true,
+ mProviderContext);
+ Account acct2 = setupTestAccount("acct2", true);
+ Mailbox box3 = EmailContentSetupUtils.setupMailbox("box3", acct2.mId, true,
+ mProviderContext);
+ Mailbox box4 = EmailContentSetupUtils.setupMailbox("box4", acct2.mId, true,
+ mProviderContext);
+
+ ConcurrentHashMap<Long, SyncError> errorMap = exchangeService.mSyncErrorMap;
+ // Add errors into the map
+ errorMap.put(box1.mId, securityErrorAccount1);
+ errorMap.put(box2.mId, ioError);
+ errorMap.put(box3.mId, securityErrorAccount2);
+ errorMap.put(box4.mId, securityErrorAccount2);
+ // We should have 4
+ assertEquals(4, errorMap.keySet().size());
+ // Release the holds on acct2 (there are two of them)
+ assertTrue(exchangeService.releaseSyncHolds(mProviderContext,
+ AbstractSyncService.EXIT_SECURITY_FAILURE, acct2));
+ // There should be two left
+ assertEquals(2, errorMap.keySet().size());
+ // And these are the two...
+ assertNotNull(errorMap.get(box2.mId));
+ assertNotNull(errorMap.get(box1.mId));
+
+ // Put the two back
+ errorMap.put(box3.mId, securityErrorAccount2);
+ errorMap.put(box4.mId, securityErrorAccount2);
+ // We should have 4 again
+ assertEquals(4, errorMap.keySet().size());
+ // Release all of the security holds
+ assertTrue(exchangeService.releaseSyncHolds(mProviderContext,
+ AbstractSyncService.EXIT_SECURITY_FAILURE, null));
+ // There should be one left
+ assertEquals(1, errorMap.keySet().size());
+ // And this is the one
+ assertNotNull(errorMap.get(box2.mId));
+
+ // Release the i/o holds on account 2 (there aren't any)
+ assertFalse(exchangeService.releaseSyncHolds(mProviderContext,
+ AbstractSyncService.EXIT_IO_ERROR, acct2));
+ // There should still be one left
+ assertEquals(1, errorMap.keySet().size());
+
+ // Release the i/o holds on account 1 (there's one)
+ assertTrue(exchangeService.releaseSyncHolds(mProviderContext,
+ AbstractSyncService.EXIT_IO_ERROR, acct1));
+ // There should still be one left
+ assertEquals(0, errorMap.keySet().size());
+ }
+
+ public void testIsSyncable() {
+ Account acct1 = setupTestAccount("acct1", true);
+ Mailbox box1 = EmailContentSetupUtils.setupMailbox("box1", acct1.mId, true,
+ mProviderContext, Mailbox.TYPE_DRAFTS);
+ Mailbox box2 = EmailContentSetupUtils.setupMailbox("box2", acct1.mId, true,
+ mProviderContext, Mailbox.TYPE_OUTBOX);
+ Mailbox box3 = EmailContentSetupUtils.setupMailbox("box2", acct1.mId, true,
+ mProviderContext, Mailbox.TYPE_ATTACHMENT);
+ Mailbox box4 = EmailContentSetupUtils.setupMailbox("box2", acct1.mId, true,
+ mProviderContext, Mailbox.TYPE_NOT_SYNCABLE + 64);
+ Mailbox box5 = EmailContentSetupUtils.setupMailbox("box2", acct1.mId, true,
+ mProviderContext, Mailbox.TYPE_MAIL);
+ assertFalse(ExchangeService.isSyncable(box1));
+ assertFalse(ExchangeService.isSyncable(box2));
+ assertFalse(ExchangeService.isSyncable(box3));
+ assertFalse(ExchangeService.isSyncable(box4));
+ assertTrue(ExchangeService.isSyncable(box5));
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/ExchangeServiceTest.java b/exchange2/tests/src/com/android/exchange/ExchangeServiceTest.java
new file mode 100644
index 0000000..b4be23f
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/ExchangeServiceTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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 android.content.Context;
+import android.content.ContextWrapper;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.io.File;
+@SmallTest
+public class ExchangeServiceTest extends AndroidTestCase {
+ private static class MyContext extends ContextWrapper {
+ public boolean isGetFileStreamPathCalled;
+
+ public MyContext(Context base) {
+ super(base);
+ }
+
+ @Override
+ public File getFileStreamPath(String name) {
+ isGetFileStreamPathCalled = true;
+ return super.getFileStreamPath(name);
+ }
+ }
+
+ public void testGetDeviceId() throws Exception {
+ final MyContext context = new MyContext(getContext());
+
+ final String id = ExchangeService.getDeviceId(context);
+
+ // Consists of alpha-numeric
+ assertTrue(id.matches("^[a-zA-Z0-9]+$"));
+
+ // getDeviceId may have been called in other tests, so we don't check
+ // isGetFileStreamPathCalled here.
+
+ context.isGetFileStreamPathCalled = false;
+ final String cachedId = ExchangeService.getDeviceId(context);
+
+ // Should be the same.
+ assertEquals(id, cachedId);
+ // Should be cached. (If cached, this method won't be called.)
+ assertFalse(context.isGetFileStreamPathCalled);
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/RequestTests.java b/exchange2/tests/src/com/android/exchange/RequestTests.java
new file mode 100644
index 0000000..d736404
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/RequestTests.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import com.android.emailcommon.provider.EmailContent.Attachment;
+
+import android.test.AndroidTestCase;
+
+/**
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.RequestTests exchange
+ */
+public class RequestTests extends AndroidTestCase {
+
+ public void testPartRequestEquals() {
+ Attachment att1 = new Attachment();
+ att1.mId = 1;
+ Attachment att2 = new Attachment();
+ att2.mId = 2;
+ // For part requests, the attachment id's must be ==
+ PartRequest req1 = new PartRequest(att1, "dest1", "content1");
+ PartRequest req2 = new PartRequest(att2, "dest2", "content2");
+ assertFalse(req1.equals(req2));
+ Attachment att3 = new Attachment();
+ att3.mId = 1;
+ PartRequest req3 = new PartRequest(att3, "dest3", "content3");
+ assertTrue(req1.equals(req3));
+ MessageMoveRequest req4 = new MessageMoveRequest(10L, 12L);
+ assertFalse(req1.equals(req4));
+ }
+
+ public void testRequestEquals() {
+ // Only the messageId needs to be ==
+ MessageMoveRequest req1 = new MessageMoveRequest(1L, 10L);
+ MessageMoveRequest req2 = new MessageMoveRequest(1L, 11L);
+ assertTrue(req1.equals(req2));
+ MessageMoveRequest req3 = new MessageMoveRequest(2L, 11L);
+ assertFalse(req3.equals(req2));
+ MeetingResponseRequest req4 = new MeetingResponseRequest(1L, 3);
+ assertFalse(req4.equals(req1));
+ MeetingResponseRequest req5 = new MeetingResponseRequest(1L, 4);
+ assertTrue(req5.equals(req4));
+ }
+}
\ No newline at end of file
diff --git a/exchange2/tests/src/com/android/exchange/TagsTests.java b/exchange2/tests/src/com/android/exchange/TagsTests.java
new file mode 100644
index 0000000..55b27c0
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/TagsTests.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import com.android.exchange.adapter.Tags;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.HashMap;
+@SmallTest
+public class TagsTests extends AndroidTestCase {
+
+ // Make sure there are no duplicates in the tags table
+ // This test is no longer required - tags can be duplicated
+ public void disable_testNoDuplicates() {
+ String[][] allTags = Tags.pages;
+ HashMap<String, Boolean> map = new HashMap<String, Boolean>();
+ for (String[] page: allTags) {
+ for (String tag: page) {
+ assertTrue(tag, !map.containsKey(tag));
+ map.put(tag, true);
+ }
+ }
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/adapter/AttachmentLoaderTests.java b/exchange2/tests/src/com/android/exchange/adapter/AttachmentLoaderTests.java
new file mode 100644
index 0000000..2e16ac8
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/adapter/AttachmentLoaderTests.java
@@ -0,0 +1,40 @@
+/* Copyright (C) 2011 The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.adapter.AttachmentLoaderTests exchange
+ */
+package com.android.exchange.adapter;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+@SmallTest
+public class AttachmentLoaderTests extends AndroidTestCase {
+ private static final String TEST_LOCATION =
+ "Inbox/FW:%204G%20Netbook%20|%20Now%20Available%20for%20Order.EML/image012.jpg";
+
+ public void testEncodeForExchange2003() {
+ assertEquals("abc", AttachmentLoader.encodeForExchange2003("abc"));
+ // We don't encode the four characters after abc
+ assertEquals("abc_:/.", AttachmentLoader.encodeForExchange2003("abc_:/."));
+ // We don't re-encode escaped characters
+ assertEquals("%20%33", AttachmentLoader.encodeForExchange2003("%20%33"));
+ // Test with the location that failed in use
+ assertEquals(TEST_LOCATION.replace("|", "%7C"),
+ AttachmentLoader.encodeForExchange2003(TEST_LOCATION));
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java b/exchange2/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
new file mode 100644
index 0000000..2e2f553
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
@@ -0,0 +1,477 @@
+/*
+ * 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.adapter.AbstractSyncAdapter.Operation;
+import com.android.exchange.adapter.CalendarSyncAdapter.CalendarOperations;
+import com.android.exchange.adapter.CalendarSyncAdapter.EasCalendarSyncParser;
+import com.android.exchange.provider.MockProvider;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.os.RemoteException;
+import android.provider.CalendarContract.Attendees;
+import android.provider.CalendarContract.Events;
+import android.test.IsolatedContext;
+import android.test.RenamingDelegatingContext;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.TimeZone;
+
+/**
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.adapter.CalendarSyncAdapterTests exchange
+ */
+@MediumTest
+public class CalendarSyncAdapterTests extends SyncAdapterTestCase<CalendarSyncAdapter> {
+ private static final String[] ATTENDEE_PROJECTION = new String[] {Attendees.ATTENDEE_EMAIL,
+ Attendees.ATTENDEE_NAME, Attendees.ATTENDEE_STATUS};
+ private static final int ATTENDEE_EMAIL = 0;
+ private static final int ATTENDEE_NAME = 1;
+ private static final int ATTENDEE_STATUS = 2;
+
+ private static final String SINGLE_ATTENDEE_EMAIL = "attendee@host.com";
+ private static final String SINGLE_ATTENDEE_NAME = "Bill Attendee";
+
+ private Context mMockContext;
+ private MockContentResolver mMockResolver;
+
+ // This is the US/Pacific time zone as a base64-encoded TIME_ZONE_INFORMATION structure, as
+ // it would appear coming from an Exchange server
+ private static final String TEST_TIME_ZONE = "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByA" +
+ "GQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAY" +
+ "QBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==";
+
+ private class MockContext2 extends MockContext {
+
+ @Override
+ public Resources getResources() {
+ return getContext().getResources();
+ }
+
+ @Override
+ public File getDir(String name, int mode) {
+ // name the directory so the directory will be separated from
+ // one created through the regular Context
+ return getContext().getDir("mockcontext2_" + name, mode);
+ }
+
+ @Override
+ public Context getApplicationContext() {
+ return this;
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mMockResolver = new MockContentResolver();
+ final String filenamePrefix = "test.";
+ RenamingDelegatingContext targetContextWrapper = new
+ RenamingDelegatingContext(
+ new MockContext2(), // The context that most methods are delegated to
+ getContext(), // The context that file methods are delegated to
+ filenamePrefix);
+ mMockContext = new IsolatedContext(mMockResolver, targetContextWrapper);
+ mMockResolver.addProvider(MockProvider.AUTHORITY, new MockProvider(mMockContext));
+ }
+
+ public CalendarSyncAdapterTests() {
+ super();
+ }
+
+ public void testSetTimeRelatedValues_NonRecurring() throws IOException {
+ CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
+ EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
+ ContentValues cv = new ContentValues();
+ // Basic, one-time meeting lasting an hour
+ GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 10, 8, 30);
+ Long startTime = startCalendar.getTimeInMillis();
+ GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 10, 9, 30);
+ Long endTime = endCalendar.getTimeInMillis();
+
+ p.setTimeRelatedValues(cv, startTime, endTime, 0);
+ assertNull(cv.getAsInteger(Events.DURATION));
+ assertEquals(startTime, cv.getAsLong(Events.DTSTART));
+ assertEquals(endTime, cv.getAsLong(Events.DTEND));
+ assertEquals(endTime, cv.getAsLong(Events.LAST_DATE));
+ assertNull(cv.getAsString(Events.EVENT_TIMEZONE));
+ }
+
+ public void testSetTimeRelatedValues_Recurring() throws IOException {
+ CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
+ EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
+ ContentValues cv = new ContentValues();
+ // Recurring meeting lasting an hour
+ GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 10, 8, 30);
+ Long startTime = startCalendar.getTimeInMillis();
+ GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 10, 9, 30);
+ Long endTime = endCalendar.getTimeInMillis();
+ cv.put(Events.RRULE, "FREQ=DAILY");
+ p.setTimeRelatedValues(cv, startTime, endTime, 0);
+ assertEquals("P60M", cv.getAsString(Events.DURATION));
+ assertEquals(startTime, cv.getAsLong(Events.DTSTART));
+ assertNull(cv.getAsLong(Events.DTEND));
+ assertNull(cv.getAsLong(Events.LAST_DATE));
+ assertNull(cv.getAsString(Events.EVENT_TIMEZONE));
+ }
+
+ public void testSetTimeRelatedValues_AllDay() throws IOException {
+ CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
+ EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
+ ContentValues cv = new ContentValues();
+ GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 10, 8, 30);
+ Long startTime = startCalendar.getTimeInMillis();
+ GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 11, 8, 30);
+ Long endTime = endCalendar.getTimeInMillis();
+ cv.put(Events.RRULE, "FREQ=WEEKLY;BYDAY=MO");
+ p.setTimeRelatedValues(cv, startTime, endTime, 1);
+
+ // The start time should have hour/min/sec zero'd out
+ startCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+ startCalendar.set(2010, 5, 10, 0, 0, 0);
+ startCalendar.set(GregorianCalendar.MILLISECOND, 0);
+ startTime = startCalendar.getTimeInMillis();
+ assertEquals(startTime, cv.getAsLong(Events.DTSTART));
+
+ // The duration should be in days
+ assertEquals("P1D", cv.getAsString(Events.DURATION));
+ assertNull(cv.getAsLong(Events.DTEND));
+ assertNull(cv.getAsLong(Events.LAST_DATE));
+ // There must be a timezone
+ assertNotNull(cv.getAsString(Events.EVENT_TIMEZONE));
+ }
+
+ public void testSetTimeRelatedValues_Recurring_AllDay_Exception () throws IOException {
+ CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
+ EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
+ ContentValues cv = new ContentValues();
+
+ // Recurrence exception for all-day event; the exception is NOT all-day
+ GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 17, 8, 30);
+ Long startTime = startCalendar.getTimeInMillis();
+ GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 17, 9, 30);
+ Long endTime = endCalendar.getTimeInMillis();
+ cv.put(Events.ORIGINAL_ALL_DAY, 1);
+ GregorianCalendar instanceCalendar = new GregorianCalendar(2010, 5, 17, 8, 30);
+ cv.put(Events.ORIGINAL_INSTANCE_TIME, instanceCalendar.getTimeInMillis());
+ p.setTimeRelatedValues(cv, startTime, endTime, 0);
+
+ // The original instance time should have hour/min/sec zero'd out
+ GregorianCalendar testCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+ testCalendar.set(2010, 5, 17, 0, 0, 0);
+ testCalendar.set(GregorianCalendar.MILLISECOND, 0);
+ Long testTime = testCalendar.getTimeInMillis();
+ assertEquals(testTime, cv.getAsLong(Events.ORIGINAL_INSTANCE_TIME));
+
+ // The exception isn't all-day, so we should have DTEND and LAST_DATE and no EVENT_TIMEZONE
+ assertNull(cv.getAsString(Events.DURATION));
+ assertEquals(endTime, cv.getAsLong(Events.DTEND));
+ assertEquals(endTime, cv.getAsLong(Events.LAST_DATE));
+ assertNull(cv.getAsString(Events.EVENT_TIMEZONE));
+ }
+
+ public void testIsValidEventValues() throws IOException {
+ CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
+ EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
+
+ long validTime = System.currentTimeMillis();
+ String validData = "foo-bar-bletch";
+ String validDuration = "P30M";
+ String validRrule = "FREQ=DAILY";
+
+ ContentValues cv = new ContentValues();
+
+ cv.put(Events.DTSTART, validTime);
+ // Needs _SYNC_DATA and DTEND/DURATION
+ assertFalse(p.isValidEventValues(cv));
+ cv.put(Events.SYNC_DATA2, validData);
+ // Needs DTEND/DURATION since not an exception
+ assertFalse(p.isValidEventValues(cv));
+ cv.put(Events.DURATION, validDuration);
+ // Valid (DTSTART, _SYNC_DATA, DURATION)
+ assertTrue(p.isValidEventValues(cv));
+ cv.remove(Events.DURATION);
+ cv.put(Events.ORIGINAL_INSTANCE_TIME, validTime);
+ // Needs DTEND since it's an exception
+ assertFalse(p.isValidEventValues(cv));
+ cv.put(Events.DTEND, validTime);
+ // Valid (DTSTART, DTEND, ORIGINAL_INSTANCE_TIME)
+ cv.remove(Events.ORIGINAL_INSTANCE_TIME);
+ // Valid (DTSTART, _SYNC_DATA, DTEND)
+ assertTrue(p.isValidEventValues(cv));
+ cv.remove(Events.DTSTART);
+ // Needs DTSTART
+ assertFalse(p.isValidEventValues(cv));
+ cv.put(Events.DTSTART, validTime);
+ cv.put(Events.RRULE, validRrule);
+ // With RRULE, needs DURATION
+ assertFalse(p.isValidEventValues(cv));
+ cv.put(Events.DURATION, "P30M");
+ // Valid (DTSTART, RRULE, DURATION)
+ assertTrue(p.isValidEventValues(cv));
+ cv.put(Events.ALL_DAY, "1");
+ // Needs DURATION in the form P<n>D
+ assertFalse(p.isValidEventValues(cv));
+ // Valid (DTSTART, RRULE, ALL_DAY, DURATION(P<n>D)
+ cv.put(Events.DURATION, "P1D");
+ assertTrue(p.isValidEventValues(cv));
+ }
+
+ private void addAttendeesToSerializer(Serializer s, int num) throws IOException {
+ for (int i = 0; i < num; i++) {
+ s.start(Tags.CALENDAR_ATTENDEE);
+ s.data(Tags.CALENDAR_ATTENDEE_EMAIL, "frederick" + num +
+ ".flintstone@this.that.verylongservername.com");
+ s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1");
+ s.data(Tags.CALENDAR_ATTENDEE_NAME, "Frederick" + num + " Flintstone, III");
+ s.end();
+ }
+ }
+
+ private void addAttendeeToSerializer(Serializer s, String email, String name)
+ throws IOException {
+ s.start(Tags.CALENDAR_ATTENDEE);
+ s.data(Tags.CALENDAR_ATTENDEE_EMAIL, email);
+ s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1");
+ s.data(Tags.CALENDAR_ATTENDEE_NAME, name);
+ s.end();
+ }
+
+ private int countInsertOperationsForTable(CalendarOperations ops, String tableName) {
+ int cnt = 0;
+ for (Operation op: ops) {
+ ContentProviderOperation cpo =
+ AbstractSyncAdapter.operationToContentProviderOperation(op, 0);
+ List<String> segments = cpo.getUri().getPathSegments();
+ if (segments.get(0).equalsIgnoreCase(tableName) &&
+ cpo.getType() == ContentProviderOperation.TYPE_INSERT) {
+ cnt++;
+ }
+ }
+ return cnt;
+ }
+
+ class TestEvent extends Serializer {
+ CalendarSyncAdapter mAdapter;
+ EasCalendarSyncParser mParser;
+ Serializer mSerializer;
+
+ TestEvent() throws IOException {
+ super(false);
+ mAdapter = getTestSyncAdapter(CalendarSyncAdapter.class);
+ mParser = mAdapter.new EasCalendarSyncParser(getTestInputStream(), mAdapter);
+ }
+
+ void setUserEmailAddress(String addr) {
+ mAdapter.mAccount.mEmailAddress = addr;
+ mAdapter.mEmailAddress = addr;
+ }
+
+ EasCalendarSyncParser getParser() throws IOException {
+ // Set up our parser's input and eat the initial tag
+ mParser.resetInput(new ByteArrayInputStream(toByteArray()));
+ mParser.nextTag(0);
+ return mParser;
+ }
+
+ // setupPreAttendees and setupPostAttendees initialize calendar data in the order in which
+ // they would appear in an actual EAS session. Between these two calls, we initialize
+ // attendee data, which varies between the following tests
+ TestEvent setupPreAttendees() throws IOException {
+ start(Tags.SYNC_APPLICATION_DATA);
+ data(Tags.CALENDAR_TIME_ZONE, TEST_TIME_ZONE);
+ data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
+ data(Tags.CALENDAR_START_TIME, "20100518T220000Z");
+ data(Tags.CALENDAR_SUBJECT, "Documentation");
+ data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
+ data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
+ data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
+ return this;
+ }
+
+ TestEvent setupPostAttendees()throws IOException {
+ data(Tags.CALENDAR_LOCATION, "CR SF 601T2/North Shore Presentation Self Service (16)");
+ data(Tags.CALENDAR_END_TIME, "20100518T223000Z");
+ start(Tags.BASE_BODY);
+ data(Tags.BASE_BODY_PREFERENCE, "1");
+ data(Tags.BASE_ESTIMATED_DATA_SIZE, "69105"); // The number is ignored by the parser
+ data(Tags.BASE_DATA,
+ "This is the event description; we should probably make it longer");
+ end(); // BASE_BODY
+ start(Tags.CALENDAR_RECURRENCE);
+ data(Tags.CALENDAR_RECURRENCE_TYPE, "1"); // weekly
+ data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1");
+ data(Tags.CALENDAR_RECURRENCE_OCCURRENCES, "10");
+ data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, "12"); // tue, wed
+ data(Tags.CALENDAR_RECURRENCE_UNTIL, "2005-04-14T00:00:00.000Z");
+ end(); // CALENDAR_RECURRENCE
+ data(Tags.CALENDAR_SENSITIVITY, "0");
+ data(Tags.CALENDAR_BUSY_STATUS, "2");
+ data(Tags.CALENDAR_ALL_DAY_EVENT, "0");
+ data(Tags.CALENDAR_MEETING_STATUS, "3");
+ data(Tags.BASE_NATIVE_BODY_TYPE, "3");
+ end().done(); // SYNC_APPLICATION_DATA
+ return this;
+ }
+ }
+
+ public void testAddEvent() throws IOException {
+ TestEvent event = new TestEvent();
+ event.setupPreAttendees();
+ event.start(Tags.CALENDAR_ATTENDEES);
+ addAttendeesToSerializer(event, 10);
+ event.end(); // CALENDAR_ATTENDEES
+ event.setupPostAttendees();
+
+ EasCalendarSyncParser p = event.getParser();
+ p.addEvent(p.mOps, "1:1", false);
+ // There should be 1 event
+ assertEquals(1, countInsertOperationsForTable(p.mOps, "events"));
+ // Two attendees (organizer and 10 attendees)
+ assertEquals(11, countInsertOperationsForTable(p.mOps, "attendees"));
+ // dtstamp, meeting status, attendees, attendees redacted, and upsync prohibited
+ assertEquals(5, countInsertOperationsForTable(p.mOps, "extendedproperties"));
+ }
+
+ public void testAddEventIllegal() throws IOException {
+ // We don't send a start time; the event is illegal and nothing should be added
+ TestEvent event = new TestEvent();
+ event.start(Tags.SYNC_APPLICATION_DATA);
+ event.data(Tags.CALENDAR_TIME_ZONE, TEST_TIME_ZONE);
+ event.data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
+ event.data(Tags.CALENDAR_SUBJECT, "Documentation");
+ event.data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
+ event.data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
+ event.data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
+ event.start(Tags.CALENDAR_ATTENDEES);
+ addAttendeesToSerializer(event, 10);
+ event.end(); // CALENDAR_ATTENDEES
+ event.setupPostAttendees();
+
+ EasCalendarSyncParser p = event.getParser();
+ p.addEvent(p.mOps, "1:1", false);
+ assertEquals(0, countInsertOperationsForTable(p.mOps, "events"));
+ assertEquals(0, countInsertOperationsForTable(p.mOps, "attendees"));
+ assertEquals(0, countInsertOperationsForTable(p.mOps, "extendedproperties"));
+ }
+
+ public void testAddEventRedactedAttendees() throws IOException {
+ TestEvent event = new TestEvent();
+ event.setupPreAttendees();
+ event.start(Tags.CALENDAR_ATTENDEES);
+ addAttendeesToSerializer(event, 100);
+ event.end(); // CALENDAR_ATTENDEES
+ event.setupPostAttendees();
+
+ EasCalendarSyncParser p = event.getParser();
+ p.addEvent(p.mOps, "1:1", false);
+ // There should be 1 event
+ assertEquals(1, countInsertOperationsForTable(p.mOps, "events"));
+ // One attendees (organizer; all others are redacted)
+ assertEquals(1, countInsertOperationsForTable(p.mOps, "attendees"));
+ // dtstamp, meeting status, and attendees redacted
+ assertEquals(3, countInsertOperationsForTable(p.mOps, "extendedproperties"));
+ }
+
+ /**
+ * Setup for the following three tests, which check attendee status of an added event
+ * @param userEmail the email address of the user
+ * @param update whether or not the event is an update (rather than new)
+ * @return a Cursor to the Attendee records added to our MockProvider
+ * @throws IOException
+ * @throws RemoteException
+ * @throws OperationApplicationException
+ */
+ private Cursor setupAddEventOneAttendee(String userEmail, boolean update)
+ throws IOException, RemoteException, OperationApplicationException {
+ TestEvent event = new TestEvent();
+ event.setupPreAttendees();
+ event.start(Tags.CALENDAR_ATTENDEES);
+ addAttendeeToSerializer(event, SINGLE_ATTENDEE_EMAIL, SINGLE_ATTENDEE_NAME);
+ event.setUserEmailAddress(userEmail);
+ event.end(); // CALENDAR_ATTENDEES
+ event.setupPostAttendees();
+
+ EasCalendarSyncParser p = event.getParser();
+ p.addEvent(p.mOps, "1:1", update);
+ // Send the CPO's to the mock provider
+ ArrayList<ContentProviderOperation> cpos = new ArrayList<ContentProviderOperation>();
+ for (Operation op: p.mOps) {
+ cpos.add(AbstractSyncAdapter.operationToContentProviderOperation(op, 0));
+ }
+ mMockResolver.applyBatch(MockProvider.AUTHORITY, cpos);
+ return mMockResolver.query(MockProvider.uri(Attendees.CONTENT_URI), ATTENDEE_PROJECTION,
+ null, null, null);
+ }
+
+ public void testAddEventOneAttendee() throws IOException, RemoteException,
+ OperationApplicationException {
+ Cursor c = setupAddEventOneAttendee("foo@bar.com", false);
+ assertEquals(2, c.getCount());
+ // The organizer should be "accepted", the unknown attendee "none"
+ while (c.moveToNext()) {
+ if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) {
+ assertEquals(Attendees.ATTENDEE_STATUS_NONE, c.getInt(ATTENDEE_STATUS));
+ } else {
+ assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
+ }
+ }
+ }
+
+ public void testAddEventSelfAttendee() throws IOException, RemoteException,
+ OperationApplicationException {
+ Cursor c = setupAddEventOneAttendee(SINGLE_ATTENDEE_EMAIL, false);
+ // The organizer should be "accepted", and our user/attendee should be "done" even though
+ // the busy status = 2 (because we can't tell from a status of 2 on new events)
+ while (c.moveToNext()) {
+ if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) {
+ assertEquals(Attendees.ATTENDEE_STATUS_NONE, c.getInt(ATTENDEE_STATUS));
+ } else {
+ assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
+ }
+ }
+ }
+
+ public void testAddEventSelfAttendeeUpdate() throws IOException, RemoteException,
+ OperationApplicationException {
+ Cursor c = setupAddEventOneAttendee(SINGLE_ATTENDEE_EMAIL, true);
+ // The organizer should be "accepted", and our user/attendee should be "accepted" (because
+ // busy status = 2 and this is an update
+ while (c.moveToNext()) {
+ if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) {
+ assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
+ } else {
+ assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
+ }
+ }
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java b/exchange2/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
new file mode 100644
index 0000000..1ee64fe
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
@@ -0,0 +1,304 @@
+/*
+ * 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 android.content.ContentUris;
+import android.content.ContentValues;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.EmailContent.Body;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.EmailContent.MessageColumns;
+import com.android.emailcommon.provider.EmailContent.SyncColumns;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
+import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser.ServerChange;
+import com.android.exchange.provider.EmailContentSetupUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+@SmallTest
+public class EmailSyncAdapterTests extends SyncAdapterTestCase<EmailSyncAdapter> {
+
+ private static final String WHERE_ACCOUNT_KEY = Message.ACCOUNT_KEY + "=?";
+ private static final String[] ACCOUNT_ARGUMENT = new String[1];
+
+ // A server id that is guaranteed to be test-related
+ private static final String TEST_SERVER_ID = "__1:22";
+
+ public EmailSyncAdapterTests() {
+ super();
+ }
+
+ /**
+ * Check functionality for getting mime type from a file name (using its extension)
+ * The default for all unknown files is application/octet-stream
+ */
+ public void testGetMimeTypeFromFileName() throws IOException {
+ EasSyncService service = getTestService();
+ EmailSyncAdapter adapter = new EmailSyncAdapter(service);
+ EasEmailSyncParser p = adapter.new EasEmailSyncParser(getTestInputStream(), adapter);
+ // Test a few known types
+ String mimeType = p.getMimeTypeFromFileName("foo.jpg");
+ assertEquals("image/jpeg", mimeType);
+ // Make sure this is case insensitive
+ mimeType = p.getMimeTypeFromFileName("foo.JPG");
+ assertEquals("image/jpeg", mimeType);
+ mimeType = p.getMimeTypeFromFileName("this_is_a_weird_filename.gif");
+ assertEquals("image/gif", mimeType);
+ // Test an illegal file name ending with the extension prefix
+ mimeType = p.getMimeTypeFromFileName("foo.");
+ assertEquals("application/octet-stream", mimeType);
+ // Test a really awful name
+ mimeType = p.getMimeTypeFromFileName(".....");
+ assertEquals("application/octet-stream", mimeType);
+ // Test a bare file name (no extension)
+ mimeType = p.getMimeTypeFromFileName("foo");
+ assertEquals("application/octet-stream", mimeType);
+ // And no name at all (null isn't a valid input)
+ mimeType = p.getMimeTypeFromFileName("");
+ assertEquals("application/octet-stream", mimeType);
+ }
+
+ public void testFormatDateTime() throws IOException {
+ EmailSyncAdapter adapter = getTestSyncAdapter(EmailSyncAdapter.class);
+ GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
+ // Calendar is odd, months are zero based, so the first 11 below is December...
+ calendar.set(2008, 11, 11, 18, 19, 20);
+ String date = adapter.formatDateTime(calendar);
+ assertEquals("2008-12-11T18:19:20.000Z", date);
+ calendar.clear();
+ calendar.set(2012, 0, 2, 23, 0, 1);
+ date = adapter.formatDateTime(calendar);
+ assertEquals("2012-01-02T23:00:01.000Z", date);
+ }
+
+ public void testSendDeletedItems() throws IOException {
+ setupAccountMailboxAndMessages(0);
+ // Setup our adapter and parser
+ setupSyncParserAndAdapter(mAccount, mMailbox);
+
+ Serializer s = new Serializer();
+ ArrayList<Long> ids = new ArrayList<Long>();
+ ArrayList<Long> deletedIds = new ArrayList<Long>();
+
+ // Create account and two mailboxes
+ mSyncAdapter.mAccount = mAccount;
+ Mailbox box1 = EmailContentSetupUtils.setupMailbox("box1", mAccount.mId, true,
+ mProviderContext);
+ mSyncAdapter.mMailbox = box1;
+
+ // Create 3 messages
+ Message msg1 = EmailContentSetupUtils.setupMessage("message1", mAccount.mId, box1.mId,
+ true, true, mProviderContext);
+ ids.add(msg1.mId);
+ Message msg2 = EmailContentSetupUtils.setupMessage("message2", mAccount.mId, box1.mId,
+ true, true, mProviderContext);
+ ids.add(msg2.mId);
+ Message msg3 = EmailContentSetupUtils.setupMessage("message3", mAccount.mId, box1.mId,
+ true, true, mProviderContext);
+ ids.add(msg3.mId);
+ assertEquals(3, EmailContent.count(mProviderContext, Message.CONTENT_URI, WHERE_ACCOUNT_KEY,
+ getAccountArgument(mAccount.mId)));
+
+ // Delete them
+ for (long id: ids) {
+ mResolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id),
+ null, null);
+ }
+
+ // Confirm that the messages are in the proper table
+ assertEquals(0, EmailContent.count(mProviderContext, Message.CONTENT_URI, WHERE_ACCOUNT_KEY,
+ getAccountArgument(mAccount.mId)));
+ assertEquals(3, EmailContent.count(mProviderContext, Message.DELETED_CONTENT_URI,
+ WHERE_ACCOUNT_KEY, getAccountArgument(mAccount.mId)));
+
+ // Call code to send deletions; the id's of the ones actually deleted will be in the
+ // deletedIds list
+ mSyncAdapter.sendDeletedItems(s, deletedIds, true);
+ assertEquals(3, deletedIds.size());
+
+ // Clear this out for the next test
+ deletedIds.clear();
+
+ // Create a new message
+ Message msg4 = EmailContentSetupUtils.setupMessage("message4", mAccount.mId, box1.mId,
+ true, true, mProviderContext);
+ assertEquals(1, EmailContent.count(mProviderContext, Message.CONTENT_URI, WHERE_ACCOUNT_KEY,
+ getAccountArgument(mAccount.mId)));
+ // Find the body for this message
+ Body body = Body.restoreBodyWithMessageId(mProviderContext, msg4.mId);
+ // Set its source message to msg2's id
+ ContentValues values = new ContentValues();
+ values.put(Body.SOURCE_MESSAGE_KEY, msg2.mId);
+ body.update(mProviderContext, values);
+
+ // Now send deletions again; this time only two should get deleted; msg2 should NOT be
+ // deleted as it's referenced by msg4
+ mSyncAdapter.sendDeletedItems(s, deletedIds, true);
+ assertEquals(2, deletedIds.size());
+ assertFalse(deletedIds.contains(msg2.mId));
+ }
+
+ private String[] getAccountArgument(long id) {
+ ACCOUNT_ARGUMENT[0] = Long.toString(id);
+ return ACCOUNT_ARGUMENT;
+ }
+
+ void setupSyncParserAndAdapter(Account account, Mailbox mailbox) throws IOException {
+ EasSyncService service = getTestService(account, mailbox);
+ mSyncAdapter = new EmailSyncAdapter(service);
+ mSyncParser = mSyncAdapter.new EasEmailSyncParser(getTestInputStream(), mSyncAdapter);
+ }
+
+ ArrayList<Long> setupAccountMailboxAndMessages(int numMessages) {
+ ArrayList<Long> ids = new ArrayList<Long>();
+
+ // Create account and two mailboxes
+ mAccount = EmailContentSetupUtils.setupAccount("account", true, mProviderContext);
+ mMailbox = EmailContentSetupUtils.setupMailbox("box1", mAccount.mId, true,
+ mProviderContext);
+
+ for (int i = 0; i < numMessages; i++) {
+ Message msg = EmailContentSetupUtils.setupMessage("message" + i, mAccount.mId,
+ mMailbox.mId, true, true, mProviderContext);
+ ids.add(msg.mId);
+ }
+
+ assertEquals(numMessages, EmailContent.count(mProviderContext, Message.CONTENT_URI,
+ WHERE_ACCOUNT_KEY, getAccountArgument(mAccount.mId)));
+ return ids;
+ }
+
+ public void testDeleteParser() throws IOException {
+ // Setup some messages
+ ArrayList<Long> messageIds = setupAccountMailboxAndMessages(3);
+ ContentValues cv = new ContentValues();
+ cv.put(SyncColumns.SERVER_ID, TEST_SERVER_ID);
+ long deleteMessageId = messageIds.get(1);
+ mResolver.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, TEST_SERVER_ID).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();
+ int randomFlags = Message.FLAG_INCOMING_MEETING_CANCEL | Message.FLAG_TYPE_FORWARD;
+ cv.put(SyncColumns.SERVER_ID, TEST_SERVER_ID);
+ cv.put(MessageColumns.FLAGS, randomFlags);
+ long changeMessageId = messageIds.get(1);
+ mResolver.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 TEST_SERVER_ID 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, TEST_SERVER_ID);
+ s.start(Tags.SYNC_APPLICATION_DATA);
+ s.data(Tags.EMAIL_READ, "0");
+ s.data(Tags.EMAIL2_LAST_VERB_EXECUTED,
+ Integer.toString(EmailSyncAdapter.LAST_VERB_FORWARD));
+ s.end().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);
+ // Make sure we see the forwarded flag AND that the original flags are preserved
+ assertEquals((Integer)(randomFlags | Message.FLAG_FORWARDED), change.flags);
+ }
+
+ 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);
+ mResolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id),
+ null, null);
+ mSyncAdapter.mDeletedIdList.add(id);
+ id = messageIds.get(1);
+ mResolver.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);
+ mResolver.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(mProviderContext, Message.CONTENT_URI, WHERE_ACCOUNT_KEY,
+ getAccountArgument(mAccount.mId)));
+
+ // As well, the two deletions and one update
+ assertEquals(2, EmailContent.count(mProviderContext, Message.DELETED_CONTENT_URI,
+ WHERE_ACCOUNT_KEY, getAccountArgument(mAccount.mId)));
+ assertEquals(1, EmailContent.count(mProviderContext, Message.UPDATED_CONTENT_URI,
+ WHERE_ACCOUNT_KEY, getAccountArgument(mAccount.mId)));
+
+ // Cleanup (i.e. after sync); should remove items from delete/update tables
+ mSyncAdapter.cleanup();
+
+ // The three should be gone
+ assertEquals(0, EmailContent.count(mProviderContext, Message.DELETED_CONTENT_URI,
+ WHERE_ACCOUNT_KEY, getAccountArgument(mAccount.mId)));
+ assertEquals(0, EmailContent.count(mProviderContext, Message.UPDATED_CONTENT_URI,
+ WHERE_ACCOUNT_KEY, getAccountArgument(mAccount.mId)));
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/adapter/FolderSyncParserTests.java b/exchange2/tests/src/com/android/exchange/adapter/FolderSyncParserTests.java
new file mode 100644
index 0000000..dc5d15b
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/adapter/FolderSyncParserTests.java
@@ -0,0 +1,418 @@
+/*
+ * 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 android.content.ContentResolver;
+import android.content.res.AssetManager;
+import android.database.Cursor;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.service.SyncWindow;
+import com.android.exchange.CommandStatusException;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.provider.EmailContentSetupUtils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+
+/**
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.adapter.FolderSyncParserTests exchange
+ */
+@MediumTest
+public class FolderSyncParserTests extends SyncAdapterTestCase<EmailSyncAdapter> {
+
+ // We increment this to generate unique server id's
+ private int mServerIdCount = 0;
+ private final long mCreationTime = System.currentTimeMillis();
+ private final String[] mMailboxQueryArgs = new String[2];
+
+ public FolderSyncParserTests() {
+ super();
+ }
+
+ public void testIsValidMailFolder() throws IOException {
+ EasSyncService service = getTestService();
+ EmailSyncAdapter adapter = new EmailSyncAdapter(service);
+ FolderSyncParser parser = new FolderSyncParser(getTestInputStream(), adapter);
+ HashMap<String, Mailbox> mailboxMap = new HashMap<String, Mailbox>();
+ // The parser needs the mAccount set
+ parser.mAccount = mAccount;
+ mAccount.save(getContext());
+
+ // Don't save the box; just create it, and give it a server id
+ Mailbox boxMailType = EmailContentSetupUtils.setupMailbox("box1", mAccount.mId, false,
+ mProviderContext, Mailbox.TYPE_MAIL);
+ boxMailType.mServerId = "__1:1";
+ // Automatically valid since TYPE_MAIL
+ assertTrue(parser.isValidMailFolder(boxMailType, mailboxMap));
+
+ Mailbox boxCalendarType = EmailContentSetupUtils.setupMailbox("box", mAccount.mId, false,
+ mProviderContext, Mailbox.TYPE_CALENDAR);
+ Mailbox boxContactsType = EmailContentSetupUtils.setupMailbox("box", mAccount.mId, false,
+ mProviderContext, Mailbox.TYPE_CONTACTS);
+ Mailbox boxTasksType = EmailContentSetupUtils.setupMailbox("box", mAccount.mId, false,
+ mProviderContext, Mailbox.TYPE_TASKS);
+ // Automatically invalid since TYPE_CALENDAR and TYPE_CONTACTS
+ assertFalse(parser.isValidMailFolder(boxCalendarType, mailboxMap));
+ assertFalse(parser.isValidMailFolder(boxContactsType, mailboxMap));
+ assertFalse(parser.isValidMailFolder(boxTasksType, mailboxMap));
+
+ // Unknown boxes are invalid unless they have a parent that's valid
+ Mailbox boxUnknownType = EmailContentSetupUtils.setupMailbox("box", mAccount.mId, false,
+ mProviderContext, Mailbox.TYPE_UNKNOWN);
+ assertFalse(parser.isValidMailFolder(boxUnknownType, mailboxMap));
+ boxUnknownType.mParentServerId = boxMailType.mServerId;
+ // We shouldn't find the parent yet
+ assertFalse(parser.isValidMailFolder(boxUnknownType, mailboxMap));
+ // Put the mailbox in the map; the unknown box should now be valid
+ mailboxMap.put(boxMailType.mServerId, boxMailType);
+ assertTrue(parser.isValidMailFolder(boxUnknownType, mailboxMap));
+
+ // Clear the map, but save away the parent box
+ mailboxMap.clear();
+ assertFalse(parser.isValidMailFolder(boxUnknownType, mailboxMap));
+ boxMailType.save(mProviderContext);
+ // The box should now be valid
+ assertTrue(parser.isValidMailFolder(boxUnknownType, mailboxMap));
+
+ // Somewhat harder case. The parent will be in the map, but also unknown. The parent's
+ // parent will be in the database.
+ Mailbox boxParentUnknownType = EmailContentSetupUtils.setupMailbox("box", mAccount.mId,
+ false, mProviderContext, Mailbox.TYPE_UNKNOWN);
+ assertFalse(parser.isValidMailFolder(boxParentUnknownType, mailboxMap));
+ // Give the unknown type parent a parent (boxMailType)
+ boxParentUnknownType.mServerId = "__1:2";
+ boxParentUnknownType.mParentServerId = boxMailType.mServerId;
+ // Give our unknown box an unknown parent
+ boxUnknownType.mParentServerId = boxParentUnknownType.mServerId;
+ // Confirm the box is still invalid
+ assertFalse(parser.isValidMailFolder(boxUnknownType, mailboxMap));
+ // Put the unknown type parent into the mailbox map
+ mailboxMap.put(boxParentUnknownType.mServerId, boxParentUnknownType);
+ // Our unknown box should now be valid, because 1) the parent is unknown, BUT 2) the
+ // parent's parent is a mail type
+ assertTrue(parser.isValidMailFolder(boxUnknownType, mailboxMap));
+ }
+
+ private Mailbox setupBoxSync(int interval, int lookback, String serverId) {
+ // Don't save the box; just create it, and give it a server id
+ Mailbox box = EmailContentSetupUtils.setupMailbox("box1", mAccount.mId, false,
+ mProviderContext, Mailbox.TYPE_MAIL);
+ box.mSyncInterval = interval;
+ box.mSyncLookback = lookback;
+ if (serverId != null) {
+ box.mServerId = serverId;
+ } else {
+ box.mServerId = "serverId-" + mCreationTime + '-' + mServerIdCount++;
+ }
+ box.save(mProviderContext);
+ return box;
+ }
+
+ private boolean syncOptionsSame(Mailbox a, Mailbox b) {
+ if (a.mSyncInterval != b.mSyncInterval) return false;
+ if (a.mSyncLookback != b.mSyncLookback) return false;
+ return true;
+ }
+
+ public void testSaveAndRestoreMailboxSyncOptions() throws IOException {
+ EasSyncService service = getTestService();
+ EmailSyncAdapter adapter = new EmailSyncAdapter(service);
+ FolderSyncParser parser = new FolderSyncParser(getTestInputStream(), adapter);
+ mAccount.save(mProviderContext);
+
+ parser.mAccount = mAccount;
+ parser.mAccountId = mAccount.mId;
+ parser.mAccountIdAsString = Long.toString(mAccount.mId);
+ parser.mContext = mProviderContext;
+ parser.mContentResolver = mProviderContext.getContentResolver();
+
+ // Don't save the box; just create it, and give it a server id
+ Mailbox box1 = setupBoxSync(Account.CHECK_INTERVAL_NEVER, SyncWindow.SYNC_WINDOW_UNKNOWN,
+ null);
+ Mailbox box2 = setupBoxSync(Account.CHECK_INTERVAL_NEVER, SyncWindow.SYNC_WINDOW_UNKNOWN,
+ null);
+ Mailbox boxa = setupBoxSync(Account.CHECK_INTERVAL_NEVER, SyncWindow.SYNC_WINDOW_1_MONTH,
+ null);
+ Mailbox boxb = setupBoxSync(Account.CHECK_INTERVAL_NEVER, SyncWindow.SYNC_WINDOW_2_WEEKS,
+ null);
+ Mailbox boxc = setupBoxSync(Account.CHECK_INTERVAL_PUSH, SyncWindow.SYNC_WINDOW_UNKNOWN,
+ null);
+ Mailbox boxd = setupBoxSync(Account.CHECK_INTERVAL_PUSH, SyncWindow.SYNC_WINDOW_UNKNOWN,
+ null);
+ Mailbox boxe = setupBoxSync(Account.CHECK_INTERVAL_PUSH, SyncWindow.SYNC_WINDOW_1_DAY,
+ null);
+
+ // Save the options (for a, b, c, d, e);
+ parser.saveMailboxSyncOptions();
+ // There should be 5 entries in the map, and they should be the correct ones
+ assertNotNull(parser.mSyncOptionsMap.get(boxa.mServerId));
+ assertNotNull(parser.mSyncOptionsMap.get(boxb.mServerId));
+ assertNotNull(parser.mSyncOptionsMap.get(boxc.mServerId));
+ assertNotNull(parser.mSyncOptionsMap.get(boxd.mServerId));
+ assertNotNull(parser.mSyncOptionsMap.get(boxe.mServerId));
+
+ // Delete all the mailboxes in the account
+ ContentResolver cr = mProviderContext.getContentResolver();
+ cr.delete(Mailbox.CONTENT_URI, Mailbox.ACCOUNT_KEY + "=?",
+ new String[] {parser.mAccountIdAsString});
+
+ // Create new boxes, all with default values for interval & window
+ Mailbox box1x = setupBoxSync(Account.CHECK_INTERVAL_NEVER, SyncWindow.SYNC_WINDOW_UNKNOWN,
+ box1.mServerId);
+ Mailbox box2x = setupBoxSync(Account.CHECK_INTERVAL_NEVER, SyncWindow.SYNC_WINDOW_UNKNOWN,
+ box2.mServerId);
+ Mailbox boxax = setupBoxSync(Account.CHECK_INTERVAL_NEVER, SyncWindow.SYNC_WINDOW_UNKNOWN,
+ boxa.mServerId);
+ Mailbox boxbx = setupBoxSync(Account.CHECK_INTERVAL_NEVER, SyncWindow.SYNC_WINDOW_UNKNOWN,
+ boxb.mServerId);
+ Mailbox boxcx = setupBoxSync(Account.CHECK_INTERVAL_NEVER, SyncWindow.SYNC_WINDOW_UNKNOWN,
+ boxc.mServerId);
+ Mailbox boxdx = setupBoxSync(Account.CHECK_INTERVAL_NEVER, SyncWindow.SYNC_WINDOW_UNKNOWN,
+ boxd.mServerId);
+ Mailbox boxex = setupBoxSync(Account.CHECK_INTERVAL_NEVER, SyncWindow.SYNC_WINDOW_UNKNOWN,
+ boxe.mServerId);
+
+ // Restore the sync options
+ parser.restoreMailboxSyncOptions();
+ box1x = Mailbox.restoreMailboxWithId(mProviderContext, box1x.mId);
+ box2x = Mailbox.restoreMailboxWithId(mProviderContext, box2x.mId);
+ boxax = Mailbox.restoreMailboxWithId(mProviderContext, boxax.mId);
+ boxbx = Mailbox.restoreMailboxWithId(mProviderContext, boxbx.mId);
+ boxcx = Mailbox.restoreMailboxWithId(mProviderContext, boxcx.mId);
+ boxdx = Mailbox.restoreMailboxWithId(mProviderContext, boxdx.mId);
+ boxex = Mailbox.restoreMailboxWithId(mProviderContext, boxex.mId);
+
+ assertTrue(syncOptionsSame(box1, box1x));
+ assertTrue(syncOptionsSame(box2, box2x));
+ assertTrue(syncOptionsSame(boxa, boxax));
+ assertTrue(syncOptionsSame(boxb, boxbx));
+ assertTrue(syncOptionsSame(boxc, boxcx));
+ assertTrue(syncOptionsSame(boxd, boxdx));
+ assertTrue(syncOptionsSame(boxe, boxex));
+ }
+
+ private static class MockFolderSyncParser extends FolderSyncParser {
+ private BufferedReader mReader;
+ private int mDepth = 0;
+ private String[] mStack = new String[32];
+ private HashMap<String, Integer> mTagMap;
+
+
+ public MockFolderSyncParser(String fileName, AbstractSyncAdapter adapter)
+ throws IOException {
+ super(null, adapter);
+ AssetManager am = mContext.getAssets();
+ InputStream is = am.open(fileName);
+ if (is != null) {
+ mReader = new BufferedReader(new InputStreamReader(is));
+ }
+ mInUnitTest = true;
+ }
+
+ private void initTagMap() {
+ mTagMap = new HashMap<String, Integer>();
+ int pageNum = 0;
+ for (String[] page: Tags.pages) {
+ int tagNum = 5;
+ for (String tag: page) {
+ if (mTagMap.containsKey(tag)) {
+ System.err.println("Duplicate tag: " + tag);
+ }
+ int val = (pageNum << Tags.PAGE_SHIFT) + tagNum;
+ mTagMap.put(tag, val);
+ tagNum++;
+ }
+ pageNum++;
+ }
+ }
+
+ private int lookupTag(String tagName) {
+ if (mTagMap == null) {
+ initTagMap();
+ }
+ int res = mTagMap.get(tagName);
+ return res;
+ }
+
+ private String getLine() throws IOException {
+ while (true) {
+ String line = mReader.readLine();
+ if (line == null) {
+ return null;
+ }
+ int start = line.indexOf("| ");
+ if (start > 2) {
+ return line.substring(start + 2);
+ }
+ // Keep looking for a suitable line
+ }
+ }
+
+ @Override
+ public int getValueInt() throws IOException {
+ return Integer.parseInt(getValue());
+ }
+
+ @Override
+ public String getValue() throws IOException {
+ String line = getLine();
+ if (line == null) throw new IOException();
+ int start = line.indexOf(": ");
+ if (start < 0) throw new IOException("Line has no value: " + line);
+ try {
+ return line.substring(start + 2).trim();
+ } finally {
+ if (nextTag(0) != END) {
+ throw new IOException("Value not followed by end tag: " + name);
+ }
+ }
+ }
+
+ @Override
+ public void skipTag() throws IOException {
+ if (nextTag(0) == -1) {
+ nextTag(0);
+ }
+ }
+
+ @Override
+ public int nextTag(int endingTag) throws IOException {
+ String line = getLine();
+ if (line == null) {
+ return DONE;
+ }
+ if (line.startsWith("</")) {
+ int end = line.indexOf('>');
+ String tagName = line.substring(2, end).trim();
+ if (!tagName.equals(mStack[--mDepth])) {
+ throw new IOException("Tag end doesn't match tag");
+ }
+ mStack[mDepth] = null;
+ return END;
+ } else if (line.startsWith("<")) {
+ int end = line.indexOf('>');
+ String tagName = line.substring(1, end).trim();
+ mStack[mDepth++] = tagName;
+ tag = lookupTag(tagName);
+ return tag;
+ } else {
+ return -1;
+ }
+ }
+ }
+
+ private Mailbox getMailboxWithName(String folderName) {
+ mMailboxQueryArgs[1] = folderName;
+ Cursor c = mResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
+ Mailbox.ACCOUNT_KEY + "=? AND " + Mailbox.DISPLAY_NAME + "=?", mMailboxQueryArgs,
+ null);
+ try {
+ assertTrue(c.getCount() == 1);
+ c.moveToFirst();
+ Mailbox m = new Mailbox();
+ m.restore(c);
+ return m;
+ } finally {
+ c.close();
+ }
+ }
+
+ private boolean isTopLevel(String folderName) {
+ Mailbox m = getMailboxWithName(folderName);
+ assertNotNull(m);
+ return m.mParentKey == Mailbox.NO_MAILBOX;
+ }
+
+ private boolean isSubfolder(String parentName, String childName) {
+ Mailbox parent = getMailboxWithName(parentName);
+ Mailbox child = getMailboxWithName(childName);
+ assertNotNull(parent);
+ assertNotNull(child);
+ assertTrue((parent.mFlags & Mailbox.FLAG_HAS_CHILDREN) != 0);
+ return child.mParentKey == parent.mId;
+ }
+
+ /**
+ * Parse a set of EAS FolderSync commands and create the Mailbox tree accordingly
+ *
+ * @param fileName the name of the file containing emaillog data for folder sync
+ * @throws IOException
+ * @throws CommandStatusException
+ */
+ private void testComplexFolderListParse(String fileName) throws IOException,
+ CommandStatusException {
+ EasSyncService service = getTestService();
+ EmailSyncAdapter adapter = new EmailSyncAdapter(service);
+ FolderSyncParser parser = new MockFolderSyncParser(fileName, adapter);
+ mAccount.save(mProviderContext);
+ mMailboxQueryArgs[0] = Long.toString(mAccount.mId);
+ parser.mAccount = mAccount;
+ parser.mAccountId = mAccount.mId;
+ parser.mAccountIdAsString = Long.toString(mAccount.mId);
+ parser.mContext = mProviderContext;
+ parser.mContentResolver = mResolver;
+
+ parser.parse();
+
+ assertTrue(isTopLevel("Inbox"));
+ assertTrue(isSubfolder("Inbox", "Gecko"));
+ assertTrue(isSubfolder("Inbox", "Wombat"));
+ assertTrue(isSubfolder("Inbox", "Laslo"));
+ assertTrue(isSubfolder("Inbox", "Tomorrow"));
+ assertTrue(isSubfolder("Inbox", "Vader"));
+ assertTrue(isSubfolder("Inbox", "Personal"));
+ assertTrue(isSubfolder("Laslo", "Lego"));
+ assertTrue(isSubfolder("Tomorrow", "HomeRun"));
+ assertTrue(isSubfolder("Tomorrow", "Services"));
+ assertTrue(isSubfolder("HomeRun", "Review"));
+ assertTrue(isSubfolder("Vader", "Max"));
+ assertTrue(isSubfolder("Vader", "Parser"));
+ assertTrue(isSubfolder("Vader", "Scott"));
+ assertTrue(isSubfolder("Vader", "Surfing"));
+ assertTrue(isSubfolder("Max", "Thomas"));
+ assertTrue(isSubfolder("Personal", "Famine"));
+ assertTrue(isSubfolder("Personal", "Bar"));
+ assertTrue(isSubfolder("Personal", "Bill"));
+ assertTrue(isSubfolder("Personal", "Boss"));
+ assertTrue(isSubfolder("Personal", "Houston"));
+ assertTrue(isSubfolder("Personal", "Mistake"));
+ assertTrue(isSubfolder("Personal", "Online"));
+ assertTrue(isSubfolder("Personal", "Sports"));
+ assertTrue(isSubfolder("Famine", "Buffalo"));
+ assertTrue(isSubfolder("Famine", "CornedBeef"));
+ assertTrue(isSubfolder("Houston", "Rebar"));
+ assertTrue(isSubfolder("Mistake", "Intro"));
+ }
+
+ // FolderSyncParserTest.txt is based on customer data (all names changed) that failed to
+ // properly create the Mailbox list
+ public void testComplexFolderListParse1() throws CommandStatusException, IOException {
+ testComplexFolderListParse("FolderSyncParserTest.txt");
+ }
+
+ // As above, with the order changed (putting children before parents; a more difficult case
+ public void testComplexFolderListParse2() throws CommandStatusException, IOException {
+ testComplexFolderListParse("FolderSyncParserTest2.txt");
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/adapter/ProvisionParserTests.java b/exchange2/tests/src/com/android/exchange/adapter/ProvisionParserTests.java
new file mode 100644
index 0000000..95b76a6
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/adapter/ProvisionParserTests.java
@@ -0,0 +1,147 @@
+/*
+ * 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.emailcommon.provider.Policy;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+/**
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.adapter.ProvisionParserTests exchange
+ */
+@SmallTest
+public class ProvisionParserTests extends SyncAdapterTestCase {
+ private final ByteArrayInputStream mTestInputStream =
+ new ByteArrayInputStream("ABCDEFG".getBytes());
+
+ // A good sample of an Exchange 2003 (WAP) provisioning document for end-to-end testing
+ private String mWapProvisioningDoc1 =
+ "<wap-provisioningdoc>" +
+ "<characteristic type=\"SecurityPolicy\"><parm name=\"4131\" value=\"0\"/>" +
+ "</characteristic>" +
+ "<characteristic type=\"Registry\">" +
+ "<characteristic type=\"HKLM\\Comm\\Security\\Policy\\LASSD\\AE\\" +
+ "{50C13377-C66D-400C-889E-C316FC4AB374}\">" +
+ "<parm name=\"AEFrequencyType\" value=\"1\"/>" +
+ "<parm name=\"AEFrequencyValue\" value=\"5\"/>" +
+ "</characteristic>" +
+ "<characteristic type=\"HKLM\\Comm\\Security\\Policy\\LASSD\">" +
+ "<parm name=\"DeviceWipeThreshold\" value=\"20\"/>" +
+ "</characteristic>" +
+ "<characteristic type=\"HKLM\\Comm\\Security\\Policy\\LASSD\">" +
+ "<parm name=\"CodewordFrequency\" value=\"5\"/>" +
+ "</characteristic>" +
+ "<characteristic type=\"HKLM\\Comm\\Security\\Policy\\LASSD\\LAP\\lap_pw\">" +
+ "<parm name=\"MinimumPasswordLength\" value=\"8\"/>" +
+ "</characteristic>" +
+ "<characteristic type=\"HKLM\\Comm\\Security\\Policy\\LASSD\\LAP\\lap_pw\">" +
+ "<parm name=\"PasswordComplexity\" value=\"0\"/>" +
+ "</characteristic>" +
+ "</characteristic>" +
+ "</wap-provisioningdoc>";
+
+ // Provisioning document with passwords turned off
+ private String mWapProvisioningDoc2 =
+ "<wap-provisioningdoc>" +
+ "<characteristic type=\"SecurityPolicy\"><parm name=\"4131\" value=\"1\"/>" +
+ "</characteristic>" +
+ "<characteristic type=\"Registry\">" +
+ "<characteristic type=\"HKLM\\Comm\\Security\\Policy\\LASSD\\AE\\" +
+ "{50C13377-C66D-400C-889E-C316FC4AB374}\">" +
+ "<parm name=\"AEFrequencyType\" value=\"0\"/>" +
+ "<parm name=\"AEFrequencyValue\" value=\"5\"/>" +
+ "</characteristic>" +
+ "<characteristic type=\"HKLM\\Comm\\Security\\Policy\\LASSD\">" +
+ "<parm name=\"DeviceWipeThreshold\" value=\"20\"/>" +
+ "</characteristic>" +
+ "<characteristic type=\"HKLM\\Comm\\Security\\Policy\\LASSD\">" +
+ "<parm name=\"CodewordFrequency\" value=\"5\"/>" +
+ "</characteristic>" +
+ "<characteristic type=\"HKLM\\Comm\\Security\\Policy\\LASSD\\LAP\\lap_pw\">" +
+ "<parm name=\"MinimumPasswordLength\" value=\"8\"/>" +
+ "</characteristic>" +
+ "<characteristic type=\"HKLM\\Comm\\Security\\Policy\\LASSD\\LAP\\lap_pw\">" +
+ "<parm name=\"PasswordComplexity\" value=\"0\"/>" +
+ "</characteristic>" +
+ "</characteristic>" +
+ "</wap-provisioningdoc>";
+
+ // Provisioning document with simple password, 4 chars, 5 failures
+ private String mWapProvisioningDoc3 =
+ "<wap-provisioningdoc>" +
+ "<characteristic type=\"SecurityPolicy\"><parm name=\"4131\" value=\"0\"/>" +
+ "</characteristic>" +
+ "<characteristic type=\"Registry\">" +
+ "<characteristic type=\"HKLM\\Comm\\Security\\Policy\\LASSD\\AE\\" +
+ "{50C13377-C66D-400C-889E-C316FC4AB374}\">" +
+ "<parm name=\"AEFrequencyType\" value=\"1\"/>" +
+ "<parm name=\"AEFrequencyValue\" value=\"2\"/>" +
+ "</characteristic>" +
+ "<characteristic type=\"HKLM\\Comm\\Security\\Policy\\LASSD\">" +
+ "<parm name=\"DeviceWipeThreshold\" value=\"5\"/>" +
+ "</characteristic>" +
+ "<characteristic type=\"HKLM\\Comm\\Security\\Policy\\LASSD\">" +
+ "<parm name=\"CodewordFrequency\" value=\"5\"/>" +
+ "</characteristic>" +
+ "<characteristic type=\"HKLM\\Comm\\Security\\Policy\\LASSD\\LAP\\lap_pw\">" +
+ "<parm name=\"MinimumPasswordLength\" value=\"4\"/>" +
+ "</characteristic>" +
+ "<characteristic type=\"HKLM\\Comm\\Security\\Policy\\LASSD\\LAP\\lap_pw\">" +
+ "<parm name=\"PasswordComplexity\" value=\"1\"/>" +
+ "</characteristic>" +
+ "</characteristic>" +
+ "</wap-provisioningdoc>";
+
+ public void testWapProvisionParser1() throws IOException {
+ ProvisionParser parser = new ProvisionParser(mTestInputStream, getTestService());
+ parser.parseProvisionDocXml(mWapProvisioningDoc1);
+ Policy policy = parser.getPolicy();
+ assertNotNull(policy);
+ // Check the settings to make sure they were parsed correctly
+ assertEquals(5*60, policy.mMaxScreenLockTime); // Screen lock time is in seconds
+ assertEquals(8, policy.mPasswordMinLength);
+ assertEquals(Policy.PASSWORD_MODE_STRONG, policy.mPasswordMode);
+ assertEquals(20, policy.mPasswordMaxFails);
+ assertTrue(policy.mRequireRemoteWipe);
+ }
+
+ public void testWapProvisionParser2() throws IOException {
+ ProvisionParser parser = new ProvisionParser(mTestInputStream, getTestService());
+ parser.parseProvisionDocXml(mWapProvisioningDoc2);
+ Policy policy = parser.getPolicy();
+ assertNotNull(policy);
+ // Password should be set to none; others are ignored in this case.
+ assertEquals(Policy.PASSWORD_MODE_NONE, policy.mPasswordMode);
+ }
+
+ public void testWapProvisionParser3() throws IOException {
+ ProvisionParser parser = new ProvisionParser(mTestInputStream, getTestService());
+ parser.parseProvisionDocXml(mWapProvisioningDoc3);
+ Policy policy = parser.getPolicy();
+ assertNotNull(policy);
+ // Password should be set to simple
+ assertEquals(2*60, policy.mMaxScreenLockTime); // Screen lock time is in seconds
+ assertEquals(4, policy.mPasswordMinLength);
+ assertEquals(Policy.PASSWORD_MODE_SIMPLE, policy.mPasswordMode);
+ assertEquals(5, policy.mPasswordMaxFails);
+ assertTrue(policy.mRequireRemoteWipe);
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/adapter/SerializerTests.java b/exchange2/tests/src/com/android/exchange/adapter/SerializerTests.java
new file mode 100644
index 0000000..c26bcab
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/adapter/SerializerTests.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import android.content.ContentValues;
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+/** You can run this entire test case with:
+ * runtest -c com.android.exchange.adapter.SerializerTests exchange
+ */
+public class SerializerTests extends AndroidTestCase {
+
+ private static final byte[] BYTE_ARRAY = new byte[] {1, 2, 3, 4, 5};
+ private static final int BYTE_ARRAY_LENGTH = 5;
+ private static final String ID = "ID";
+ private static final String KEY = "Key";
+
+ // Basic test for use of start, end, tag, data, opaque, and done
+ public void testSerializer() throws IOException {
+ ContentValues values = new ContentValues();
+ // Create a test stream
+ Serializer s = new Serializer();
+ s.start(Tags.COMPOSE_SEND_MAIL);
+
+ // Test writeStringValue without and with data
+ s.writeStringValue(values, KEY, Tags.COMPOSE_ACCOUNT_ID);
+ values.put(KEY, ID);
+ s.writeStringValue(values, KEY, Tags.COMPOSE_ACCOUNT_ID);
+
+ s.data(Tags.COMPOSE_CLIENT_ID, ID);
+ s.tag(Tags.COMPOSE_SAVE_IN_SENT_ITEMS);
+ s.start(Tags.COMPOSE_MIME);
+ s.opaque(new ByteArrayInputStream(BYTE_ARRAY), BYTE_ARRAY_LENGTH);
+ s.end(); // COMPOSE_MIME
+ s.end(); // COMPOSE_SEND_MAIL
+ s.done(); // DOCUMENT
+ // Get the bytes for the stream
+ byte[] bytes = s.toByteArray();
+ // These are the expected bytes (self-explanatory)
+ byte[] expectedBytes = new byte[] {
+ 3, // Version 1.3
+ 1, // unknown or missing public identifier
+ 106, // UTF-8
+ 0, // String array length
+ Wbxml.SWITCH_PAGE,
+ Tags.COMPOSE,
+ Tags.COMPOSE_SEND_MAIL - Tags.COMPOSE_PAGE + Wbxml.WITH_CONTENT,
+ Tags.COMPOSE_ACCOUNT_ID - Tags.COMPOSE_PAGE,
+ Tags.COMPOSE_ACCOUNT_ID - Tags.COMPOSE_PAGE + Wbxml.WITH_CONTENT,
+ Wbxml.STR_I, // 0-terminated string
+ (byte)ID.charAt(0),
+ (byte)ID.charAt(1),
+ 0,
+ Wbxml.END, // COMPOSE_ACCOUNT_ID
+ Tags.COMPOSE_CLIENT_ID - Tags.COMPOSE_PAGE + Wbxml.WITH_CONTENT,
+ Wbxml.STR_I, // 0-terminated string
+ (byte)ID.charAt(0),
+ (byte)ID.charAt(1),
+ 0,
+ Wbxml.END, // COMPOSE_CLIENT_ID
+ Tags.COMPOSE_SAVE_IN_SENT_ITEMS - Tags.COMPOSE_PAGE,
+ Tags.COMPOSE_MIME - Tags.COMPOSE_PAGE + Wbxml.WITH_CONTENT,
+ (byte)Wbxml.OPAQUE,
+ BYTE_ARRAY_LENGTH,
+ BYTE_ARRAY[0],
+ BYTE_ARRAY[1],
+ BYTE_ARRAY[2],
+ BYTE_ARRAY[3],
+ BYTE_ARRAY[4],
+ Wbxml.END, // COMPOSE_MIME
+ Wbxml.END // COMPOSE_SEND_MAIL
+ };
+ // Make sure we get what's expected
+ MoreAsserts.assertEquals("Serializer mismatch", bytes, expectedBytes);
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java b/exchange2/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
new file mode 100644
index 0000000..e8553ca
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
@@ -0,0 +1,110 @@
+/*
+ * 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 android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.test.AndroidTestCase;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.adapter.AbstractSyncAdapter;
+import com.android.exchange.adapter.EmailSyncAdapter;
+import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+public class SyncAdapterTestCase<T extends AbstractSyncAdapter> extends AndroidTestCase {
+ public Context mContext;
+ public Context mProviderContext;
+ public ContentResolver mResolver;
+ public Mailbox mMailbox;
+ public Account mAccount;
+ public EmailSyncAdapter mSyncAdapter;
+ public EasEmailSyncParser mSyncParser;
+
+ public SyncAdapterTestCase() {
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mContext = getContext();
+ // This could be used with a MockContext if we switch over
+ mProviderContext = mContext;
+ mResolver = mContext.getContentResolver();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ // If we've created and saved an account, delete it
+ if (mAccount != null && mAccount.mId > 0) {
+ mResolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, mAccount.mId), null,
+ null);
+ }
+ }
+
+ /**
+ * 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() {
+ mAccount = new Account();
+ mAccount.mEmailAddress = "__test__@android.com";
+ mAccount.mId = -1;
+ Mailbox mailbox = new Mailbox();
+ mailbox.mId = -1;
+ return getTestService(mAccount, mailbox);
+ }
+
+ EasSyncService getTestService(Account account, Mailbox mailbox) {
+ EasSyncService service = new EasSyncService();
+ service.mContext = mContext;
+ service.mMailbox = mailbox;
+ service.mAccount = account;
+ service.mContentResolver = mContext.getContentResolver();
+ return service;
+ }
+
+ protected T getTestSyncAdapter(Class<T> klass) {
+ EasSyncService service = getTestService();
+ Constructor<T> c;
+ try {
+ c = klass.getDeclaredConstructor(new Class[] {EasSyncService.class});
+ return c.newInstance(service);
+ } catch (SecurityException e) {
+ } catch (NoSuchMethodException e) {
+ } catch (IllegalArgumentException e) {
+ } catch (InstantiationException e) {
+ } catch (IllegalAccessException e) {
+ } catch (InvocationTargetException e) {
+ }
+ return null;
+ }
+
+}
diff --git a/exchange2/tests/src/com/android/exchange/provider/EmailContentSetupUtils.java b/exchange2/tests/src/com/android/exchange/provider/EmailContentSetupUtils.java
new file mode 100644
index 0000000..c302b02
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/provider/EmailContentSetupUtils.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.provider;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.provider.Mailbox;
+
+import android.content.Context;
+
+/**
+ * Simplified EmailContent class setup (condensed from ProviderTestUtils in com.android.email)
+ */
+public class EmailContentSetupUtils {
+
+ /**
+ * No constructor - statics only
+ */
+ private EmailContentSetupUtils() {
+ }
+
+ /**
+ * Create an account for test purposes
+ */
+ public static Account setupAccount(String name, boolean saveIt, Context context) {
+ Account account = new Account();
+
+ account.mDisplayName = name;
+ account.mEmailAddress = name + "@android.com";
+ account.mProtocolVersion = "2.5" + name;
+ if (saveIt) {
+ account.save(context);
+ }
+ return account;
+ }
+
+ /**
+ * Create a mailbox for test purposes
+ */
+ public static Mailbox setupMailbox(String name, long accountId, boolean saveIt,
+ Context context) {
+ return setupMailbox(name, accountId, saveIt, context, Mailbox.TYPE_MAIL, null);
+ }
+
+ public static Mailbox setupMailbox(String name, long accountId, boolean saveIt,
+ Context context, int type) {
+ return setupMailbox(name, accountId, saveIt, context, type, null);
+ }
+
+ public static Mailbox setupMailbox(String name, long accountId, boolean saveIt,
+ Context context, int type, Mailbox parentBox) {
+ Mailbox box = new Mailbox();
+
+ box.mDisplayName = name;
+ box.mAccountKey = accountId;
+ box.mSyncKey = "sync-key-" + name;
+ box.mSyncLookback = 2;
+ box.mSyncInterval = Account.CHECK_INTERVAL_NEVER;
+ box.mType = type;
+ box.mServerId = "serverid-" + name;
+ box.mParentServerId = parentBox != null ? parentBox.mServerId : "parent-serverid-" + name;
+
+ if (saveIt) {
+ box.save(context);
+ }
+ return box;
+ }
+
+ /**
+ * Create a message for test purposes
+ */
+ public static Message setupMessage(String name, long accountId, long mailboxId,
+ boolean addBody, boolean saveIt, Context context) {
+ // Default starred, read, (backword compatibility)
+ return setupMessage(name, accountId, mailboxId, addBody, saveIt, context, true, true);
+ }
+
+ /**
+ * Create a message for test purposes
+ */
+ public static Message setupMessage(String name, long accountId, long mailboxId,
+ boolean addBody, boolean saveIt, Context context, boolean starred, boolean read) {
+ Message message = new Message();
+
+ message.mDisplayName = name;
+ message.mMailboxKey = mailboxId;
+ message.mAccountKey = accountId;
+ message.mFlagRead = read;
+ message.mFlagLoaded = Message.FLAG_LOADED_UNLOADED;
+ message.mFlagFavorite = starred;
+ message.mServerId = "serverid " + name;
+
+ if (addBody) {
+ message.mText = "body text " + name;
+ message.mHtml = "body html " + name;
+ }
+
+ if (saveIt) {
+ message.save(context);
+ }
+ return message;
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java b/exchange2/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java
new file mode 100644
index 0000000..e4bd791
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java
@@ -0,0 +1,146 @@
+/*
+ * 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.provider;
+
+import com.android.emailcommon.mail.PackedString;
+import com.android.emailcommon.provider.Account;
+import com.android.exchange.provider.GalResult.GalData;
+import com.android.exchange.utility.ExchangeTestCase;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.test.suitebuilder.annotation.SmallTest;
+
+/**
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.provider.ExchangeDirectoryProviderTests exchange
+ */
+@SmallTest
+public class ExchangeDirectoryProviderTests extends ExchangeTestCase {
+
+ public ExchangeDirectoryProviderTests() {
+ }
+
+ // Create a test projection; we should only get back values for display name and email address
+ private static final String[] GAL_RESULT_PROJECTION =
+ new String[] {Contacts.DISPLAY_NAME, CommonDataKinds.Email.ADDRESS, Contacts.CONTENT_TYPE,
+ Contacts.LOOKUP_KEY};
+ private static final int GAL_RESULT_COLUMN_DISPLAY_NAME = 0;
+ private static final int GAL_RESULT_COLUMN_EMAIL_ADDRESS = 1;
+ private static final int GAL_RESULT_COLUMN_CONTENT_TYPE = 2;
+ private static final int GAL_RESULT_COLUMN_LOOKUP_KEY = 3;
+
+ public void testBuildSimpleGalResultCursor() {
+ GalResult result = new GalResult();
+ result.addGalData(1, "Alice Aardvark", "alice@aardvark.com");
+ result.addGalData(2, "Bob Badger", "bob@badger.com");
+ result.addGalData(3, "Clark Cougar", "clark@cougar.com");
+ result.addGalData(4, "Dan Dolphin", "dan@dolphin.com");
+ // Make sure our returned cursor has the expected contents
+ ExchangeDirectoryProvider provider = new ExchangeDirectoryProvider();
+ Cursor c = provider.buildGalResultCursor(GAL_RESULT_PROJECTION, result);
+ assertNotNull(c);
+ assertEquals(MatrixCursor.class, c.getClass());
+ assertEquals(4, c.getCount());
+ for (int i = 0; i < 4; i++) {
+ GalData data = result.galData.get(i);
+ assertTrue(c.moveToNext());
+ assertEquals(data.displayName, c.getString(GAL_RESULT_COLUMN_DISPLAY_NAME));
+ assertEquals(data.emailAddress, c.getString(GAL_RESULT_COLUMN_EMAIL_ADDRESS));
+ assertNull(c.getString(GAL_RESULT_COLUMN_CONTENT_TYPE));
+ }
+ }
+
+ private static final String[][] DISPLAY_NAME_TEST_FIELDS = {
+ {"Alice", "Aardvark", "Another Name"},
+ {"Alice", "Aardvark", null},
+ {"Alice", null, null},
+ {null, "Aardvark", null},
+ {null, null, null}
+ };
+ private static final int TEST_FIELD_FIRST_NAME = 0;
+ private static final int TEST_FIELD_LAST_NAME = 1;
+ private static final int TEST_FIELD_DISPLAY_NAME = 2;
+ private static final String[] EXPECTED_DISPLAY_NAMES = new String[] {"Another Name",
+ "Alice Aardvark", "Alice", "Aardvark", null};
+
+ private GalResult getTestDisplayNameResult() {
+ GalResult result = new GalResult();
+ for (int i = 0; i < DISPLAY_NAME_TEST_FIELDS.length; i++) {
+ GalData galData = new GalData();
+ String[] names = DISPLAY_NAME_TEST_FIELDS[i];
+ galData.put(GalData.FIRST_NAME, names[TEST_FIELD_FIRST_NAME]);
+ galData.put(GalData.LAST_NAME, names[TEST_FIELD_LAST_NAME]);
+ galData.put(GalData.DISPLAY_NAME, names[TEST_FIELD_DISPLAY_NAME]);
+ result.addGalData(galData);
+ }
+ return result;
+ }
+
+ public void testDisplayNameLogic() {
+ GalResult result = getTestDisplayNameResult();
+ // Make sure our returned cursor has the expected contents
+ ExchangeDirectoryProvider provider = new ExchangeDirectoryProvider();
+ Cursor c = provider.buildGalResultCursor(GAL_RESULT_PROJECTION, result);
+ assertNotNull(c);
+ assertEquals(MatrixCursor.class, c.getClass());
+ assertEquals(DISPLAY_NAME_TEST_FIELDS.length, c.getCount());
+ for (int i = 0; i < EXPECTED_DISPLAY_NAMES.length; i++) {
+ assertTrue(c.moveToNext());
+ assertEquals(EXPECTED_DISPLAY_NAMES[i], c.getString(GAL_RESULT_COLUMN_DISPLAY_NAME));
+ }
+ }
+
+ public void testLookupKeyLogic() {
+ GalResult result = getTestDisplayNameResult();
+ // Make sure our returned cursor has the expected contents
+ ExchangeDirectoryProvider provider = new ExchangeDirectoryProvider();
+ Cursor c = provider.buildGalResultCursor(GAL_RESULT_PROJECTION, result);
+ assertNotNull(c);
+ assertEquals(MatrixCursor.class, c.getClass());
+ assertEquals(DISPLAY_NAME_TEST_FIELDS.length, c.getCount());
+ for (int i = 0; i < EXPECTED_DISPLAY_NAMES.length; i++) {
+ assertTrue(c.moveToNext());
+ PackedString ps =
+ new PackedString(Uri.decode(c.getString(GAL_RESULT_COLUMN_LOOKUP_KEY)));
+ String[] testFields = DISPLAY_NAME_TEST_FIELDS[i];
+ assertEquals(testFields[TEST_FIELD_FIRST_NAME], ps.get(GalData.FIRST_NAME));
+ assertEquals(testFields[TEST_FIELD_LAST_NAME], ps.get(GalData.LAST_NAME));
+ assertEquals(EXPECTED_DISPLAY_NAMES[i], ps.get(GalData.DISPLAY_NAME));
+ }
+ }
+
+ public void testGetAccountIdByName() {
+ Context context = getContext(); //getMockContext();
+ ExchangeDirectoryProvider provider = new ExchangeDirectoryProvider();
+ // Nothing up my sleeve
+ assertNull(provider.mAccountIdMap.get("foo@android.com"));
+ assertNull(provider.mAccountIdMap.get("bar@android.com"));
+ // Create accounts; the email addresses will be the first argument + "@android.com"
+ Account acctFoo = setupTestAccount("foo", true);
+ Account acctBar = setupTestAccount("bar", true);
+ // Make sure we can retrieve them, and that the map is populated
+ assertEquals(acctFoo.mId, provider.getAccountIdByName(context, "foo@android.com"));
+ assertEquals(acctBar.mId, provider.getAccountIdByName(context, "bar@android.com"));
+ assertEquals((Long)acctFoo.mId, provider.mAccountIdMap.get("foo@android.com"));
+ assertEquals((Long)acctBar.mId, provider.mAccountIdMap.get("bar@android.com"));
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/provider/MailboxUtilitiesTests.java b/exchange2/tests/src/com/android/exchange/provider/MailboxUtilitiesTests.java
new file mode 100644
index 0000000..a7425d7
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/provider/MailboxUtilitiesTests.java
@@ -0,0 +1,762 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.provider;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.exchange.utility.ExchangeTestCase;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.test.suitebuilder.annotation.MediumTest;
+
+/**
+ * Tests of MailboxUtilities.
+ *
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.provider.MailboxUtilitiesTests exchange
+ */
+@MediumTest
+public class MailboxUtilitiesTests extends ExchangeTestCase {
+
+ // All tests must build their accounts in mAccount so it will be deleted from live data
+ private Account mAccount;
+ private ContentResolver mResolver;
+ private ContentValues mNullParentKey;
+
+ // Flag sets found in regular email folders that are parents or children
+ private static final int PARENT_FLAGS =
+ Mailbox.FLAG_ACCEPTS_MOVED_MAIL | Mailbox.FLAG_HOLDS_MAIL |
+ Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE;
+ private static final int CHILD_FLAGS =
+ Mailbox.FLAG_ACCEPTS_MOVED_MAIL | Mailbox.FLAG_HOLDS_MAIL;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mAccount = null;
+ mResolver = mProviderContext.getContentResolver();
+ mNullParentKey = new ContentValues();
+ mNullParentKey.putNull(Mailbox.PARENT_KEY);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ // If we've created and saved an account, delete it
+ if (mAccount != null && mAccount.mId > 0) {
+ mResolver.delete(
+ ContentUris.withAppendedId(Account.CONTENT_URI, mAccount.mId), null, null);
+ }
+ }
+
+ public void testSetupParentKeyAndFlag() {
+ // Set up account and various mailboxes with/without parents
+ mAccount = setupTestAccount("acct1", true);
+ Mailbox box1 = EmailContentSetupUtils.setupMailbox("box1", mAccount.mId, true,
+ mProviderContext, Mailbox.TYPE_DRAFTS);
+ Mailbox box2 = EmailContentSetupUtils.setupMailbox("box2", mAccount.mId, true,
+ mProviderContext, Mailbox.TYPE_OUTBOX, box1);
+ Mailbox box3 = EmailContentSetupUtils.setupMailbox("box3", mAccount.mId, true,
+ mProviderContext, Mailbox.TYPE_ATTACHMENT, box1);
+ Mailbox box4 = EmailContentSetupUtils.setupMailbox("box4", mAccount.mId, true,
+ mProviderContext, Mailbox.TYPE_NOT_SYNCABLE + 64, box3);
+ Mailbox box5 = EmailContentSetupUtils.setupMailbox("box5", mAccount.mId, true,
+ mProviderContext, Mailbox.TYPE_MAIL, box3);
+
+ // To make this work, we need to manually set parentKey to null for all mailboxes
+ // This simulates an older database needing update
+ mResolver.update(Mailbox.CONTENT_URI, mNullParentKey, null, null);
+
+ // Hand-create the account selector for our account
+ String accountSelector = MailboxColumns.ACCOUNT_KEY + " IN (" + mAccount.mId + ")";
+
+ // Fix up the database and restore the mailboxes
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector);
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+ box4 = Mailbox.restoreMailboxWithId(mProviderContext, box4.mId);
+ box5 = Mailbox.restoreMailboxWithId(mProviderContext, box5.mId);
+
+ // Test that flags and parent key are set properly
+ assertEquals(Mailbox.FLAG_HOLDS_MAIL | Mailbox.FLAG_HAS_CHILDREN |
+ Mailbox.FLAG_CHILDREN_VISIBLE, box1.mFlags);
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(Mailbox.FLAG_HOLDS_MAIL, box2.mFlags);
+ assertEquals(box1.mId, box2.mParentKey);
+
+ assertEquals(Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE, box3.mFlags);
+ assertEquals(box1.mId, box3.mParentKey);
+
+ assertEquals(0, box4.mFlags);
+ assertEquals(box3.mId, box4.mParentKey);
+
+ assertEquals(Mailbox.FLAG_HOLDS_MAIL | Mailbox.FLAG_ACCEPTS_MOVED_MAIL, box5.mFlags);
+ assertEquals(box3.mId, box5.mParentKey);
+ }
+
+ private void simulateFolderSyncChangeHandling(String accountSelector, Mailbox...mailboxes) {
+ // Run the parent key analyzer and reload the mailboxes
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector);
+ for (Mailbox mailbox: mailboxes) {
+ String serverId = mailbox.mServerId;
+ MailboxUtilities.setFlagsAndChildrensParentKey(mProviderContext, accountSelector,
+ serverId);
+ }
+ }
+
+ /**
+ * Test three cases of adding a folder to an existing hierarchy. Case 1: Add to parent.
+ */
+ public void testParentKeyAddFolder1() {
+ // Set up account and various mailboxes with/without parents
+ mAccount = setupTestAccount("acct1", true);
+ String accountSelector = MailboxColumns.ACCOUNT_KEY + " IN (" + mAccount.mId + ")";
+
+ Mailbox box1 = EmailContentSetupUtils.setupMailbox(
+ "box1", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL);
+ Mailbox box2 = EmailContentSetupUtils.setupMailbox(
+ "box2", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL, box1);
+
+ // Manually set parentKey to null for all mailboxes, as if an initial sync or post-upgrade
+ mResolver.update(Mailbox.CONTENT_URI, mNullParentKey, null, null);
+
+ // Run the parent key analyzer to set up the initial hierarchy.
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector);
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box1.mFlags);
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box2.mFlags);
+ assertEquals(box1.mId, box2.mParentKey);
+
+ // The specific test: Create a 3rd mailbox and attach it to box1 (already a parent)
+
+ Mailbox box3 = EmailContentSetupUtils.setupMailbox(
+ "box3", mAccount.mId, false, mProviderContext, Mailbox.TYPE_MAIL, box1);
+ box3.mParentKey = Mailbox.PARENT_KEY_UNINITIALIZED;
+ box3.save(mProviderContext);
+ simulateFolderSyncChangeHandling(accountSelector, box1 /*box3's parent*/);
+
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box1.mFlags);
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box2.mFlags);
+ assertEquals(box1.mId, box2.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box3.mFlags);
+ assertEquals(box1.mId, box3.mParentKey);
+ }
+
+ /**
+ * Test three cases of adding a folder to an existing hierarchy. Case 2: Add to child.
+ */
+ public void testParentKeyAddFolder2() {
+ // Set up account and various mailboxes with/without parents
+ mAccount = setupTestAccount("acct1", true);
+ String accountSelector = MailboxColumns.ACCOUNT_KEY + " IN (" + mAccount.mId + ")";
+
+ Mailbox box1 = EmailContentSetupUtils.setupMailbox(
+ "box1", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL);
+ Mailbox box2 = EmailContentSetupUtils.setupMailbox(
+ "box2", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL, box1);
+
+ // Manually set parentKey to null for all mailboxes, as if an initial sync or post-upgrade
+ mResolver.update(Mailbox.CONTENT_URI, mNullParentKey, null, null);
+
+ // Run the parent key analyzer to set up the initial hierarchy.
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector);
+
+ // Skipping tests of initial hierarchy - see testParentKeyAddFolder1()
+
+ // The specific test: Create a 3rd mailbox and attach it to box2 (currently a child)
+
+ Mailbox box3 = EmailContentSetupUtils.setupMailbox(
+ "box3", mAccount.mId, false, mProviderContext, Mailbox.TYPE_MAIL, box2);
+ box3.mParentKey = Mailbox.PARENT_KEY_UNINITIALIZED;
+ box3.save(mProviderContext);
+ simulateFolderSyncChangeHandling(accountSelector, box3 /*box3's parent*/);
+
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box1.mFlags);
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(PARENT_FLAGS, box2.mFlags); // should become a parent
+ assertEquals(box1.mId, box2.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box3.mFlags); // should be child of box2
+ assertEquals(box2.mId, box3.mParentKey);
+ }
+
+ /**
+ * Test three cases of adding a folder to an existing hierarchy. Case 3: Add to root.
+ */
+ public void testParentKeyAddFolder3() {
+ // Set up account and various mailboxes with/without parents
+ mAccount = setupTestAccount("acct1", true);
+ String accountSelector = MailboxColumns.ACCOUNT_KEY + " IN (" + mAccount.mId + ")";
+
+ Mailbox box1 = EmailContentSetupUtils.setupMailbox(
+ "box1", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL);
+ Mailbox box2 = EmailContentSetupUtils.setupMailbox(
+ "box2", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL, box1);
+
+ // Manually set parentKey to null for all mailboxes, as if an initial sync or post-upgrade
+ mResolver.update(Mailbox.CONTENT_URI, mNullParentKey, null, null);
+
+ // Run the parent key analyzer to set up the initial hierarchy.
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector);
+
+ // Skipping tests of initial hierarchy - see testParentKeyAddFolder1()
+
+ // The specific test: Create a 3rd mailbox and give it no parent (it's top-level).
+
+ Mailbox box3 = EmailContentSetupUtils.setupMailbox(
+ "box3", mAccount.mId, false, mProviderContext, Mailbox.TYPE_MAIL);
+ box3.mParentKey = Mailbox.PARENT_KEY_UNINITIALIZED;
+ box3.save(mProviderContext);
+
+ simulateFolderSyncChangeHandling(accountSelector);
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box1.mFlags);
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box2.mFlags);
+ assertEquals(box1.mId, box2.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box3.mFlags);
+ assertEquals(-1, box3.mParentKey);
+ }
+
+ /**
+ * Test three cases of removing a folder from the hierarchy. Case 1: Remove from parent.
+ */
+ public void testParentKeyRemoveFolder1() {
+ // Set up account and mailboxes
+ mAccount = setupTestAccount("acct1", true);
+ String accountSelector = MailboxColumns.ACCOUNT_KEY + " IN (" + mAccount.mId + ")";
+
+ // Initial configuration for this test: box1 has two children.
+ Mailbox box1 = EmailContentSetupUtils.setupMailbox(
+ "box1", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL);
+ Mailbox box2 = EmailContentSetupUtils.setupMailbox(
+ "box2", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL, box1);
+ Mailbox box3 = EmailContentSetupUtils.setupMailbox(
+ "box3", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL, box1);
+
+ // Manually set parentKey to null for all mailboxes, as if an initial sync or post-upgrade
+ mResolver.update(Mailbox.CONTENT_URI, mNullParentKey, null, null);
+
+ // Confirm initial configuration as expected
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector);
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box1.mFlags);
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box2.mFlags);
+ assertEquals(box1.mId, box2.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box3.mFlags);
+ assertEquals(box1.mId, box3.mParentKey);
+
+ // The specific test: Delete box3 and check remaining configuration
+ mResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, box3.mId), null, null);
+ simulateFolderSyncChangeHandling(accountSelector, box1 /*box3's parent*/);
+
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box1.mFlags); // Should still be a parent
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box2.mFlags);
+ assertEquals(box1.mId, box2.mParentKey);
+
+ assertNull(box3);
+ }
+
+ /**
+ * Test three cases of removing a folder from the hierarchy. Case 2: Remove from child.
+ */
+ public void testParentKeyRemoveFolder2() {
+ // Set up account and mailboxes
+ mAccount = setupTestAccount("acct1", true);
+ String accountSelector = MailboxColumns.ACCOUNT_KEY + " IN (" + mAccount.mId + ")";
+
+ // Initial configuration for this test: box1 has box2 and box2 has box3.
+ Mailbox box1 = EmailContentSetupUtils.setupMailbox(
+ "box1", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL);
+ Mailbox box2 = EmailContentSetupUtils.setupMailbox(
+ "box2", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL, box1);
+ Mailbox box3 = EmailContentSetupUtils.setupMailbox(
+ "box3", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL, box2);
+
+ // Manually set parentKey to null for all mailboxes, as if an initial sync or post-upgrade
+ mResolver.update(Mailbox.CONTENT_URI, mNullParentKey, null, null);
+
+ // Confirm initial configuration as expected
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector);
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box1.mFlags);
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(PARENT_FLAGS, box2.mFlags); // becomes a parent
+ assertEquals(box1.mId, box2.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box3.mFlags);
+ assertEquals(box2.mId, box3.mParentKey);
+
+ // The specific test: Delete box3 and check remaining configuration
+ mResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, box3.mId), null, null);
+ simulateFolderSyncChangeHandling(accountSelector, box2 /*box3's parent*/);
+
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box1.mFlags); // Should still be a parent
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box2.mFlags); // Becomes a child
+ assertEquals(box1.mId, box2.mParentKey);
+
+ assertNull(box3);
+ }
+
+ /**
+ * Test three cases of removing a folder from the hierarchy. Case 3: Remove from root.
+ */
+ public void testParentKeyRemoveFolder3() {
+ // Set up account and mailboxes
+ mAccount = setupTestAccount("acct1", true);
+ String accountSelector = MailboxColumns.ACCOUNT_KEY + " IN (" + mAccount.mId + ")";
+
+ // Initial configuration for this test: box1 has box2, box3 is also at root.
+ Mailbox box1 = EmailContentSetupUtils.setupMailbox(
+ "box1", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL);
+ Mailbox box2 = EmailContentSetupUtils.setupMailbox(
+ "box2", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL, box1);
+ Mailbox box3 = EmailContentSetupUtils.setupMailbox(
+ "box3", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL);
+
+ // Manually set parentKey to null for all mailboxes, as if an initial sync or post-upgrade
+ mResolver.update(Mailbox.CONTENT_URI, mNullParentKey, null, null);
+
+ // Confirm initial configuration as expected
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector);
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box1.mFlags);
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box2.mFlags);
+ assertEquals(box1.mId, box2.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box3.mFlags);
+ assertEquals(-1, box3.mParentKey);
+
+ // The specific test: Delete box3 and check remaining configuration
+ mResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, box3.mId), null, null);
+ simulateFolderSyncChangeHandling(accountSelector);
+
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box1.mFlags); // Should still be a parent
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box2.mFlags); // Should still be a child
+ assertEquals(box1.mId, box2.mParentKey);
+
+ assertNull(box3);
+ }
+
+ /**
+ * Test changing a parent from none
+ */
+ public void testChangeFromNoParentToParent() {
+ // Set up account and mailboxes
+ mAccount = setupTestAccount("acct1", true);
+ String accountSelector = MailboxColumns.ACCOUNT_KEY + " IN (" + mAccount.mId + ")";
+
+ // Initial configuration for this test: box1 has box2, box3 is also at root.
+ Mailbox box1 = EmailContentSetupUtils.setupMailbox(
+ "box1", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL);
+ Mailbox box2 = EmailContentSetupUtils.setupMailbox(
+ "box2", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL, box1);
+ Mailbox box3 = EmailContentSetupUtils.setupMailbox(
+ "box3", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL);
+
+ // Manually set parentKey to null for all mailboxes, as if an initial sync or post-upgrade
+ mResolver.update(Mailbox.CONTENT_URI, mNullParentKey, null, null);
+
+ // Confirm initial configuration as expected
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector);
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box1.mFlags);
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box2.mFlags);
+ assertEquals(box1.mId, box2.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box3.mFlags);
+ assertEquals(-1, box3.mParentKey);
+
+ // The specific test: Give box 3 a new parent (box 2) and check remaining configuration
+ ContentValues values = new ContentValues();
+ values.put(Mailbox.PARENT_SERVER_ID, box2.mServerId);
+ mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, box3.mId), values,
+ null, null);
+ simulateFolderSyncChangeHandling(accountSelector, box2 /*box3's new parent*/);
+
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box1.mFlags); // Should still be a parent
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(PARENT_FLAGS, box2.mFlags); // Should now be a parent
+ assertEquals(box1.mId, box2.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box3.mFlags); // Should still be a child (of box2)
+ assertEquals(box2.mId, box3.mParentKey);
+ }
+
+ /**
+ * Test changing to no parent from a parent
+ */
+ public void testChangeFromParentToNoParent() {
+ // Set up account and mailboxes
+ mAccount = setupTestAccount("acct1", true);
+ String accountSelector = MailboxColumns.ACCOUNT_KEY + " IN (" + mAccount.mId + ")";
+
+ // Initial configuration for this test: box1 has box2, box3 is also at root.
+ Mailbox box1 = EmailContentSetupUtils.setupMailbox(
+ "box1", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL);
+ Mailbox box2 = EmailContentSetupUtils.setupMailbox(
+ "box2", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL, box1);
+
+ // Manually set parentKey to null for all mailboxes, as if an initial sync or post-upgrade
+ mResolver.update(Mailbox.CONTENT_URI, mNullParentKey, null, null);
+
+ // Confirm initial configuration as expected
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector);
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box1.mFlags);
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box2.mFlags);
+ assertEquals(box1.mId, box2.mParentKey);
+
+ // The specific test: Remove the parent from box2 and check remaining configuration
+ ContentValues values = new ContentValues();
+ values.putNull(Mailbox.PARENT_SERVER_ID);
+ mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, box2.mId), values,
+ null, null);
+ // Note: FolderSync handling of changed folder would cause parent key to be uninitialized
+ // so we do so here
+ mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, box2.mId), mNullParentKey,
+ null, null);
+ simulateFolderSyncChangeHandling(accountSelector, box1 /*box2's old parent*/);
+
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(CHILD_FLAGS, box1.mFlags); // Should no longer be a parent
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box2.mFlags); // Should still be a child (no parent)
+ assertEquals(-1, box2.mParentKey);
+ }
+
+ /**
+ * Test a mailbox that has no server id (Hotmail Outbox is an example of this)
+ */
+ public void testNoServerId() {
+ // Set up account and mailboxes
+ mAccount = setupTestAccount("acct1", true);
+ String accountSelector = MailboxColumns.ACCOUNT_KEY + " IN (" + mAccount.mId + ")";
+
+ // Initial configuration for this test: box1 has no serverId, box2 is a child of box1
+ Mailbox box1 = EmailContentSetupUtils.setupMailbox(
+ "box1", mAccount.mId, false, mProviderContext, Mailbox.TYPE_MAIL);
+ box1.mServerId = null;
+ box1.save(mProviderContext);
+ Mailbox box2 = EmailContentSetupUtils.setupMailbox(
+ "box2", mAccount.mId, true, mProviderContext, Mailbox.TYPE_OUTBOX, box1);
+
+ // Manually set parentKey to null for all mailboxes, as if an initial sync or post-upgrade
+ mResolver.update(Mailbox.CONTENT_URI, mNullParentKey, null, null);
+
+ // Confirm initial configuration as expected
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector);
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+
+ // Box 1 should be a child, even though it is defined as the parent of box2, because it
+ // has no serverId (presumably, this case can't happen, because a child stores the parent's
+ // serverId, but it's nice to know it's handled properly). Box 1 should have no parent.
+ assertEquals(Mailbox.NO_MAILBOX, box1.mParentKey);
+ assertEquals(CHILD_FLAGS, box1.mFlags);
+ // Box 2 should be a child with no parent (see above). Since it's an outbox, the flags are
+ // only "holds mail".
+ assertEquals(Mailbox.NO_MAILBOX, box2.mParentKey);
+ assertEquals(Mailbox.FLAG_HOLDS_MAIL, box2.mFlags);
+ }
+
+ /**
+ * Test changing a parent from one mailbox to another
+ */
+ public void testChangeParent() {
+ // Set up account and mailboxes
+ mAccount = setupTestAccount("acct1", true);
+ String accountSelector = MailboxColumns.ACCOUNT_KEY + " IN (" + mAccount.mId + ")";
+
+ // Initial configuration for this test: box1 has box2, box3 is also at root.
+ Mailbox box1 = EmailContentSetupUtils.setupMailbox(
+ "box1", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL);
+ Mailbox box2 = EmailContentSetupUtils.setupMailbox(
+ "box2", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL);
+ Mailbox box3 = EmailContentSetupUtils.setupMailbox(
+ "box3", mAccount.mId, true, mProviderContext, Mailbox.TYPE_MAIL, box1);
+
+ // Manually set parentKey to null for all mailboxes, as if an initial sync or post-upgrade
+ mResolver.update(Mailbox.CONTENT_URI, mNullParentKey, null, null);
+
+ // Confirm initial configuration as expected
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector);
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box1.mFlags);
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box2.mFlags);
+ assertEquals(-1, box2.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box3.mFlags);
+ assertEquals(box1.mId, box3.mParentKey);
+
+ // The specific test: Give box 3 a new parent (box 2) and check remaining configuration
+ ContentValues values = new ContentValues();
+ values.put(Mailbox.PARENT_SERVER_ID, box2.mServerId);
+ mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, box3.mId), values,
+ null, null);
+ // Changes to old and new parent
+ simulateFolderSyncChangeHandling(accountSelector, box2 /*box3's new parent*/,
+ box1 /*box3's old parent*/);
+
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(CHILD_FLAGS, box1.mFlags); // Should no longer be a parent
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(PARENT_FLAGS, box2.mFlags); // Should now be a parent
+ assertEquals(-1, box2.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box3.mFlags); // Should still be a child (of box2)
+ assertEquals(box2.mId, box3.mParentKey);
+ }
+
+
+ /**
+ * Tests the proper separation of two accounts using the methodology from the previous test.
+ * This test will fail if MailboxUtilities fails to distinguish between mailboxes in different
+ * accounts that happen to have the same serverId
+ */
+ public void testChangeParentTwoAccounts() {
+ // Set up account and mailboxes
+ mAccount = setupTestAccount("acct1", true);
+ Account acct2 = setupTestAccount("acct2", true);
+
+ String accountSelector1 = MailboxColumns.ACCOUNT_KEY + " IN (" + mAccount.mId + ")";
+ String accountSelector2 = MailboxColumns.ACCOUNT_KEY + " IN (" + acct2.mId + ")";
+
+ // Box3 is in Box1
+ Mailbox box1 = EmailContentSetupUtils.setupMailbox(
+ "box1", mAccount.mId, false, mProviderContext, Mailbox.TYPE_MAIL);
+ box1.mServerId = "1:1";
+ box1.save(mProviderContext);
+ Mailbox box2 = EmailContentSetupUtils.setupMailbox(
+ "box2", mAccount.mId, false, mProviderContext, Mailbox.TYPE_MAIL);
+ box2.mServerId = "1:2";
+ box2.save(mProviderContext);
+ Mailbox box3 = EmailContentSetupUtils.setupMailbox(
+ "box3", mAccount.mId, false, mProviderContext, Mailbox.TYPE_MAIL, box1);
+ box3.mServerId = "1:3";
+ box3.save(mProviderContext);
+
+ // Box5 is in Box4; Box 6 is in Box5
+ // Note that the three serverId's are identical to those in acct1; we want to make sure
+ // that children get associated only with boxes in their own account
+ Mailbox box4 = EmailContentSetupUtils.setupMailbox(
+ "box4", acct2.mId, false, mProviderContext, Mailbox.TYPE_MAIL, null);
+ box4.mServerId = "1:1";
+ box4.save(mProviderContext);
+ Mailbox box5 = EmailContentSetupUtils.setupMailbox(
+ "box5", acct2.mId, false, mProviderContext, Mailbox.TYPE_MAIL, box4);
+ box5.mServerId = "1:2";
+ box5.save(mProviderContext);
+ Mailbox box6 = EmailContentSetupUtils.setupMailbox(
+ "box6", acct2.mId, false, mProviderContext, Mailbox.TYPE_MAIL, box5);
+ box6.mServerId = "1:3";
+ box6.save(mProviderContext);
+
+ // Manually set parentKey to null for all mailboxes, as if an initial sync or post-upgrade
+ mResolver.update(Mailbox.CONTENT_URI, mNullParentKey, null, null);
+
+ // Confirm initial configuration as expected for mAccount
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector1);
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box1.mFlags);
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box2.mFlags);
+ assertEquals(-1, box2.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box3.mFlags);
+ assertEquals(box1.mId, box3.mParentKey);
+
+ // Confirm initial configuration as expected for acct2
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector2);
+ box4 = Mailbox.restoreMailboxWithId(mProviderContext, box4.mId);
+ box5 = Mailbox.restoreMailboxWithId(mProviderContext, box5.mId);
+ box6 = Mailbox.restoreMailboxWithId(mProviderContext, box6.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(PARENT_FLAGS, box4.mFlags);
+ assertEquals(-1, box4.mParentKey);
+
+ assertEquals(PARENT_FLAGS, box5.mFlags);
+ assertEquals(box4.mId, box5.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box6.mFlags);
+ assertEquals(box5.mId, box6.mParentKey);
+
+ // The specific test: Change box1 to have a different serverId
+ ContentValues values = new ContentValues();
+ values.put(MailboxColumns.SERVER_ID, "1:4");
+ mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, box1.mId), values,
+ null, null);
+ // Manually set parentKey to null for all mailboxes, as if an initial sync or post-upgrade
+ mResolver.update(Mailbox.CONTENT_URI, mNullParentKey, null, null);
+ // Fix up the parent keys
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector1);
+
+ // Make sure that box1 reflects the change properly AND that other boxes remain correct
+ // The reason for all of the seemingly-duplicated tests is to make sure that the fixup of
+ // any account doesn't end up affecting the other account's mailboxes
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(CHILD_FLAGS, box1.mFlags);
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box2.mFlags);
+ assertEquals(-1, box2.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box3.mFlags);
+ assertEquals(-1, box3.mParentKey);
+
+ // Fix up the 2nd account now, and check that ALL boxes are correct
+ MailboxUtilities.fixupUninitializedParentKeys(mProviderContext, accountSelector2);
+
+ box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
+ box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);
+ box3 = Mailbox.restoreMailboxWithId(mProviderContext, box3.mId);
+
+ // Confirm flags and parent key(s) as expected
+ assertEquals(CHILD_FLAGS, box1.mFlags);
+ assertEquals(-1, box1.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box2.mFlags);
+ assertEquals(-1, box2.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box3.mFlags);
+ assertEquals(-1, box3.mParentKey);
+
+ box4 = Mailbox.restoreMailboxWithId(mProviderContext, box4.mId);
+ box5 = Mailbox.restoreMailboxWithId(mProviderContext, box5.mId);
+ box6 = Mailbox.restoreMailboxWithId(mProviderContext, box6.mId);
+
+ assertEquals(PARENT_FLAGS, box4.mFlags);
+ assertEquals(-1, box4.mParentKey);
+
+ assertEquals(PARENT_FLAGS, box5.mFlags);
+ assertEquals(box4.mId, box5.mParentKey);
+
+ assertEquals(CHILD_FLAGS, box6.mFlags);
+ assertEquals(box5.mId, box6.mParentKey);
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/provider/MockProvider.java b/exchange2/tests/src/com/android/exchange/provider/MockProvider.java
new file mode 100644
index 0000000..177ec55
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/provider/MockProvider.java
@@ -0,0 +1,227 @@
+/*
+ * 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.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Set;
+import java.util.Map.Entry;
+
+/**
+ * MockProvider is a ContentProvider that can be used to simulate the storage and retrieval of
+ * records from any ContentProvider, even if that ContentProvider does not exist in the caller's
+ * package. It is specifically designed to enable testing of sync adapters that create
+ * ContentProviderOperations (CPOs) that are then executed using ContentResolver.applyBatch()
+ *
+ * Why is this useful? Because we can't instantiate CalendarProvider or ContactsProvider from our
+ * package, as required by MockContentResolver.addProvider()
+ *
+ * Usage:
+ * ContentResolver.applyBatch(MockProvider.AUTHORITY, batch) will cause the CPOs to be executed,
+ * returning an array of ContentProviderResult; in the case of inserts, the result will include
+ * a Uri that can be used via query(). Note that the CPOs in the batch can contain references
+ * to any authority.
+ *
+ * query() does not allow non-null selection, selectionArgs, or sortOrder arguments; the
+ * presence of these will result in an UnsupportedOperationException
+ *
+ * insert() acts as expected, returning a Uri that can be directly used in a query
+ *
+ * delete() and update() do not allow non-null selection or selectionArgs arguments; the
+ * presence of these will result in an UnsupportedOperationException
+ *
+ * NOTE: When using any operation other than applyBatch, the Uri to be used must be created
+ * with MockProvider.uri(yourUri). This guarantees that the operation is sent to MockProvider
+ *
+ * NOTE: MockProvider only simulates direct storage/retrieval of rows; it does not (and can not)
+ * simulate other actions (e.g. creation of ancillary data) that the actual provider might
+ * perform
+ *
+ * NOTE: See MockProviderTests for usage examples
+ **/
+public class MockProvider extends ContentProvider {
+ public static final String AUTHORITY = "com.android.exchange.mock.provider";
+ /*package*/ static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ /*package*/ static final int TABLE = 100;
+ /*package*/ static final int RECORD = 101;
+
+ public static final String ID_COLUMN = "_id";
+
+ public MockProvider(Context context) {
+ super(context, null, null, null);
+ }
+
+ public MockProvider() {
+ super();
+ }
+
+ // We'll store our values here
+ private HashMap<String, ContentValues> mMockStore = new HashMap<String, ContentValues>();
+ // And we'll generate new id's from here
+ long mMockId = 1;
+
+ /**
+ * Create a Uri for MockProvider from a given Uri
+ * @param uri the Uri from which the MockProvider Uri will be created
+ * @return a Uri that can be used with MockProvider
+ */
+ public static Uri uri(Uri uri) {
+ return new Uri.Builder().scheme("content").authority(AUTHORITY)
+ .path(uri.getPath().substring(1)).build();
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ if (selection != null || selectionArgs != null) {
+ throw new UnsupportedOperationException();
+ }
+ String path = uri.getPath();
+ if (mMockStore.containsKey(path)) {
+ mMockStore.remove(path);
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ // Remove the leading slash
+ String table = uri.getPath().substring(1);
+ long id = mMockId++;
+ Uri newUri = new Uri.Builder().scheme("content").authority(AUTHORITY).path(table)
+ .appendPath(Long.toString(id)).build();
+ // Remember to store the _id
+ values.put(ID_COLUMN, id);
+ mMockStore.put(newUri.getPath(), values);
+ int match = sURIMatcher.match(uri);
+ if (match == UriMatcher.NO_MATCH) {
+ sURIMatcher.addURI(AUTHORITY, table, TABLE);
+ sURIMatcher.addURI(AUTHORITY, table + "/#", RECORD);
+ }
+ return newUri;
+ }
+
+ @Override
+ public boolean onCreate() {
+ return false;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ if (selection != null || selectionArgs != null || sortOrder != null || projection == null) {
+ throw new UnsupportedOperationException();
+ }
+ final int match = sURIMatcher.match(uri(uri));
+ ArrayList<ContentValues> valuesList = new ArrayList<ContentValues>();
+ switch(match) {
+ case TABLE:
+ Set<Entry<String, ContentValues>> entrySet = mMockStore.entrySet();
+ String prefix = uri.getPath() + "/";
+ for (Entry<String, ContentValues> entry: entrySet) {
+ if (entry.getKey().startsWith(prefix)) {
+ valuesList.add(entry.getValue());
+ }
+ }
+ break;
+ case RECORD:
+ ContentValues values = mMockStore.get(uri.getPath());
+ if (values != null) {
+ valuesList.add(values);
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ MatrixCursor cursor = new MatrixCursor(projection, 1);
+ for (ContentValues cv: valuesList) {
+ Object[] rowValues = new Object[projection.length];
+ int i = 0;
+ for (String column: projection) {
+ rowValues[i++] = cv.get(column);
+ }
+ cursor.addRow(rowValues);
+ }
+ return cursor;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues newValues, String selection, String[] selectionArgs) {
+ if (selection != null || selectionArgs != null) {
+ throw new UnsupportedOperationException();
+ }
+ final int match = sURIMatcher.match(uri(uri));
+ ArrayList<ContentValues> updateValuesList = new ArrayList<ContentValues>();
+ String path = uri.getPath();
+ switch(match) {
+ case TABLE:
+ Set<Entry<String, ContentValues>> entrySet = mMockStore.entrySet();
+ String prefix = path + "/";
+ for (Entry<String, ContentValues> entry: entrySet) {
+ if (entry.getKey().startsWith(prefix)) {
+ updateValuesList.add(entry.getValue());
+ }
+ }
+ break;
+ case RECORD:
+ ContentValues cv = mMockStore.get(path);
+ if (cv != null) {
+ updateValuesList.add(cv);
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ Set<Entry<String, Object>> newValuesSet = newValues.valueSet();
+ for (Entry<String, Object> entry: newValuesSet) {
+ String key = entry.getKey();
+ Object value = entry.getValue();
+ for (ContentValues targetValues: updateValuesList) {
+ if (value instanceof Integer) {
+ targetValues.put(key, (Integer)value);
+ } else if (value instanceof Long) {
+ targetValues.put(key, (Long)value);
+ } else if (value instanceof String) {
+ targetValues.put(key, (String)value);
+ } else if (value instanceof Boolean) {
+ targetValues.put(key, (Boolean)value);
+ } else {
+ throw new IllegalArgumentException();
+ }
+ }
+ }
+ for (ContentValues targetValues: updateValuesList) {
+ mMockStore.put(path + "/" + targetValues.getAsLong(ID_COLUMN), targetValues);
+ }
+ return updateValuesList.size();
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/provider/MockProviderTests.java b/exchange2/tests/src/com/android/exchange/provider/MockProviderTests.java
new file mode 100644
index 0000000..897f0d8
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/provider/MockProviderTests.java
@@ -0,0 +1,192 @@
+/*
+ * 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.provider;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.test.ProviderTestCase2;
+import android.test.mock.MockContentResolver;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.ArrayList;
+
+/**
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.provider.MockProviderTests exchange
+ */
+@SmallTest
+public class MockProviderTests extends ProviderTestCase2<MockProvider> {
+ Context mMockContext;
+ MockContentResolver mMockResolver;
+
+ private static final String CANHAZ_AUTHORITY = "com.android.canhaz";
+ private static final String PONY_TABLE = "pony";
+ private static final String PONY_COLUMN_NAME = "name";
+ private static final String PONY_COLUMN_TYPE = "type";
+ private static final String PONY_COLUMN_LEGS= "legs";
+ private static final String PONY_COLUMN_CAN_RIDE = "canRide";
+ private static final String[] PONY_PROJECTION = {MockProvider.ID_COLUMN, PONY_COLUMN_NAME,
+ PONY_COLUMN_TYPE, PONY_COLUMN_LEGS, PONY_COLUMN_CAN_RIDE};
+ private static final int PONY_ID = 0;
+ private static final int PONY_NAME = 1;
+ private static final int PONY_TYPE = 2;
+ private static final int PONY_LEGS = 3;
+ private static final int PONY_CAN_RIDE = 4;
+
+ public MockProviderTests() {
+ super(MockProvider.class, MockProvider.AUTHORITY);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mMockContext = getMockContext();
+ mMockResolver = (MockContentResolver)mMockContext.getContentResolver();
+ mMockResolver.addProvider(CANHAZ_AUTHORITY, new MockProvider(mMockContext));
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ private ContentValues ponyValues(String name, String type, int legs, boolean canRide) {
+ ContentValues cv = new ContentValues();
+ cv.put(PONY_COLUMN_NAME, name);
+ cv.put(PONY_COLUMN_TYPE, type);
+ cv.put(PONY_COLUMN_LEGS, legs);
+ cv.put(PONY_COLUMN_CAN_RIDE, canRide ? 1 : 0);
+ return cv;
+ }
+
+ private ContentProviderResult[] setupPonies() throws RemoteException,
+ OperationApplicationException {
+ // The Uri is content://com.android.canhaz/pony
+ Uri uri = new Uri.Builder().scheme("content").authority(CANHAZ_AUTHORITY)
+ .path(PONY_TABLE).build();
+ // Our array of CPO's to be used with applyBatch
+ ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+
+ // Insert two ponies
+ ContentValues pony1 = ponyValues("Flicka", "wayward", 4, true);
+ ops.add(ContentProviderOperation.newInsert(uri).withValues(pony1).build());
+ ContentValues pony2 = ponyValues("Elise", "dastardly", 3, false);
+ ops.add(ContentProviderOperation.newInsert(uri).withValues(pony2).build());
+ // Apply the batch with one insert operation
+ return mMockResolver.applyBatch(MockProvider.AUTHORITY, ops);
+ }
+
+ private Uri getPonyUri() {
+ return new Uri.Builder().scheme("content").authority(CANHAZ_AUTHORITY)
+ .path(PONY_TABLE).build();
+ }
+
+ public void testInsertQueryandDelete() throws RemoteException, OperationApplicationException {
+ // The Uri is content://com.android.canhaz/pony
+ ContentProviderResult[] results = setupPonies();
+ Uri uri = getPonyUri();
+
+ // Check the results
+ assertNotNull(results);
+ assertEquals(2, results.length);
+ // Make sure that we've created matcher entries for pony and pony/#
+ assertEquals(MockProvider.TABLE, MockProvider.sURIMatcher.match(MockProvider.uri(uri)));
+ assertEquals(MockProvider.RECORD,
+ MockProvider.sURIMatcher.match(MockProvider.uri(results[0].uri)));
+ Cursor c = mMockResolver.query(MockProvider.uri(uri), PONY_PROJECTION, null, null, null);
+ assertNotNull(c);
+ assertEquals(2, c.getCount());
+ long eliseId = -1;
+ long flickaId = -1;
+ while (c.moveToNext()) {
+ String name = c.getString(PONY_NAME);
+ if ("Flicka".equals(name)) {
+ assertEquals("Flicka", c.getString(PONY_NAME));
+ assertEquals("wayward", c.getString(PONY_TYPE));
+ assertEquals(4, c.getInt(PONY_LEGS));
+ assertEquals(1, c.getInt(PONY_CAN_RIDE));
+ flickaId = c.getLong(PONY_ID);
+ } else if ("Elise".equals(name)) {
+ assertEquals("dastardly", c.getString(PONY_TYPE));
+ assertEquals(3, c.getInt(PONY_LEGS));
+ assertEquals(0, c.getInt(PONY_CAN_RIDE));
+ eliseId = c.getLong(PONY_ID);
+ } else {
+ fail("Wrong record: " + name);
+ }
+ }
+
+ // eliseId and flickaId should have been set
+ assertNotSame(-1, eliseId);
+ assertNotSame(-1, flickaId);
+ // Delete the elise record
+ assertEquals(1, mMockResolver.delete(ContentUris.withAppendedId(MockProvider.uri(uri),
+ eliseId), null, null));
+ c = mMockResolver.query(MockProvider.uri(uri), PONY_PROJECTION, null, null, null);
+ assertNotNull(c);
+ // There should be one left (Flicka)
+ assertEquals(1, c.getCount());
+ assertTrue(c.moveToNext());
+ assertEquals("Flicka", c.getString(PONY_NAME));
+ }
+
+ public void testUpdate() throws RemoteException, OperationApplicationException {
+ // The Uri is content://com.android.canhaz/pony
+ Uri uri = getPonyUri();
+ setupPonies();
+ Cursor c = mMockResolver.query(MockProvider.uri(uri), PONY_PROJECTION, null, null, null);
+ assertNotNull(c);
+ assertEquals(2, c.getCount());
+ // Give all the ponies 5 legs
+ ContentValues cv = new ContentValues();
+ cv.put(PONY_COLUMN_LEGS, 5);
+ assertEquals(2, mMockResolver.update(MockProvider.uri(uri), cv, null, null));
+ c = mMockResolver.query(MockProvider.uri(uri), PONY_PROJECTION, null, null, null);
+ assertNotNull(c);
+ // We should still have two records, and each should have 5 legs, but otherwise be the same
+ assertEquals(2, c.getCount());
+ long eliseId = -1;
+ long flickaId = -1;
+ while (c.moveToNext()) {
+ String name = c.getString(PONY_NAME);
+ if ("Flicka".equals(name)) {
+ assertEquals("Flicka", c.getString(PONY_NAME));
+ assertEquals("wayward", c.getString(PONY_TYPE));
+ assertEquals(5, c.getInt(PONY_LEGS));
+ assertEquals(1, c.getInt(PONY_CAN_RIDE));
+ flickaId = c.getLong(PONY_ID);
+ } else if ("Elise".equals(name)) {
+ assertEquals("dastardly", c.getString(PONY_TYPE));
+ assertEquals(5, c.getInt(PONY_LEGS));
+ assertEquals(0, c.getInt(PONY_CAN_RIDE));
+ eliseId = c.getLong(PONY_ID);
+ } else {
+ fail("Wrong record: " + name);
+ }
+ }
+ // eliseId and flickaId should have been set
+ assertNotSame(-1, eliseId);
+ assertNotSame(-1, flickaId);
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java b/exchange2/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
new file mode 100644
index 0000000..0ecdb8f
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
@@ -0,0 +1,1003 @@
+/*
+ * 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.content.ContentValues;
+import android.content.Entity;
+import android.content.res.Resources;
+import android.provider.CalendarContract.Attendees;
+import android.provider.CalendarContract.Events;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
+import com.android.emailcommon.mail.Address;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent.Attachment;
+import com.android.emailcommon.provider.EmailContent.Message;
+import com.android.emailcommon.utility.Utility;
+import com.android.exchange.R;
+import com.android.exchange.adapter.CalendarSyncAdapter;
+import com.android.exchange.adapter.CalendarSyncAdapter.EasCalendarSyncParser;
+import com.android.exchange.adapter.Parser;
+import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.SyncAdapterTestCase;
+import com.android.exchange.adapter.Tags;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.TimeZone;
+
+/**
+ * Tests of EAS Calendar Utilities
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.utility.CalendarUtilitiesTests exchange
+ *
+ * Please see RFC2445 for RRULE definition
+ * http://www.ietf.org/rfc/rfc2445.txt
+ */
+@MediumTest
+public class CalendarUtilitiesTests extends SyncAdapterTestCase<CalendarSyncAdapter> {
+
+ // 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
+
+ // This time zone has no DST
+ private static final String ASIA_CALCUTTA_TIME =
+ "tv7//0kAbgBkAGkAYQAgAFMAdABhAG4AZABhAHIAZAAgAFQAaQBtAGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEkAbgBkAGkAYQAgAEQAYQB5AGwAaQBnAGgAdAAgAFQAaQBtAGUA" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
+
+ // This time zone is equivalent to PST and uses DST
+ private static final String AMERICA_DAWSON_TIME =
+ "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByAGQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAYQBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkA" +
+ "bQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==";
+
+ // Test a southern hemisphere time zone w/ DST
+ private static final String AUSTRALIA_ACT_TIME =
+ "qP3//0EAVQBTACAARQBhAHMAdABlAHIAbgAgAFMAdABhAG4AZABhAHIAZAAgAFQAaQBtAGUAAAAAAAAAAAAA" +
+ "AAAAAAAAAAQAAAABAAMAAAAAAAAAAAAAAEEAVQBTACAARQBhAHMAdABlAHIAbgAgAEQAYQB5AGwAaQBnAGgA" +
+ "dAAgAFQAaQBtAGUAAAAAAAAAAAAAAAAAAAAAAAoAAAABAAIAAAAAAAAAxP///w==";
+
+ // Test a timezone with GMT bias but bogus DST parameters (there is no equivalent time zone
+ // in the database)
+ private static final String GMT_UNKNOWN_DAYLIGHT_TIME =
+ "AAAAACgARwBNAFQAKwAwADAAOgAwADAAKQAgAFQAaQBtAGUAIABaAG8AbgBlAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAACgARwBNAFQAKwAwADAAOgAwADAAKQAgAFQAaQBtAGUAIABaAG8A" +
+ "bgBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAFAAEAAAAAAAAAxP///w==";
+
+ // This time zone has no DST, but earlier, buggy code retrieved a TZ WITH DST
+ private static final String ARIZONA_TIME =
+ "pAEAAFUAUwAgAE0AbwB1AG4AdABhAGkAbgAgAFMAdABhAG4AZABhAHIAZAAgAFQAaQBtAGUAAAAAAAAAAAAA" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFUAUwAgAE0AbwB1AG4AdABhAGkAbgAgAEQAYQB5AGwAaQBnAGgA" +
+ "dAAgAFQAaQBtAGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
+
+ private static final String HAWAII_TIME =
+ "WAIAAEgAYQB3AGEAaQBpAGEAbgAgAFMAdABhAG4AZABhAHIAZAAgAFQAaQBtAGUAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEgAYQB3AGEAaQBpAGEAbgAgAEQAYQB5AGwAaQBnAGgAdAAgAFQA" +
+ "aQBtAGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
+
+ // This is time zone sent by Exchange 2007, apparently; the start time of DST for the eastern
+ // time zone (EST) is off by two hours, which we should correct in our new "lenient" code
+ private static final String LENIENT_EASTERN_TIME =
+ "LAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAAAxP///w==";
+
+ private static final String ORGANIZER = "organizer@server.com";
+ private static final String ATTENDEE = "attendee@server.com";
+
+ 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(AMERICA_DAWSON_TIME);
+ assertEquals("America/Dawson", tz.getID());
+ tz = CalendarUtilities.tziStringToTimeZone(ASIA_CALCUTTA_TIME);
+ assertEquals("Asia/Calcutta", tz.getID());
+ tz = CalendarUtilities.tziStringToTimeZone(AUSTRALIA_ACT_TIME);
+ assertEquals("Australia/ACT", tz.getID());
+
+ // Test peculiar MS sent EST data with and without lenient precision; send standard
+ // precision + 1 (i.e. 1ms) to make sure the code doesn't automatically flip to lenient
+ // when the tz isn't found
+ tz = CalendarUtilities.tziStringToTimeZoneImpl(LENIENT_EASTERN_TIME,
+ CalendarUtilities.STANDARD_DST_PRECISION+1);
+ assertEquals("America/Atikokan", tz.getID());
+ tz = CalendarUtilities.tziStringToTimeZoneImpl(LENIENT_EASTERN_TIME,
+ CalendarUtilities.LENIENT_DST_PRECISION);
+ assertEquals("America/Detroit", tz.getID());
+
+ tz = CalendarUtilities.tziStringToTimeZone(GMT_UNKNOWN_DAYLIGHT_TIME);
+ int bias = tz.getOffset(System.currentTimeMillis());
+ assertEquals(0, bias);
+ // Make sure non-DST TZ's work properly when the tested zone is the default zone
+ TimeZone.setDefault(TimeZone.getTimeZone("America/Phoenix"));
+ tz = CalendarUtilities.tziStringToTimeZone(ARIZONA_TIME);
+ assertEquals("America/Phoenix", tz.getID());
+ TimeZone.setDefault(TimeZone.getTimeZone("Pacific/Honolulu"));
+ tz = CalendarUtilities.tziStringToTimeZone(HAWAII_TIME);
+ assertEquals("Pacific/Honolulu", tz.getID());
+ // Make sure non-DST TZ's get the proper offset and DST status otherwise
+ CalendarUtilities.clearTimeZoneCache();
+ TimeZone azTime = TimeZone.getTimeZone("America/Phoenix");
+ TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
+ tz = CalendarUtilities.tziStringToTimeZone(ARIZONA_TIME);
+ assertFalse("America/Phoenix".equals(tz.getID()));
+ assertFalse(tz.useDaylightTime());
+ // It doesn't matter what time is passed in, since neither TZ has dst
+ long now = System.currentTimeMillis();
+ assertEquals(azTime.getOffset(now), tz.getOffset(now));
+ }
+
+ 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="));
+ assertEquals("WE,TH,SA", CalendarUtilities.tokenFromRrule(rrule, "BYDAY="));
+ assertNull(CalendarUtilities.tokenFromRrule(rrule, "UNTIL="));
+ }
+
+ public void testRecurrenceUntilToEasUntil() {
+ TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
+ // Case where local time crosses into next day in GMT
+ assertEquals("20110730T000000Z",
+ CalendarUtilities.recurrenceUntilToEasUntil("20110731T025959Z"));
+ // Case where local time does not cross into next day in GMT
+ assertEquals("20110730T000000Z",
+ CalendarUtilities.recurrenceUntilToEasUntil("20110730T235959Z"));
+ }
+
+ public void testParseEmailDateTimeToMillis(String date) {
+ // Format for email date strings is 2010-02-23T16:00:00.000Z
+ String dateString = "2010-02-23T15:16:17.000Z";
+ long dateTime = Utility.parseEmailDateTimeToMillis(dateString);
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.setTimeInMillis(dateTime);
+ cal.setTimeZone(TimeZone.getTimeZone("GMT"));
+ assertEquals(cal.get(Calendar.YEAR), 2010);
+ assertEquals(cal.get(Calendar.MONTH), 1); // 0 based
+ assertEquals(cal.get(Calendar.DAY_OF_MONTH), 23);
+ assertEquals(cal.get(Calendar.HOUR_OF_DAY), 16);
+ assertEquals(cal.get(Calendar.MINUTE), 16);
+ assertEquals(cal.get(Calendar.SECOND), 17);
+ }
+
+ public void testParseDateTimeToMillis(String date) {
+ // Format for calendar date strings is 20100223T160000000Z
+ String dateString = "20100223T151617000Z";
+ long dateTime = Utility.parseDateTimeToMillis(dateString);
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.setTimeInMillis(dateTime);
+ cal.setTimeZone(TimeZone.getTimeZone("GMT"));
+ assertEquals(cal.get(Calendar.YEAR), 2010);
+ assertEquals(cal.get(Calendar.MONTH), 1); // 0 based
+ assertEquals(cal.get(Calendar.DAY_OF_MONTH), 23);
+ assertEquals(cal.get(Calendar.HOUR_OF_DAY), 16);
+ assertEquals(cal.get(Calendar.MINUTE), 16);
+ assertEquals(cal.get(Calendar.SECOND), 17);
+ }
+
+ private Entity setupTestEventEntity(String organizer, String attendee, String title) {
+ // Create an Entity for an Event
+ ContentValues entityValues = new ContentValues();
+ Entity entity = new Entity(entityValues);
+
+ // Set up values for the Event
+ String location = "Meeting Location";
+
+ // Fill in times, location, title, and organizer
+ entityValues.put("DTSTAMP",
+ CalendarUtilities.convertEmailDateTimeToCalendarDateTime("2010-04-05T14:30:51Z"));
+ entityValues.put(Events.DTSTART,
+ Utility.parseEmailDateTimeToMillis("2010-04-12T18:30:00Z"));
+ entityValues.put(Events.DTEND,
+ Utility.parseEmailDateTimeToMillis("2010-04-12T19:30:00Z"));
+ entityValues.put(Events.EVENT_LOCATION, location);
+ entityValues.put(Events.TITLE, title);
+ entityValues.put(Events.ORGANIZER, organizer);
+ entityValues.put(Events.SYNC_DATA2, "31415926535");
+
+ // Add the attendee
+ ContentValues attendeeValues = new ContentValues();
+ attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
+ attendeeValues.put(Attendees.ATTENDEE_EMAIL, attendee);
+ entity.addSubValue(Attendees.CONTENT_URI, attendeeValues);
+
+ // Add the organizer
+ ContentValues organizerValues = new ContentValues();
+ organizerValues.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
+ organizerValues.put(Attendees.ATTENDEE_EMAIL, organizer);
+ entity.addSubValue(Attendees.CONTENT_URI, organizerValues);
+ return entity;
+ }
+
+ private Entity setupTestExceptionEntity(String organizer, String attendee, String title) {
+ Entity entity = setupTestEventEntity(organizer, attendee, title);
+ ContentValues entityValues = entity.getEntityValues();
+ entityValues.put(Events.ORIGINAL_SYNC_ID, 69);
+ // The exception will be on April 26th
+ entityValues.put(Events.ORIGINAL_INSTANCE_TIME,
+ Utility.parseEmailDateTimeToMillis("2010-04-26T18:30:00Z"));
+ return entity;
+ }
+
+ public void testCreateMessageForEntity_Reply() {
+ // Set up the "event"
+ String title = "Discuss Unit Tests";
+ Entity entity = setupTestEventEntity(ORGANIZER, ATTENDEE, title);
+
+ // Create a dummy account for the attendee
+ Account account = new Account();
+ account.mEmailAddress = ATTENDEE;
+
+ // The uid is required, but can be anything
+ String uid = "31415926535";
+
+ // Create the outgoing message
+ Message msg = CalendarUtilities.createMessageForEntity(mContext, entity,
+ Message.FLAG_OUTGOING_MEETING_ACCEPT, uid, account);
+
+ // First, we should have a message
+ assertNotNull(msg);
+
+ // Now check some of the fields of the message
+ assertEquals(Address.pack(new Address[] {new Address(ORGANIZER)}), msg.mTo);
+ Resources resources = getContext().getResources();
+ String accept = resources.getString(R.string.meeting_accepted, title);
+ assertEquals(accept, msg.mSubject);
+ assertNotNull(msg.mText);
+ assertTrue(msg.mText.contains(resources.getString(R.string.meeting_where, "")));
+
+ // And make sure we have an attachment
+ assertNotNull(msg.mAttachments);
+ assertEquals(1, msg.mAttachments.size());
+ Attachment att = msg.mAttachments.get(0);
+ // And that the attachment has the correct elements
+ assertEquals("invite.ics", att.mFileName);
+ assertEquals(Attachment.FLAG_ICS_ALTERNATIVE_PART,
+ att.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART);
+ assertEquals("text/calendar; method=REPLY", att.mMimeType);
+ assertNotNull(att.mContentBytes);
+ assertEquals(att.mSize, att.mContentBytes.length);
+
+ //TODO Check the contents of the attachment using an iCalendar parser
+ }
+
+ public void testCreateMessageForEntity_Invite_AllDay() throws IOException {
+ // Set up the "event"
+ String title = "Discuss Unit Tests";
+ Entity entity = setupTestEventEntity(ORGANIZER, ATTENDEE, title);
+ ContentValues entityValues = entity.getEntityValues();
+ entityValues.put(Events.ALL_DAY, 1);
+ entityValues.put(Events.DURATION, "P1D");
+ entityValues.remove(Events.DTEND);
+
+ // Create a dummy account for the attendee
+ Account account = new Account();
+ account.mEmailAddress = ORGANIZER;
+
+ // The uid is required, but can be anything
+ String uid = "31415926535";
+
+ // Create the outgoing message
+ Message msg = CalendarUtilities.createMessageForEntity(mContext, entity,
+ Message.FLAG_OUTGOING_MEETING_INVITE, uid, account);
+
+ // First, we should have a message
+ assertNotNull(msg);
+
+ // Now check some of the fields of the message
+ assertEquals(Address.pack(new Address[] {new Address(ATTENDEE)}), msg.mTo);
+ assertEquals(title, msg.mSubject);
+
+ // And make sure we have an attachment
+ assertNotNull(msg.mAttachments);
+ assertEquals(1, msg.mAttachments.size());
+ Attachment att = msg.mAttachments.get(0);
+ // And that the attachment has the correct elements
+ assertEquals("invite.ics", att.mFileName);
+ assertEquals(Attachment.FLAG_ICS_ALTERNATIVE_PART,
+ att.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART);
+ assertEquals("text/calendar; method=REQUEST", att.mMimeType);
+ assertNotNull(att.mContentBytes);
+ assertEquals(att.mSize, att.mContentBytes.length);
+
+ // We'll check the contents of the ics file here
+ BlockHash vcalendar = parseIcsContent(att.mContentBytes);
+ assertNotNull(vcalendar);
+
+ // We should have a VCALENDAR with a REQUEST method
+ assertEquals("VCALENDAR", vcalendar.name);
+ assertEquals("REQUEST", vcalendar.get("METHOD"));
+
+ // We should have one block under VCALENDAR
+ assertEquals(1, vcalendar.blocks.size());
+ BlockHash vevent = vcalendar.blocks.get(0);
+ // It's a VEVENT with the following fields
+ assertEquals("VEVENT", vevent.name);
+ assertEquals("Meeting Location", vevent.get("LOCATION"));
+ assertEquals("0", vevent.get("SEQUENCE"));
+ assertEquals("Discuss Unit Tests", vevent.get("SUMMARY"));
+ assertEquals(uid, vevent.get("UID"));
+ assertEquals("MAILTO:" + ATTENDEE,
+ vevent.get("ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE"));
+
+ // These next two fields should have a date only
+ assertEquals("20100412", vevent.get("DTSTART;VALUE=DATE"));
+ assertEquals("20100413", vevent.get("DTEND;VALUE=DATE"));
+ // This should be set to TRUE for all-day events
+ assertEquals("TRUE", vevent.get("X-MICROSOFT-CDO-ALLDAYEVENT"));
+ }
+
+ public void testCreateMessageForEntity_Invite() throws IOException {
+ // Set up the "event"
+ String title = "Discuss Unit Tests";
+ Entity entity = setupTestEventEntity(ORGANIZER, ATTENDEE, title);
+
+ // Create a dummy account for the attendee
+ Account account = new Account();
+ account.mEmailAddress = ORGANIZER;
+
+ // The uid is required, but can be anything
+ String uid = "31415926535";
+
+ // Create the outgoing message
+ Message msg = CalendarUtilities.createMessageForEntity(mContext, entity,
+ Message.FLAG_OUTGOING_MEETING_INVITE, uid, account);
+
+ // First, we should have a message
+ assertNotNull(msg);
+
+ // Now check some of the fields of the message
+ assertEquals(Address.pack(new Address[] {new Address(ATTENDEE)}), msg.mTo);
+ assertEquals(title, msg.mSubject);
+
+ // And make sure we have an attachment
+ assertNotNull(msg.mAttachments);
+ assertEquals(1, msg.mAttachments.size());
+ Attachment att = msg.mAttachments.get(0);
+ // And that the attachment has the correct elements
+ assertEquals("invite.ics", att.mFileName);
+ assertEquals(Attachment.FLAG_ICS_ALTERNATIVE_PART,
+ att.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART);
+ assertEquals("text/calendar; method=REQUEST", att.mMimeType);
+ assertNotNull(att.mContentBytes);
+ assertEquals(att.mSize, att.mContentBytes.length);
+
+ // We'll check the contents of the ics file here
+ BlockHash vcalendar = parseIcsContent(att.mContentBytes);
+ assertNotNull(vcalendar);
+
+ // We should have a VCALENDAR with a REQUEST method
+ assertEquals("VCALENDAR", vcalendar.name);
+ assertEquals("REQUEST", vcalendar.get("METHOD"));
+
+ // We should have one block under VCALENDAR
+ assertEquals(1, vcalendar.blocks.size());
+ BlockHash vevent = vcalendar.blocks.get(0);
+ // It's a VEVENT with the following fields
+ assertEquals("VEVENT", vevent.name);
+ assertEquals("Meeting Location", vevent.get("LOCATION"));
+ assertEquals("0", vevent.get("SEQUENCE"));
+ assertEquals("Discuss Unit Tests", vevent.get("SUMMARY"));
+ assertEquals(uid, vevent.get("UID"));
+ assertEquals("MAILTO:" + ATTENDEE,
+ vevent.get("ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE"));
+
+ // These next two fields should exist (without the VALUE=DATE suffix)
+ assertNotNull(vevent.get("DTSTART"));
+ assertNotNull(vevent.get("DTEND"));
+ assertNull(vevent.get("DTSTART;VALUE=DATE"));
+ assertNull(vevent.get("DTEND;VALUE=DATE"));
+ // This shouldn't exist for this event
+ assertNull(vevent.get("X-MICROSOFT-CDO-ALLDAYEVENT"));
+ }
+
+ public void testCreateMessageForEntity_Recurring() throws IOException {
+ // Set up the "event"
+ String title = "Discuss Unit Tests";
+ Entity entity = setupTestEventEntity(ORGANIZER, ATTENDEE, title);
+ // Set up a RRULE for this event
+ entity.getEntityValues().put(Events.RRULE, "FREQ=DAILY");
+
+ // Create a dummy account for the attendee
+ Account account = new Account();
+ account.mEmailAddress = ORGANIZER;
+
+ // The uid is required, but can be anything
+ String uid = "31415926535";
+
+ // Create the outgoing message
+ Message msg = CalendarUtilities.createMessageForEntity(mContext, entity,
+ Message.FLAG_OUTGOING_MEETING_INVITE, uid, account);
+
+ // First, we should have a message
+ assertNotNull(msg);
+
+ // Now check some of the fields of the message
+ assertEquals(Address.pack(new Address[] {new Address(ATTENDEE)}), msg.mTo);
+ assertEquals(title, msg.mSubject);
+
+ // And make sure we have an attachment
+ assertNotNull(msg.mAttachments);
+ assertEquals(1, msg.mAttachments.size());
+ Attachment att = msg.mAttachments.get(0);
+ // And that the attachment has the correct elements
+ assertEquals("invite.ics", att.mFileName);
+ assertEquals(Attachment.FLAG_ICS_ALTERNATIVE_PART,
+ att.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART);
+ assertEquals("text/calendar; method=REQUEST", att.mMimeType);
+ assertNotNull(att.mContentBytes);
+ assertEquals(att.mSize, att.mContentBytes.length);
+
+ // We'll check the contents of the ics file here
+ BlockHash vcalendar = parseIcsContent(att.mContentBytes);
+ assertNotNull(vcalendar);
+
+ // We should have a VCALENDAR with a REQUEST method
+ assertEquals("VCALENDAR", vcalendar.name);
+ assertEquals("REQUEST", vcalendar.get("METHOD"));
+
+ // We should have two blocks under VCALENDAR (VTIMEZONE and VEVENT)
+ assertEquals(2, vcalendar.blocks.size());
+
+ // This is the time zone that should be used
+ TimeZone timeZone = TimeZone.getDefault();
+
+ BlockHash vtimezone = vcalendar.blocks.get(0);
+ // It should be a VTIMEZONE for timeZone
+ assertEquals("VTIMEZONE", vtimezone.name);
+ assertEquals(timeZone.getID(), vtimezone.get("TZID"));
+
+ BlockHash vevent = vcalendar.blocks.get(1);
+ // It's a VEVENT with the following fields
+ assertEquals("VEVENT", vevent.name);
+ assertEquals("Meeting Location", vevent.get("LOCATION"));
+ assertEquals("0", vevent.get("SEQUENCE"));
+ assertEquals("Discuss Unit Tests", vevent.get("SUMMARY"));
+ assertEquals(uid, vevent.get("UID"));
+ assertEquals("MAILTO:" + ATTENDEE,
+ vevent.get("ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE"));
+
+ // We should have DTSTART/DTEND with time zone
+ assertNotNull(vevent.get("DTSTART;TZID=" + timeZone.getID()));
+ assertNotNull(vevent.get("DTEND;TZID=" + timeZone.getID()));
+ assertNull(vevent.get("DTSTART"));
+ assertNull(vevent.get("DTEND"));
+ assertNull(vevent.get("DTSTART;VALUE=DATE"));
+ assertNull(vevent.get("DTEND;VALUE=DATE"));
+ // This shouldn't exist for this event
+ assertNull(vevent.get("X-MICROSOFT-CDO-ALLDAYEVENT"));
+ }
+
+ public void testCreateMessageForEntity_Exception_Cancel() throws IOException {
+ // Set up the "exception"...
+ String title = "Discuss Unit Tests";
+ Entity entity = setupTestExceptionEntity(ORGANIZER, ATTENDEE, title);
+
+ ContentValues entityValues = entity.getEntityValues();
+ // Mark the Exception as dirty
+ entityValues.put(Events.DIRTY, 1);
+ // And mark it canceled
+ entityValues.put(Events.STATUS, Events.STATUS_CANCELED);
+
+ // Create a dummy account for the attendee
+ Account account = new Account();
+ account.mEmailAddress = ORGANIZER;
+
+ // The uid is required, but can be anything
+ String uid = "31415926535";
+
+ // Create the outgoing message
+ Message msg = CalendarUtilities.createMessageForEntity(mContext, entity,
+ Message.FLAG_OUTGOING_MEETING_CANCEL, uid, account);
+
+ // First, we should have a message
+ assertNotNull(msg);
+
+ // Now check some of the fields of the message
+ assertEquals(Address.pack(new Address[] {new Address(ATTENDEE)}), msg.mTo);
+ String cancel = getContext().getResources().getString(R.string.meeting_canceled, title);
+ assertEquals(cancel, msg.mSubject);
+
+ // And make sure we have an attachment
+ assertNotNull(msg.mAttachments);
+ assertEquals(1, msg.mAttachments.size());
+ Attachment att = msg.mAttachments.get(0);
+ // And that the attachment has the correct elements
+ assertEquals("invite.ics", att.mFileName);
+ assertEquals(Attachment.FLAG_ICS_ALTERNATIVE_PART,
+ att.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART);
+ assertEquals("text/calendar; method=CANCEL", att.mMimeType);
+ assertNotNull(att.mContentBytes);
+
+ // We'll check the contents of the ics file here
+ BlockHash vcalendar = parseIcsContent(att.mContentBytes);
+ assertNotNull(vcalendar);
+
+ // We should have a VCALENDAR with a CANCEL method
+ assertEquals("VCALENDAR", vcalendar.name);
+ assertEquals("CANCEL", vcalendar.get("METHOD"));
+
+ // This is the time zone that should be used
+ TimeZone timeZone = TimeZone.getDefault();
+
+ // We should have two blocks under VCALENDAR (VTIMEZONE and VEVENT)
+ assertEquals(2, vcalendar.blocks.size());
+
+ BlockHash vtimezone = vcalendar.blocks.get(0);
+ // It should be a VTIMEZONE for timeZone
+ assertEquals("VTIMEZONE", vtimezone.name);
+ assertEquals(timeZone.getID(), vtimezone.get("TZID"));
+
+ BlockHash vevent = vcalendar.blocks.get(1);
+ // It's a VEVENT with the following fields
+ assertEquals("VEVENT", vevent.name);
+ assertEquals("Meeting Location", vevent.get("LOCATION"));
+ assertEquals("0", vevent.get("SEQUENCE"));
+ assertEquals("Discuss Unit Tests", vevent.get("SUMMARY"));
+ assertEquals(uid, vevent.get("UID"));
+ assertEquals("MAILTO:" + ATTENDEE,
+ vevent.get("ATTENDEE;ROLE=REQ-PARTICIPANT"));
+ long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
+ assertNotSame(0, originalTime);
+ // For an exception, RECURRENCE-ID is critical
+ assertEquals(CalendarUtilities.millisToEasDateTime(originalTime, timeZone,
+ true /*withTime*/), vevent.get("RECURRENCE-ID" + ";TZID=" + timeZone.getID()));
+ }
+
+ public void testUtcOffsetString() {
+ assertEquals(CalendarUtilities.utcOffsetString(540), "+0900");
+ assertEquals(CalendarUtilities.utcOffsetString(-480), "-0800");
+ assertEquals(CalendarUtilities.utcOffsetString(0), "+0000");
+ }
+
+ public void testFindTransitionDate() {
+ // We'll find some transitions and make sure that we're properly in or out of daylight time
+ // on either side of the transition.
+ // Use CST for testing (any other will do as well, as long as it has DST)
+ TimeZone tz = TimeZone.getTimeZone("US/Central");
+ // Confirm that this time zone uses DST
+ assertTrue(tz.useDaylightTime());
+ // Get a calendar at January 1st of the current year
+ GregorianCalendar calendar = new GregorianCalendar(tz);
+ calendar.set(CalendarUtilities.sCurrentYear, Calendar.JANUARY, 1);
+ // Get start and end times at start and end of year
+ long startTime = calendar.getTimeInMillis();
+ long endTime = startTime + (365*CalendarUtilities.DAYS);
+ // Find the first transition
+ GregorianCalendar transitionCalendar =
+ CalendarUtilities.findTransitionDate(tz, startTime, endTime, false);
+ long transitionTime = transitionCalendar.getTimeInMillis();
+ // Before should be in standard time; after in daylight time
+ Date beforeDate = new Date(transitionTime - CalendarUtilities.HOURS);
+ Date afterDate = new Date(transitionTime + CalendarUtilities.HOURS);
+ assertFalse(tz.inDaylightTime(beforeDate));
+ assertTrue(tz.inDaylightTime(afterDate));
+
+ // Find the next one...
+ transitionCalendar = CalendarUtilities.findTransitionDate(tz, transitionTime +
+ CalendarUtilities.DAYS, endTime, true);
+ transitionTime = transitionCalendar.getTimeInMillis();
+ // This time, Before should be in daylight time; after in standard time
+ beforeDate = new Date(transitionTime - CalendarUtilities.HOURS);
+ afterDate = new Date(transitionTime + CalendarUtilities.HOURS);
+ assertTrue(tz.inDaylightTime(beforeDate));
+ assertFalse(tz.inDaylightTime(afterDate));
+
+ // Kinshasa has no daylight savings time
+ tz = TimeZone.getTimeZone("Africa/Kinshasa");
+ // Confirm that there's no DST for this time zone
+ assertFalse(tz.useDaylightTime());
+ // Get a calendar at January 1st of the current year
+ calendar = new GregorianCalendar(tz);
+ calendar.set(CalendarUtilities.sCurrentYear, Calendar.JANUARY, 1);
+ // Get start and end times at start and end of year
+ startTime = calendar.getTimeInMillis();
+ endTime = startTime + (365*CalendarUtilities.DAYS);
+ // Find the first transition
+ transitionCalendar = CalendarUtilities.findTransitionDate(tz, startTime, endTime, false);
+ // There had better not be one
+ assertNull(transitionCalendar);
+ }
+
+ public void testRruleFromRecurrence() {
+ // Every Monday for 2 weeks
+ String rrule = CalendarUtilities.rruleFromRecurrence(
+ 1 /*Weekly*/, 2 /*Occurrences*/, 1 /*Interval*/, 2 /*Monday*/, 0, 0, 0, null);
+ assertEquals("FREQ=WEEKLY;COUNT=2;INTERVAL=1;BYDAY=MO", rrule);
+ // Every Tuesday and Friday
+ rrule = CalendarUtilities.rruleFromRecurrence(
+ 1 /*Weekly*/, 0 /*Occurrences*/, 0 /*Interval*/, 36 /*Tue&Fri*/, 0, 0, 0, null);
+ assertEquals("FREQ=WEEKLY;BYDAY=TU,FR", rrule);
+ // The last Saturday of the month
+ rrule = CalendarUtilities.rruleFromRecurrence(
+ 1 /*Weekly*/, 0, 0, 64 /*Sat*/, 0, 5 /*Last*/, 0, null);
+ assertEquals("FREQ=WEEKLY;BYDAY=-1SA", rrule);
+ // The third Wednesday and Thursday of the month
+ rrule = CalendarUtilities.rruleFromRecurrence(
+ 1 /*Weekly*/, 0, 0, 24 /*Wed&Thu*/, 0, 3 /*3rd*/, 0, null);
+ assertEquals("FREQ=WEEKLY;BYDAY=3WE,3TH", rrule);
+ rrule = CalendarUtilities.rruleFromRecurrence(
+ 3 /*Monthly/Day*/, 0, 0, 127 /*LastDay*/, 0, 0, 0, null);
+ assertEquals("FREQ=MONTHLY;BYMONTHDAY=-1", rrule);
+ rrule = CalendarUtilities.rruleFromRecurrence(
+ 3 /*Monthly/Day*/, 0, 0, 62 /*M-F*/, 0, 5 /*Last week*/, 0, null);
+ assertEquals("FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1", rrule);
+ // The 14th of the every month
+ rrule = CalendarUtilities.rruleFromRecurrence(
+ 2 /*Monthly/Date*/, 0, 0, 0, 14 /*14th*/, 0, 0, null);
+ assertEquals("FREQ=MONTHLY;BYMONTHDAY=14", rrule);
+ // Every 31st of October
+ rrule = CalendarUtilities.rruleFromRecurrence(
+ 5 /*Yearly/Date*/, 0, 0, 0, 31 /*31st*/, 0, 10 /*October*/, null);
+ assertEquals("FREQ=YEARLY;BYMONTHDAY=31;BYMONTH=10", rrule);
+ // The first Tuesday of June
+ rrule = CalendarUtilities.rruleFromRecurrence(
+ 6 /*Yearly/Month/DayOfWeek*/, 0, 0, 4 /*Tue*/, 0, 1 /*1st*/, 6 /*June*/, null);
+ assertEquals("FREQ=YEARLY;BYDAY=1TU;BYMONTH=6", rrule);
+ }
+
+ /**
+ * Given a CalendarSyncAdapter and an RRULE, serialize the RRULE via recurrentFromRrule and
+ * then parse the result. Assert that the resulting RRULE is the same as the original.
+ * @param adapter a CalendarSyncAdapter
+ * @param rrule an RRULE string that will be tested
+ * @throws IOException
+ */
+ private void testSingleRecurrenceFromRrule(CalendarSyncAdapter adapter, String rrule)
+ throws IOException {
+ Serializer s = new Serializer();
+ CalendarUtilities.recurrenceFromRrule(rrule, 0, s);
+ s.done();
+ EasCalendarSyncParser parser = adapter.new EasCalendarSyncParser(
+ new ByteArrayInputStream(s.toByteArray()), adapter);
+ // The first element should be the outer CALENDAR_RECURRENCE tag
+ assertEquals(Tags.CALENDAR_RECURRENCE, parser.nextTag(Parser.START_DOCUMENT));
+ assertEquals(rrule, parser.recurrenceParser());
+ }
+
+ /**
+ * Round-trip test of RRULE handling; we serialize an RRULE and then parse the result; the
+ * result should be identical to the original RRULE
+ */
+ public void testRecurrenceFromRrule() throws IOException {
+ // A test sync adapter we can use throughout the test
+ CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
+
+ testSingleRecurrenceFromRrule(adapter, "FREQ=WEEKLY;COUNT=2;INTERVAL=1;BYDAY=MO");
+ testSingleRecurrenceFromRrule(adapter, "FREQ=WEEKLY;BYDAY=TU,FR");
+ testSingleRecurrenceFromRrule(adapter, "FREQ=WEEKLY;BYDAY=-1SA");
+ testSingleRecurrenceFromRrule(adapter, "FREQ=WEEKLY;BYDAY=3WE,3TH");
+ testSingleRecurrenceFromRrule(adapter, "FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1");
+ testSingleRecurrenceFromRrule(adapter, "FREQ=MONTHLY;BYMONTHDAY=17");
+ testSingleRecurrenceFromRrule(adapter, "FREQ=YEARLY;BYMONTHDAY=31;BYMONTH=10");
+ testSingleRecurrenceFromRrule(adapter, "FREQ=YEARLY;BYDAY=1TU;BYMONTH=6");
+ }
+
+ /**
+ * For debugging purposes, to help keep track of parsing errors.
+ */
+ private class UnterminatedBlockException extends IOException {
+ private static final long serialVersionUID = 1L;
+ UnterminatedBlockException(String name) {
+ super(name);
+ }
+ }
+
+ /**
+ * A lightweight representation of block object containing a hash of individual values and an
+ * array of inner blocks. The object is build by pulling elements from a BufferedReader.
+ * NOTE: Multiple values of a given field are not supported. We'd see this with ATTENDEEs, for
+ * example, and possibly RDATEs in VTIMEZONEs without an RRULE; these cases will be handled
+ * at a later time.
+ */
+ private class BlockHash {
+ String name;
+ HashMap<String, String> hash = new HashMap<String, String>();
+ ArrayList<BlockHash> blocks = new ArrayList<BlockHash>();
+
+ BlockHash (String _name, BufferedReader reader) throws IOException {
+ name = _name;
+ String lastField = null;
+ String lastValue = null;
+ while (true) {
+ // Get a line; we're done if it's null
+ String line = reader.readLine();
+ if (line == null) {
+ throw new UnterminatedBlockException(name);
+ }
+ int length = line.length();
+ if (length == 0) {
+ // We shouldn't ever see an empty line
+ throw new IllegalArgumentException();
+ }
+ // A line starting with tab is a continuation
+ if (line.charAt(0) == '\t') {
+ // Remember the line and length
+ lastValue = line.substring(1);
+ // Save the concatenation of old and new values
+ hash.put(lastField, hash.get(lastField) + lastValue);
+ continue;
+ }
+ // Find the field delimiter
+ int pos = line.indexOf(':');
+ // If not found, or at EOL, this is a bad ics
+ if (pos < 0 || pos >= length) {
+ throw new IllegalArgumentException();
+ }
+ // Remember the field, value, and length
+ lastField = line.substring(0, pos);
+ lastValue = line.substring(pos + 1);
+ if (lastField.equals("BEGIN")) {
+ blocks.add(new BlockHash(lastValue, reader));
+ continue;
+ } else if (lastField.equals("END")) {
+ if (!lastValue.equals(name)) {
+ throw new UnterminatedBlockException(name);
+ }
+ break;
+ }
+
+ // Save it away and continue
+ hash.put(lastField, lastValue);
+ }
+ }
+
+ String get(String field) {
+ return hash.get(field);
+ }
+ }
+
+ private BlockHash parseIcsContent(byte[] bytes) throws IOException {
+ BufferedReader reader = new BufferedReader(new StringReader(Utility.fromUtf8(bytes)));
+ String line = reader.readLine();
+ if (!line.equals("BEGIN:VCALENDAR")) {
+ throw new IllegalArgumentException();
+ }
+ return new BlockHash("VCALENDAR", reader);
+ }
+
+ public void testBuildMessageTextFromEntityValues() {
+ // Set up a test event
+ String title = "Event Title";
+ Entity entity = setupTestEventEntity(ORGANIZER, ATTENDEE, title);
+ ContentValues entityValues = entity.getEntityValues();
+
+ // Save this away; we'll use it a few times below
+ Resources resources = mContext.getResources();
+ Date date = new Date(entityValues.getAsLong(Events.DTSTART));
+ String dateTimeString = DateFormat.getDateTimeInstance().format(date);
+
+ // Get the text for this message
+ StringBuilder sb = new StringBuilder();
+ CalendarUtilities.buildMessageTextFromEntityValues(mContext, entityValues, sb);
+ String text = sb.toString();
+ // We'll just check the when and where
+ assertTrue(text.contains(resources.getString(R.string.meeting_when, dateTimeString)));
+ String location = entityValues.getAsString(Events.EVENT_LOCATION);
+ assertTrue(text.contains(resources.getString(R.string.meeting_where, location)));
+
+ // Make this event recurring
+ entity.getEntityValues().put(Events.RRULE, "FREQ=WEEKLY;BYDAY=MO");
+ sb = new StringBuilder();
+ CalendarUtilities.buildMessageTextFromEntityValues(mContext, entityValues, sb);
+ text = sb.toString();
+ assertTrue(text.contains(resources.getString(R.string.meeting_recurring, dateTimeString)));
+ }
+
+ /**
+ * Sanity test for time zone generation. Most important, make sure that we can run through
+ * all of the time zones without generating an exception. Second, make sure that we're finding
+ * rules for at least 90% of time zones that use daylight time (empirically, it's more like
+ * 95%). Log those without rules.
+ * @throws IOException
+ */
+ public void testTimeZoneToVTimezone() throws IOException {
+ SimpleIcsWriter writer = new SimpleIcsWriter();
+ int rule = 0;
+ int nodst = 0;
+ int norule = 0;
+ ArrayList<String> norulelist = new ArrayList<String>();
+ for (String tzs: TimeZone.getAvailableIDs()) {
+ TimeZone tz = TimeZone.getTimeZone(tzs);
+ writer = new SimpleIcsWriter();
+ CalendarUtilities.timeZoneToVTimezone(tz, writer);
+ String vc = writer.toString();
+ boolean hasRule = vc.indexOf("RRULE") > 0;
+ if (hasRule) {
+ rule++;
+ } else if (tz.useDaylightTime()) {
+ norule++;
+ norulelist.add(tz.getID());
+ } else {
+ nodst++;
+ }
+ }
+ Log.d("TimeZoneGeneration",
+ "Rule: " + rule + ", No DST: " + nodst + ", No rule: " + norule);
+ for (String nr: norulelist) {
+ Log.d("TimeZoneGeneration", "No rule: " + nr);
+ }
+ // This is an empirical sanity test; we shouldn't have too many time zones with DST and
+ // without a rule.
+ assertTrue(norule < rule/8);
+ }
+
+ public void testGetUidFromGlobalObjId() {
+ // This is a "foreign" uid (from some vCalendar client)
+ String globalObjId = "BAAAAIIA4AB0xbcQGoLgCAAAAAAAAAAAAAAAAAAAAAAAAAAAMQAAA" +
+ "HZDYWwtVWlkAQAAADI3NjU1NmRkLTg1MzAtNGZiZS1iMzE0LThiM2JlYTYwMjE0OQA=";
+ String uid = CalendarUtilities.getUidFromGlobalObjId(globalObjId);
+ assertEquals(uid, "276556dd-8530-4fbe-b314-8b3bea602149");
+ // This is a native EAS uid
+ globalObjId =
+ "BAAAAIIA4AB0xbcQGoLgCAAAAADACTu7KbPKAQAAAAAAAAAAEAAAAObgsG6HVt1Fmy+7GlLbGhY=";
+ uid = CalendarUtilities.getUidFromGlobalObjId(globalObjId);
+ assertEquals(uid, "040000008200E00074C5B7101A82E00800000000C0093BBB29B3CA" +
+ "01000000000000000010000000E6E0B06E8756DD459B2FBB1A52DB1A16");
+ }
+
+ public void testSelfAttendeeStatusFromBusyStatus() {
+ assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED,
+ CalendarUtilities.attendeeStatusFromBusyStatus(
+ CalendarUtilities.BUSY_STATUS_BUSY));
+ assertEquals(Attendees.ATTENDEE_STATUS_TENTATIVE,
+ CalendarUtilities.attendeeStatusFromBusyStatus(
+ CalendarUtilities.BUSY_STATUS_TENTATIVE));
+ assertEquals(Attendees.ATTENDEE_STATUS_NONE,
+ CalendarUtilities.attendeeStatusFromBusyStatus(
+ CalendarUtilities.BUSY_STATUS_FREE));
+ assertEquals(Attendees.ATTENDEE_STATUS_NONE,
+ CalendarUtilities.attendeeStatusFromBusyStatus(
+ CalendarUtilities.BUSY_STATUS_OUT_OF_OFFICE));
+ }
+
+ public void testBusyStatusFromSelfStatus() {
+ assertEquals(CalendarUtilities.BUSY_STATUS_FREE,
+ CalendarUtilities.busyStatusFromAttendeeStatus(
+ Attendees.ATTENDEE_STATUS_DECLINED));
+ assertEquals(CalendarUtilities.BUSY_STATUS_FREE,
+ CalendarUtilities.busyStatusFromAttendeeStatus(
+ Attendees.ATTENDEE_STATUS_NONE));
+ assertEquals(CalendarUtilities.BUSY_STATUS_FREE,
+ CalendarUtilities.busyStatusFromAttendeeStatus(
+ Attendees.ATTENDEE_STATUS_INVITED));
+ assertEquals(CalendarUtilities.BUSY_STATUS_TENTATIVE,
+ CalendarUtilities.busyStatusFromAttendeeStatus(
+ Attendees.ATTENDEE_STATUS_TENTATIVE));
+ assertEquals(CalendarUtilities.BUSY_STATUS_BUSY,
+ CalendarUtilities.busyStatusFromAttendeeStatus(
+ Attendees.ATTENDEE_STATUS_ACCEPTED));
+ }
+
+ public void testGetUtcAllDayCalendarTime() {
+ GregorianCalendar correctUtc = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+ correctUtc.set(2011, 2, 10, 0, 0, 0);
+ long correctUtcTime = correctUtc.getTimeInMillis();
+
+ TimeZone localTimeZone = TimeZone.getTimeZone("GMT-0700");
+ GregorianCalendar localCalendar = new GregorianCalendar(localTimeZone);
+ localCalendar.set(2011, 2, 10, 12, 23, 34);
+ long localTimeMillis = localCalendar.getTimeInMillis();
+ long convertedUtcTime =
+ CalendarUtilities.getUtcAllDayCalendarTime(localTimeMillis, localTimeZone);
+ // Milliseconds aren't zeroed out and may not be the same
+ assertEquals(convertedUtcTime/1000, correctUtcTime/1000);
+
+ localTimeZone = TimeZone.getTimeZone("GMT+0700");
+ localCalendar = new GregorianCalendar(localTimeZone);
+ localCalendar.set(2011, 2, 10, 12, 23, 34);
+ localTimeMillis = localCalendar.getTimeInMillis();
+ convertedUtcTime =
+ CalendarUtilities.getUtcAllDayCalendarTime(localTimeMillis, localTimeZone);
+ assertEquals(convertedUtcTime/1000, correctUtcTime/1000);
+ }
+
+ public void testGetLocalAllDayCalendarTime() {
+ TimeZone utcTimeZone = TimeZone.getTimeZone("UTC");
+ TimeZone localTimeZone = TimeZone.getTimeZone("GMT-0700");
+ GregorianCalendar correctLocal = new GregorianCalendar(localTimeZone);
+ correctLocal.set(2011, 2, 10, 0, 0, 0);
+ long correctLocalTime = correctLocal.getTimeInMillis();
+
+ GregorianCalendar utcCalendar = new GregorianCalendar(utcTimeZone);
+ utcCalendar.set(2011, 2, 10, 12, 23, 34);
+ long utcTimeMillis = utcCalendar.getTimeInMillis();
+ long convertedLocalTime =
+ CalendarUtilities.getLocalAllDayCalendarTime(utcTimeMillis, localTimeZone);
+ // Milliseconds aren't zeroed out and may not be the same
+ assertEquals(convertedLocalTime/1000, correctLocalTime/1000);
+
+ localTimeZone = TimeZone.getTimeZone("GMT+0700");
+ correctLocal = new GregorianCalendar(localTimeZone);
+ correctLocal.set(2011, 2, 10, 0, 0, 0);
+ correctLocalTime = correctLocal.getTimeInMillis();
+
+ utcCalendar = new GregorianCalendar(utcTimeZone);
+ utcCalendar.set(2011, 2, 10, 12, 23, 34);
+ utcTimeMillis = utcCalendar.getTimeInMillis();
+ convertedLocalTime =
+ CalendarUtilities.getLocalAllDayCalendarTime(utcTimeMillis, localTimeZone);
+ // Milliseconds aren't zeroed out and may not be the same
+ assertEquals(convertedLocalTime/1000, correctLocalTime/1000);
+ }
+
+ public void testGetIntegerValueAsBoolean() {
+ ContentValues cv = new ContentValues();
+ cv.put("A", 1);
+ cv.put("B", 69);
+ cv.put("C", 0);
+ assertTrue(CalendarUtilities.getIntegerValueAsBoolean(cv, "A"));
+ assertTrue(CalendarUtilities.getIntegerValueAsBoolean(cv, "B"));
+ assertFalse(CalendarUtilities.getIntegerValueAsBoolean(cv, "C"));
+ assertFalse(CalendarUtilities.getIntegerValueAsBoolean(cv, "D"));
+ }
+}
+
+ // TODO Planned unit tests
+ // findNextTransition
+ // recurrenceFromRrule
+ // timeZoneToTziStringImpl
+ // getDSTCalendars
+ // millisToVCalendarTime
+ // millisToEasDateTime
+ // getTrueTransitionMinute
+ // getTrueTransitionHour
+
diff --git a/exchange2/tests/src/com/android/exchange/utility/ExchangeTestCase.java b/exchange2/tests/src/com/android/exchange/utility/ExchangeTestCase.java
new file mode 100644
index 0000000..86ea0c5
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/utility/ExchangeTestCase.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.utility;
+
+import com.android.emailcommon.provider.Account;
+import com.android.exchange.provider.EmailContentSetupUtils;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.test.AndroidTestCase;
+
+import java.util.ArrayList;
+
+public abstract class ExchangeTestCase extends AndroidTestCase {
+ private final ArrayList<Long> mCreatedAccountIds = new ArrayList<Long>();
+ protected Context mProviderContext;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mContext = getContext();
+ // Could use MockContext here if we switch over
+ mProviderContext = mContext;
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ ContentResolver resolver = mProviderContext.getContentResolver();
+ for (Long accountId: mCreatedAccountIds) {
+ resolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, accountId), null,
+ null);
+ }
+ }
+
+ /**
+ * Add an account to our list of test accounts; we'll delete it automatically in tearDown()
+ * @param account the account to be added to our list of test accounts
+ */
+ protected void addTestAccount(Account account) {
+ if (account.mId > 0) {
+ mCreatedAccountIds.add(account.mId);
+ }
+ }
+
+ /**
+ * Create a test account that will be automatically deleted when the test is finished
+ * @param name the name of the account
+ * @param saveIt whether or not to save the account in EmailProvider
+ * @return the account created
+ */
+ protected Account setupTestAccount(String name, boolean saveIt) {
+ Account account = EmailContentSetupUtils.setupAccount(name, saveIt, mProviderContext);
+ addTestAccount(account);
+ return account;
+ }
+}
diff --git a/exchange2/tests/src/com/android/exchange/utility/SimpleIcsWriterTests.java b/exchange2/tests/src/com/android/exchange/utility/SimpleIcsWriterTests.java
new file mode 100644
index 0000000..94c53b7
--- /dev/null
+++ b/exchange2/tests/src/com/android/exchange/utility/SimpleIcsWriterTests.java
@@ -0,0 +1,115 @@
+/* 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.emailcommon.utility.Utility;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Test for {@link SimpleIcsWriter}.
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.utility.SimpleIcsWriterTests exchange
+ */
+@SmallTest
+public class SimpleIcsWriterTests extends TestCase {
+ private static final String CRLF = "\r\n";
+ private static final String UTF8_1_BYTE = "a";
+ private static final String UTF8_2_BYTES = "\u00A2";
+ private static final String UTF8_3_BYTES = "\u20AC";
+ private static final String UTF8_4_BYTES = "\uD852\uDF62";
+
+ /**
+ * Test for {@link SimpleIcsWriter#writeTag}. It also covers {@link SimpleIcsWriter#getBytes()}
+ * and {@link SimpleIcsWriter#escapeTextValue}.
+ */
+ public void testWriteTag() {
+ final SimpleIcsWriter ics = new SimpleIcsWriter();
+ ics.writeTag("TAG1", null);
+ ics.writeTag("TAG2", "");
+ ics.writeTag("TAG3", "xyz");
+ ics.writeTag("SUMMARY", "TEST-TEST,;\r\n\\TEST");
+ ics.writeTag("SUMMARY2", "TEST-TEST,;\r\n\\TEST");
+ final String actual = Utility.fromUtf8(ics.getBytes());
+
+ assertEquals(
+ "TAG3:xyz" + CRLF +
+ "SUMMARY:TEST-TEST\\,\\;\\n\\\\TEST" + CRLF + // escaped
+ "SUMMARY2:TEST-TEST,;\r\n\\TEST" + CRLF // not escaped
+ , actual);
+ }
+
+ /**
+ * Verify that: We're folding lines correctly, and we're not splitting up a UTF-8 character.
+ */
+ public void testWriteLine() {
+ for (String last : new String[] {UTF8_1_BYTE, UTF8_2_BYTES, UTF8_3_BYTES, UTF8_4_BYTES}) {
+ for (int i = 70; i < 160; i++) {
+ String input = stringOfLength(i) + last;
+ checkWriteLine(input);
+ }
+ }
+ }
+
+ /**
+ * @return a String of {@code length} bytes in UTF-8.
+ */
+ private static String stringOfLength(int length) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ sb.append('0' +(i % 10));
+ }
+ return sb.toString();
+ }
+
+ private void checkWriteLine(String input) {
+ final SimpleIcsWriter ics = new SimpleIcsWriter();
+ ics.writeLine(input);
+ final byte[] bytes = ics.getBytes();
+
+ // Verify that no lines are longer than 75 bytes.
+ int numBytes = 0;
+ for (byte b : bytes) {
+ if (b == '\r') {
+ continue; // ignore
+ }
+ if (b == '\n') {
+ assertTrue("input=" + input, numBytes <= 75);
+ numBytes = 0;
+ continue;
+ }
+ numBytes++;
+ }
+ assertTrue("input=" + input, numBytes <= 75);
+
+ // If we're splitting up a UTF-8 character, fromUtf8() won't restore it correctly.
+ // If it becomes the same as input, we're doing the right thing.
+ final String actual = Utility.fromUtf8(bytes);
+ final String unfolded = actual.replace("\r\n\t", "");
+ assertEquals("input=" + input, input + "\r\n", unfolded);
+ }
+
+ public void testQuoteParamValue() {
+ assertNull(SimpleIcsWriter.quoteParamValue(null));
+ assertEquals("\"\"", SimpleIcsWriter.quoteParamValue(""));
+ assertEquals("\"a\"", SimpleIcsWriter.quoteParamValue("a"));
+ assertEquals("\"''\"", SimpleIcsWriter.quoteParamValue("\"'"));
+ assertEquals("\"abc\"", SimpleIcsWriter.quoteParamValue("abc"));
+ assertEquals("\"a'b'c\"", SimpleIcsWriter.quoteParamValue("a\"b\"c"));
+ }
+}
diff --git a/src/com/android/exchange/EasOutboxService.java b/src/com/android/exchange/EasOutboxService.java
index 9ff1ffc..4466925 100644
--- a/src/com/android/exchange/EasOutboxService.java
+++ b/src/com/android/exchange/EasOutboxService.java
@@ -80,7 +80,7 @@
// failure would probably generate an Exception before timing out anyway
public static final int SEND_MAIL_TIMEOUT = 15*MINUTES;
- protected EasOutboxService(Context _context, Mailbox _mailbox) {
+ public EasOutboxService(Context _context, Mailbox _mailbox) {
super(_context, _mailbox);
}
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index f1e544d..148a348 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -28,6 +28,7 @@
import android.os.Build;
import android.os.Bundle;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.provider.CalendarContract.Attendees;
import android.provider.CalendarContract.Events;
import android.text.TextUtils;
@@ -42,6 +43,8 @@
import com.android.emailcommon.mail.PackedString;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
+import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.emailcommon.provider.EmailContent.Message;
import com.android.emailcommon.provider.EmailContent.MessageColumns;
import com.android.emailcommon.provider.EmailContent.SyncColumns;
@@ -52,7 +55,6 @@
import com.android.emailcommon.service.EmailServiceConstants;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.EmailServiceStatus;
-import com.android.emailcommon.service.PolicyServiceProxy;
import com.android.emailcommon.utility.EmailClientConnectionManager;
import com.android.emailcommon.utility.Utility;
import com.android.exchange.CommandStatusException.CommandStatus;
@@ -66,12 +68,15 @@
import com.android.exchange.adapter.GalParser;
import com.android.exchange.adapter.MeetingResponseParser;
import com.android.exchange.adapter.MoveItemsParser;
+import com.android.exchange.adapter.Parser.EasParserException;
import com.android.exchange.adapter.Parser.EmptyStreamException;
+import com.android.exchange.adapter.PingParser;
import com.android.exchange.adapter.ProvisionParser;
import com.android.exchange.adapter.Serializer;
import com.android.exchange.adapter.SettingsParser;
import com.android.exchange.adapter.Tags;
import com.android.exchange.provider.GalResult;
+import com.android.exchange.provider.MailboxUtilities;
import com.android.exchange.utility.CalendarUtilities;
import com.google.common.annotations.VisibleForTesting;
@@ -100,12 +105,27 @@
import java.lang.Thread.State;
import java.net.URI;
import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.HashMap;
public class EasSyncService extends AbstractSyncService {
// DO NOT CHECK IN SET TO TRUE
public static final boolean DEBUG_GAL_SERVICE = false;
- protected static final String PING_COMMAND = "Ping";
+ private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID =
+ MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?";
+ private static final String WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING =
+ MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
+ '=' + Mailbox.CHECK_INTERVAL_PING;
+ private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " +
+ MailboxColumns.SYNC_INTERVAL + " IN (" + Mailbox.CHECK_INTERVAL_PING +
+ ',' + Mailbox.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.TYPE + "!=\"" +
+ Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + '\"';
+ private static final String WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX =
+ MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
+ '=' + Mailbox.CHECK_INTERVAL_PUSH_HOLD;
+
+ static private final String PING_COMMAND = "Ping";
// Command timeout is the the time allowed for reading data from an open connection before an
// IOException is thrown. After a small added allowance, our watchdog alarm goes off (allowing
// us to detect a silently dropped connection). The allowance is defined below.
@@ -115,10 +135,17 @@
// The extra time allowed beyond the COMMAND_TIMEOUT before which our watchdog alarm triggers
static private final int WATCHDOG_TIMEOUT_ALLOWANCE = 30*SECONDS;
+ // The amount of time the account mailbox will sleep if there are no pingable mailboxes
+ // This could happen if the sync time is set to "never"; we always want to check in from time
+ // to time, however, for folder list/policy changes
+ static private final int ACCOUNT_MAILBOX_SLEEP_TIME = 20*MINUTES;
+ static private final String ACCOUNT_MAILBOX_SLEEP_TEXT =
+ "Account mailbox sleeping for " + (ACCOUNT_MAILBOX_SLEEP_TIME / MINUTES) + "m";
+
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 protected final int EAS_REDIRECT_CODE = 451;
+ static private final int EAS_REDIRECT_CODE = 451;
static public final int INTERNAL_SERVER_ERROR_CODE = 500;
@@ -126,21 +153,47 @@
static public final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML";
static public final int MESSAGE_FLAG_MOVED_MESSAGE = 1 << Message.FLAG_SYNC_ADAPTER_SHIFT;
+
+ /**
+ * We start with an 8 minute timeout, and increase/decrease by 3 minutes at a time. There's
+ * no point having a timeout shorter than 5 minutes, I think; at that point, we can just let
+ * the ping exception out. The maximum I use is 17 minutes, which is really an empirical
+ * choice; too long and we risk silent connection loss and loss of push for that period. Too
+ * short and we lose efficiency/battery life.
+ *
+ * If we ever have to drop the ping timeout, we'll never increase it again. There's no point
+ * going into hysteresis; the NAT timeout isn't going to change without a change in connection,
+ * which will cause the sync service to be restarted at the starting heartbeat and going through
+ * the process again.
+ */
+ static private final int PING_MINUTES = 60; // in seconds
+ static private final int PING_FUDGE_LOW = 10;
+ static private final int PING_STARTING_HEARTBEAT = (8*PING_MINUTES)-PING_FUDGE_LOW;
+ static private final int PING_HEARTBEAT_INCREMENT = 3*PING_MINUTES;
+
+ // Maximum number of times we'll allow a sync to "loop" with MoreAvailable true before
+ // forcing it to stop. This number has been determined empirically.
+ static private final int MAX_LOOPING_COUNT = 100;
+
+ static private final int PROTOCOL_PING_STATUS_COMPLETED = 1;
+
// The amount of time we allow for a thread to release its post lock after receiving an alert
static private final int POST_LOCK_TIMEOUT = 10*SECONDS;
+ // Fallbacks (in minutes) for ping loop failures
+ static private final int MAX_PING_FAILURES = 1;
+ static private final int PING_FALLBACK_INBOX = 5;
+ static private final int PING_FALLBACK_PIM = 25;
+
// The EAS protocol Provision status for "we implement all of the policies"
static private final String PROVISION_STATUS_OK = "1";
// The EAS protocol Provision status meaning "we partially implement the policies"
static private final String PROVISION_STATUS_PARTIAL = "2";
static /*package*/ final String DEVICE_TYPE = "Android";
- static final String USER_AGENT = DEVICE_TYPE + '/' + Build.VERSION.RELEASE + '-' +
+ static private final String USER_AGENT = DEVICE_TYPE + '/' + Build.VERSION.RELEASE + '-' +
Eas.CLIENT_VERSION;
- // Maximum number of times we'll allow a sync to "loop" with MoreAvailable true before
- // forcing it to stop. This number has been determined empirically.
- static private final int MAX_LOOPING_COUNT = 100;
// Reasonable default
public String mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
public Double mProtocolVersionDouble;
@@ -155,26 +208,39 @@
public String mUserName;
public String mPassword;
- // The HttpPost in progress
- private volatile HttpPost mPendingPost = null;
- // Whether a POST was aborted due to alarm (watchdog alarm)
- protected boolean mPostAborted = false;
- // Whether a POST was aborted due to reset
- protected boolean mPostReset = false;
-
// The parameters for the connection must be modified through setConnectionParameters
private boolean mSsl = true;
private boolean mTrustSsl = false;
private String mClientCertAlias = null;
public ContentResolver mContentResolver;
+ private final String[] mBindArguments = new String[2];
+ private ArrayList<String> mPingChangeList;
+ // The HttpPost in progress
+ private volatile HttpPost mPendingPost = null;
+ // Our heartbeat when we are waiting for ping boxes to be ready
+ /*package*/ int mPingForceHeartbeat = 2*PING_MINUTES;
+ // The minimum heartbeat we will send
+ /*package*/ int mPingMinHeartbeat = (5*PING_MINUTES)-PING_FUDGE_LOW;
+ // The maximum heartbeat we will send
+ /*package*/ int mPingMaxHeartbeat = (17*PING_MINUTES)-PING_FUDGE_LOW;
+ // The ping time (in seconds)
+ /*package*/ int mPingHeartbeat = PING_STARTING_HEARTBEAT;
+ // The longest successful ping heartbeat
+ private int mPingHighWaterMark = 0;
+ // Whether we've ever lowered the heartbeat
+ /*package*/ boolean mPingHeartbeatDropped = false;
+ // Whether a POST was aborted due to alarm (watchdog alarm)
+ private boolean mPostAborted = false;
+ // Whether a POST was aborted due to reset
+ private boolean mPostReset = false;
// Whether or not the sync service is valid (usable)
public boolean mIsValid = true;
// Whether the most recent upsync failed (status 7)
public boolean mUpsyncFailed = false;
- protected EasSyncService(Context _context, Mailbox _mailbox) {
+ public EasSyncService(Context _context, Mailbox _mailbox) {
super(_context, _mailbox);
mContentResolver = _context.getContentResolver();
if (mAccount == null) {
@@ -198,17 +264,6 @@
this("EAS Validation");
}
- public static EasSyncService getServiceForMailbox(Context context, Mailbox m) {
- switch(m.mType) {
- case Mailbox.TYPE_EAS_ACCOUNT_MAILBOX:
- return new EasAccountService(context, m);
- case Mailbox.TYPE_OUTBOX:
- return new EasOutboxService(context, m);
- default:
- return new EasSyncService(context, m);
- }
- }
-
/**
* Try to wake up a sync thread that is waiting on an HttpClient POST and has waited past its
* socket timeout without having thrown an Exception
@@ -311,7 +366,7 @@
super.addRequest(request);
}
- void setupProtocolVersion(EasSyncService service, Header versionHeader)
+ private void setupProtocolVersion(EasSyncService service, Header versionHeader)
throws MessagingException {
// The string is a comma separated list of EAS versions in ascending order
// e.g. 1.0,2.0,2.5,12.0,12.1,14.0,14.1
@@ -407,7 +462,7 @@
* @param hostAuth the HostAuth we're using to validate
* @return true if we have an updated HostAuth (with redirect address); false otherwise
*/
- protected boolean getValidateRedirect(EasResponse resp, HostAuth hostAuth) {
+ private boolean getValidateRedirect(EasResponse resp, HostAuth hostAuth) {
Header locHeader = resp.getHeader("X-MS-Location");
if (locHeader != null) {
String loc;
@@ -541,7 +596,7 @@
int status = e.mStatus;
if (CommandStatus.isNeedsProvisioning(status)) {
// Get the policies and see if we are able to support them
- ProvisionParser pp = canProvision(this);
+ ProvisionParser pp = canProvision();
if (pp != null && pp.hasSupportablePolicySet()) {
// Set the proper result code and save the PolicySet in our Bundle
resultCode = MessagingException.SECURITY_POLICIES_REQUIRED;
@@ -554,12 +609,12 @@
resultCode = MessagingException.ACCESS_DENIED;
}
}
- } else {
+ } else
// If not, set the proper code (the account will not be created)
resultCode = MessagingException.SECURITY_POLICIES_UNSUPPORTED;
- bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET,
- pp.getPolicy());
- }
+ bundle.putStringArray(
+ EmailServiceProxy.VALIDATE_BUNDLE_UNSUPPORTED_POLICIES,
+ ((pp == null) ? null : pp.getUnsupportedPolicies()));
} else if (CommandStatus.isDeniedAccess(status)) {
userLog("Denied access: ", CommandStatus.toString(status));
resultCode = MessagingException.ACCESS_DENIED;
@@ -661,17 +716,6 @@
}
/**
- * Convert an EAS server url to a HostAuth host address
- * @param url a url, as provided by the Exchange server
- * @return our equivalent host address
- */
- protected String autodiscoverUrlToHostAddress(String url) {
- if (url == null) return null;
- // We need to extract the server address from a url
- return Uri.parse(url).getHost();
- }
-
- /**
* Use the Exchange 2007 AutoDiscover feature to try to retrieve server information using
* only an email address and the password
*
@@ -821,11 +865,14 @@
mobileSync = true;
}
} else if (mobileSync && name.equals("Url")) {
- String hostAddress =
- autodiscoverUrlToHostAddress(parser.nextText());
- if (hostAddress != null) {
- hostAuth.mAddress = hostAddress;
- userLog("Autodiscover, server: " + hostAddress);
+ 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);
}
}
}
@@ -1256,6 +1303,9 @@
protected EasResponse sendPing(byte[] bytes, int heartbeat) throws IOException {
Thread.currentThread().setName(mAccount.mDisplayName + ": Ping");
+ if (Eas.USER_LOG) {
+ userLog("Send ping, timeout: " + heartbeat + "s, high: " + mPingHighWaterMark + 's');
+ }
return sendHttpClientPost(PING_COMMAND, new ByteArrayEntity(bytes), (heartbeat+5)*SECONDS);
}
@@ -1351,7 +1401,7 @@
return EasResponse.fromHttpRequest(getClientConnectionManager(), client, method);
}
- String getTargetCollectionClassFromCursor(Cursor c) {
+ private String getTargetCollectionClassFromCursor(Cursor c) {
int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
if (type == Mailbox.TYPE_CONTACTS) {
return "Contacts";
@@ -1371,77 +1421,88 @@
* @return whether or not provisioning has been successful
* @throws IOException
*/
- public static boolean tryProvision(EasSyncService svc) throws IOException {
+ private boolean tryProvision() throws IOException {
// First, see if provisioning is even possible, i.e. do we support the policies required
// by the server
- ProvisionParser pp = canProvision(svc);
- if (pp == null) return false;
- Context context = svc.mContext;
- Account account = svc.mAccount;
- // Get the policies from ProvisionParser
- Policy policy = pp.getPolicy();
- Policy oldPolicy = null;
- // Grab the old policy (if any)
- if (svc.mAccount.mPolicyKey > 0) {
- oldPolicy = Policy.restorePolicyWithId(context, account.mPolicyKey);
- }
- // Update the account with a null policyKey (the key we've gotten is
- // temporary and cannot be used for syncing)
- PolicyServiceProxy.setAccountPolicy(context, account.mId, policy, null);
- // Make sure mAccount is current (with latest policy key)
- account.refresh(context);
- if (pp.getRemoteWipe()) {
- // We've gotten a remote wipe command
- ExchangeService.alwaysLog("!!! Remote wipe request received");
- // Start by setting the account to security hold
- PolicyServiceProxy.setAccountHoldFlag(context, account, true);
- // Force a stop to any running syncs for this account (except this one)
- ExchangeService.stopNonAccountMailboxSyncsForAccount(account.mId);
+ ProvisionParser pp = canProvision();
+ if (pp != null && pp.hasSupportablePolicySet()) {
+ // Get the policies from ProvisionParser
+ Policy policy = pp.getPolicy();
+ Policy oldPolicy = null;
+ // Grab the old policy (if any)
+ if (mAccount.mPolicyKey > 0) {
+ oldPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
+ }
+ // Update the account with a null policyKey (the key we've gotten is
+ // temporary and cannot be used for syncing)
+ Policy.setAccountPolicy(mContext, mAccount, policy, null);
+ // Make sure mAccount is current (with latest policy key)
+ mAccount.refresh(mContext);
+ // Make sure that SecurityPolicy is up-to-date
+ SecurityPolicyDelegate.policiesUpdated(mContext, mAccount.mId);
+ if (pp.getRemoteWipe()) {
+ // We've gotten a remote wipe command
+ ExchangeService.alwaysLog("!!! Remote wipe request received");
+ // Start by setting the account to security hold
+ SecurityPolicyDelegate.setAccountHoldFlag(mContext, mAccount, true);
+ // Force a stop to any running syncs for this account (except this one)
+ ExchangeService.stopNonAccountMailboxSyncsForAccount(mAccount.mId);
- // First, we've got to acknowledge it, but wrap the wipe in try/catch so that
- // we wipe the device regardless of any errors in acknowledgment
- try {
- ExchangeService.alwaysLog("!!! Acknowledging remote wipe to server");
- acknowledgeRemoteWipe(svc, pp.getSecuritySyncKey());
- } catch (Exception e) {
- // Because remote wipe is such a high priority task, we don't want to
- // circumvent it if there's an exception in acknowledgment
- }
- // Then, tell SecurityPolicy to wipe the device
- ExchangeService.alwaysLog("!!! Executing remote wipe");
- PolicyServiceProxy.remoteWipe(context);
- return false;
- } else if (pp.hasSupportablePolicySet() && PolicyServiceProxy.isActive(context, policy)) {
- // See if the required policies are in force; if they are, acknowledge the policies
- // to the server and get the final policy key
- // NOTE: For EAS 14.0, we already have the acknowledgment in the ProvisionParser
- String securitySyncKey;
- if (svc.mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
- securitySyncKey = pp.getSecuritySyncKey();
- } else {
- securitySyncKey = acknowledgeProvision(svc, pp.getSecuritySyncKey(),
- PROVISION_STATUS_OK);
- }
- if (securitySyncKey != null) {
- // If attachment policies have changed, fix up any affected attachment records
- if (oldPolicy != null) {
- if ((oldPolicy.mDontAllowAttachments != policy.mDontAllowAttachments) ||
- (oldPolicy.mMaxAttachmentSize != policy.mMaxAttachmentSize)) {
- Policy.setAttachmentFlagsForNewPolicy(context, account, policy);
- }
+ // If we're not the admin, we can't do the wipe, so just return
+ if (!SecurityPolicyDelegate.isActiveAdmin(mContext)) {
+ ExchangeService.alwaysLog("!!! Not device admin; can't wipe");
+ return false;
}
- // Write the final policy key to the Account and say we've been successful
- PolicyServiceProxy.setAccountPolicy(context, account.mId, policy, securitySyncKey);
- // Release any mailboxes that might be in a security hold
- ExchangeService.releaseSecurityHold(account);
- return true;
+
+ // First, we've got to acknowledge it, but wrap the wipe in try/catch so that
+ // we wipe the device regardless of any errors in acknowledgment
+ try {
+ ExchangeService.alwaysLog("!!! Acknowledging remote wipe to server");
+ acknowledgeRemoteWipe(pp.getSecuritySyncKey());
+ } catch (Exception e) {
+ // Because remote wipe is such a high priority task, we don't want to
+ // circumvent it if there's an exception in acknowledgment
+ }
+ // Then, tell SecurityPolicy to wipe the device
+ ExchangeService.alwaysLog("!!! Executing remote wipe");
+ SecurityPolicyDelegate.remoteWipe(mContext);
+ return false;
+ } else if (SecurityPolicyDelegate.isActive(mContext, policy)) {
+ // See if the required policies are in force; if they are, acknowledge the policies
+ // to the server and get the final policy key
+ // NOTE: For EAS 14.0, we already have the acknowledgment in the ProvisionParser
+ String securitySyncKey;
+ if (mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
+ securitySyncKey = pp.getSecuritySyncKey();
+ } else {
+ securitySyncKey = acknowledgeProvision(pp.getSecuritySyncKey(),
+ PROVISION_STATUS_OK);
+ }
+ if (securitySyncKey != null) {
+ // If attachment policies have changed, fix up any affected attachment records
+ if (oldPolicy != null) {
+ if ((oldPolicy.mDontAllowAttachments != policy.mDontAllowAttachments) ||
+ (oldPolicy.mMaxAttachmentSize != policy.mMaxAttachmentSize)) {
+ Policy.setAttachmentFlagsForNewPolicy(mContext, mAccount, policy);
+ }
+ }
+ // Write the final policy key to the Account and say we've been successful
+ Policy.setAccountPolicy(mContext, mAccount, policy, securitySyncKey);
+ // Release any mailboxes that might be in a security hold
+ ExchangeService.releaseSecurityHold(mAccount);
+ return true;
+ }
+ } else {
+ // Notify that we are blocked because of policies
+ // TODO: Indicate unsupported policies here?
+ SecurityPolicyDelegate.policiesRequired(mContext, mAccount.mId);
}
}
return false;
}
- private static String getPolicyType(Double protocolVersion) {
- return (protocolVersion >=
+ private String getPolicyType() {
+ return (mProtocolVersionDouble >=
Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) ? EAS_12_POLICY_TYPE : EAS_2_POLICY_TYPE;
}
@@ -1451,11 +1512,10 @@
* @return the ProvisionParser (holds policies and key) if we receive policies; null otherwise
* @throws IOException
*/
- public static ProvisionParser canProvision(EasSyncService svc) throws IOException {
+ private ProvisionParser canProvision() throws IOException {
Serializer s = new Serializer();
- Double protocolVersion = svc.mProtocolVersionDouble;
s.start(Tags.PROVISION_PROVISION);
- if (svc.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_SP1_DOUBLE) {
+ if (mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_SP1_DOUBLE) {
// Send settings information in 14.1 and greater
s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
s.data(Tags.SETTINGS_MODEL, Build.MODEL);
@@ -1465,27 +1525,27 @@
//s.data(Tags.SETTINGS_OS_LANGUAGE, "");
//s.data(Tags.SETTINGS_PHONE_NUMBER, "");
//s.data(Tags.SETTINGS_MOBILE_OPERATOR, "");
- s.data(Tags.SETTINGS_USER_AGENT, EasSyncService.USER_AGENT);
+ s.data(Tags.SETTINGS_USER_AGENT, USER_AGENT);
s.end().end(); // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION
}
s.start(Tags.PROVISION_POLICIES);
- s.start(Tags.PROVISION_POLICY);
- s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType(protocolVersion));
- s.end().end().end().done(); // PROVISION_POLICY, PROVISION_POLICIES, PROVISION_PROVISION
- EasResponse resp = svc.sendHttpClientPost("Provision", s.toByteArray());
+ s.start(Tags.PROVISION_POLICY).data(Tags.PROVISION_POLICY_TYPE, getPolicyType()).end();
+ s.end(); // PROVISION_POLICIES
+ s.end().done(); // PROVISION_PROVISION
+ EasResponse resp = sendHttpClientPost("Provision", s.toByteArray());
try {
int code = resp.getStatus();
if (code == HttpStatus.SC_OK) {
InputStream is = resp.getInputStream();
- ProvisionParser pp = new ProvisionParser(is, svc);
+ ProvisionParser pp = new ProvisionParser(is, this);
if (pp.parse()) {
// The PolicySet in the ProvisionParser will have the requirements for all KNOWN
// policies. If others are required, hasSupportablePolicySet will be false
if (pp.hasSupportablePolicySet() &&
- svc.mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
+ mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
// In EAS 14.0, we need the final security key in order to use the settings
// command
- String policyKey = acknowledgeProvision(svc, pp.getSecuritySyncKey(),
+ String policyKey = acknowledgeProvision(pp.getSecuritySyncKey(),
PROVISION_STATUS_OK);
if (policyKey != null) {
pp.setSecuritySyncKey(policyKey);
@@ -1495,11 +1555,11 @@
// accommodate the required policies). The server will agree to this if the
// "allow non-provisionable devices" setting is enabled on the server
ExchangeService.log("PolicySet is NOT fully supportable");
- if (acknowledgeProvision(svc, pp.getSecuritySyncKey(),
- PROVISION_STATUS_PARTIAL) != null) {
- // The server's ok with our inability to support policies, so we'll
- // clear them
- pp.clearUnsupportablePolicies();
+ String policyKey = acknowledgeProvision(pp.getSecuritySyncKey(),
+ PROVISION_STATUS_PARTIAL);
+ // Return either the parser (success) or null (failure)
+ if (policyKey != null) {
+ pp.clearUnsupportedPolicies();
}
}
return pp;
@@ -1520,24 +1580,22 @@
* @return the final policy key, which can be used for syncing
* @throws IOException
*/
- private static void acknowledgeRemoteWipe(EasSyncService svc, String tempKey)
- throws IOException {
- acknowledgeProvisionImpl(svc, tempKey, PROVISION_STATUS_OK, true);
+ private void acknowledgeRemoteWipe(String tempKey) throws IOException {
+ acknowledgeProvisionImpl(tempKey, PROVISION_STATUS_OK, true);
}
- private static String acknowledgeProvision(EasSyncService svc, String tempKey, String result)
- throws IOException {
- return acknowledgeProvisionImpl(svc, tempKey, result, false);
+ private String acknowledgeProvision(String tempKey, String result) throws IOException {
+ return acknowledgeProvisionImpl(tempKey, result, false);
}
- private static String acknowledgeProvisionImpl(EasSyncService svc, String tempKey,
- String status, boolean remoteWipe) throws IOException {
+ private String acknowledgeProvisionImpl(String tempKey, String status,
+ boolean remoteWipe) throws IOException {
Serializer s = new Serializer();
s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES);
s.start(Tags.PROVISION_POLICY);
// Use the proper policy type, depending on EAS version
- s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType(svc.mProtocolVersionDouble));
+ s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType());
s.data(Tags.PROVISION_POLICY_KEY, tempKey);
s.data(Tags.PROVISION_STATUS, status);
@@ -1548,16 +1606,15 @@
s.end();
}
s.end().done(); // PROVISION_PROVISION
- EasResponse resp = svc.sendHttpClientPost("Provision", s.toByteArray());
+ EasResponse resp = sendHttpClientPost("Provision", s.toByteArray());
try {
int code = resp.getStatus();
if (code == HttpStatus.SC_OK) {
InputStream is = resp.getInputStream();
- ProvisionParser pp = new ProvisionParser(is, svc);
+ ProvisionParser pp = new ProvisionParser(is, this);
if (pp.parse()) {
// Return the final policy key from the ProvisionParser
- String result = (pp.getSecuritySyncKey() == null) ? "failed" : "confirmed";
- ExchangeService.log("Provision " + result + " for " +
+ ExchangeService.log("Provision confirmation received for " +
(PROVISION_STATUS_PARTIAL.equals(status) ? "PART" : "FULL") + " set");
return pp.getSecuritySyncKey();
}
@@ -1566,7 +1623,7 @@
resp.close();
}
// On failures, log issue and return null
- ExchangeService.log("Provisioning failed for" +
+ ExchangeService.log("Provision confirmation failed for" +
(PROVISION_STATUS_PARTIAL.equals(status) ? "PART" : "FULL") + " set");
return null;
}
@@ -1595,7 +1652,662 @@
}
/**
+ * Translate exit status code to service status code (used in callbacks)
+ * @param exitStatus the service's exit status
+ * @return the corresponding service status
+ */
+ private int exitStatusToServiceStatus(int exitStatus) {
+ switch(exitStatus) {
+ case EXIT_SECURITY_FAILURE:
+ return EmailServiceStatus.SECURITY_FAILURE;
+ case EXIT_LOGIN_FAILURE:
+ return EmailServiceStatus.LOGIN_FAILED;
+ default:
+ return EmailServiceStatus.SUCCESS;
+ }
+ }
+
+ /**
+ * If possible, update the account to the new server address; report result
+ * @param resp the EasResponse from the current POST
+ * @return whether or not the redirect is handled and the POST should be retried
+ */
+ private boolean canHandleAccountMailboxRedirect(EasResponse resp) {
+ userLog("AccountMailbox redirect error");
+ HostAuth ha =
+ HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
+ if (ha != null && getValidateRedirect(resp, ha)) {
+ // Update the account's HostAuth with new values
+ ContentValues haValues = new ContentValues();
+ haValues.put(HostAuthColumns.ADDRESS, ha.mAddress);
+ ha.update(mContext, haValues);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Performs FolderSync
+ *
+ * @throws IOException
+ * @throws EasParserException
+ */
+ public void runAccountMailbox() throws IOException, EasParserException {
+ // Check that the account's mailboxes are consistent
+ MailboxUtilities.checkMailboxConsistency(mContext, mAccount.mId);
+ // Initialize exit status to success
+ mExitStatus = EXIT_DONE;
+ try {
+ try {
+ ExchangeService.callback()
+ .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0);
+ } catch (RemoteException e1) {
+ // Don't care if this fails
+ }
+
+ if (mAccount.mSyncKey == null) {
+ mAccount.mSyncKey = "0";
+ userLog("Account syncKey INIT to 0");
+ ContentValues cv = new ContentValues();
+ cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
+ mAccount.update(mContext, cv);
+ }
+
+ boolean firstSync = mAccount.mSyncKey.equals("0");
+ if (firstSync) {
+ userLog("Initial FolderSync");
+ }
+
+ // When we first start up, change all mailboxes to push.
+ ContentValues cv = new ContentValues();
+ cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
+ if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
+ WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING,
+ new String[] {Long.toString(mAccount.mId)}) > 0) {
+ ExchangeService.kick("change ping boxes to push");
+ }
+
+ // Determine our protocol version, if we haven't already and save it in the Account
+ // Also re-check protocol version at least once a day (in case of upgrade)
+ if (mAccount.mProtocolVersion == null || firstSync ||
+ ((System.currentTimeMillis() - mMailbox.mSyncTime) > DAYS)) {
+ userLog("Determine EAS protocol version");
+ EasResponse resp = sendHttpClientOptions();
+ try {
+ int code = resp.getStatus();
+ userLog("OPTIONS response: ", code);
+ if (code == HttpStatus.SC_OK) {
+ Header header = resp.getHeader("MS-ASProtocolCommands");
+ userLog(header.getValue());
+ header = resp.getHeader("ms-asprotocolversions");
+ try {
+ setupProtocolVersion(this, header);
+ } catch (MessagingException e) {
+ // Since we've already validated, this can't really happen
+ // But if it does, we'll rethrow this...
+ throw new IOException();
+ }
+ // Save the protocol version
+ cv.clear();
+ // Save the protocol version in the account; if we're using 12.0 or greater,
+ // set the flag for support of SmartForward
+ cv.put(Account.PROTOCOL_VERSION, mProtocolVersion);
+ mAccount.update(mContext, cv);
+ cv.clear();
+ // Save the sync time of the account mailbox to current time
+ cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
+ mMailbox.update(mContext, cv);
+ } else if (code == EAS_REDIRECT_CODE && canHandleAccountMailboxRedirect(resp)) {
+ // Cause this to re-run
+ throw new IOException("Will retry after a brief hold...");
+ } else {
+ errorLog("OPTIONS command failed; throwing IOException");
+ throw new IOException();
+ }
+ } finally {
+ resp.close();
+ }
+ }
+
+ // Make sure we've upgraded flags for ICS if we're using v12.0 or later
+ if (mProtocolVersionDouble >= 12.0 &&
+ (mAccount.mFlags & Account.FLAGS_SUPPORTS_SEARCH) == 0) {
+ cv.clear();
+ mAccount.mFlags = mAccount.mFlags | Account.FLAGS_SUPPORTS_SMART_FORWARD |
+ Account.FLAGS_SUPPORTS_SEARCH | Account.FLAGS_SUPPORTS_GLOBAL_SEARCH;
+ cv.put(AccountColumns.FLAGS, mAccount.mFlags);
+ mAccount.update(mContext, cv);
+ }
+
+ // Change all pushable boxes to push when we start the account mailbox
+ if (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) {
+ cv.clear();
+ cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
+ if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
+ ExchangeService.WHERE_IN_ACCOUNT_AND_PUSHABLE,
+ new String[] {Long.toString(mAccount.mId)}) > 0) {
+ userLog("Push account; set pushable boxes to push...");
+ }
+ }
+
+ while (!mStop) {
+ // If we're not allowed to sync (e.g. roaming policy), leave now
+ if (!ExchangeService.canAutoSync(mAccount)) return;
+ 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();
+ EasResponse resp = sendHttpClientPost("FolderSync", s.toByteArray());
+ try {
+ if (mStop) break;
+ int code = resp.getStatus();
+ if (code == HttpStatus.SC_OK) {
+ if (!resp.isEmpty()) {
+ InputStream is = resp.getInputStream();
+ // Returns true if we need to sync again
+ if (new FolderSyncParser(is, new AccountSyncAdapter(this)).parse()) {
+ continue;
+ }
+ }
+ } else if (EasResponse.isProvisionError(code)) {
+ userLog("FolderSync provisioning error: ", code);
+ throw new CommandStatusException(CommandStatus.NEEDS_PROVISIONING);
+ } else if (EasResponse.isAuthError(code)) {
+ userLog("FolderSync auth error: ", code);
+ mExitStatus = EXIT_LOGIN_FAILURE;
+ return;
+ } else if (code == EAS_REDIRECT_CODE && canHandleAccountMailboxRedirect(resp)) {
+ // This will cause a retry of the FolderSync
+ continue;
+ } else {
+ userLog("FolderSync response error: ", code);
+ }
+ } finally {
+ resp.close();
+ }
+
+ // Change all push/hold boxes to push
+ cv.clear();
+ cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH);
+ if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
+ WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX,
+ new String[] {Long.toString(mAccount.mId)}) > 0) {
+ userLog("Set push/hold boxes to push...");
+ }
+
+ try {
+ ExchangeService.callback()
+ .syncMailboxListStatus(mAccount.mId, exitStatusToServiceStatus(mExitStatus),
+ 0);
+ } catch (RemoteException e1) {
+ // Don't care if this fails
+ }
+
+ // Before each run of the pingLoop, if this Account has a PolicySet, make sure it's
+ // active; otherwise, clear out the key/flag. This should cause a provisioning
+ // error on the next POST, and start the security sequence over again
+ String key = mAccount.mSecuritySyncKey;
+ if (!TextUtils.isEmpty(key)) {
+ Policy policy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
+ if ((policy != null) && !SecurityPolicyDelegate.isActive(mContext, policy)) {
+ resetSecurityPolicies();
+ }
+ }
+
+ // Wait for push notifications.
+ String threadName = Thread.currentThread().getName();
+ try {
+ runPingLoop();
+ } catch (StaleFolderListException e) {
+ // We break out if we get told about a stale folder list
+ userLog("Ping interrupted; folder list requires sync...");
+ } catch (IllegalHeartbeatException e) {
+ // If we're sending an illegal heartbeat, reset either the min or the max to
+ // that heartbeat
+ resetHeartbeats(e.mLegalHeartbeat);
+ } finally {
+ Thread.currentThread().setName(threadName);
+ }
+ }
+ } catch (CommandStatusException e) {
+ // If the sync error is a provisioning failure (perhaps policies changed),
+ // let's try the provisioning procedure
+ // Provisioning must only be attempted for the account mailbox - trying to
+ // provision any other mailbox may result in race conditions and the
+ // creation of multiple policy keys.
+ int status = e.mStatus;
+ if (CommandStatus.isNeedsProvisioning(status)) {
+ if (!tryProvision()) {
+ // Set the appropriate failure status
+ mExitStatus = EXIT_SECURITY_FAILURE;
+ return;
+ }
+ } else if (CommandStatus.isDeniedAccess(status)) {
+ mExitStatus = EXIT_ACCESS_DENIED;
+ try {
+ ExchangeService.callback().syncMailboxListStatus(mAccount.mId,
+ EmailServiceStatus.ACCESS_DENIED, 0);
+ } catch (RemoteException e1) {
+ // Don't care if this fails
+ }
+ return;
+ } else {
+ userLog("Unexpected status: " + CommandStatus.toString(status));
+ mExitStatus = EXIT_EXCEPTION;
+ }
+ } catch (IOException e) {
+ // We catch this here to send the folder sync status callback
+ // A folder sync failed callback will get sent from run()
+ try {
+ if (!mStop) {
+ // NOTE: The correct status is CONNECTION_ERROR, but the UI displays this, and
+ // it's not really appropriate for EAS as this is not unexpected for a ping and
+ // connection errors are retried in any case
+ ExchangeService.callback()
+ .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.SUCCESS, 0);
+ }
+ } catch (RemoteException e1) {
+ // Don't care if this fails
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Reset either our minimum or maximum ping heartbeat to a heartbeat known to be legal
+ * @param legalHeartbeat a known legal heartbeat (from the EAS server)
+ */
+ /*package*/ void resetHeartbeats(int legalHeartbeat) {
+ userLog("Resetting min/max heartbeat, legal = " + legalHeartbeat);
+ // We are here because the current heartbeat (mPingHeartbeat) is invalid. Depending on
+ // whether the argument is above or below the current heartbeat, we can infer the need to
+ // change either the minimum or maximum heartbeat
+ if (legalHeartbeat > mPingHeartbeat) {
+ // The legal heartbeat is higher than the ping heartbeat; therefore, our minimum was
+ // too low. We respond by raising either or both of the minimum heartbeat or the
+ // force heartbeat to the argument value
+ if (mPingMinHeartbeat < legalHeartbeat) {
+ mPingMinHeartbeat = legalHeartbeat;
+ }
+ if (mPingForceHeartbeat < legalHeartbeat) {
+ mPingForceHeartbeat = legalHeartbeat;
+ }
+ // If our minimum is now greater than the max, bring them together
+ if (mPingMinHeartbeat > mPingMaxHeartbeat) {
+ mPingMaxHeartbeat = legalHeartbeat;
+ }
+ } else if (legalHeartbeat < mPingHeartbeat) {
+ // The legal heartbeat is lower than the ping heartbeat; therefore, our maximum was
+ // too high. We respond by lowering the maximum to the argument value
+ mPingMaxHeartbeat = legalHeartbeat;
+ // If our maximum is now less than the minimum, bring them together
+ if (mPingMaxHeartbeat < mPingMinHeartbeat) {
+ mPingMinHeartbeat = legalHeartbeat;
+ }
+ }
+ // Set current heartbeat to the legal heartbeat
+ mPingHeartbeat = legalHeartbeat;
+ // Allow the heartbeat logic to run
+ mPingHeartbeatDropped = false;
+ }
+
+ private void pushFallback(long mailboxId) {
+ Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
+ if (mailbox == null) {
+ return;
+ }
+ ContentValues cv = new ContentValues();
+ int mins = PING_FALLBACK_PIM;
+ if (mailbox.mType == Mailbox.TYPE_INBOX) {
+ mins = PING_FALLBACK_INBOX;
+ }
+ cv.put(Mailbox.SYNC_INTERVAL, mins);
+ mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
+ cv, null, null);
+ errorLog("*** PING ERROR LOOP: Set " + mailbox.mDisplayName + " to " + mins + " min sync");
+ ExchangeService.kick("push fallback");
+ }
+
+ /**
+ * Simplistic attempt to determine a NAT timeout, based on experience with various carriers
+ * and networks. The string "reset by peer" is very common in these situations, so we look for
+ * that specifically. We may add additional tests here as more is learned.
+ * @param message
+ * @return whether this message is likely associated with a NAT failure
+ */
+ private boolean isLikelyNatFailure(String message) {
+ if (message == null) return false;
+ if (message.contains("reset by peer")) {
+ return true;
+ }
+ return false;
+ }
+
+ private void runPingLoop() throws IOException, StaleFolderListException,
+ IllegalHeartbeatException, CommandStatusException {
+ int pingHeartbeat = mPingHeartbeat;
+ userLog("runPingLoop");
+ // Do push for all sync services here
+ long endTime = System.currentTimeMillis() + (30*MINUTES);
+ HashMap<String, Integer> pingErrorMap = new HashMap<String, Integer>();
+ ArrayList<String> readyMailboxes = new ArrayList<String>();
+ ArrayList<String> notReadyMailboxes = new ArrayList<String>();
+ int pingWaitCount = 0;
+ long inboxId = -1;
+
+ while ((System.currentTimeMillis() < endTime) && !mStop) {
+ // Count of pushable mailboxes
+ int pushCount = 0;
+ // Count of mailboxes that can be pushed right now
+ int canPushCount = 0;
+ // Count of uninitialized boxes
+ int uninitCount = 0;
+
+ Serializer s = new Serializer();
+ Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
+ MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId +
+ AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null);
+ if (c == null) throw new ProviderUnavailableException();
+ notReadyMailboxes.clear();
+ readyMailboxes.clear();
+ // Look for an inbox, and remember its id
+ if (inboxId == -1) {
+ inboxId = Mailbox.findMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_INBOX);
+ }
+ try {
+ // Loop through our pushed boxes seeing what is available to push
+ while (c.moveToNext()) {
+ pushCount++;
+ // Two requirements for push:
+ // 1) ExchangeService tells us the mailbox is syncable (not running/not stopped)
+ // 2) The syncKey isn't "0" (i.e. it's synced at least once)
+ long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
+ int pingStatus = ExchangeService.pingStatus(mailboxId);
+ String mailboxName = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
+ if (pingStatus == ExchangeService.PING_STATUS_OK) {
+ String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN);
+ if ((syncKey == null) || syncKey.equals("0")) {
+ // We can't push until the initial sync is done
+ pushCount--;
+ uninitCount++;
+ continue;
+ }
+
+ if (canPushCount++ == 0) {
+ // Initialize the Ping command
+ s.start(Tags.PING_PING)
+ .data(Tags.PING_HEARTBEAT_INTERVAL,
+ Integer.toString(pingHeartbeat))
+ .start(Tags.PING_FOLDERS);
+ }
+
+ String folderClass = getTargetCollectionClassFromCursor(c);
+ s.start(Tags.PING_FOLDER)
+ .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN))
+ .data(Tags.PING_CLASS, folderClass)
+ .end();
+ readyMailboxes.add(mailboxName);
+ } else if ((pingStatus == ExchangeService.PING_STATUS_RUNNING) ||
+ (pingStatus == ExchangeService.PING_STATUS_WAITING)) {
+ notReadyMailboxes.add(mailboxName);
+ } else if (pingStatus == ExchangeService.PING_STATUS_UNABLE) {
+ pushCount--;
+ userLog(mailboxName, " in error state; ignore");
+ continue;
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ if (Eas.USER_LOG) {
+ if (!notReadyMailboxes.isEmpty()) {
+ userLog("Ping not ready for: " + notReadyMailboxes);
+ }
+ if (!readyMailboxes.isEmpty()) {
+ userLog("Ping ready for: " + readyMailboxes);
+ }
+ }
+
+ // If we've waited 10 seconds or more, just ping with whatever boxes are ready
+ // But use a shorter than normal heartbeat
+ boolean forcePing = !notReadyMailboxes.isEmpty() && (pingWaitCount > 5);
+
+ if ((canPushCount > 0) && ((canPushCount == pushCount) || forcePing)) {
+ // If all pingable boxes are ready for push, send Ping to the server
+ s.end().end().done();
+ pingWaitCount = 0;
+ mPostReset = false;
+ mPostAborted = false;
+
+ // If we've been stopped, this is a good time to return
+ if (mStop) return;
+
+ long pingTime = SystemClock.elapsedRealtime();
+ try {
+ // Send the ping, wrapped by appropriate timeout/alarm
+ if (forcePing) {
+ userLog("Forcing ping after waiting for all boxes to be ready");
+ }
+ EasResponse resp =
+ sendPing(s.toByteArray(), forcePing ? mPingForceHeartbeat : pingHeartbeat);
+
+ try {
+ int code = resp.getStatus();
+ userLog("Ping response: ", code);
+
+ // If we're not allowed to sync (e.g. roaming policy), terminate gracefully
+ // now; otherwise we might start a sync based on the response
+ if (!ExchangeService.canAutoSync(mAccount)) {
+ mStop = true;
+ }
+
+ // Return immediately if we've been asked to stop during the ping
+ if (mStop) {
+ userLog("Stopping pingLoop");
+ return;
+ }
+
+ if (code == HttpStatus.SC_OK) {
+ // Make sure to clear out any pending sync errors
+ ExchangeService.removeFromSyncErrorMap(mMailboxId);
+ if (!resp.isEmpty()) {
+ InputStream is = resp.getInputStream();
+ int pingResult = parsePingResult(is, mContentResolver,
+ pingErrorMap);
+ // If our ping completed (status = 1), and wasn't forced and we're
+ // not at the maximum, try increasing timeout by two minutes
+ if (pingResult == PROTOCOL_PING_STATUS_COMPLETED && !forcePing) {
+ if (pingHeartbeat > mPingHighWaterMark) {
+ mPingHighWaterMark = pingHeartbeat;
+ userLog("Setting high water mark at: ", mPingHighWaterMark);
+ }
+ if ((pingHeartbeat < mPingMaxHeartbeat) &&
+ !mPingHeartbeatDropped) {
+ pingHeartbeat += PING_HEARTBEAT_INCREMENT;
+ if (pingHeartbeat > mPingMaxHeartbeat) {
+ pingHeartbeat = mPingMaxHeartbeat;
+ }
+ userLog("Increase ping heartbeat to ", pingHeartbeat, "s");
+ }
+ }
+ } else {
+ userLog("Ping returned empty result; throwing IOException");
+ throw new IOException();
+ }
+ } else if (EasResponse.isAuthError(code)) {
+ mExitStatus = EXIT_LOGIN_FAILURE;
+ userLog("Authorization error during Ping: ", code);
+ throw new IOException();
+ } else if (EasResponse.isProvisionError(code)) {
+ userLog("Provisioning required during Ping: ", code);
+ throw new CommandStatusException(CommandStatus.NEEDS_PROVISIONING);
+ }
+ } finally {
+ resp.close();
+ }
+ } catch (IOException e) {
+ String message = e.getMessage();
+ // If we get the exception that is indicative of a NAT timeout and if we
+ // haven't yet "fixed" the timeout, back off by two minutes and "fix" it
+ boolean hasMessage = message != null;
+ userLog("IOException runPingLoop: " + (hasMessage ? message : "[no message]"));
+ if (mPostReset) {
+ // Nothing to do in this case; this is ExchangeService telling us to try
+ // another ping.
+ } else if (mPostAborted || isLikelyNatFailure(message)) {
+ long pingLength = SystemClock.elapsedRealtime() - pingTime;
+ if ((pingHeartbeat > mPingMinHeartbeat) &&
+ (pingHeartbeat > mPingHighWaterMark)) {
+ pingHeartbeat -= PING_HEARTBEAT_INCREMENT;
+ mPingHeartbeatDropped = true;
+ if (pingHeartbeat < mPingMinHeartbeat) {
+ pingHeartbeat = mPingMinHeartbeat;
+ }
+ userLog("Decreased ping heartbeat to ", pingHeartbeat, "s");
+ } else if (mPostAborted) {
+ // There's no point in throwing here; this can happen in two cases
+ // 1) An alarm, which indicates minutes without activity; no sense
+ // backing off
+ // 2) ExchangeService abort, due to sync of mailbox. Again, we want to
+ // keep on trying to ping
+ userLog("Ping aborted; retry");
+ } else if (pingLength < 2000) {
+ userLog("Abort or NAT type return < 2 seconds; throwing IOException");
+ throw e;
+ } else {
+ userLog("NAT type IOException");
+ }
+ } else if (hasMessage && message.contains("roken pipe")) {
+ // The "broken pipe" error (uppercase or lowercase "b") seems to be an
+ // internal error, so let's not throw an exception (which leads to delays)
+ // but rather simply run through the loop again
+ } else {
+ throw e;
+ }
+ }
+ } else if (forcePing) {
+ // In this case, there aren't any boxes that are pingable, but there are boxes
+ // waiting (for IOExceptions)
+ userLog("pingLoop waiting 60s for any pingable boxes");
+ sleep(60*SECONDS, true);
+ } else if (pushCount > 0) {
+ // If we want to Ping, but can't just yet, wait a little bit
+ // TODO Change sleep to wait and use notify from ExchangeService when a sync ends
+ sleep(2*SECONDS, false);
+ pingWaitCount++;
+ //userLog("pingLoop waited 2s for: ", (pushCount - canPushCount), " box(es)");
+ } else if (uninitCount > 0) {
+ // In this case, we're doing an initial sync of at least one mailbox. Since this
+ // is typically a one-time case, I'm ok with trying again every 10 seconds until
+ // we're in one of the other possible states.
+ userLog("pingLoop waiting for initial sync of ", uninitCount, " box(es)");
+ sleep(10*SECONDS, true);
+ } else if (inboxId == -1) {
+ // In this case, we're still syncing mailboxes, so sleep for only a short time
+ sleep(45*SECONDS, true);
+ } else {
+ // We've got nothing to do, so we'll check again in 20 minutes at which time
+ // we'll update the folder list, check for policy changes and/or remote wipe, etc.
+ // Let the device sleep in the meantime...
+ userLog(ACCOUNT_MAILBOX_SLEEP_TEXT);
+ sleep(ACCOUNT_MAILBOX_SLEEP_TIME, true);
+ }
+ }
+
+ // Save away the current heartbeat
+ mPingHeartbeat = pingHeartbeat;
+ }
+
+ private void sleep(long ms, boolean runAsleep) {
+ if (runAsleep) {
+ ExchangeService.runAsleep(mMailboxId, ms+(5*SECONDS));
+ }
+ try {
+ Thread.sleep(ms);
+ } catch (InterruptedException e) {
+ // Doesn't matter whether we stop early; it's the thought that counts
+ } finally {
+ if (runAsleep) {
+ ExchangeService.runAwake(mMailboxId);
+ }
+ }
+ }
+
+ private int parsePingResult(InputStream is, ContentResolver cr,
+ HashMap<String, Integer> errorMap)
+ throws IOException, StaleFolderListException, IllegalHeartbeatException,
+ CommandStatusException {
+ PingParser pp = new PingParser(is, this);
+ if (pp.parse()) {
+ // True indicates some mailboxes need syncing...
+ // syncList has the serverId's of the mailboxes...
+ mBindArguments[0] = Long.toString(mAccount.mId);
+ mPingChangeList = pp.getSyncList();
+ for (String serverId: mPingChangeList) {
+ mBindArguments[1] = serverId;
+ Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
+ WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null);
+ if (c == null) throw new ProviderUnavailableException();
+ try {
+ if (c.moveToFirst()) {
+
+ /**
+ * Check the boxes reporting changes to see if there really were any...
+ * We do this because bugs in various Exchange servers can put us into a
+ * looping behavior by continually reporting changes in a mailbox, even when
+ * there aren't any.
+ *
+ * This behavior is seemingly random, and therefore we must code defensively
+ * by backing off of push behavior when it is detected.
+ *
+ * One known cause, on certain Exchange 2003 servers, is acknowledged by
+ * Microsoft, and the server hotfix for this case can be found at
+ * http://support.microsoft.com/kb/923282
+ */
+
+ // Check the status of the last sync
+ String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN);
+ int type = ExchangeService.getStatusType(status);
+ // This check should always be true...
+ if (type == ExchangeService.SYNC_PING) {
+ int changeCount = ExchangeService.getStatusChangeCount(status);
+ if (changeCount > 0) {
+ errorMap.remove(serverId);
+ } else if (changeCount == 0) {
+ // This means that a ping reported changes in error; we keep a count
+ // of consecutive errors of this kind
+ String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
+ Integer failures = errorMap.get(serverId);
+ if (failures == null) {
+ userLog("Last ping reported changes in error for: ", name);
+ errorMap.put(serverId, 1);
+ } else if (failures > MAX_PING_FAILURES) {
+ // We'll back off of push for this box
+ pushFallback(c.getLong(Mailbox.CONTENT_ID_COLUMN));
+ continue;
+ } else {
+ userLog("Last ping reported changes in error for: ", name);
+ errorMap.put(serverId, failures + 1);
+ }
+ }
+ }
+
+ // If there were no problems with previous sync, we'll start another one
+ ExchangeService.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN),
+ ExchangeService.SYNC_PING, null);
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+ return pp.getSyncStatus();
+ }
+
+ /**
* Common code to sync E+PIM data
+ *
* @param target an EasMailbox, EasContacts, or EasCalendar object
*/
public void sync(AbstractSyncAdapter target) throws IOException {
@@ -1845,14 +2557,14 @@
* Clears out the security policies associated with the account, forcing a provision error
* and a re-sync of the policy information for the account.
*/
- @SuppressWarnings("deprecation")
- void resetSecurityPolicies() {
+ private void resetSecurityPolicies() {
ContentValues cv = new ContentValues();
cv.put(AccountColumns.SECURITY_FLAGS, 0);
cv.putNull(AccountColumns.SECURITY_SYNC_KEY);
long accountId = mAccount.mId;
mContentResolver.update(ContentUris.withAppendedId(
Account.CONTENT_URI, accountId), cv, null, null);
+ SecurityPolicyDelegate.policiesRequired(mContext, accountId);
}
@Override
@@ -1869,6 +2581,9 @@
int trafficFlags = TrafficFlags.getSyncFlags(mContext, mAccount);
if ((mMailbox == null) || (mAccount == null)) {
return;
+ } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
+ TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_EMAIL);
+ runAccountMailbox();
} else {
AbstractSyncAdapter target;
if (mMailbox.mType == Mailbox.TYPE_CONTACTS) {
diff --git a/src/com/android/exchange/ExchangeService.java b/src/com/android/exchange/ExchangeService.java
index 0107d55..833e0c4 100644
--- a/src/com/android/exchange/ExchangeService.java
+++ b/src/com/android/exchange/ExchangeService.java
@@ -53,8 +53,6 @@
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.Attachment;
-import com.android.emailcommon.provider.EmailContent.Body;
-import com.android.emailcommon.provider.EmailContent.BodyColumns;
import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.emailcommon.provider.EmailContent.Message;
@@ -177,8 +175,6 @@
public static final int PING_STATUS_WAITING = 2;
// Service had a fatal error; can't run
public static final int PING_STATUS_UNABLE = 3;
- // Service is disabled by user (checkbox)
- public static final int PING_STATUS_DISABLED = 4;
private static final int MAX_CLIENT_CONNECTION_MANAGER_SHUTDOWNS = 1;
@@ -287,7 +283,6 @@
}
}
- @Override
public void loadAttachmentStatus(final long messageId, final long attachmentId,
final int status, final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@@ -298,7 +293,6 @@
});
}
- @Override
public void sendMessageStatus(final long messageId, final String subject, final int status,
final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@@ -309,7 +303,6 @@
});
}
- @Override
public void syncMailboxListStatus(final long accountId, final int status,
final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@@ -320,7 +313,6 @@
});
}
- @Override
public void syncMailboxStatus(final long mailboxId, final int status,
final int progress) {
broadcastCallback(new ServiceCallbackWrapper() {
@@ -330,11 +322,6 @@
}
});
}
-
- @Override
- public void loadMessageStatus(long messageId, int statusCode, int progress)
- throws RemoteException {
- }
};
/**
@@ -342,23 +329,19 @@
*/
private final IEmailService.Stub mBinder = new IEmailService.Stub() {
- @Override
public int getApiLevel() {
return Api.LEVEL;
}
- @Override
public Bundle validate(HostAuth hostAuth) throws RemoteException {
return AbstractSyncService.validate(EasSyncService.class,
hostAuth, ExchangeService.this);
}
- @Override
public Bundle autoDiscover(String userName, String password) throws RemoteException {
return new EasSyncService().tryAutodiscover(userName, password);
}
- @Override
public void startSync(long mailboxId, boolean userRequest) throws RemoteException {
ExchangeService exchangeService = INSTANCE;
if (exchangeService == null) return;
@@ -417,24 +400,20 @@
ExchangeService.SYNC_SERVICE_START_SYNC, null);
}
- @Override
public void stopSync(long mailboxId) throws RemoteException {
stopManualSync(mailboxId);
}
- @Override
public void loadAttachment(long attachmentId, boolean background) throws RemoteException {
Attachment att = Attachment.restoreAttachmentWithId(ExchangeService.this, attachmentId);
log("loadAttachment " + attachmentId + ": " + att.mFileName);
sendMessageRequest(new PartRequest(att, null, null));
}
- @Override
public void updateFolderList(long accountId) throws RemoteException {
reloadFolderList(ExchangeService.this, accountId, false);
}
- @Override
public void hostChanged(long accountId) throws RemoteException {
ExchangeService exchangeService = INSTANCE;
if (exchangeService == null) return;
@@ -459,38 +438,31 @@
kick("host changed");
}
- @Override
public void setLogging(int flags) throws RemoteException {
Eas.setUserDebug(flags);
}
- @Override
public void sendMeetingResponse(long messageId, int response) throws RemoteException {
sendMessageRequest(new MeetingResponseRequest(messageId, response));
}
- @Override
public void loadMore(long messageId) throws RemoteException {
}
// The following three methods are not implemented in this version
- @Override
public boolean createFolder(long accountId, String name) throws RemoteException {
return false;
}
- @Override
public boolean deleteFolder(long accountId, String name) throws RemoteException {
return false;
}
- @Override
public boolean renameFolder(long accountId, String oldName, String newName)
throws RemoteException {
return false;
}
- @Override
public void setCallback(IEmailServiceCallback cb) throws RemoteException {
mCallbackList.register(cb);
}
@@ -501,39 +473,19 @@
* @param accountId the account whose data should be deleted
* @throws RemoteException
*/
- @Override
public void deleteAccountPIMData(long accountId) throws RemoteException {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return;
// Stop any running syncs
ExchangeService.stopAccountSyncs(accountId);
// Delete the data
ExchangeService.deleteAccountPIMData(accountId);
- long accountMailboxId = Mailbox.findMailboxOfType(exchangeService, accountId,
- Mailbox.TYPE_EAS_ACCOUNT_MAILBOX);
- if (accountMailboxId != Mailbox.NO_MAILBOX) {
- // Make sure the account mailbox is held due to security
- synchronized(sSyncLock) {
- mSyncErrorMap.put(accountMailboxId, exchangeService.new SyncError(
- AbstractSyncService.EXIT_SECURITY_FAILURE, false));
-
- }
- }
- // Make sure the reconciler runs
- runAccountReconcilerSync(ExchangeService.this);
}
- @Override
public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) {
ExchangeService exchangeService = INSTANCE;
if (exchangeService == null) return 0;
return Search.searchMessages(exchangeService, accountId, searchParams,
destMailboxId);
}
-
- @Override
- public void sendMail(long accountId) throws RemoteException {
- }
};
/**
@@ -628,14 +580,14 @@
Mailbox mailbox =
Mailbox.restoreMailboxOfType(exchangeService, accountId, Mailbox.TYPE_CONTACTS);
if (mailbox != null) {
- EasSyncService service = EasSyncService.getServiceForMailbox(exchangeService, mailbox);
+ EasSyncService service = new EasSyncService(exchangeService, mailbox);
ContactsSyncAdapter adapter = new ContactsSyncAdapter(service);
adapter.wipe();
}
mailbox =
Mailbox.restoreMailboxOfType(exchangeService, accountId, Mailbox.TYPE_CALENDAR);
if (mailbox != null) {
- EasSyncService service = EasSyncService.getServiceForMailbox(exchangeService, mailbox);
+ EasSyncService service = new EasSyncService(exchangeService, mailbox);
CalendarSyncAdapter adapter = new CalendarSyncAdapter(service);
adapter.wipe();
}
@@ -682,13 +634,17 @@
public void run() {
synchronized (mAccountList) {
for (Account account : mAccountList) {
- if (onSecurityHold(account)) {
+ if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
// If we're in a security hold, and our policies are active, release
- // the hold
+ // the hold; otherwise, ping PolicyService that this account's
+ // policies are required
if (PolicyServiceProxy.isActive(ExchangeService.this, null)) {
PolicyServiceProxy.setAccountHoldFlag(ExchangeService.this,
account, false);
log("isActive true; release hold for " + account.mDisplayName);
+ } else {
+ PolicyServiceProxy.policiesRequired(ExchangeService.this,
+ account.mId);
}
}
}
@@ -848,8 +804,7 @@
@Override
public void onChange(boolean selfChange) {
new Thread(new Runnable() {
- @Override
- public void run() {
+ public void run() {
onAccountChanged();
}}, "Account Observer").start();
}
@@ -951,7 +906,6 @@
// See if the user has changed syncing of our calendar
if (!selfChange) {
new Thread(new Runnable() {
- @Override
public void run() {
try {
Cursor c = mResolver.query(Calendars.CONTENT_URI,
@@ -978,8 +932,7 @@
stopManualSync(mailbox.mId);
// Set the syncKey to 0 (reset)
EasSyncService service =
- EasSyncService.getServiceForMailbox(
- INSTANCE, mailbox);
+ new EasSyncService(INSTANCE, mailbox);
CalendarSyncAdapter adapter =
new CalendarSyncAdapter(service);
try {
@@ -1257,7 +1210,6 @@
}
static public ConnPerRoute sConnPerRoute = new ConnPerRoute() {
- @Override
public int getMaxForRoute(HttpRoute route) {
return 8;
}
@@ -1545,8 +1497,7 @@
threadName += service.mMailbox.mDisplayName;
}
new Thread(new Runnable() {
- @Override
- public void run() {
+ public void run() {
Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, id);
if (m != null) {
// We ignore drafts completely (doesn't sync). Changes in Outbox are
@@ -1637,18 +1588,18 @@
* @param service the service to start
* @param m the Mailbox on which the service will operate
*/
- private void startServiceThread(AbstractSyncService service) {
+ private void startServiceThread(AbstractSyncService service, Mailbox m) {
+ if (m == null) return;
synchronized (sSyncLock) {
- Mailbox mailbox = service.mMailbox;
- String mailboxName = mailbox.mDisplayName;
+ String mailboxName = m.mDisplayName;
String accountName = service.mAccount.mDisplayName;
Thread thread = new Thread(service, mailboxName + "[" + accountName + "]");
log("Starting thread for " + mailboxName + " in account " + accountName);
thread.start();
- mServiceMap.put(mailbox.mId, service);
- runAwake(mailbox.mId);
- if (mailbox.mServerId != null && mailbox.mType != Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
- stopPing(mailbox.mAccountKey);
+ mServiceMap.put(m.mId, service);
+ runAwake(m.mId);
+ if ((m.mServerId != null) && !m.mServerId.startsWith(Eas.ACCOUNT_MAILBOX_PREFIX)) {
+ stopPing(m.mAccountKey);
}
}
}
@@ -1665,7 +1616,7 @@
if (m != null) {
String serverId = m.mServerId;
if (m.mAccountKey == accountId && serverId != null &&
- m.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
+ serverId.startsWith(Eas.ACCOUNT_MAILBOX_PREFIX)) {
// Here's our account mailbox; reset him (stopping pings)
AbstractSyncService svc = mServiceMap.get(mailboxId);
svc.reset();
@@ -1676,7 +1627,6 @@
}
private void requestSync(Mailbox m, int reason, Request req) {
- int syncStatus = EmailContent.SYNC_STATUS_BACKGROUND;
// Don't sync if there's no connectivity
if (sConnectivityHold || (m == null) || sStop) {
if (reason >= SYNC_CALLBACK_START) {
@@ -1694,34 +1644,18 @@
// Always make sure there's not a running instance of this service
AbstractSyncService service = mServiceMap.get(m.mId);
if (service == null) {
- service = EasSyncService.getServiceForMailbox(this, m);
+ service = new EasSyncService(this, m);
if (!((EasSyncService)service).mIsValid) return;
service.mSyncReason = reason;
if (req != null) {
service.addRequest(req);
}
- startServiceThread(service);
- if (reason >= SYNC_CALLBACK_START) {
- syncStatus = EmailContent.SYNC_STATUS_USER;
- }
- setMailboxSyncStatus(m.mId, syncStatus);
+ startServiceThread(service, m);
}
}
}
}
- private void setMailboxSyncStatus(long id, int status) {
- ContentValues values = new ContentValues();
- values.put(Mailbox.UI_SYNC_STATUS, status);
- mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), values, null, null);
- }
-
- private void setMailboxLastSyncResult(long id, int result) {
- ContentValues values = new ContentValues();
- values.put(Mailbox.UI_LAST_SYNC_RESULT, result);
- mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id), values, null, null);
- }
-
private void stopServiceThreads() {
synchronized (sSyncLock) {
ArrayList<Long> toStop = new ArrayList<Long>();
@@ -1937,7 +1871,6 @@
}
}
- @Override
public void run() {
sStop = false;
alwaysLog("ExchangeService thread running");
@@ -2156,7 +2089,6 @@
if (policy == null) {
policy = Policy.restorePolicyWithId(INSTANCE, policyKey);
account.mPolicy = policy;
- if (!PolicyServiceProxy.isActive(exchangeService, policy)) return false;
}
if (policy != null && policy.mRequireManualSyncWhenRoaming && networkInfo.isRoaming()) {
return false;
@@ -2187,12 +2119,9 @@
private boolean isMailboxSyncable(Account account, int type) {
// This 'if' statement performs checks to see whether or not a mailbox is a
// candidate for syncing based on policies, user settings, & other restrictions
- if (type == Mailbox.TYPE_OUTBOX) {
- // Outbox is always syncable
+ if (type == Mailbox.TYPE_OUTBOX || type == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
+ // Outbox and account mailbox are always syncable
return true;
- } else if (type == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
- // Always sync EAS mailbox unless master sync is off
- return ContentResolver.getMasterSyncAutomatically();
} else if (type == Mailbox.TYPE_CONTACTS || type == Mailbox.TYPE_CALENDAR) {
// Contacts/Calendar obey this setting from ContentResolver
if (!ContentResolver.getMasterSyncAutomatically()) {
@@ -2317,7 +2246,7 @@
} else if (mailboxType == Mailbox.TYPE_OUTBOX) {
if (hasSendableMessages(c)) {
Mailbox m = EmailContent.getContent(c, Mailbox.class);
- startServiceThread(EasSyncService.getServiceForMailbox(this, m));
+ startServiceThread(new EasOutboxService(this, m), m);
}
} else if (syncInterval > 0 && syncInterval <= ONE_DAY_MINUTES) {
long lastSync = c.getLong(Mailbox.CONTENT_SYNC_TIME_COLUMN);
@@ -2424,29 +2353,14 @@
static public void sendMessageRequest(Request req) {
ExchangeService exchangeService = INSTANCE;
+ if (exchangeService == null) return;
Message msg = Message.restoreMessageWithId(exchangeService, req.mMessageId);
- if (msg == null) return;
- long mailboxId = msg.mMailboxKey;
- Mailbox mailbox = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
- if (mailbox == null) return;
-
- // If we're loading an attachment for Outbox, we want to look at the source message
- // to find the loading mailbox
- if (mailbox.mType == Mailbox.TYPE_OUTBOX) {
- long sourceId = Utility.getFirstRowLong(exchangeService, Body.CONTENT_URI,
- new String[] {BodyColumns.SOURCE_MESSAGE_KEY},
- BodyColumns.MESSAGE_KEY + "=?",
- new String[] {Long.toString(msg.mId)}, null, 0, -1L);
- if (sourceId != -1L) {
- EmailContent.Message sourceMsg =
- EmailContent.Message.restoreMessageWithId(exchangeService, sourceId);
- if (sourceMsg != null) {
- mailboxId = sourceMsg.mMailboxKey;
- }
- }
+ if (msg == null) {
+ return;
}
-
+ long mailboxId = msg.mMailboxKey;
AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId);
+
if (service == null) {
startManualSync(mailboxId, SYNC_SERVICE_PART_REQUEST, req);
kick("part request");
@@ -2582,7 +2496,6 @@
return;
}
exchangeService.releaseMailbox(mailboxId);
- exchangeService.setMailboxSyncStatus(mailboxId, EmailContent.SYNC_STATUS_NONE);
ConcurrentHashMap<Long, SyncError> errorMap = exchangeService.mSyncErrorMap;
SyncError syncError = errorMap.get(mailboxId);
@@ -2601,53 +2514,38 @@
}
}
- int lastResult = EmailContent.LAST_SYNC_RESULT_SUCCESS;
- // For error states, whether the error is fatal (won't automatically be retried)
- boolean errorIsFatal = true;
- try {
- switch (exitStatus) {
- case AbstractSyncService.EXIT_DONE:
- if (svc.hasPendingRequests()) {
- // TODO Handle this case
- }
- errorMap.remove(mailboxId);
- // If we've had a successful sync, clear the shutdown count
- synchronized (ExchangeService.class) {
- sClientConnectionManagerShutdownCount = 0;
- }
- // Leave now; other statuses are errors
- return;
- // I/O errors get retried at increasing intervals
- case AbstractSyncService.EXIT_IO_ERROR:
- if (syncError != null) {
- syncError.escalate();
- log(m.mDisplayName + " held for " + syncError.holdDelay + "ms");
- } else {
- log(m.mDisplayName + " added to syncErrorMap, hold for 15s");
- }
- lastResult = EmailContent.LAST_SYNC_RESULT_CONNECTION_ERROR;
- errorIsFatal = false;
- break;
- // These errors are not retried automatically
- case AbstractSyncService.EXIT_LOGIN_FAILURE:
- new AccountServiceProxy(exchangeService).notifyLoginFailed(m.mAccountKey);
- lastResult = EmailContent.LAST_SYNC_RESULT_AUTH_ERROR;
- break;
- case AbstractSyncService.EXIT_SECURITY_FAILURE:
- case AbstractSyncService.EXIT_ACCESS_DENIED:
- lastResult = EmailContent.LAST_SYNC_RESULT_SECURITY_ERROR;
- break;
- case AbstractSyncService.EXIT_EXCEPTION:
- lastResult = EmailContent.LAST_SYNC_RESULT_INTERNAL_ERROR;
- break;
- }
- // Add this box to the error map
- errorMap.put(mailboxId, exchangeService.new SyncError(exitStatus, errorIsFatal));
- } finally {
- // Always set the last result
- exchangeService.setMailboxLastSyncResult(mailboxId, lastResult);
- kick("sync completed");
+ switch (exitStatus) {
+ case AbstractSyncService.EXIT_DONE:
+ if (svc.hasPendingRequests()) {
+ // TODO Handle this case
+ }
+ errorMap.remove(mailboxId);
+ // If we've had a successful sync, clear the shutdown count
+ synchronized (ExchangeService.class) {
+ sClientConnectionManagerShutdownCount = 0;
+ }
+ break;
+ // I/O errors get retried at increasing intervals
+ case AbstractSyncService.EXIT_IO_ERROR:
+ if (syncError != null) {
+ syncError.escalate();
+ log(m.mDisplayName + " held for " + syncError.holdDelay + "ms");
+ } else {
+ errorMap.put(mailboxId, exchangeService.new SyncError(exitStatus, false));
+ log(m.mDisplayName + " added to syncErrorMap, hold for 15s");
+ }
+ break;
+ // These errors are not retried automatically
+ case AbstractSyncService.EXIT_LOGIN_FAILURE:
+ new AccountServiceProxy(exchangeService).notifyLoginFailed(m.mAccountKey);
+ // Fall through
+ case AbstractSyncService.EXIT_SECURITY_FAILURE:
+ case AbstractSyncService.EXIT_ACCESS_DENIED:
+ case AbstractSyncService.EXIT_EXCEPTION:
+ errorMap.put(mailboxId, exchangeService.new SyncError(exitStatus, true));
+ break;
}
+ kick("sync completed");
}
}
diff --git a/src/com/android/exchange/SecurityPolicyDelegate.java b/src/com/android/exchange/SecurityPolicyDelegate.java
new file mode 100644
index 0000000..5025c8f
--- /dev/null
+++ b/src/com/android/exchange/SecurityPolicyDelegate.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.Policy;
+import com.android.emailcommon.service.PolicyServiceProxy;
+
+import android.content.Context;
+import android.os.RemoteException;
+
+public class SecurityPolicyDelegate {
+
+ public static boolean isActive(Context context, Policy policy) {
+ try {
+ return new PolicyServiceProxy(context).isActive(policy);
+ } catch (RemoteException e) {
+ }
+ return false;
+ }
+
+ public static void policiesRequired(Context context, long accountId) {
+ try {
+ new PolicyServiceProxy(context).policiesRequired(accountId);
+ } catch (RemoteException e) {
+ throw new IllegalStateException("PolicyService transaction failed");
+ }
+ }
+
+ public static void policiesUpdated(Context context, long accountId) {
+ try {
+ new PolicyServiceProxy(context).policiesUpdated(accountId);
+ } catch (RemoteException e) {
+ throw new IllegalStateException("PolicyService transaction failed");
+ }
+ }
+
+ public static void setAccountHoldFlag(Context context, Account account, boolean newState) {
+ try {
+ new PolicyServiceProxy(context).setAccountHoldFlag(account.mId, newState);
+ } catch (RemoteException e) {
+ throw new IllegalStateException("PolicyService transaction failed");
+ }
+ }
+
+ public static boolean isActiveAdmin(Context context) {
+ try {
+ return new PolicyServiceProxy(context).isActiveAdmin();
+ } catch (RemoteException e) {
+ }
+ return false;
+ }
+
+ public static void remoteWipe(Context context) {
+ try {
+ new PolicyServiceProxy(context).remoteWipe();
+ } catch (RemoteException e) {
+ throw new IllegalStateException("PolicyService transaction failed");
+ }
+ }
+
+ public static boolean isSupported(Context context, Policy policy) {
+ try {
+ return new PolicyServiceProxy(context).isSupported(policy);
+ } catch (RemoteException e) {
+ }
+ return false;
+ }
+
+ public static Policy clearUnsupportedPolicies(Context context, Policy policy) {
+ try {
+ return new PolicyServiceProxy(context).clearUnsupportedPolicies(policy);
+ } catch (RemoteException e) {
+ }
+ throw new IllegalStateException("PolicyService transaction failed");
+ }
+}
diff --git a/src/com/android/exchange/adapter/AttachmentLoader.java b/src/com/android/exchange/adapter/AttachmentLoader.java
index 04cf555..4d14934 100644
--- a/src/com/android/exchange/adapter/AttachmentLoader.java
+++ b/src/com/android/exchange/adapter/AttachmentLoader.java
@@ -32,7 +32,6 @@
import com.android.exchange.ExchangeService;
import com.android.exchange.PartRequest;
import com.android.exchange.utility.UriCodec;
-import com.android.mail.providers.UIProvider;
import com.google.common.annotations.VisibleForTesting;
import org.apache.http.HttpStatus;
@@ -95,7 +94,6 @@
private void finishLoadAttachment() {
ContentValues cv = new ContentValues();
cv.put(AttachmentColumns.CONTENT_URI, mAttachmentUri.toString());
- cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.SAVED);
mAttachment.update(mContext, cv);
doStatusCallback(EmailServiceStatus.SUCCESS);
}
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
index 4df45a9..c8ab766 100644
--- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -43,8 +43,6 @@
import android.text.TextUtils;
import android.util.Log;
-import com.android.calendarcommon.DateException;
-import com.android.calendarcommon.Duration;
import com.android.emailcommon.AccountManagerTypes;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.Message;
@@ -55,9 +53,11 @@
import com.android.exchange.EasSyncService;
import com.android.exchange.ExchangeService;
import com.android.exchange.utility.CalendarUtilities;
+import com.android.exchange.utility.Duration;
import java.io.IOException;
import java.io.InputStream;
+import java.text.ParseException;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.Map.Entry;
@@ -1566,7 +1566,7 @@
try {
duration.parse(entityValues.getAsString(Events.DURATION));
durationMillis = duration.getMillis();
- } catch (DateException e) {
+ } catch (ParseException e) {
// Can't do much about this; use the default (1 hour)
}
}
diff --git a/src/com/android/exchange/adapter/ContactsSyncAdapter.java b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
index 1b21d11..ec0299b 100644
--- a/src/com/android/exchange/adapter/ContactsSyncAdapter.java
+++ b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
@@ -17,6 +17,10 @@
package com.android.exchange.adapter;
+import com.android.exchange.CommandStatusException;
+import com.android.exchange.Eas;
+import com.android.exchange.EasSyncService;
+
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentProviderOperation.Builder;
@@ -58,10 +62,6 @@
import android.util.Base64;
import android.util.Log;
-import com.android.exchange.CommandStatusException;
-import com.android.exchange.Eas;
-import com.android.exchange.EasSyncService;
-
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -1535,14 +1535,7 @@
}
// Compose address from name and addr
if (addr != null) {
- String value;
- // Only send the raw email address for EAS 2.5 (Hotmail, in particular, chokes on
- // an RFC822 address)
- if (mService.mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
- value = addr;
- } else {
- value = '\"' + name + "\" <" + addr + '>';
- }
+ String value = '\"' + name + "\" <" + addr + '>';
if (count < MAX_EMAIL_ROWS) {
s.data(EMAIL_TAGS[count], value);
}
diff --git a/exchange1_src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java
similarity index 99%
rename from exchange1_src/com/android/exchange/adapter/EmailSyncAdapter.java
rename to src/com/android/exchange/adapter/EmailSyncAdapter.java
index c498eb6..042ec32 100644
--- a/exchange1_src/com/android/exchange/adapter/EmailSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -60,6 +60,7 @@
import com.android.exchange.MessageMoveRequest;
import com.android.exchange.R;
import com.android.exchange.utility.CalendarUtilities;
+
import com.google.common.annotations.VisibleForTesting;
import org.apache.http.HttpStatus;
diff --git a/src/com/android/exchange/adapter/ProvisionParser.java b/src/com/android/exchange/adapter/ProvisionParser.java
index de30111..ca71cf9 100644
--- a/src/com/android/exchange/adapter/ProvisionParser.java
+++ b/src/com/android/exchange/adapter/ProvisionParser.java
@@ -23,7 +23,9 @@
import com.android.emailcommon.provider.Policy;
import com.android.exchange.EasSyncService;
+import com.android.exchange.ExchangeService;
import com.android.exchange.R;
+import com.android.exchange.SecurityPolicyDelegate;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -36,20 +38,22 @@
/**
* 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 final EasSyncService mService;
- private Policy mPolicy = null;
- private String mSecuritySyncKey = null;
- private boolean mRemoteWipe = false;
- private boolean mIsSupportable = true;
- private boolean smimeRequired = false;
- private final Resources mResources;
+ Policy mPolicy = null;
+ String mSecuritySyncKey = null;
+ boolean mRemoteWipe = false;
+ boolean mIsSupportable = true;
+ // An array of string resource id's describing policies that are unsupported by the device/app
+ String[] mUnsupportedPolicies;
+ boolean smimeRequired = false;
public ProvisionParser(InputStream in, EasSyncService service) throws IOException {
super(in);
mService = service;
- mResources = service.mContext.getResources();
}
public Policy getPolicy() {
@@ -72,32 +76,18 @@
return (mPolicy != null) && mIsSupportable;
}
- public void clearUnsupportablePolicies() {
+ public void clearUnsupportedPolicies() {
+ mPolicy = SecurityPolicyDelegate.clearUnsupportedPolicies(mService.mContext, mPolicy);
mIsSupportable = true;
- mPolicy.mProtocolPoliciesUnsupported = null;
+ mUnsupportedPolicies = null;
}
- private void addPolicyString(StringBuilder sb, int res) {
- sb.append(mResources.getString(res));
- sb.append(Policy.POLICY_STRING_DELIMITER);
+ public String[] getUnsupportedPolicies() {
+ return mUnsupportedPolicies;
}
- /**
- * Complete setup of a Policy; we normalize it first (removing inconsistencies, etc.) and then
- * generate the tokenized "protocol policies enforced" string. Note that unsupported policies
- * must have been added prior to calling this method (this is only a possibility with wbxml
- * policy documents, as all versions of the OS support the policies in xml documents).
- */
private void setPolicy(Policy policy) {
policy.normalize();
- StringBuilder sb = new StringBuilder();
- if (policy.mDontAllowAttachments) {
- addPolicyString(sb, R.string.policy_dont_allow_attachments);
- }
- if (policy.mRequireManualSyncWhenRoaming) {
- addPolicyString(sb, R.string.policy_require_manual_sync_roaming);
- }
- policy.mProtocolPoliciesEnforced = sb.toString();
mPolicy = policy;
}
@@ -361,16 +351,26 @@
if (!passwordEnabled) {
policy.mPasswordMode = Policy.PASSWORD_MODE_NONE;
}
+ setPolicy(policy);
- if (!unsupportedList.isEmpty()) {
- StringBuilder sb = new StringBuilder();
- for (int res: unsupportedList) {
- addPolicyString(sb, res);
- }
- policy.mProtocolPoliciesUnsupported = sb.toString();
+ // We can only determine whether encryption is supported on device by using isSupported here
+ if (!SecurityPolicyDelegate.isSupported(mService.mContext, policy)) {
+ log("SecurityPolicy reports PolicySet not supported.");
+ mIsSupportable = false;
+ unsupportedList.add(R.string.policy_require_encryption);
}
- setPolicy(policy);
+ if (!unsupportedList.isEmpty()) {
+ mUnsupportedPolicies = new String[unsupportedList.size()];
+ int i = 0;
+ Context context = ExchangeService.getContext();
+ if (context != null) {
+ Resources resources = context.getResources();
+ for (int res: unsupportedList) {
+ mUnsupportedPolicies[i++] = resources.getString(res);
+ }
+ }
+ }
}
/**
diff --git a/src/com/android/exchange/adapter/Search.java b/src/com/android/exchange/adapter/Search.java
index 62cda19..309ff79 100644
--- a/src/com/android/exchange/adapter/Search.java
+++ b/src/com/android/exchange/adapter/Search.java
@@ -17,7 +17,6 @@
package com.android.exchange.adapter;
import android.content.ContentProviderOperation;
-import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.os.RemoteException;
@@ -36,7 +35,6 @@
import com.android.exchange.EasSyncService;
import com.android.exchange.ExchangeService;
import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
-import com.android.mail.providers.UIProvider;
import org.apache.http.HttpStatus;
@@ -69,15 +67,10 @@
if (account == null) return res;
EasSyncService svc = EasSyncService.setupServiceForAccount(context, account);
if (svc == null) return res;
- Mailbox searchMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId);
- // Sanity check; account might have been deleted?
- if (searchMailbox == null) return res;
- ContentValues statusValues = new ContentValues();
try {
- // Set the status of this mailbox to indicate query
- statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.USER_QUERY);
- searchMailbox.update(context, statusValues);
-
+ Mailbox searchMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId);
+ // Sanity check; account might have been deleted?
+ if (searchMailbox == null) return res;
svc.mMailbox = searchMailbox;
svc.mAccount = account;
Serializer s = new Serializer();
@@ -131,10 +124,6 @@
svc.userLog("Search exception " + e);
} finally {
try {
- // TODO: Handle error states
- // Set the status of this mailbox to indicate query over
- statusValues.put(Mailbox.UI_SYNC_STATUS, UIProvider.SyncStatus.NO_SYNC);
- searchMailbox.update(context, statusValues);
ExchangeService.callback().syncMailboxStatus(destMailboxId,
EmailServiceStatus.SUCCESS, 100);
} catch (RemoteException e) {
diff --git a/exchange1_src/com/android/exchange/provider/MailboxUtilities.java b/src/com/android/exchange/provider/MailboxUtilities.java
similarity index 93%
rename from exchange1_src/com/android/exchange/provider/MailboxUtilities.java
rename to src/com/android/exchange/provider/MailboxUtilities.java
index 6be7920..6089257 100644
--- a/exchange1_src/com/android/exchange/provider/MailboxUtilities.java
+++ b/src/com/android/exchange/provider/MailboxUtilities.java
@@ -130,22 +130,21 @@
// We'll loop through mailboxes with an uninitialized parent key
ContentResolver resolver = context.getContentResolver();
- Cursor noParentKeyMailboxCursor =
- resolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
- noParentKeySelection, null, null);
- if (noParentKeyMailboxCursor == null) return;
+ Cursor parentCursor = resolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
+ noParentKeySelection, null, null);
+ if (parentCursor == null) return;
try {
- while (noParentKeyMailboxCursor.moveToNext()) {
- setFlagsAndChildrensParentKey(context, noParentKeyMailboxCursor, accountSelector);
+ while (parentCursor.moveToNext()) {
+ setFlagsAndChildrensParentKey(context, parentCursor, accountSelector);
+ // Fix up the parent as well
String parentServerId =
- noParentKeyMailboxCursor.getString(Mailbox.CONTENT_PARENT_SERVER_ID_COLUMN);
- // Fixup the parent so that the children's parentKey is updated
+ parentCursor.getString(Mailbox.CONTENT_PARENT_SERVER_ID_COLUMN);
if (parentServerId != null) {
setFlagsAndChildrensParentKey(context, accountSelector, parentServerId);
}
}
} finally {
- noParentKeyMailboxCursor.close();
+ parentCursor.close();
}
// Any mailboxes without a parent key should have parentKey set to -1 (no parent)
diff --git a/src/com/android/exchange/utility/CalendarUtilities.java b/src/com/android/exchange/utility/CalendarUtilities.java
index 2302b7a..2f466b8 100644
--- a/src/com/android/exchange/utility/CalendarUtilities.java
+++ b/src/com/android/exchange/utility/CalendarUtilities.java
@@ -35,8 +35,6 @@
import android.util.Base64;
import android.util.Log;
-import com.android.calendarcommon.DateException;
-import com.android.calendarcommon.Duration;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.Address;
import com.android.emailcommon.provider.Account;
@@ -57,6 +55,7 @@
import java.io.IOException;
import java.text.DateFormat;
+import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
@@ -1497,6 +1496,7 @@
sb = new StringBuilder();
}
Resources resources = context.getResources();
+ Date date = new Date(entityValues.getAsLong(Events.DTSTART));
// TODO: Add more detail to message text
// Right now, we're using.. When: Tuesday, March 5th at 2:00pm
// What we're missing is the duration and any recurrence information. So this should be
@@ -1514,13 +1514,11 @@
String dateTimeString;
int res;
- long startTime = entityValues.getAsLong(Events.DTSTART);
if (allDayEvent) {
- Date date = new Date(getLocalAllDayCalendarTime(startTime, TimeZone.getDefault()));
dateTimeString = DateFormat.getDateInstance().format(date);
res = recurringEvent ? R.string.meeting_allday_recurring : R.string.meeting_allday;
} else {
- dateTimeString = DateFormat.getDateTimeInstance().format(new Date(startTime));
+ dateTimeString = DateFormat.getDateTimeInstance().format(date);
res = recurringEvent ? R.string.meeting_recurring : R.string.meeting_when;
}
sb.append(resources.getString(res, dateTimeString));
@@ -1703,7 +1701,7 @@
try {
duration.parse(entityValues.getAsString(Events.DURATION));
durationMillis = duration.getMillis();
- } catch (DateException e) {
+ } catch (ParseException e) {
// We'll use the default in this case
}
ics.writeTag("DTEND" + vCalendarDateSuffix,
diff --git a/src/com/android/exchange/utility/Duration.java b/src/com/android/exchange/utility/Duration.java
new file mode 100644
index 0000000..f6e6525
--- /dev/null
+++ b/src/com/android/exchange/utility/Duration.java
@@ -0,0 +1,127 @@
+/* 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 java.text.ParseException;
+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 ParseException {
+ 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 ParseException (
+ "Duration.parse(str='" + str + "') expected 'P' at index="
+ + 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 ParseException (
+ "Duration.parse(str='" + str + "') unexpected char '"
+ + c + "' at index=" + 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 150b1f1..54d76b3 100644
--- a/tests/src/com/android/exchange/EasSyncServiceTests.java
+++ b/tests/src/com/android/exchange/EasSyncServiceTests.java
@@ -128,4 +128,46 @@
assertEquals("https://" + HOST + "/Microsoft-Server-ActiveSync?Cmd=Sync" +
svc.mUserString, uriString);
}
+
+ public void testResetHeartbeats() {
+ EasSyncService svc = new EasSyncService();
+ // Test case in which the minimum and force heartbeats need to come up
+ svc.mPingMaxHeartbeat = 1000;
+ svc.mPingMinHeartbeat = 200;
+ svc.mPingHeartbeat = 300;
+ svc.mPingForceHeartbeat = 100;
+ svc.mPingHeartbeatDropped = true;
+ svc.resetHeartbeats(400);
+ assertEquals(400, svc.mPingMinHeartbeat);
+ assertEquals(1000, svc.mPingMaxHeartbeat);
+ assertEquals(400, svc.mPingHeartbeat);
+ assertEquals(400, svc.mPingForceHeartbeat);
+ assertFalse(svc.mPingHeartbeatDropped);
+
+ // Test case in which the force heartbeat needs to come up
+ svc.mPingMaxHeartbeat = 1000;
+ svc.mPingMinHeartbeat = 200;
+ svc.mPingHeartbeat = 100;
+ svc.mPingForceHeartbeat = 100;
+ svc.mPingHeartbeatDropped = true;
+ svc.resetHeartbeats(150);
+ assertEquals(200, svc.mPingMinHeartbeat);
+ assertEquals(1000, svc.mPingMaxHeartbeat);
+ assertEquals(150, svc.mPingHeartbeat);
+ assertEquals(150, svc.mPingForceHeartbeat);
+ assertFalse(svc.mPingHeartbeatDropped);
+
+ // Test case in which the maximum needs to come down
+ svc.mPingMaxHeartbeat = 1000;
+ svc.mPingMinHeartbeat = 200;
+ svc.mPingHeartbeat = 800;
+ svc.mPingForceHeartbeat = 100;
+ svc.mPingHeartbeatDropped = true;
+ svc.resetHeartbeats(600);
+ assertEquals(200, svc.mPingMinHeartbeat);
+ assertEquals(600, svc.mPingMaxHeartbeat);
+ assertEquals(600, svc.mPingHeartbeat);
+ assertEquals(100, svc.mPingForceHeartbeat);
+ assertFalse(svc.mPingHeartbeatDropped);
+ }
}
diff --git a/tests/src/com/android/exchange/provider/MailboxUtilitiesTests.java b/tests/src/com/android/exchange/provider/MailboxUtilitiesTests.java
index a7425d7..57b5fb9 100644
--- a/tests/src/com/android/exchange/provider/MailboxUtilitiesTests.java
+++ b/tests/src/com/android/exchange/provider/MailboxUtilitiesTests.java
@@ -201,7 +201,7 @@
"box3", mAccount.mId, false, mProviderContext, Mailbox.TYPE_MAIL, box2);
box3.mParentKey = Mailbox.PARENT_KEY_UNINITIALIZED;
box3.save(mProviderContext);
- simulateFolderSyncChangeHandling(accountSelector, box3 /*box3's parent*/);
+ simulateFolderSyncChangeHandling(accountSelector, box2 /*box3's parent*/);
box1 = Mailbox.restoreMailboxWithId(mProviderContext, box1.mId);
box2 = Mailbox.restoreMailboxWithId(mProviderContext, box2.mId);