Merge "Change mdpi-icon for Email Application"
diff --git a/Android.mk b/Android.mk
index 1fd0799..f1e3f85 100644
--- a/Android.mk
+++ b/Android.mk
@@ -18,9 +18,11 @@
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under, src)
+# EXCHANGE-REMOVE-SECTION-START
LOCAL_SRC_FILES += \
- src/com/android/exchange/IEmailService.aidl \
- src/com/android/exchange/IEmailServiceCallback.aidl
+ src/com/android/email/service/IEmailService.aidl \
+ src/com/android/email/service/IEmailServiceCallback.aidl
+# EXCHANGE-REMOVE-SECTION-END
LOCAL_PACKAGE_NAME := Email
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9b376af..76ff915 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -31,6 +31,8 @@
<!-- For EAS purposes; could be removed when EAS has a permanent home -->
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
+ <uses-permission android:name="android.permission.WRITE_CALENDAR"/>
+ <uses-permission android:name="android.permission.READ_CALENDAR"/>
<!-- Only required if a store implements push mail and needs to keep network open -->
<uses-permission android:name="android.permission.WAKE_LOCK"/>
@@ -83,11 +85,13 @@
android:label="@string/account_setup_outgoing_title"
>
</activity>
+ <!--EXCHANGE-REMOVE-SECTION-START-->
<activity
android:name=".activity.setup.AccountSetupExchange"
android:label="@string/account_setup_exchange_title"
>
</activity>
+ <!--EXCHANGE-REMOVE-SECTION-END-->
<activity
android:name=".activity.setup.AccountSetupOptions"
android:label="@string/account_setup_options_title"
@@ -171,6 +175,7 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
+ <!--EXCHANGE-REMOVE-SECTION-START-->
<receiver android:name="com.android.exchange.EmailSyncAlarmReceiver"/>
<receiver android:name="com.android.exchange.MailboxAlarmReceiver"/>
<receiver android:name="com.android.exchange.BootReceiver" android:enabled="true">
@@ -178,6 +183,7 @@
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
+ <!--EXCHANGE-REMOVE-SECTION-END-->
<receiver android:name=".service.BootReceiver" android:enabled="true">
<intent-filter>
@@ -191,12 +197,36 @@
</intent-filter>
</receiver>
+ <!-- Support for DeviceAdmin / DevicePolicyManager. See SecurityPolicy class for impl. -->
+ <receiver
+ android:name=".SecurityPolicy$PolicyAdmin"
+ android:label="@string/device_admin_label"
+ android:description="@string/device_admin_description"
+ android:permission="android.permission.BIND_DEVICE_ADMIN" >
+ <meta-data
+ android:name="android.app.device_admin"
+ android:resource="@xml/device_admin" />
+ <intent-filter>
+ <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+ </intent-filter>
+ </receiver>
+
+ <receiver
+ android:name=".OneTimeInitializer"
+ android:enabled="true"
+ >
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ </intent-filter>
+ </receiver>
+
<service
android:name=".service.MailService"
android:enabled="false"
>
</service>
+ <!--EXCHANGE-REMOVE-SECTION-START-->
<!--Required stanza to register the ContactsSyncAdapterService with SyncManager -->
<service
android:name="com.android.exchange.ContactsSyncAdapterService"
@@ -208,6 +238,17 @@
android:resource="@xml/syncadapter_contacts" />
</service>
+ <!--Required stanza to register the CalendarSyncAdapterService with SyncManager -->
+ <service
+ android:name="com.android.exchange.CalendarSyncAdapterService"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.content.SyncAdapter" />
+ </intent-filter>
+ <meta-data android:name="android.content.SyncAdapter"
+ android:resource="@xml/syncadapter_calendar" />
+ </service>
+
<!-- Add android:process=":remote" below to enable SyncManager as a separate process -->
<service
android:name="com.android.exchange.SyncManager"
@@ -216,13 +257,37 @@
</service>
<!--Required stanza to register the EasAuthenticatorService with AccountManager -->
- <service android:name=".service.EasAuthenticatorService" android:exported="true">
+ <service
+ android:name=".service.EasAuthenticatorService"
+ android:exported="true"
+ android:enabled="true"
+ >
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
- <meta-data android:name="android.accounts.AccountAuthenticator"
- android:resource="@xml/authenticator" />
+ <meta-data
+ android:name="android.accounts.AccountAuthenticator"
+ android:resource="@xml/authenticator"
+ />
</service>
+ <!--
+ EasAuthenticatorService with the altenative label. Disabled by default,
+ and OneTimeInitializer enables it if the vendor policy tells so.
+ -->
+ <service
+ android:name=".service.EasAuthenticatorServiceAlternate"
+ android:exported="true"
+ android:enabled="false"
+ >
+ <intent-filter>
+ <action android:name="android.accounts.AccountAuthenticator" />
+ </intent-filter>
+ <meta-data
+ android:name="android.accounts.AccountAuthenticator"
+ android:resource="@xml/authenticator_alternate"
+ />
+ </service>
+ <!--EXCHANGE-REMOVE-SECTION-END-->
<provider
android:name=".provider.AttachmentProvider"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 4e2df73..154deee 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -295,12 +295,21 @@
<!-- Title of screen when setting up new email account -->
<string name="account_setup_basics_title">Set up email</string>
+ <!-- Title of the screen when adding exchange account -->
+ <string name="account_setup_basics_exchange_title">
+ Add an Exchange account</string>
+ <!-- Title of the screen when adding exchange account -->
+ <string name="account_setup_basics_exchange_title_alternate">
+ Add an Exchange ActiveSync account</string>
<!-- On "Set up email" screen, enthusiastic welcome message. -->
<string name="accounts_welcome">You can configure Email for most accounts in just a few steps.
</string>
<!-- On "Set up email" screen, enthusiastic welcome message (in EAS mode). -->
<string name="accounts_welcome_exchange">You can configure an Exchange account in just a few
steps.</string>
+ <!-- On "Set up email" screen, enthusiastic welcome message (in EAS mode). -->
+ <string name="accounts_welcome_exchange_alternate">
+ You can configure an Exchange ActiveSync account in just a few steps.</string>
<!-- On "Set up email" screen, brief instructions -->
<!-- DEPRECATED - REMOVE -->
<string name="account_setup_basics_instructions"></string>
@@ -356,6 +365,10 @@
<!-- Do Not Translate. "Add new email account" screen, button name in response to what
type of account this is -->
<string name="account_setup_account_type_exchange_action">Exchange</string>
+ <!-- Do Not Translate. "Add new email account" screen, button name in
+ response to what type of account this is -->
+ <string name="account_setup_account_type_exchange_action_alternate">
+ Microsoft Exchange ActiveSync</string>
<!-- "Incoming server settings" screen, label for text field -->
<string name="account_setup_incoming_title">Incoming server settings</string>
@@ -447,7 +460,11 @@
<!-- In Account setup options & Account Settings screens, check box for new-mail notification -->
<string name="account_setup_options_notify_label">Notify me when email arrives.</string>
<!-- In Account setup options screen, optional check box to also sync contacts -->
- <string name="account_setup_options_sync_contacts_label">Sync contacts from this account.</string>
+ <string name="account_setup_options_sync_contacts_label">Sync contacts from this account.
+ </string>
+ <!-- In Account setup options screen, optional check box to also sync contacts -->
+ <string name="account_setup_options_sync_calendar_label">Sync calendar from this account.
+ </string>
<!-- Dialog title when "setup" could not finish -->
<string name="account_setup_failed_dlg_title">Setup could not finish</string>
<!-- In Account setup options screen, label for email check frequency selector -->
@@ -518,6 +535,10 @@
<string name="account_settings_description_label">Account name</string>
<!-- On Settings screen, setting option name -->
<string name="account_settings_name_label">Your name</string>
+ <!-- On Settings screen, setting option name -->
+ <string name="account_settings_signature_label">Signature</string>
+ <!-- On Settings screen, setting option name -->
+ <string name="account_settings_signature_hint">Append text to messages you send</string>
<!-- On Settings screen, section heading -->
<string name="account_settings_notifications">Notification settings</string>
@@ -557,7 +578,16 @@
<!-- Name of Microsoft Exchange account type; used by AccountManager -->
<string name="exchange_name">Corporate</string>
+ <!-- Name of Microsoft Exchange account type; used by AccountManager -->
+ <string name="exchange_name_alternate">Microsoft Exchange ActiveSync</string>
<!-- Message that appears if the AccountManager cannot create the system Account -->
<string name="system_account_create_failed">The AccountManager could not create the Account; please try again.</string>
+
+ <!-- Strings that support the DeviceAdmin / DevicePolicyManager API -->
+ <!-- Name of the DeviceAdmin (seen in settings - anywhere else?) -->
+ <string name="device_admin_label">Email Device Administrator</string>
+ <!-- Long-form description of the DeviceAdmin (seen in settings - anywhere else?) -->
+ <string name="device_admin_description">Email Device Administrator - Long Description</string>
+
</resources>
diff --git a/res/xml/syncadapter_calendar.xml b/res/xml/syncadapter_calendar.xml
new file mode 100644
index 0000000..a6c0fc6
--- /dev/null
+++ b/res/xml/syncadapter_calendar.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2010, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- The attributes in this XML file provide configuration information -->
+<!-- for the SyncAdapter. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="com.android.calendar"
+ android:accountType="com.android.exchange"
+/>
diff --git a/src/com/android/exchange/AbstractSyncService.java b/src/com/android/exchange/AbstractSyncService.java
index 425424d..08583ec 100644
--- a/src/com/android/exchange/AbstractSyncService.java
+++ b/src/com/android/exchange/AbstractSyncService.java
@@ -76,8 +76,8 @@
protected Object mSynchronizer = new Object();
protected volatile long mRequestTime = 0;
- protected ArrayList<PartRequest> mPartRequests = new ArrayList<PartRequest>();
- protected PartRequest mPendingPartRequest = null;
+ protected ArrayList<Request> mRequests = new ArrayList<Request>();
+ protected PartRequest mPendingRequest = null;
/**
* Sent by SyncManager to request that the service stop itself cleanly
@@ -282,54 +282,23 @@
}
/**
- * PartRequest handling (common functionality)
- * Can be overridden if desired, but IMAP/EAS both use the next three methods as-is
+ * Request handling (common functionality)
+ * Can be overridden if desired
*/
- public void addPartRequest(PartRequest req) {
- synchronized (mPartRequests) {
- mPartRequests.add(req);
+ public void addRequest(Request req) {
+ synchronized (mRequests) {
+ mRequests.add(req);
mRequestTime = System.currentTimeMillis();
}
}
- public void removePartRequest(PartRequest req) {
- synchronized (mPartRequests) {
- mPartRequests.remove(req);
+ public void removeRequest(Request req) {
+ synchronized (mRequests) {
+ mRequests.remove(req);
}
}
- public PartRequest hasPartRequest(long emailId, String part) {
- synchronized (mPartRequests) {
- for (PartRequest pr : mPartRequests) {
- if (pr.emailId == emailId && pr.loc.equals(part))
- return pr;
- }
- }
- return null;
- }
-
- // cancelPartRequest is sent in response to user input to stop an attachment load
- // that is in progress. This will almost certainly require code overriding the base
- // functionality, as sockets may need to be closed, etc. and this functionality will be
- // service dependent. This returns the canceled PartRequest or null
- public PartRequest cancelPartRequest(long emailId, String part) {
- synchronized (mPartRequests) {
- PartRequest p = null;
- for (PartRequest pr : mPartRequests) {
- if (pr.emailId == emailId && pr.loc.equals(part)) {
- p = pr;
- break;
- }
- }
- if (p != null) {
- mPartRequests.remove(p);
- return p;
- }
- }
- return null;
- }
-
/**
* Convenience method wrapping calls to retrieve columns from a single row, via EmailProvider.
* The arguments are exactly the same as to contentResolver.query(). Results are returned in
diff --git a/src/com/android/exchange/BootReceiver.java b/src/com/android/exchange/BootReceiver.java
index 5312bfb..1ebfa7b 100644
--- a/src/com/android/exchange/BootReceiver.java
+++ b/src/com/android/exchange/BootReceiver.java
@@ -16,6 +16,8 @@
package com.android.exchange;
+import com.android.email.ExchangeUtils;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -25,6 +27,6 @@
@Override
public void onReceive(Context context, Intent intent) {
Log.d("Exchange", "BootReceiver onReceive");
- context.startService(new Intent(context, SyncManager.class));
+ ExchangeUtils.startExchangeService(context);
}
}
diff --git a/src/com/android/exchange/CalendarSyncAdapterService.java b/src/com/android/exchange/CalendarSyncAdapterService.java
new file mode 100644
index 0000000..d2f4ecf
--- /dev/null
+++ b/src/com/android/exchange/CalendarSyncAdapterService.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailContent.AccountColumns;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.MailboxColumns;
+
+import android.accounts.Account;
+import android.accounts.OperationCanceledException;
+import android.app.Service;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SyncResult;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.Calendar.Events;
+import android.util.Log;
+
+public class CalendarSyncAdapterService extends Service {
+ private static final String TAG = "EAS CalendarSyncAdapterService";
+ private static SyncAdapterImpl sSyncAdapter = null;
+ private static final Object sSyncAdapterLock = new Object();
+
+ private static final String ACCOUNT_AND_TYPE_CALENDAR =
+ MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_CALENDAR;
+
+ public CalendarSyncAdapterService() {
+ super();
+ }
+
+ private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
+ private Context mContext;
+
+ public SyncAdapterImpl(Context context) {
+ super(context, true /* autoInitialize */);
+ mContext = context;
+ }
+
+ @Override
+ public void onPerformSync(Account account, Bundle extras,
+ String authority, ContentProviderClient provider, SyncResult syncResult) {
+ try {
+ CalendarSyncAdapterService.performSync(mContext, account, extras,
+ authority, provider, syncResult);
+ } catch (OperationCanceledException e) {
+ }
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ synchronized (sSyncAdapterLock) {
+ if (sSyncAdapter == null) {
+ sSyncAdapter = new SyncAdapterImpl(getApplicationContext());
+ }
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return sSyncAdapter.getSyncAdapterBinder();
+ }
+
+ /**
+ * Partial integration with system SyncManager; we tell our EAS SyncManager to start a calendar
+ * sync when we get the signal from the system SyncManager.
+ * The missing piece at this point is integration with the push/ping mechanism in EAS; this will
+ * be put in place at a later time.
+ */
+ private static void performSync(Context context, Account account, Bundle extras,
+ String authority, ContentProviderClient provider, SyncResult syncResult)
+ throws OperationCanceledException {
+ ContentResolver cr = context.getContentResolver();
+ boolean logging = Eas.USER_LOG;
+ if (logging) {
+ Log.d(TAG, "performSync");
+ }
+ if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
+ Cursor c = cr.query(Events.CONTENT_URI,
+ new String[] {Events._ID}, Events._SYNC_DIRTY + "=1", null, null);
+ try {
+ if (!c.moveToFirst()) {
+ if (logging) {
+ Log.d(TAG, "Upload sync; no changes");
+ }
+ return;
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ // Find the (EmailProvider) account associated with this email address
+ Cursor accountCursor =
+ cr.query(EmailContent.Account.CONTENT_URI,
+ EmailContent.ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?",
+ new String[] {account.name}, null);
+ try {
+ if (accountCursor.moveToFirst()) {
+ long accountId = accountCursor.getLong(0);
+ // Now, find the calendar mailbox associated with the account
+ Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION,
+ ACCOUNT_AND_TYPE_CALENDAR, new String[] {Long.toString(accountId)}, null);
+ try {
+ if (mailboxCursor.moveToFirst()) {
+ if (logging) {
+ Log.d(TAG, "Calendar sync requested for " + account.name);
+ }
+ // Ask for a sync from our sync manager
+ SyncManager.serviceRequest(mailboxCursor.getLong(0),
+ SyncManager.SYNC_UPSYNC);
+ }
+ } finally {
+ mailboxCursor.close();
+ }
+ }
+ } finally {
+ accountCursor.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/ContactsSyncAdapterService.java b/src/com/android/exchange/ContactsSyncAdapterService.java
index 69fe792..b39d789 100644
--- a/src/com/android/exchange/ContactsSyncAdapterService.java
+++ b/src/com/android/exchange/ContactsSyncAdapterService.java
@@ -16,6 +16,7 @@
package com.android.exchange;
+import com.android.email.Email;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.AccountColumns;
import com.android.email.provider.EmailContent.Mailbox;
@@ -98,7 +99,7 @@
if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
Uri uri = RawContacts.CONTENT_URI.buildUpon()
.appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
- .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE)
.build();
Cursor c = cr.query(uri,
new String[] {RawContacts._ID}, RawContacts.DIRTY + "=1", null, null);
diff --git a/src/com/android/exchange/Eas.java b/src/com/android/exchange/Eas.java
index c005544..86db9ab 100644
--- a/src/com/android/exchange/Eas.java
+++ b/src/com/android/exchange/Eas.java
@@ -38,7 +38,6 @@
public static final int DEBUG_FILE_BIT = 4;
public static final String VERSION = "0.3";
- public static final String ACCOUNT_MANAGER_TYPE = "com.android.exchange";
public static final String ACCOUNT_MAILBOX = "__eas";
// From EAS spec
diff --git a/src/com/android/exchange/EasAuthenticationException.java b/src/com/android/exchange/EasAuthenticationException.java
new file mode 100644
index 0000000..f5b14b9
--- /dev/null
+++ b/src/com/android/exchange/EasAuthenticationException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import java.io.IOException;
+
+/**
+ * Use this to be able to distinguish login (authentication) failures from other I/O
+ * exceptions during a sync, as they are handled very differently.
+ */
+public class EasAuthenticationException extends IOException {
+ private static final long serialVersionUID = 1L;
+
+ EasAuthenticationException() {
+ super();
+ }
+}
diff --git a/src/com/android/exchange/EasOutboxService.java b/src/com/android/exchange/EasOutboxService.java
index 38c9603..d827d51 100644
--- a/src/com/android/exchange/EasOutboxService.java
+++ b/src/com/android/exchange/EasOutboxService.java
@@ -26,6 +26,7 @@
import com.android.email.provider.EmailContent.Message;
import com.android.email.provider.EmailContent.MessageColumns;
import com.android.email.provider.EmailContent.SyncColumns;
+import com.android.email.service.EmailServiceStatus;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index e8a35eb..544a82f 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -29,11 +29,14 @@
import com.android.email.provider.EmailContent.MailboxColumns;
import com.android.email.provider.EmailContent.Message;
import com.android.email.service.EmailServiceProxy;
+import com.android.email.service.EmailServiceStatus;
import com.android.exchange.adapter.AbstractSyncAdapter;
import com.android.exchange.adapter.AccountSyncAdapter;
+import com.android.exchange.adapter.CalendarSyncAdapter;
import com.android.exchange.adapter.ContactsSyncAdapter;
import com.android.exchange.adapter.EmailSyncAdapter;
import com.android.exchange.adapter.FolderSyncParser;
+import com.android.exchange.adapter.MeetingResponseParser;
import com.android.exchange.adapter.PingParser;
import com.android.exchange.adapter.Serializer;
import com.android.exchange.adapter.Tags;
@@ -628,7 +631,7 @@
* @throws IOException
*/
protected void getAttachment(PartRequest req) throws IOException {
- Attachment att = req.att;
+ Attachment att = req.mAttachment;
Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey);
doProgressCallback(msg.mId, att.mId, 0);
@@ -640,9 +643,9 @@
HttpEntity e = res.getEntity();
int len = (int)e.getContentLength();
InputStream is = res.getEntity().getContent();
- File f = (req.destination != null)
- ? new File(req.destination)
- : createUniqueFileInternal(req.destination, att.mFileName);
+ File f = (req.mDestination != null)
+ ? new File(req.mDestination)
+ : createUniqueFileInternal(req.mDestination, att.mFileName);
if (f != null) {
// Ensure that the target directory exists
File destDir = f.getParentFile();
@@ -654,7 +657,7 @@
// len < 0 means "chunked" transfer-encoding
if (len != 0) {
try {
- mPendingPartRequest = req;
+ mPendingRequest = req;
byte[] bytes = new byte[CHUNK_SIZE];
int length = len;
// Loop terminates 1) when EOF is reached or 2) if an IOException occurs
@@ -689,7 +692,7 @@
}
}
} finally {
- mPendingPartRequest = null;
+ mPendingRequest = null;
}
}
os.flush();
@@ -697,8 +700,8 @@
// EmailProvider will throw an exception if we try to update an unsaved attachment
if (att.isSaved()) {
- String contentUriString = (req.contentUriString != null)
- ? req.contentUriString
+ String contentUriString = (req.mContentUriString != null)
+ ? req.mContentUriString
: "file://" + f.getAbsolutePath();
ContentValues cv = new ContentValues();
cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
@@ -711,6 +714,37 @@
}
}
+ /**
+ * Responds to a meeting request. The MeetingResponseRequest is basically our
+ * wrapper for the meetingResponse service call
+ * @param req the request (message id and response code)
+ * @throws IOException
+ */
+ protected void sendMeetingResponse(MeetingResponseRequest req) throws IOException {
+ Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
+ Serializer s = new Serializer();
+ s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST);
+ s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(req.mResponse));
+ s.data(Tags.MREQ_COLLECTION_ID, Long.toString(msg.mMailboxKey));
+ s.data(Tags.MREQ_REQ_ID, msg.mServerId);
+ s.end().end().done();
+ HttpResponse res = sendHttpClientPost("MeetingResponse", s.toByteArray());
+ int status = res.getStatusLine().getStatusCode();
+ if (status == HttpStatus.SC_OK) {
+ HttpEntity e = res.getEntity();
+ int len = (int)e.getContentLength();
+ InputStream is = res.getEntity().getContent();
+ if (len != 0) {
+ new MeetingResponseParser(is, this).parse();
+ }
+ } else if (isAuthError(status)) {
+ throw new EasAuthenticationException();
+ } else {
+ userLog("Meeting response request failed, code: " + status);
+ throw new IOException();
+ }
+ }
+
@SuppressWarnings("deprecation")
private String makeUriString(String cmd, String extra) throws IOException {
// Cache the authentication string and the command string
@@ -1274,7 +1308,7 @@
return pp.getSyncStatus();
}
- private String getFilterType() {
+ private String getEmailFilter() {
String filter = Eas.FILTER_1_WEEK;
switch (mAccount.mSyncLookback) {
case com.android.email.Account.SYNC_WINDOW_1_DAY: {
@@ -1323,18 +1357,29 @@
return;
}
+ // Now, handle various requests
while (true) {
- PartRequest req = null;
- synchronized (mPartRequests) {
- if (mPartRequests.isEmpty()) {
+ Request req = null;
+ synchronized (mRequests) {
+ if (mRequests.isEmpty()) {
break;
} else {
- req = mPartRequests.get(0);
+ req = mRequests.get(0);
}
}
- getAttachment(req);
- synchronized(mPartRequests) {
- mPartRequests.remove(req);
+
+ // Our two request types are PartRequest (loading attachment) and
+ // MeetingResponseRequest (respond to a meeting request)
+ if (req instanceof PartRequest) {
+ getAttachment((PartRequest)req);
+ } else if (req instanceof MeetingResponseRequest) {
+ sendMeetingResponse((MeetingResponseRequest)req);
+ }
+
+ // If there's an exception handling the request, we'll throw it
+ // Otherwise, we remove the request
+ synchronized(mRequests) {
+ mRequests.remove(req);
}
}
@@ -1360,8 +1405,11 @@
// Handle options
s.start(Tags.SYNC_OPTIONS);
// Set the lookback appropriately (EAS calls this a "filter") for all but Contacts
- if (!className.equals("Contacts")) {
- s.data(Tags.SYNC_FILTER_TYPE, getFilterType());
+ if (className.equals("Email")) {
+ s.data(Tags.SYNC_FILTER_TYPE, getEmailFilter());
+ } else if (className.equals("Calendar")) {
+ // TODO Force one month for calendar until we can set this!
+ s.data(Tags.SYNC_FILTER_TYPE, Eas.FILTER_1_MONTH);
}
// Set the truncation amount for all classes
if (mProtocolVersionDouble >= 12.0) {
@@ -1452,6 +1500,8 @@
AbstractSyncAdapter target;
if (mMailbox.mType == Mailbox.TYPE_CONTACTS) {
target = new ContactsSyncAdapter(mMailbox, this);
+ } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) {
+ target = new CalendarSyncAdapter(mMailbox, this);
} else {
target = new EmailSyncAdapter(mMailbox, this);
}
@@ -1465,6 +1515,9 @@
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);
diff --git a/src/com/android/exchange/EmailServiceStatus.java b/src/com/android/exchange/EmailServiceStatus.java
deleted file mode 100644
index 697548c..0000000
--- a/src/com/android/exchange/EmailServiceStatus.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2008-2009 Marc Blank
- * Licensed to The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-/**
- * Definitions of service status codes returned to IEmailServiceCallback's status method
- */
-public interface EmailServiceStatus {
- public static final int SUCCESS = 0;
- public static final int IN_PROGRESS = 1;
-
- public static final int MESSAGE_NOT_FOUND = 0x10;
- public static final int ATTACHMENT_NOT_FOUND = 0x11;
- public static final int FOLDER_NOT_DELETED = 0x12;
- public static final int FOLDER_NOT_RENAMED = 0x13;
- public static final int FOLDER_NOT_CREATED = 0x14;
- public static final int REMOTE_EXCEPTION = 0x15;
- public static final int LOGIN_FAILED = 0x16;
-
- // Maybe we should automatically retry these?
- public static final int CONNECTION_ERROR = 0x20;
-}
diff --git a/src/com/android/exchange/IEmailService.aidl b/src/com/android/exchange/IEmailService.aidl
deleted file mode 100644
index 81181eb..0000000
--- a/src/com/android/exchange/IEmailService.aidl
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2008-2009 Marc Blank
- * Licensed to The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.exchange;
-
-import com.android.exchange.IEmailServiceCallback;
-import com.android.email.provider.EmailContent;
-import android.os.Bundle;
-
-interface IEmailService {
- int validate(in String protocol, in String host, in String userName, in String password,
- int port, boolean ssl, boolean trustCertificates) ;
-
- void startSync(long mailboxId);
- void stopSync(long mailboxId);
-
- void loadMore(long messageId);
- void loadAttachment(long attachmentId, String destinationFile, String contentUriString);
-
- void updateFolderList(long accountId);
-
- boolean createFolder(long accountId, String name);
- boolean deleteFolder(long accountId, String name);
- boolean renameFolder(long accountId, String oldName, String newName);
-
- void setCallback(IEmailServiceCallback cb);
-
- void setLogging(int on);
-
- void hostChanged(long accountId);
-
- Bundle autoDiscover(String userName, String password);
-}
\ No newline at end of file
diff --git a/src/com/android/exchange/IEmailServiceCallback.aidl b/src/com/android/exchange/IEmailServiceCallback.aidl
deleted file mode 100644
index a789c4a..0000000
--- a/src/com/android/exchange/IEmailServiceCallback.aidl
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2008-2009 Marc Blank
- * Licensed to The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-oneway interface IEmailServiceCallback {
- /*
- * Ordinary results:
- * statuscode = 1, progress = 0: "starting"
- * statuscode = 0, progress = n/a: "finished"
- *
- * If there is an error, it must be reported as follows:
- * statuscode = err, progress = n/a: "stopping due to error"
- *
- * *Optionally* a callback can also include intermediate values from 1..99 e.g.
- * statuscode = 1, progress = 0: "starting"
- * statuscode = 1, progress = 30: "working"
- * statuscode = 1, progress = 60: "working"
- * statuscode = 0, progress = n/a: "finished"
- */
-
- /**
- * Callback to indicate that an account is being synced (updating folder list)
- * accountId = the account being synced
- * statusCode = 0 for OK, 1 for progress, other codes for error
- * progress = 0 for "start", 1..100 for optional progress reports
- */
- void syncMailboxListStatus(long accountId, int statusCode, int progress);
-
- /**
- * Callback to indicate that a mailbox is being synced
- * mailboxId = the mailbox being synced
- * statusCode = 0 for OK, 1 for progress, other codes for error
- * progress = 0 for "start", 1..100 for optional progress reports
- */
- void syncMailboxStatus(long mailboxId, int statusCode, int progress);
-
- /**
- * Callback to indicate that a particular attachment is being synced
- * messageId = the message that owns the attachment
- * attachmentId = the attachment being synced
- * statusCode = 0 for OK, 1 for progress, other codes for error
- * progress = 0 for "start", 1..100 for optional progress reports
- */
- void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, int progress);
-
- /**
- * Callback to indicate that a particular message is being sent
- * messageId = the message being sent
- * statusCode = 0 for OK, 1 for progress, other codes for error
- * progress = 0 for "start", 1..100 for optional progress reports
- */
- void sendMessageStatus(long messageId, String subject, int statusCode, int progress);
-}
diff --git a/src/com/android/exchange/MeetingResponseRequest.java b/src/com/android/exchange/MeetingResponseRequest.java
new file mode 100644
index 0000000..1d69a6c
--- /dev/null
+++ b/src/com/android/exchange/MeetingResponseRequest.java
@@ -0,0 +1,29 @@
+/*
+ * 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 int mResponse;
+
+ MeetingResponseRequest(long messageId, int response) {
+ mMessageId = messageId;
+ mResponse = response;
+ }
+}
diff --git a/src/com/android/exchange/PartRequest.java b/src/com/android/exchange/PartRequest.java
index 72b79f4..b6b281d 100644
--- a/src/com/android/exchange/PartRequest.java
+++ b/src/com/android/exchange/PartRequest.java
@@ -24,24 +24,21 @@
* the attachment to be loaded, it also contains the callback to be used for status/progress
* updates to the UI.
*/
-public class PartRequest {
- public long timeStamp;
- public long emailId;
- public Attachment att;
- public String destination;
- public String contentUriString;
- public String loc;
+public class PartRequest extends Request {
+ public Attachment mAttachment;
+ public String mDestination;
+ public String mContentUriString;
+ public String mLocation;
public PartRequest(Attachment _att) {
- timeStamp = System.currentTimeMillis();
- emailId = _att.mMessageKey;
- att = _att;
- loc = att.mLocation;
+ mMessageId = _att.mMessageKey;
+ mAttachment = _att;
+ mLocation = mAttachment.mLocation;
}
public PartRequest(Attachment _att, String _destination, String _contentUriString) {
this(_att);
- destination = _destination;
- contentUriString = _contentUriString;
+ mDestination = _destination;
+ mContentUriString = _contentUriString;
}
}
diff --git a/src/com/android/exchange/Request.java b/src/com/android/exchange/Request.java
new file mode 100644
index 0000000..185dd7f
--- /dev/null
+++ b/src/com/android/exchange/Request.java
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ * Two subclasses are now defined: PartRequest (attachment load) and MeetingResponseRequest
+ * (respond to a meeting invitation)
+ */
+public abstract class Request {
+ public long mTimeStamp = System.currentTimeMillis();
+ public long mMessageId;
+}
diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java
index 2648b8c..58f976d 100644
--- a/src/com/android/exchange/SyncManager.java
+++ b/src/com/android/exchange/SyncManager.java
@@ -18,6 +18,7 @@
package com.android.exchange;
import com.android.email.AccountBackupRestore;
+import com.android.email.Email;
import com.android.email.mail.MessagingException;
import com.android.email.mail.store.TrustManagerFactory;
import com.android.email.provider.EmailContent;
@@ -29,6 +30,8 @@
import com.android.email.provider.EmailContent.MailboxColumns;
import com.android.email.provider.EmailContent.Message;
import com.android.email.provider.EmailContent.SyncColumns;
+import com.android.email.service.IEmailService;
+import com.android.email.service.IEmailServiceCallback;
import com.android.exchange.utility.FileLogger;
import org.apache.http.conn.ClientConnectionManager;
@@ -312,7 +315,7 @@
public void loadAttachment(long attachmentId, String destinationFile,
String contentUriString) throws RemoteException {
Attachment att = Attachment.restoreAttachmentWithId(SyncManager.this, attachmentId);
- partRequest(new PartRequest(att, destinationFile, contentUriString));
+ sendMessageRequest(new PartRequest(att, destinationFile, contentUriString));
}
public void updateFolderList(long accountId) throws RemoteException {
@@ -348,8 +351,11 @@
Eas.setUserDebug(on);
}
+ public void sendMeetingResponse(long messageId, int response) throws RemoteException {
+ sendMessageRequest(new MeetingResponseRequest(messageId, response));
+ }
+
public void loadMore(long messageId) throws RemoteException {
- // TODO Auto-generated method stub
}
// The following three methods are not implemented in this version
@@ -507,7 +513,7 @@
// Delete this from AccountManager...
android.accounts.Account acct =
new android.accounts.Account(account.mEmailAddress,
- Eas.ACCOUNT_MANAGER_TYPE);
+ Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
AccountManager.get(SyncManager.this).removeAccount(acct, null, null);
mSyncableEasMailboxSelector = null;
mEasAccountSelector = null;
@@ -702,7 +708,8 @@
public class EasAccountsUpdatedListener implements OnAccountsUpdateListener {
public void onAccountsUpdated(android.accounts.Account[] accounts) {
reconcileAccountsWithAccountManager(INSTANCE, getAccountList(),
- AccountManager.get(INSTANCE).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE));
+ AccountManager.get(INSTANCE).getAccountsByType(
+ Email.EXCHANGE_ACCOUNT_MANAGER_TYPE));
}
}
@@ -1195,7 +1202,7 @@
// Create an AccountManager style Account
android.accounts.Account acct =
new android.accounts.Account(easAccount.mEmailAddress,
- Eas.ACCOUNT_MANAGER_TYPE);
+ Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
// Get the Contacts mailbox; this happens rarely so it's ok to get it all
Mailbox contacts = Mailbox.restoreMailboxWithId(this, contactsId);
int syncInterval = contacts.mSyncInterval;
@@ -1338,7 +1345,7 @@
}
}
- private void startService(Mailbox m, int reason, PartRequest req) {
+ private void startService(Mailbox m, int reason, Request req) {
// Don't sync if there's no connectivity
if (sConnectivityHold) return;
synchronized (sSyncToken) {
@@ -1351,7 +1358,7 @@
if (!((EasSyncService)service).mIsValid) return;
service.mSyncReason = reason;
if (req != null) {
- service.addPartRequest(req);
+ service.addRequest(req);
}
startService(service, m);
}
@@ -1607,7 +1614,7 @@
if (account != null) {
android.accounts.Account a =
new android.accounts.Account(account.mEmailAddress,
- Eas.ACCOUNT_MANAGER_TYPE);
+ Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
if (!ContentResolver.getSyncAutomatically(a,
ContactsContract.AUTHORITY)) {
continue;
@@ -1726,9 +1733,9 @@
}
}
- static public void partRequest(PartRequest req) {
+ static public void sendMessageRequest(Request req) {
if (INSTANCE == null) return;
- Message msg = Message.restoreMessageWithId(INSTANCE, req.emailId);
+ Message msg = Message.restoreMessageWithId(INSTANCE, req.mMessageId);
if (msg == null) {
return;
}
@@ -1739,33 +1746,7 @@
service = startManualSync(mailboxId, SYNC_SERVICE_PART_REQUEST, req);
kick("part request");
} else {
- service.addPartRequest(req);
- }
- }
-
- static public PartRequest hasPartRequest(long emailId, String part) {
- if (INSTANCE == null) return null;
- Message msg = Message.restoreMessageWithId(INSTANCE, emailId);
- if (msg == null) {
- return null;
- }
- long mailboxId = msg.mMailboxKey;
- AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
- if (service != null) {
- return service.hasPartRequest(emailId, part);
- }
- return null;
- }
-
- static public void cancelPartRequest(long emailId, String part) {
- Message msg = Message.restoreMessageWithId(INSTANCE, emailId);
- if (msg == null) {
- return;
- }
- long mailboxId = msg.mMailboxKey;
- AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
- if (service != null) {
- service.cancelPartRequest(emailId, part);
+ service.addRequest(req);
}
}
@@ -1793,7 +1774,7 @@
return PING_STATUS_OK;
}
- static public AbstractSyncService startManualSync(long mailboxId, int reason, PartRequest req) {
+ static public AbstractSyncService startManualSync(long mailboxId, int reason, Request req) {
if (INSTANCE == null || INSTANCE.mServiceMap == null) return null;
synchronized (sSyncToken) {
if (INSTANCE.mServiceMap.get(mailboxId) == null) {
@@ -1866,13 +1847,14 @@
int exitStatus = svc.mExitStatus;
switch (exitStatus) {
case AbstractSyncService.EXIT_DONE:
- if (!svc.mPartRequests.isEmpty()) {
+ if (!svc.mRequests.isEmpty()) {
// TODO Handle this case
}
errorMap.remove(mailboxId);
break;
case AbstractSyncService.EXIT_IO_ERROR:
Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+ if (m == null) return;
if (syncError != null) {
syncError.escalate();
INSTANCE.log(m.mDisplayName + " held for " + syncError.holdDelay + "ms");
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
index 739c9e2..5a202bb 100644
--- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -17,11 +17,44 @@
package com.android.exchange.adapter;
+import com.android.email.Email;
import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.Eas;
import com.android.exchange.EasSyncService;
+import com.android.exchange.utility.CalendarUtilities;
+import com.android.exchange.utility.Duration;
+
+import android.content.ContentProviderClient;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.content.OperationApplicationException;
+import android.content.Entity.NamedContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.pim.DateException;
+import android.provider.Calendar;
+import android.provider.SyncStateContract;
+import android.provider.Calendar.Attendees;
+import android.provider.Calendar.Calendars;
+import android.provider.Calendar.Events;
+import android.provider.Calendar.EventsEntity;
+import android.provider.Calendar.ExtendedProperties;
+import android.provider.Calendar.Reminders;
+import android.provider.Calendar.SyncState;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
/**
* Sync adapter class for EAS calendars
@@ -29,8 +62,56 @@
*/
public class CalendarSyncAdapter extends AbstractSyncAdapter {
+ private static final String TAG = "EasCalendarSyncAdapter";
+ // Since exceptions will have the same _SYNC_ID as the original event we have to check that
+ // there's no original event when finding an item by _SYNC_ID
+ private static final String SERVER_ID = Events._SYNC_ID + "=? AND " +
+ Events.ORIGINAL_EVENT + " ISNULL";
+ private static final String DIRTY_TOP_LEVEL =
+ Events._SYNC_DIRTY + "=1 AND " + Events.ORIGINAL_EVENT + " ISNULL";
+ private static final String DIRTY_EXCEPTION =
+ Events._SYNC_DIRTY + "=1 AND " + Events.ORIGINAL_EVENT + " NOTNULL";
+ private static final String DIRTY_IN_CALENDAR =
+ Events._SYNC_DIRTY + "=1 AND " + Events.CALENDAR_ID + "=?";
+ private static final String CLIENT_ID_SELECTION = Events._SYNC_LOCAL_ID + "=?";
+ private static final String ATTENDEES_EXCEPT_ORGANIZER = Attendees.EVENT_ID + "=? AND " +
+ Attendees.ATTENDEE_RELATIONSHIP + "!=" + Attendees.RELATIONSHIP_ORGANIZER;
+ private static final String[] ID_PROJECTION = new String[] {Events._ID};
+ private static final String[] ORIGINAL_EVENT_PROJECTION = new String[] {Events.ORIGINAL_EVENT};
+
+ private static final String CALENDAR_SELECTION =
+ Calendars._SYNC_ACCOUNT + "=? AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?";
+ private static final int CALENDAR_SELECTION_ID = 0;
+
+ private static final String CATEGORY_TOKENIZER_DELIMITER = "\\";
+
+ private static final ContentProviderOperation PLACEHOLDER_OPERATION =
+ ContentProviderOperation.newInsert(Uri.EMPTY).build();
+
+ private static final Uri sEventsUri = asSyncAdapter(Events.CONTENT_URI);
+ private static final Uri sAttendeesUri = asSyncAdapter(Attendees.CONTENT_URI);
+ private static final Uri sExtendedPropertiesUri = asSyncAdapter(ExtendedProperties.CONTENT_URI);
+ private static final Uri sRemindersUri = asSyncAdapter(Reminders.CONTENT_URI);
+
+ private android.accounts.Account mAccountManagerAccount;
+ private long mCalendarId = -1;
+
+ private ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
+ private ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
+
public CalendarSyncAdapter(Mailbox mailbox, EasSyncService service) {
super(mailbox, service);
+
+ Cursor c = mService.mContentResolver.query(Calendars.CONTENT_URI,
+ new String[] {Calendars._ID}, CALENDAR_SELECTION,
+ new String[] {mAccount.mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE}, null);
+ try {
+ if (c.moveToFirst()) {
+ mCalendarId = c.getLong(CALENDAR_SELECTION_ID);
+ }
+ } finally {
+ c.close();
+ }
}
@Override
@@ -39,19 +120,1133 @@
}
@Override
- public boolean sendLocalChanges(Serializer s) throws IOException {
- // TODO Auto-generated method stub
- return false;
- }
-
- @Override
public void cleanup() {
- // TODO Auto-generated method stub
}
@Override
public boolean parse(InputStream is) throws IOException {
- // TODO Auto-generated method stub
+ EasCalendarSyncParser p = new EasCalendarSyncParser(is, this);
+ return p.parse();
+ }
+
+ static Uri asSyncAdapter(Uri uri) {
+ return uri.buildUpon().appendQueryParameter(Calendar.CALLER_IS_SYNCADAPTER, "true").build();
+ }
+
+ /**
+ * Generate the uri for the data row associated with this NamedContentValues object
+ * @param ncv the NamedContentValues object
+ * @return a uri that can be used to refer to this row
+ */
+ public Uri dataUriFromNamedContentValues(NamedContentValues ncv) {
+ long id = ncv.values.getAsLong(RawContacts._ID);
+ Uri dataUri = ContentUris.withAppendedId(ncv.uri, id);
+ return dataUri;
+ }
+
+ /**
+ * We get our SyncKey from CalendarProvider. If there's not one, we set it to "0" (the reset
+ * state) and save that away.
+ */
+ @Override
+ public String getSyncKey() throws IOException {
+ ContentProviderClient client =
+ mService.mContentResolver.acquireContentProviderClient(Calendar.CONTENT_URI);
+ try {
+ byte[] data = SyncStateContract.Helpers.get(client,
+ asSyncAdapter(Calendar.SyncState.CONTENT_URI), getAccountManagerAccount());
+ if (data == null || data.length == 0) {
+ // Initialize the SyncKey
+ setSyncKey("0", false);
+ return "0";
+ } else {
+ return new String(data);
+ }
+ } catch (RemoteException e) {
+ throw new IOException("Can't get SyncKey from ContactsProvider");
+ }
+ }
+
+ /**
+ * We only need to set this when we're forced to make the SyncKey "0" (a reset). In all other
+ * cases, the SyncKey is set within Calendar
+ */
+ @Override
+ public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
+ if ("0".equals(syncKey) || !inCommands) {
+ ContentProviderClient client =
+ mService.mContentResolver
+ .acquireContentProviderClient(Calendar.CONTENT_URI);
+ try {
+ SyncStateContract.Helpers.set(client, asSyncAdapter(Calendar.SyncState.CONTENT_URI),
+ getAccountManagerAccount(), syncKey.getBytes());
+ userLog("SyncKey set to ", syncKey, " in CalendarProvider");
+ } catch (RemoteException e) {
+ throw new IOException("Can't set SyncKey in CalendarProvider");
+ }
+ }
+ mMailbox.mSyncKey = syncKey;
+ }
+
+ public android.accounts.Account getAccountManagerAccount() {
+ if (mAccountManagerAccount == null) {
+ mAccountManagerAccount =
+ new android.accounts.Account(mAccount.mEmailAddress,
+ Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ }
+ return mAccountManagerAccount;
+ }
+
+ class EasCalendarSyncParser extends AbstractSyncParser {
+
+ String[] mBindArgument = new String[1];
+ String mMailboxIdAsString;
+ Uri mAccountUri;
+ CalendarOperations mOps = new CalendarOperations();
+
+ public EasCalendarSyncParser(InputStream in, CalendarSyncAdapter adapter)
+ throws IOException {
+ super(in, adapter);
+ setLoggingTag("CalendarParser");
+ mAccountUri = Events.CONTENT_URI;
+ }
+
+ @Override
+ public void wipe() {
+ // Delete the calendar associated with this account
+ // TODO Make sure the Events, etc. are also deleted
+ mContentResolver.delete(Calendars.CONTENT_URI, CALENDAR_SELECTION,
+ new String[] {mAccount.mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE});
+ }
+
+ public void addEvent(CalendarOperations ops, String serverId, boolean update)
+ throws IOException {
+ ContentValues cv = new ContentValues();
+ cv.put(Events.CALENDAR_ID, mCalendarId);
+ cv.put(Events._SYNC_ACCOUNT, mAccount.mEmailAddress);
+ cv.put(Events._SYNC_ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ cv.put(Events._SYNC_ID, serverId);
+
+ int allDayEvent = 0;
+ String organizerName = null;
+ String organizerEmail = null;
+ int eventOffset = -1;
+
+ boolean firstTag = true;
+ long eventId = -1;
+ long startTime = -1;
+ long endTime = -1;
+ while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+ if (update && firstTag) {
+ // Find the event that's being updated
+ Cursor c = getServerIdCursor(serverId);
+ long id = -1;
+ try {
+ if (c.moveToFirst()) {
+ id = c.getLong(0);
+ }
+ } finally {
+ c.close();
+ }
+ if (id > 0) {
+ if (tag == Tags.CALENDAR_ATTENDEES) {
+ // This is an attendees-only update; just delete/re-add attendees
+ mBindArgument[0] = Long.toString(id);
+ ops.add(ContentProviderOperation.newDelete(Attendees.CONTENT_URI)
+ .withSelection(ATTENDEES_EXCEPT_ORGANIZER, mBindArgument)
+ .build());
+ eventId = id;
+ } else {
+ // Otherwise, delete the original event and recreate it
+ userLog("Changing (delete/add) event ", serverId);
+ ops.delete(id);
+ // Add a placeholder event so that associated tables can reference
+ // this as a back reference. We add the event at the end of the method
+ eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
+ }
+ }
+ } else if (firstTag) {
+ // Add a placeholder event so that associated tables can reference
+ // this as a back reference. We add the event at the end of the method
+ eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
+ }
+ firstTag = false;
+ switch (tag) {
+ case Tags.CALENDAR_ALL_DAY_EVENT:
+ allDayEvent = getValueInt();
+ cv.put(Events.ALL_DAY, allDayEvent);
+ break;
+ case Tags.CALENDAR_ATTENDEES:
+ // If eventId >= 0, this is an update; otherwise, a new Event
+ attendeesParser(ops, organizerName, organizerEmail, eventId);
+ break;
+ case Tags.BASE_BODY:
+ cv.put(Events.DESCRIPTION, bodyParser());
+ break;
+ case Tags.CALENDAR_BODY:
+ cv.put(Events.DESCRIPTION, getValue());
+ break;
+ case Tags.CALENDAR_TIME_ZONE:
+ TimeZone tz = CalendarUtilities.tziStringToTimeZone(getValue());
+ if (tz != null) {
+ cv.put(Events.EVENT_TIMEZONE, tz.getID());
+ } else {
+ cv.put(Events.EVENT_TIMEZONE, TimeZone.getDefault().getID());
+ }
+ break;
+ case Tags.CALENDAR_START_TIME:
+ startTime = CalendarUtilities.parseDateTimeToMillis(getValue());
+ cv.put(Events.DTSTART, startTime);
+ cv.put(Events.ORIGINAL_INSTANCE_TIME, startTime);
+ break;
+ case Tags.CALENDAR_END_TIME:
+ endTime = CalendarUtilities.parseDateTimeToMillis(getValue());
+ break;
+ case Tags.CALENDAR_EXCEPTIONS:
+ exceptionsParser(ops, cv);
+ break;
+ case Tags.CALENDAR_LOCATION:
+ cv.put(Events.EVENT_LOCATION, getValue());
+ break;
+ case Tags.CALENDAR_RECURRENCE:
+ String rrule = recurrenceParser(ops);
+ if (rrule != null) {
+ cv.put(Events.RRULE, rrule);
+ }
+ break;
+ case Tags.CALENDAR_ORGANIZER_EMAIL:
+ organizerEmail = getValue();
+ cv.put(Events.ORGANIZER, organizerEmail);
+ break;
+ case Tags.CALENDAR_SUBJECT:
+ cv.put(Events.TITLE, getValue());
+ break;
+ case Tags.CALENDAR_SENSITIVITY:
+ cv.put(Events.VISIBILITY, encodeVisibility(getValueInt()));
+ break;
+ case Tags.CALENDAR_ORGANIZER_NAME:
+ organizerName = getValue();
+ break;
+ case Tags.CALENDAR_REMINDER_MINS_BEFORE:
+ ops.newReminder(getValueInt());
+ cv.put(Events.HAS_ALARM, 1);
+ break;
+ // The following are fields we should save (for changes), though they don't
+ // relate to data used by CalendarProvider at this point
+ case Tags.CALENDAR_UID:
+ ops.newExtendedProperty("uid", getValue());
+ break;
+ case Tags.CALENDAR_DTSTAMP:
+ ops.newExtendedProperty("dtstamp", getValue());
+ break;
+ case Tags.CALENDAR_MEETING_STATUS:
+ ops.newExtendedProperty("meeting_status", getValue());
+ break;
+ case Tags.CALENDAR_BUSY_STATUS:
+ ops.newExtendedProperty("busy_status", getValue());
+ break;
+ case Tags.CALENDAR_CATEGORIES:
+ String categories = categoriesParser(ops);
+ if (categories.length() > 0) {
+ ops.newExtendedProperty("categories", categories);
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+
+ // If there's no recurrence, set DTEND to the end time
+ if (!cv.containsKey(Events.RRULE)) {
+ cv.put(Events.DTEND, endTime);
+ cv.put(Events.LAST_DATE, endTime);
+ }
+
+ // Set the DURATION using rfc2445
+ if (allDayEvent != 0) {
+ cv.put(Events.DURATION, "P1D");
+ } else {
+ cv.put(Events.DURATION, "P" + ((endTime - startTime) / MINUTES) + "M");
+ }
+
+ // Put the real event in the proper place in the ops ArrayList
+ if (eventOffset >= 0) {
+ ops.set(eventOffset, ContentProviderOperation
+ .newInsert(sEventsUri).withValues(cv).build());
+ }
+ }
+
+ private String recurrenceParser(CalendarOperations ops) throws IOException {
+ // Turn this information into an RRULE
+ int type = -1;
+ int occurrences = -1;
+ int interval = -1;
+ int dow = -1;
+ int dom = -1;
+ int wom = -1;
+ int moy = -1;
+ String until = null;
+
+ while (nextTag(Tags.CALENDAR_RECURRENCE) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_RECURRENCE_TYPE:
+ type = getValueInt();
+ break;
+ case Tags.CALENDAR_RECURRENCE_INTERVAL:
+ interval = getValueInt();
+ break;
+ case Tags.CALENDAR_RECURRENCE_OCCURRENCES:
+ occurrences = getValueInt();
+ break;
+ case Tags.CALENDAR_RECURRENCE_DAYOFWEEK:
+ dow = getValueInt();
+ break;
+ case Tags.CALENDAR_RECURRENCE_DAYOFMONTH:
+ dom = getValueInt();
+ break;
+ case Tags.CALENDAR_RECURRENCE_WEEKOFMONTH:
+ wom = getValueInt();
+ break;
+ case Tags.CALENDAR_RECURRENCE_MONTHOFYEAR:
+ moy = getValueInt();
+ break;
+ case Tags.CALENDAR_RECURRENCE_UNTIL:
+ until = getValue();
+ break;
+ default:
+ skipTag();
+ }
+ }
+
+ return CalendarUtilities.rruleFromRecurrence(type, occurrences, interval,
+ dow, dom, wom, moy, until);
+ }
+
+ private void exceptionParser(CalendarOperations ops, ContentValues parentCv)
+ throws IOException {
+ ContentValues cv = new ContentValues();
+ cv.put(Events.CALENDAR_ID, mCalendarId);
+ cv.put(Events._SYNC_ACCOUNT, mAccount.mEmailAddress);
+ cv.put(Events._SYNC_ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+
+ // It appears that these values have to be copied from the parent if they are to appear
+ // Note that they can be overridden below
+ cv.put(Events.ORGANIZER, parentCv.getAsString(Events.ORGANIZER));
+ cv.put(Events.TITLE, parentCv.getAsString(Events.TITLE));
+ cv.put(Events.DESCRIPTION, parentCv.getAsBoolean(Events.DESCRIPTION));
+ cv.put(Events.ORIGINAL_ALL_DAY, parentCv.getAsInteger(Events.ALL_DAY));
+
+ // This column is the key that links the exception to the serverId
+ // TODO Make sure calendar knows this isn't globally unique!!
+ cv.put(Events.ORIGINAL_EVENT, parentCv.getAsString(Events._SYNC_ID));
+
+ while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_EXCEPTION_START_TIME:
+ cv.put(Events.ORIGINAL_INSTANCE_TIME,
+ CalendarUtilities.parseDateTimeToMillis(getValue()));
+ break;
+ case Tags.CALENDAR_EXCEPTION_IS_DELETED:
+ if (getValueInt() == 1) {
+ cv.put(Events.STATUS, Events.STATUS_CANCELED);
+ }
+ break;
+ case Tags.CALENDAR_ALL_DAY_EVENT:
+ cv.put(Events.ALL_DAY, getValueInt());
+ break;
+ case Tags.BASE_BODY:
+ cv.put(Events.DESCRIPTION, bodyParser());
+ break;
+ case Tags.CALENDAR_BODY:
+ cv.put(Events.DESCRIPTION, getValue());
+ break;
+ case Tags.CALENDAR_START_TIME:
+ cv.put(Events.DTSTART, CalendarUtilities.parseDateTimeToMillis(getValue()));
+ break;
+ case Tags.CALENDAR_END_TIME:
+ cv.put(Events.DTEND, CalendarUtilities.parseDateTimeToMillis(getValue()));
+ break;
+ case Tags.CALENDAR_LOCATION:
+ cv.put(Events.EVENT_LOCATION, getValue());
+ break;
+ case Tags.CALENDAR_RECURRENCE:
+ String rrule = recurrenceParser(ops);
+ if (rrule != null) {
+ cv.put(Events.RRULE, rrule);
+ }
+ break;
+ case Tags.CALENDAR_SUBJECT:
+ cv.put(Events.TITLE, getValue());
+ break;
+ case Tags.CALENDAR_SENSITIVITY:
+ cv.put(Events.VISIBILITY, encodeVisibility(getValueInt()));
+ break;
+
+ // TODO How to handle these items that are linked to event id!
+
+// case Tags.CALENDAR_DTSTAMP:
+// ops.newExtendedProperty("dtstamp", getValue());
+// break;
+// case Tags.CALENDAR_BUSY_STATUS:
+// // TODO Try to fit this into Calendar scheme
+// ops.newExtendedProperty("busy_status", getValue());
+// break;
+// case Tags.CALENDAR_REMINDER_MINS_BEFORE:
+// ops.newReminder(getValueInt());
+// break;
+
+ // Not yet handled
+ default:
+ skipTag();
+ }
+ }
+
+ if (!cv.containsKey(Events.DTSTART)) {
+ cv.put(Events.DTSTART, parentCv.getAsLong(Events.DTSTART));
+ }
+ if (!cv.containsKey(Events.DTEND)) {
+ cv.put(Events.DTEND, parentCv.getAsLong(Events.DTEND));
+ }
+ // TODO See if this is necessary
+ //cv.put(Events.LAST_DATE, cv.getAsLong(Events.DTEND));
+
+ ops.newException(cv);
+ }
+
+ private int encodeVisibility(int easVisibility) {
+ int visibility = 0;
+ switch(easVisibility) {
+ case 0:
+ visibility = Events.VISIBILITY_DEFAULT;
+ break;
+ case 1:
+ visibility = Events.VISIBILITY_PUBLIC;
+ break;
+ case 2:
+ visibility = Events.VISIBILITY_PRIVATE;
+ break;
+ case 3:
+ visibility = Events.VISIBILITY_CONFIDENTIAL;
+ break;
+ }
+ return visibility;
+ }
+
+ private void exceptionsParser(CalendarOperations ops, ContentValues cv)
+ throws IOException {
+ while (nextTag(Tags.CALENDAR_EXCEPTIONS) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_EXCEPTION:
+ exceptionParser(ops, cv);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private String categoriesParser(CalendarOperations ops) throws IOException {
+ StringBuilder categories = new StringBuilder();
+ while (nextTag(Tags.CALENDAR_CATEGORIES) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_CATEGORY:
+ // TODO Handle categories (there's no similar concept for gdata AFAIK)
+ // We need to save them and spit them back when we update the event
+ categories.append(getValue());
+ categories.append(CATEGORY_TOKENIZER_DELIMITER);
+ default:
+ skipTag();
+ }
+ }
+ return categories.toString();
+ }
+
+ private String attendeesParser(CalendarOperations ops, String organizerName,
+ String organizerEmail, long eventId) throws IOException {
+ String body = null;
+ // First, handle the organizer (who IS an attendee on device, but NOT in EAS)
+ if (organizerName != null || organizerEmail != null) {
+ ContentValues cv = new ContentValues();
+ if (organizerName != null) {
+ cv.put(Attendees.ATTENDEE_NAME, organizerName);
+ }
+ if (organizerEmail != null) {
+ cv.put(Attendees.ATTENDEE_EMAIL, organizerEmail);
+ }
+ cv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
+ cv.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
+ if (eventId < 0) {
+ ops.newAttendee(cv);
+ } else {
+ ops.updatedAttendee(cv, eventId);
+ }
+ }
+ while (nextTag(Tags.CALENDAR_ATTENDEES) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_ATTENDEE:
+ attendeeParser(ops, eventId);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ return body;
+ }
+
+ private void attendeeParser(CalendarOperations ops, long eventId) throws IOException {
+ ContentValues cv = new ContentValues();
+ while (nextTag(Tags.CALENDAR_ATTENDEE) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_ATTENDEE_EMAIL:
+ cv.put(Attendees.ATTENDEE_EMAIL, getValue());
+ break;
+ case Tags.CALENDAR_ATTENDEE_NAME:
+ cv.put(Attendees.ATTENDEE_NAME, getValue());
+ break;
+ case Tags.CALENDAR_ATTENDEE_STATUS:
+ int status = getValueInt();
+ cv.put(Attendees.ATTENDEE_STATUS,
+ (status == 2) ? Attendees.ATTENDEE_STATUS_TENTATIVE :
+ (status == 3) ? Attendees.ATTENDEE_STATUS_ACCEPTED :
+ (status == 4) ? Attendees.ATTENDEE_STATUS_DECLINED :
+ (status == 5) ? Attendees.ATTENDEE_STATUS_INVITED :
+ Attendees.ATTENDEE_STATUS_NONE);
+ break;
+ case Tags.CALENDAR_ATTENDEE_TYPE:
+ int type = Attendees.TYPE_NONE;
+ // EAS types: 1 = req'd, 2 = opt, 3 = resource
+ switch (getValueInt()) {
+ case 1:
+ type = Attendees.TYPE_REQUIRED;
+ break;
+ case 2:
+ type = Attendees.TYPE_OPTIONAL;
+ break;
+ }
+ cv.put(Attendees.ATTENDEE_TYPE, type);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ cv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
+ if (eventId < 0) {
+ ops.newAttendee(cv);
+ } else {
+ ops.updatedAttendee(cv, eventId);
+ }
+ }
+
+ private String bodyParser() throws IOException {
+ String body = null;
+ while (nextTag(Tags.BASE_BODY) != END) {
+ switch (tag) {
+ case Tags.BASE_DATA:
+ body = getValue();
+ break;
+ default:
+ skipTag();
+ }
+ }
+
+ // Handle null data without error
+ if (body == null) return "";
+ // Remove \r's from any body text
+ return body.replace("\r\n", "\n");
+ }
+
+ public void addParser(CalendarOperations ops) throws IOException {
+ String serverId = null;
+ while (nextTag(Tags.SYNC_ADD) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID: // same as
+ serverId = getValue();
+ break;
+ case Tags.SYNC_APPLICATION_DATA:
+ addEvent(ops, serverId, false);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private Cursor getServerIdCursor(String serverId) {
+ mBindArgument[0] = serverId;
+ return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID,
+ mBindArgument, null);
+ }
+
+ private Cursor getClientIdCursor(String clientId) {
+ mBindArgument[0] = clientId;
+ return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION,
+ mBindArgument, null);
+ }
+
+ public void deleteParser(CalendarOperations ops) throws IOException {
+ while (nextTag(Tags.SYNC_DELETE) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ String serverId = getValue();
+ // Find the event with the given serverId
+ Cursor c = getServerIdCursor(serverId);
+ try {
+ if (c.moveToFirst()) {
+ userLog("Deleting ", serverId);
+ ops.delete(c.getLong(0));
+ }
+ } finally {
+ c.close();
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ /**
+ * A change is handled as a delete (including all exceptions) and an add
+ * This isn't as efficient as attempting to traverse the original and all of its exceptions,
+ * but changes happen infrequently and this code is both simpler and easier to maintain
+ * @param ops the array of pending ContactProviderOperations.
+ * @throws IOException
+ */
+ public void changeParser(CalendarOperations ops) throws IOException {
+ String serverId = null;
+ while (nextTag(Tags.SYNC_CHANGE) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ serverId = getValue();
+ break;
+ case Tags.SYNC_APPLICATION_DATA:
+ userLog("Changing " + serverId);
+ addEvent(ops, serverId, true);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ @Override
+ public void commandsParser() throws IOException {
+ while (nextTag(Tags.SYNC_COMMANDS) != END) {
+ if (tag == Tags.SYNC_ADD) {
+ addParser(mOps);
+ incrementChangeCount();
+ } else if (tag == Tags.SYNC_DELETE) {
+ deleteParser(mOps);
+ incrementChangeCount();
+ } else if (tag == Tags.SYNC_CHANGE) {
+ changeParser(mOps);
+ incrementChangeCount();
+ } else
+ skipTag();
+ }
+ }
+
+ @Override
+ public void commit() throws IOException {
+ userLog("Calendar SyncKey saved as: ", mMailbox.mSyncKey);
+ // Save the syncKey here, using the Helper provider by Calendar provider
+ mOps.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI,
+ getAccountManagerAccount(), mMailbox.mSyncKey.getBytes()));
+
+ // Execute these all at once...
+ mOps.execute();
+
+ if (mOps.mResults != null) {
+ // Clear dirty flags for Events sent to server
+ ContentValues cv = new ContentValues();
+ cv.put(Events._SYNC_DIRTY, 0);
+ mContentResolver.update(sEventsUri, cv, DIRTY_IN_CALENDAR,
+ new String[] {Long.toString(mCalendarId)});
+ }
+ }
+
+ public void addResponsesParser() throws IOException {
+ String serverId = null;
+ String clientId = null;
+ int status = -1;
+ ContentValues cv = new ContentValues();
+ while (nextTag(Tags.SYNC_ADD) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ serverId = getValue();
+ break;
+ case Tags.SYNC_CLIENT_ID:
+ clientId = getValue();
+ break;
+ case Tags.SYNC_STATUS:
+ status = getValueInt();
+ if (status != 1) {
+ userLog("Attempt to add event failed with status: " + status);
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+
+ if (clientId == null) return;
+ if (serverId == null) {
+ // TODO Reconsider how to handle this
+ serverId = "FAIL:" + status;
+ }
+
+ Cursor c = getClientIdCursor(clientId);
+ try {
+ if (c.moveToFirst()) {
+ cv.put(Events._SYNC_ID, serverId);
+ mOps.add(ContentProviderOperation.newUpdate(
+ ContentUris.withAppendedId(sEventsUri, c.getLong(0)))
+ .withValues(cv)
+ .build());
+ userLog("New event " + clientId + " was given serverId: " + serverId);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ public void changeResponsesParser() throws IOException {
+ String serverId = null;
+ String status = null;
+ while (nextTag(Tags.SYNC_CHANGE) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ serverId = getValue();
+ break;
+ case Tags.SYNC_STATUS:
+ status = getValue();
+ break;
+ default:
+ skipTag();
+ }
+ }
+ if (serverId != null && status != null) {
+ userLog("Changed event " + serverId + " failed with status: " + status);
+ }
+ }
+
+
+ @Override
+ public void responsesParser() throws IOException {
+ // Handle server responses here (for Add and Change)
+ while (nextTag(Tags.SYNC_RESPONSES) != END) {
+ if (tag == Tags.SYNC_ADD) {
+ addResponsesParser();
+ } else if (tag == Tags.SYNC_CHANGE) {
+ changeResponsesParser();
+ } else
+ skipTag();
+ }
+ }
+ }
+
+ private class CalendarOperations extends ArrayList<ContentProviderOperation> {
+ private static final long serialVersionUID = 1L;
+ private int mCount = 0;
+ private ContentProviderResult[] mResults = null;
+ private int mEventStart = 0;
+
+ @Override
+ public boolean add(ContentProviderOperation op) {
+ super.add(op);
+ mCount++;
+ return true;
+ }
+
+ public int newEvent(ContentProviderOperation op) {
+ mEventStart = mCount;
+ add(op);
+ return mEventStart;
+ }
+
+ public void newAttendee(ContentValues cv) {
+ add(ContentProviderOperation
+ .newInsert(sAttendeesUri)
+ .withValues(cv)
+ .withValueBackReference(Attendees.EVENT_ID, mEventStart)
+ .build());
+ }
+
+ public void updatedAttendee(ContentValues cv, long id) {
+ cv.put(Attendees.EVENT_ID, id);
+ add(ContentProviderOperation.newInsert(sAttendeesUri).withValues(cv).build());
+ }
+
+ public void newException(ContentValues cv) {
+ add(ContentProviderOperation.newInsert(sEventsUri).withValues(cv).build());
+ }
+
+ public void newExtendedProperty(String name, String value) {
+ add(ContentProviderOperation
+ .newInsert(sExtendedPropertiesUri)
+ .withValue(ExtendedProperties.NAME, name)
+ .withValue(ExtendedProperties.VALUE, value)
+ .withValueBackReference(ExtendedProperties.EVENT_ID, mEventStart)
+ .build());
+ }
+
+ public void newReminder(int mins) {
+ add(ContentProviderOperation
+ .newInsert(sRemindersUri)
+ .withValue(Reminders.MINUTES, mins)
+ .withValue(Reminders.METHOD, Reminders.METHOD_DEFAULT)
+ .withValueBackReference(ExtendedProperties.EVENT_ID, mEventStart)
+ .build());
+ }
+
+ public void delete(long id) {
+ add(ContentProviderOperation
+ .newDelete(ContentUris.withAppendedId(sEventsUri, id)).build());
+ }
+
+ public void execute() {
+ synchronized (mService.getSynchronizer()) {
+ if (!mService.isStopped()) {
+ try {
+ if (!isEmpty()) {
+ mService.userLog("Executing ", size(), " CPO's");
+ mResults = mContext.getContentResolver().applyBatch(
+ Calendar.AUTHORITY, this);
+ }
+ } catch (RemoteException e) {
+ // There is nothing sensible to be done here
+ Log.e(TAG, "problem inserting event during server update", e);
+ } catch (OperationApplicationException e) {
+ // There is nothing sensible to be done here
+ Log.e(TAG, "problem inserting event during server update", e);
+ }
+ }
+ }
+ }
+ }
+
+ private String decodeVisibility(int visibility) {
+ int easVisibility = 0;
+ switch(visibility) {
+ case Events.VISIBILITY_DEFAULT:
+ easVisibility = 0;
+ break;
+ case Events.VISIBILITY_PUBLIC:
+ easVisibility = 1;
+ break;
+ case Events.VISIBILITY_PRIVATE:
+ easVisibility = 2;
+ break;
+ case Events.VISIBILITY_CONFIDENTIAL:
+ easVisibility = 3;
+ break;
+ }
+ return Integer.toString(easVisibility);
+ }
+
+ private void sendEvent(Entity entity, String clientId, Serializer s)
+ throws IOException {
+ // Serialize for EAS here
+ // Set uid with the client id we created
+ // 1) Serialize the top-level event
+ // 2) Serialize attendees and reminders from subvalues
+ // 3) Look for exceptions and serialize with the top-level event
+ ContentValues entityValues = entity.getEntityValues();
+ boolean isException = (clientId == null);
+
+ if (entityValues.containsKey(Events.ALL_DAY)) {
+ s.data(Tags.CALENDAR_ALL_DAY_EVENT,
+ entityValues.getAsInteger(Events.ALL_DAY).toString());
+ }
+
+ long startTime = entityValues.getAsLong(Events.DTSTART);
+ s.data(Tags.CALENDAR_START_TIME,
+ CalendarUtilities.millisToEasDateTime(startTime));
+
+ if (!entityValues.containsKey(Events.DURATION)) {
+ if (entityValues.containsKey(Events.DTEND)) {
+ s.data(Tags.CALENDAR_END_TIME, CalendarUtilities.millisToEasDateTime(
+ entityValues.getAsLong(Events.DTEND)));
+ }
+ } else {
+ // Convert this into millis and add it to DTSTART for DTEND
+ // We'll use 1 hour as a default
+ long durationMillis = HOURS;
+ Duration duration = new Duration();
+ try {
+ duration.parse(entityValues.getAsString(Events.DURATION));
+ } catch (DateException e) {
+ // Can't do much about this; use the default (1 hour)
+ }
+ s.data(Tags.CALENDAR_END_TIME,
+ CalendarUtilities.millisToEasDateTime(startTime + durationMillis));
+ }
+
+ s.data(Tags.CALENDAR_DTSTAMP,
+ CalendarUtilities.millisToEasDateTime(System.currentTimeMillis()));
+
+ if (entityValues.containsKey(Events.EVENT_LOCATION)) {
+ s.data(Tags.CALENDAR_LOCATION,
+ entityValues.getAsString(Events.EVENT_LOCATION));
+ }
+ if (entityValues.containsKey(Events.TITLE)) {
+ s.data(Tags.CALENDAR_SUBJECT, entityValues.getAsString(Events.TITLE));
+ }
+
+ if (entityValues.containsKey(Events.VISIBILITY)) {
+ s.data(Tags.CALENDAR_SENSITIVITY,
+ decodeVisibility(entityValues.getAsInteger(Events.VISIBILITY)));
+ } else {
+ // Private if not set
+ s.data(Tags.CALENDAR_SENSITIVITY, "1");
+ }
+
+ if (!isException) {
+ // A time zone is required in all EAS events; we'll use the default if none
+ // is set.
+ String timeZoneName;
+ if (entityValues.containsKey(Events.EVENT_TIMEZONE)) {
+ timeZoneName = entityValues.getAsString(Events.EVENT_TIMEZONE);
+ } else {
+ timeZoneName = TimeZone.getDefault().getID();
+ }
+ String x = CalendarUtilities.timeZoneToTziString(TimeZone.getTimeZone(timeZoneName));
+ s.data(Tags.CALENDAR_TIME_ZONE, x);
+
+ if (entityValues.containsKey(Events.DESCRIPTION)) {
+ String desc = entityValues.getAsString(Events.DESCRIPTION);
+ if (mService.mProtocolVersionDouble >= 12.0) {
+ s.start(Tags.BASE_BODY);
+ s.data(Tags.BASE_TYPE, "1");
+ s.data(Tags.BASE_DATA, desc);
+ s.end();
+ } else {
+ s.data(Tags.CALENDAR_BODY, desc);
+ }
+ }
+
+ if (entityValues.containsKey(Events.ORGANIZER)) {
+ s.data(Tags.CALENDAR_ORGANIZER_EMAIL,
+ entityValues.getAsString(Events.ORGANIZER));
+ }
+
+ if (entityValues.containsKey(Events.RRULE)) {
+ CalendarUtilities.recurrenceFromRrule(
+ entityValues.getAsString(Events.RRULE), startTime, s);
+ }
+
+ // Handle associated data EXCEPT for attendees, which have to be grouped
+ ArrayList<NamedContentValues> subValues = entity.getSubValues();
+ for (NamedContentValues ncv: subValues) {
+ Uri ncvUri = ncv.uri;
+ ContentValues ncvValues = ncv.values;
+ if (ncvUri.equals(ExtendedProperties.CONTENT_URI)) {
+ if (ncvValues.containsKey("uid")) {
+ clientId = ncvValues.getAsString("uid");
+ s.data(Tags.CALENDAR_UID, clientId);
+ }
+ if (ncvValues.containsKey("dtstamp")) {
+ s.data(Tags.CALENDAR_DTSTAMP, ncvValues.getAsString("dtstamp"));
+ }
+ if (ncvValues.containsKey("categories")) {
+ // Send all the categories back to the server
+ // We've saved them as a String of delimited tokens
+ String categories = ncvValues.getAsString("categories");
+ StringTokenizer st =
+ new StringTokenizer(categories, CATEGORY_TOKENIZER_DELIMITER);
+ if (st.countTokens() > 0) {
+ s.start(Tags.CALENDAR_CATEGORIES);
+ while (st.hasMoreTokens()) {
+ String category = st.nextToken();
+ s.data(Tags.CALENDAR_CATEGORY, category);
+ }
+ s.end();
+ }
+ }
+ } else if (ncvUri.equals(Reminders.CONTENT_URI)) {
+ if (ncvValues.containsKey(Reminders.MINUTES)) {
+ s.data(Tags.CALENDAR_REMINDER_MINS_BEFORE,
+ ncvValues.getAsString(Reminders.MINUTES));
+ }
+ }
+ }
+
+ // We've got to send a UID. If the event is new, we've generated one; if not,
+ // we should have gotten one from extended properties.
+ s.data(Tags.CALENDAR_UID, clientId);
+
+ // Handle attendee data here; keep track of organizer and stream it afterward
+ boolean hasAttendees = false;
+ String organizerName = null;
+ for (NamedContentValues ncv: subValues) {
+ Uri ncvUri = ncv.uri;
+ ContentValues ncvValues = ncv.values;
+ if (ncvUri.equals(Attendees.CONTENT_URI)) {
+ if (ncvValues.containsKey(Attendees.ATTENDEE_RELATIONSHIP)) {
+ int relationship =
+ ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
+ // Organizer isn't among attendees in EAS
+ if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
+ if (ncvValues.containsKey(Attendees.ATTENDEE_NAME)) {
+ // Remember this; we can't insert it into the stream in
+ // the middle of attendees
+ organizerName =
+ ncvValues.getAsString(Attendees.ATTENDEE_NAME);
+ }
+ continue;
+ }
+ if (!hasAttendees) {
+ s.start(Tags.CALENDAR_ATTENDEES);
+ hasAttendees = true;
+ }
+ s.start(Tags.CALENDAR_ATTENDEE);
+ if (ncvValues.containsKey(Attendees.ATTENDEE_NAME)) {
+ s.data(Tags.CALENDAR_ATTENDEE_NAME,
+ ncvValues.getAsString(Attendees.ATTENDEE_NAME));
+ }
+ if (ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
+ s.data(Tags.CALENDAR_ATTENDEE_EMAIL,
+ ncvValues.getAsString(Attendees.ATTENDEE_EMAIL));
+ }
+ s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1"); // Required
+ s.end(); // Attendee
+ }
+ // If there's no relationship, we can't create this for EAS
+ }
+ }
+ if (hasAttendees) {
+ s.end(); // Attendees
+ }
+ if (organizerName != null) {
+ s.data(Tags.CALENDAR_ORGANIZER_NAME, organizerName);
+ }
+ } else {
+ // TODO Add reminders to exceptions (allow them to be specified!)
+ if (entityValues.containsKey(Events.ORIGINAL_INSTANCE_TIME)) {
+ s.data(Tags.CALENDAR_EXCEPTION_START_TIME,
+ CalendarUtilities.millisToEasDateTime(entityValues.getAsLong(
+ Events.ORIGINAL_INSTANCE_TIME)));
+ } else {
+ // Illegal; what should we do?
+ }
+ }
+ }
+
+ @Override
+ public boolean sendLocalChanges(Serializer s) throws IOException {
+ ContentResolver cr = mService.mContentResolver;
+ Uri uri = Events.CONTENT_URI.buildUpon()
+ .appendQueryParameter(Calendar.CALLER_IS_SYNCADAPTER, "true")
+ .build();
+
+ if (getSyncKey().equals("0")) {
+ return false;
+ }
+
+ try {
+ // We've got to handle exceptions as part of the parent when changes occur, so we need
+ // to find new/changed exceptions and mark the parent dirty
+ Cursor c = cr.query(Events.CONTENT_URI, ORIGINAL_EVENT_PROJECTION, DIRTY_EXCEPTION,
+ null, null);
+ try {
+ ContentValues cv = new ContentValues();
+ cv.put(Events._SYNC_DIRTY, 1);
+ // Mark the parent dirty in this loop
+ while (c.moveToNext()) {
+ String serverId = c.getString(0);
+ cr.update(asSyncAdapter(Events.CONTENT_URI), cv, SERVER_ID,
+ new String[] {serverId});
+ }
+ } finally {
+ c.close();
+ }
+
+ // Now we can go through dirty top-level events and send them back to the server
+ EntityIterator eventIterator = EventsEntity.newEntityIterator(
+ cr.query(uri, null, DIRTY_TOP_LEVEL, null, null), cr);
+ ContentValues cidValues = new ContentValues();
+ try {
+ boolean first = true;
+ while (eventIterator.hasNext()) {
+ Entity entity = eventIterator.next();
+ String clientId = "uid_" + mMailbox.mId + '_' + System.currentTimeMillis();
+
+ // For each of these entities, create the change commands
+ ContentValues entityValues = entity.getEntityValues();
+ String serverId = entityValues.getAsString(Events._SYNC_ID);
+
+ // EAS 2.5 needs: BusyStatus DtStamp EndTime Sensitivity StartTime TimeZone UID
+ // We can generate all but what we're testing for below
+ if (!entityValues.containsKey(Events.DTSTART)
+ || (!entityValues.containsKey(Events.DURATION) &&
+ !entityValues.containsKey(Events.DTEND))) {
+ continue;
+ }
+ // TODO Handle BusyStatus for EAS 2.5
+ // What should it be??
+
+ if (first) {
+ s.start(Tags.SYNC_COMMANDS);
+ userLog("Sending Calendar changes to the server");
+ first = false;
+ }
+ if (serverId == null) {
+ // This is a new event; create a clientId
+ userLog("Creating new event with clientId: ", clientId);
+ s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
+ // And save it in the Event as the local id
+ cidValues.put(Events._SYNC_LOCAL_ID, clientId);
+ cr.update(ContentUris.
+ withAppendedId(uri,
+ entityValues.getAsLong(Events._ID)),
+ cidValues, null, null);
+ } else {
+ if (entityValues.getAsInteger(Events.DELETED) == 1) {
+ userLog("Deleting event with serverId: ", serverId);
+ s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+ mDeletedIdList.add(entityValues.getAsLong(Events._ID));
+ continue;
+ }
+ userLog("Upsync change to event with serverId: " + serverId);
+ s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
+ }
+ s.start(Tags.SYNC_APPLICATION_DATA);
+ sendEvent(entity, clientId, s);
+
+ // Now, the hard part; find exceptions for this event
+ if (serverId != null) {
+ EntityIterator exceptionIterator = EventsEntity.newEntityIterator(
+ cr.query(uri, null, Events.ORIGINAL_EVENT + "=?",
+ new String[] {serverId}, null), cr);
+ boolean exFirst = true;
+ while (exceptionIterator.hasNext()) {
+ Entity exceptionEntity = exceptionIterator.next();
+ if (exFirst) {
+ s.start(Tags.CALENDAR_EXCEPTIONS);
+ exFirst = false;
+ }
+ s.start(Tags.CALENDAR_EXCEPTION);
+ sendEvent(exceptionEntity, null, s);
+ s.end(); // EXCEPTION
+ }
+ if (!exFirst) {
+ s.end(); // EXCEPTIONS
+ }
+ }
+
+ s.end().end(); // ApplicationData & Change
+ mUpdatedIdList.add(entityValues.getAsLong(Events._ID));
+ }
+ if (!first) {
+ s.end(); // Commands
+ }
+ } finally {
+ eventIterator.close();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not read dirty events.");
+ }
+
return false;
}
}
diff --git a/src/com/android/exchange/adapter/ContactsSyncAdapter.java b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
index 7211794..b95e475 100644
--- a/src/com/android/exchange/adapter/ContactsSyncAdapter.java
+++ b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
@@ -163,7 +163,7 @@
// Make sure ungrouped contacts for Exchange are defaultly visible
ContentValues cv = new ContentValues();
cv.put(Groups.ACCOUNT_NAME, mAccount.mEmailAddress);
- cv.put(Groups.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE);
+ cv.put(Groups.ACCOUNT_TYPE, com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
cv.put(Settings.UNGROUPED_VISIBLE, true);
client.insert(addCallerIsSyncAdapterParameter(Settings.CONTENT_URI), cv);
return "0";
@@ -198,8 +198,8 @@
public android.accounts.Account getAccountManagerAccount() {
if (mAccountManagerAccount == null) {
- mAccountManagerAccount =
- new android.accounts.Account(mAccount.mEmailAddress, Eas.ACCOUNT_MANAGER_TYPE);
+ mAccountManagerAccount = new android.accounts.Account(mAccount.mEmailAddress,
+ com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
}
return mAccountManagerAccount;
}
@@ -893,7 +893,8 @@
private Uri uriWithAccountAndIsSyncAdapter(Uri uri) {
return uri.buildUpon()
.appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
- .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE,
+ com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE)
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
}
@@ -1771,7 +1772,8 @@
ContentResolver cr = mService.mContentResolver;
Uri uri = RawContactsEntity.CONTENT_URI.buildUpon()
.appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
- .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE,
+ com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE)
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
@@ -1879,7 +1881,7 @@
for (ContentValues cv: emailValues) {
sendEmail(s, cv, emailCount++, displayName);
}
-
+
// Now, we'll send up groups, if any
if (!groupIds.isEmpty()) {
boolean groupFirst = true;
diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java
index 94c5ef0..c835f85 100644
--- a/src/com/android/exchange/adapter/EmailSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -166,6 +166,14 @@
String text = getValue();
msg.mText = text;
break;
+ case Tags.EMAIL_MESSAGE_CLASS:
+ String messageClass = getValue();
+ if (messageClass.equals("IPM.Schedule.Meeting.Request")) {
+ msg.mFlags |= Message.FLAG_MEETING_INVITE;
+ } else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) {
+ msg.mFlags |= Message.FLAG_MEETING_CANCEL_NOTICE;
+ }
+ break;
default:
skipTag();
}
diff --git a/src/com/android/exchange/adapter/FolderSyncParser.java b/src/com/android/exchange/adapter/FolderSyncParser.java
index 337e329..b760dfd 100644
--- a/src/com/android/exchange/adapter/FolderSyncParser.java
+++ b/src/com/android/exchange/adapter/FolderSyncParser.java
@@ -17,6 +17,7 @@
package com.android.exchange.adapter;
+import com.android.email.Email;
import com.android.email.provider.AttachmentProvider;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailProvider;
@@ -33,7 +34,10 @@
import android.content.ContentValues;
import android.content.OperationApplicationException;
import android.database.Cursor;
+import android.net.Uri;
import android.os.RemoteException;
+import android.provider.Calendar.Calendars;
+import android.text.format.Time;
import java.io.IOException;
import java.io.InputStream;
@@ -239,7 +243,28 @@
break;
case CALENDAR_TYPE:
m.mType = Mailbox.TYPE_CALENDAR;
- // For now, no sync, since it's not yet implemented
+ m.mSyncInterval = mAccount.mSyncInterval;
+
+ // Create a Calendar object
+ ContentValues cv = new ContentValues();
+ // TODO How will this change if the user changes his account display name?
+ cv.put(Calendars.DISPLAY_NAME, mAccount.mDisplayName);
+ cv.put(Calendars._SYNC_ACCOUNT, mAccount.mEmailAddress);
+ cv.put(Calendars._SYNC_ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ cv.put(Calendars.SYNC_EVENTS, 1);
+ cv.put(Calendars.SELECTED, 1);
+ cv.put(Calendars.HIDDEN, 0);
+ // TODO Find out how to set color!!
+ cv.put(Calendars.COLOR, 0xFF228B22 /*green*/);
+ cv.put(Calendars.TIMEZONE, Time.getCurrentTimezone());
+ cv.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS);
+ cv.put(Calendars.OWNER_ACCOUNT, mAccount.mEmailAddress);
+
+ Uri uri = mService.mContentResolver.insert(Calendars.CONTENT_URI, cv);
+ // We save the id of the calendar into mSyncStatus
+ if (uri != null) {
+ m.mSyncStatus = uri.getPathSegments().get(1);
+ }
break;
}
diff --git a/src/com/android/exchange/adapter/MeetingResponseParser.java b/src/com/android/exchange/adapter/MeetingResponseParser.java
new file mode 100644
index 0000000..142c419
--- /dev/null
+++ b/src/com/android/exchange/adapter/MeetingResponseParser.java
@@ -0,0 +1,65 @@
+/* Copyright (C) 2010 The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parse the result of a MeetingRequest command.
+ */
+public class MeetingResponseParser extends Parser {
+ private EasSyncService mService;
+
+ public MeetingResponseParser(InputStream in, EasSyncService service) throws IOException {
+ super(in);
+ mService = service;
+ }
+
+ public void parseResult() throws IOException {
+ while (nextTag(Tags.MREQ_RESULT) != END) {
+ if (tag == Tags.MREQ_STATUS) {
+ int status = getValueInt();
+ if (status != 1) {
+ mService.userLog("Error in meeting response: " + status);
+ }
+ } else if (tag == Tags.MREQ_CAL_ID) {
+ mService.userLog("Meeting response calendar id: " + getValue());
+ } else {
+ skipTag();
+ }
+ }
+ }
+
+ @Override
+ public boolean parse() throws IOException {
+ boolean res = false;
+ if (nextTag(START_DOCUMENT) != Tags.MREQ_MEETING_RESPONSE) {
+ throw new IOException();
+ }
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ if (tag == Tags.MREQ_RESULT) {
+ parseResult();
+ } else {
+ skipTag();
+ }
+ }
+ return res;
+ }
+}
+
diff --git a/src/com/android/exchange/adapter/Parser.java b/src/com/android/exchange/adapter/Parser.java
index 01051c0..57e3f50 100644
--- a/src/com/android/exchange/adapter/Parser.java
+++ b/src/com/android/exchange/adapter/Parser.java
@@ -38,8 +38,6 @@
*/
public abstract class Parser {
- private static final String TAG = "EasParser";
-
// The following constants are Wbxml standard
public static final int START_DOCUMENT = 0;
public static final int DONE = 1;
@@ -52,6 +50,10 @@
private static final int EOF_BYTE = -1;
private boolean logging = false;
private boolean capture = false;
+ private String logTag = "EAS Parser";
+
+ // Where tags start in a page
+ private static final int TAG_BASE = 5;
private ArrayList<Integer> captureArray;
@@ -158,6 +160,16 @@
}
/**
+ * Set the tag used for logging. When debugging is on, every token is logged (Log.v) to
+ * the console.
+ *
+ * @param val the logging tag
+ */
+ public void setLoggingTag(String val) {
+ logTag = val;
+ }
+
+ /**
* Turns on data capture; this is used to create test streams that represent "live" data and
* can be used against the various parsers.
*/
@@ -190,6 +202,13 @@
public String getValue() throws IOException {
// The false argument tells getNext to return the value as a String
getNext(false);
+ // This means there was no value given, just <Foo/>; we'll return empty string for now
+ if (type == END) {
+ if (logging) {
+ log("No value for tag: " + tagTable[startTag - TAG_BASE]);
+ }
+ return "";
+ }
// Save the value
String val = text;
// Read the next token; it had better be the end of the current tag
@@ -211,6 +230,9 @@
public int getValueInt() throws IOException {
// The true argument to getNext indicates the desire for an integer return value
getNext(true);
+ if (type == END) {
+ return 0;
+ }
// Save the value
int val = num;
// Read the next token; it had better be the end of the current tag
@@ -313,9 +335,9 @@
if (cr > 0) {
str = str.substring(0, cr);
}
- Log.v(TAG, str);
+ Log.v(logTag, str);
if (Eas.FILE_LOG) {
- FileLogger.log(TAG, str);
+ FileLogger.log(logTag, str);
}
}
@@ -385,7 +407,7 @@
text = readInlineString();
}
if (logging) {
- name = tagTable[startTag - 5];
+ name = tagTable[startTag - TAG_BASE];
log(name + ": " + (asInt ? Integer.toString(num) : text));
}
break;
@@ -399,7 +421,7 @@
noContent = (id & 0x40) == 0;
depth++;
if (logging) {
- name = tagTable[startTag - 5];
+ name = tagTable[startTag - TAG_BASE];
//log('<' + name + '>');
nameArray[depth] = name;
}
diff --git a/src/com/android/exchange/adapter/Tags.java b/src/com/android/exchange/adapter/Tags.java
index d822181..2c5c971 100644
--- a/src/com/android/exchange/adapter/Tags.java
+++ b/src/com/android/exchange/adapter/Tags.java
@@ -39,6 +39,7 @@
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;
@@ -218,6 +219,17 @@
public static final int FOLDER_COUNT = FOLDER_PAGE + 0x17;
public static final int FOLDER_VERSION = FOLDER_PAGE + 0x18;
+ public static final int MREQ_PAGE = MREQ << PAGE_SHIFT;
+ public static final int MREQ_CAL_ID = MREQ_PAGE + 5;
+ public static final int MREQ_COLLECTION_ID = MREQ_PAGE + 6;
+ public static final int MREQ_MEETING_RESPONSE = MREQ_PAGE + 7;
+ public static final int MREQ_REQ_ID = MREQ_PAGE + 8;
+ public static final int MREQ_REQUEST = MREQ_PAGE + 9;
+ public static final int MREQ_RESULT = MREQ_PAGE + 0xA;
+ public static final int MREQ_STATUS = MREQ_PAGE + 0xB;
+ public static final int MREQ_USER_RESPONSE = MREQ_PAGE + 0xC;
+ public static final int MREQ_VERSION = MREQ_PAGE + 0xD;
+
public static final int EMAIL_PAGE = EMAIL << PAGE_SHIFT;
public static final int EMAIL_ATTACHMENT = EMAIL_PAGE + 5;
public static final int EMAIL_ATTACHMENTS = EMAIL_PAGE + 6;
@@ -441,6 +453,8 @@
},
{
// 0x08 MeetingResponse
+ "CalId", "CollectionId", "MeetingResponse", "ReqId", "Request",
+ "Result", "Status", "UserResponse", "Version"
},
{
// 0x09 Tasks
diff --git a/src/com/android/exchange/utility/CalendarUtilities.java b/src/com/android/exchange/utility/CalendarUtilities.java
new file mode 100644
index 0000000..e9945b7
--- /dev/null
+++ b/src/com/android/exchange/utility/CalendarUtilities.java
@@ -0,0 +1,728 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.utility;
+
+import com.android.exchange.Eas;
+import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.Tags;
+
+import org.bouncycastle.util.encoders.Base64;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.TimeZone;
+
+public class CalendarUtilities {
+ // NOTE: Most definitions in this class are have package visibility for testing purposes
+ private static final String TAG = "CalendarUtility";
+
+ // Time related convenience constants, in milliseconds
+ static final int SECONDS = 1000;
+ static final int MINUTES = SECONDS*60;
+ static final int HOURS = MINUTES*60;
+ static final long DAYS = HOURS*24;
+
+ // NOTE All Microsoft data structures are little endian
+
+ // The following constants relate to standard Microsoft data sizes
+ // For documentation, see http://msdn.microsoft.com/en-us/library/aa505945.aspx
+ static final int MSFT_LONG_SIZE = 4;
+ static final int MSFT_WCHAR_SIZE = 2;
+ static final int MSFT_WORD_SIZE = 2;
+
+ // The following constants relate to Microsoft's SYSTEMTIME structure
+ // For documentation, see: http://msdn.microsoft.com/en-us/library/ms724950(VS.85).aspx?ppud=4
+
+ static final int MSFT_SYSTEMTIME_YEAR = 0 * MSFT_WORD_SIZE;
+ static final int MSFT_SYSTEMTIME_MONTH = 1 * MSFT_WORD_SIZE;
+ static final int MSFT_SYSTEMTIME_DAY_OF_WEEK = 2 * MSFT_WORD_SIZE;
+ static final int MSFT_SYSTEMTIME_DAY = 3 * MSFT_WORD_SIZE;
+ static final int MSFT_SYSTEMTIME_HOUR = 4 * MSFT_WORD_SIZE;
+ static final int MSFT_SYSTEMTIME_MINUTE = 5 * MSFT_WORD_SIZE;
+ //static final int MSFT_SYSTEMTIME_SECONDS = 6 * MSFT_WORD_SIZE;
+ //static final int MSFT_SYSTEMTIME_MILLIS = 7 * MSFT_WORD_SIZE;
+ static final int MSFT_SYSTEMTIME_SIZE = 8*MSFT_WORD_SIZE;
+
+ // The following constants relate to Microsoft's TIME_ZONE_INFORMATION structure
+ // For documentation, see http://msdn.microsoft.com/en-us/library/ms725481(VS.85).aspx
+ static final int MSFT_TIME_ZONE_BIAS_OFFSET = 0;
+ static final int MSFT_TIME_ZONE_STANDARD_NAME_OFFSET =
+ MSFT_TIME_ZONE_BIAS_OFFSET + MSFT_LONG_SIZE;
+ static final int MSFT_TIME_ZONE_STANDARD_DATE_OFFSET =
+ MSFT_TIME_ZONE_STANDARD_NAME_OFFSET + (MSFT_WCHAR_SIZE*32);
+ static final int MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET =
+ MSFT_TIME_ZONE_STANDARD_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
+ static final int MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET =
+ MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET + MSFT_LONG_SIZE;
+ static final int MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET =
+ MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET + (MSFT_WCHAR_SIZE*32);
+ static final int MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET =
+ MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
+ static final int MSFT_TIME_ZONE_SIZE =
+ MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET + MSFT_LONG_SIZE;
+
+ // TimeZone cache; we parse/decode as little as possible, because the process is quite slow
+ private static HashMap<String, TimeZone> sTimeZoneCache = new HashMap<String, TimeZone>();
+ // TZI string cache; we keep around our encoded TimeZoneInformation strings
+ private static HashMap<TimeZone, String> sTziStringCache = new HashMap<TimeZone, String>();
+
+ // There is no type 4 (thus, the "")
+ static final String[] sTypeToFreq =
+ new String[] {"DAILY", "WEEKLY", "MONTHLY", "MONTHLY", "", "YEARLY", "YEARLY"};
+
+ static final String[] sDayTokens =
+ new String[] {"SU", "MO", "TU", "WE", "TH", "FR", "SA"};
+
+ static final String[] sTwoCharacterNumbers =
+ new String[] {"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"};
+
+ static final int sCurrentYear = new GregorianCalendar().get(Calendar.YEAR);
+ static final TimeZone sGmtTimeZone = TimeZone.getTimeZone("GMT");
+
+ // Return a 4-byte long from a byte array (little endian)
+ static int getLong(byte[] bytes, int offset) {
+ return (bytes[offset++] & 0xFF) | ((bytes[offset++] & 0xFF) << 8) |
+ ((bytes[offset++] & 0xFF) << 16) | ((bytes[offset] & 0xFF) << 24);
+ }
+
+ // Put a 4-byte long into a byte array (little endian)
+ static void setLong(byte[] bytes, int offset, int value) {
+ bytes[offset++] = (byte) (value & 0xFF);
+ bytes[offset++] = (byte) ((value >> 8) & 0xFF);
+ bytes[offset++] = (byte) ((value >> 16) & 0xFF);
+ bytes[offset] = (byte) ((value >> 24) & 0xFF);
+ }
+
+ // Return a 2-byte word from a byte array (little endian)
+ static int getWord(byte[] bytes, int offset) {
+ return (bytes[offset++] & 0xFF) | ((bytes[offset] & 0xFF) << 8);
+ }
+
+ // Put a 2-byte word into a byte array (little endian)
+ static void setWord(byte[] bytes, int offset, int value) {
+ bytes[offset++] = (byte) (value & 0xFF);
+ bytes[offset] = (byte) ((value >> 8) & 0xFF);
+ }
+
+ // Internal structure for storing a time zone date from a SYSTEMTIME structure
+ // This date represents either the start or the end time for DST
+ static class TimeZoneDate {
+ String year;
+ int month;
+ int dayOfWeek;
+ int day;
+ int time;
+ int hour;
+ int minute;
+ }
+
+ // Write SYSTEMTIME data into a byte array (this will either be for the standard or daylight
+ // transition)
+ static void putTimeInMillisIntoSystemTime(byte[] bytes, int offset, long millis) {
+ GregorianCalendar cal = new GregorianCalendar(TimeZone.getDefault());
+ // Round to the next highest minute; we always write seconds as zero
+ cal.setTimeInMillis(millis + 30*SECONDS);
+
+ // MSFT months are 1 based; TimeZone is 0 based
+ setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, cal.get(Calendar.MONTH) + 1);
+ // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1
+ setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, cal.get(Calendar.DAY_OF_WEEK) - 1);
+
+ // Get the "day" in TimeZone format
+ int wom = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
+ // 5 means "last" in MSFT land; for TimeZone, it's -1
+ setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, wom < 0 ? 5 : wom);
+
+ // Turn hours/minutes into ms from midnight (per TimeZone)
+ setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, cal.get(Calendar.HOUR));
+ setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, cal.get(Calendar.MINUTE));
+ }
+
+ // Build a TimeZoneDate structure from a SYSTEMTIME within a byte array at a given offset
+ static TimeZoneDate getTimeZoneDateFromSystemTime(byte[] bytes, int offset) {
+ TimeZoneDate tzd = new TimeZoneDate();
+
+ // MSFT year is an int; TimeZone is a String
+ int num = getWord(bytes, offset + MSFT_SYSTEMTIME_YEAR);
+ tzd.year = Integer.toString(num);
+
+ // MSFT month = 0 means no daylight time
+ // MSFT months are 1 based; TimeZone is 0 based
+ num = getWord(bytes, offset + MSFT_SYSTEMTIME_MONTH);
+ if (num == 0) {
+ return null;
+ } else {
+ tzd.month = num -1;
+ }
+
+ // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1
+ tzd.dayOfWeek = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK) + 1;
+
+ // Get the "day" in TimeZone format
+ num = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY);
+ // 5 means "last" in MSFT land; for TimeZone, it's -1
+ if (num == 5) {
+ tzd.day = -1;
+ } else {
+ tzd.day = num;
+ }
+
+ // Turn hours/minutes into ms from midnight (per TimeZone)
+ int hour = getWord(bytes, offset + MSFT_SYSTEMTIME_HOUR);
+ tzd.hour = hour;
+ int minute = getWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE);
+ tzd.minute = minute;
+ tzd.time = (hour*HOURS) + (minute*MINUTES);
+
+ return tzd;
+ }
+
+ // Return a String from within a byte array at the given offset with max characters
+ // Unused for now, but might be helpful for debugging
+ // String getString(byte[] bytes, int offset, int max) {
+ // StringBuilder sb = new StringBuilder();
+ // while (max-- > 0) {
+ // int b = bytes[offset];
+ // if (b == 0) break;
+ // sb.append((char)b);
+ // offset += 2;
+ // }
+ // return sb.toString();
+ // }
+
+ /**
+ * Build a GregorianCalendar, based on a time zone and TimeZoneDate.
+ * @param timeZone the time zone we're checking
+ * @param tzd the TimeZoneDate we're interested in
+ * @return a GregorianCalendar with the given time zone and date
+ */
+ static GregorianCalendar getCheckCalendar(TimeZone timeZone, TimeZoneDate tzd) {
+ GregorianCalendar testCalendar = new GregorianCalendar(timeZone);
+ testCalendar.set(GregorianCalendar.YEAR, sCurrentYear);
+ testCalendar.set(GregorianCalendar.MONTH, tzd.month);
+ testCalendar.set(GregorianCalendar.DAY_OF_WEEK, tzd.dayOfWeek);
+ testCalendar.set(GregorianCalendar.DAY_OF_WEEK_IN_MONTH, tzd.day);
+ testCalendar.set(GregorianCalendar.HOUR_OF_DAY, tzd.hour);
+ testCalendar.set(GregorianCalendar.MINUTE, tzd.minute);
+ return testCalendar;
+ }
+
+ /**
+ * Find a standard/daylight transition between a start time and an end time
+ * @param tz a TimeZone
+ * @param startTime the start time for the test
+ * @param endTime the end time for the test
+ * @param startInDaylightTime whether daylight time is in effect at the startTime
+ * @return the time in millis of the first transition, or 0 if none
+ */
+ static private long findTransition(TimeZone tz, long startTime, long endTime,
+ boolean startInDaylightTime) {
+ long startingEndTime = endTime;
+ Date date = null;
+ while ((endTime - startTime) > MINUTES) {
+ long checkTime = ((startTime + endTime) / 2) + 1;
+ date = new Date(checkTime);
+ if (tz.inDaylightTime(date) != startInDaylightTime) {
+ endTime = checkTime;
+ } else {
+ startTime = checkTime;
+ }
+ }
+ if (endTime == startingEndTime) {
+ // Really, this shouldn't happen
+ return 0;
+ }
+ return startTime;
+ }
+
+ /**
+ * Return a Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone
+ * that might be found in an Event; use cached result, if possible
+ * @param tz the TimeZone
+ * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
+ */
+ static public String timeZoneToTziString(TimeZone tz) {
+ String tziString = sTziStringCache.get(tz);
+ if (tziString != null) {
+ if (Eas.USER_LOG) {
+ Log.d(TAG, "TZI string for " + tz.getDisplayName() + " found in cache.");
+ }
+ return tziString;
+ }
+ tziString = timeZoneToTziStringImpl(tz);
+ sTziStringCache.put(tz, tziString);
+ return tziString;
+ }
+
+ /**
+ * Calculate the Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone
+ * that might be found in an Event. Since the internal representation of the TimeZone is hidden
+ * from us we'll find the DST transitions and build the structure from that information
+ * @param tz the TimeZone
+ * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
+ */
+ static public String timeZoneToTziStringImpl(TimeZone tz) {
+ String tziString;
+ long time = System.currentTimeMillis();
+ byte[] tziBytes = new byte[MSFT_TIME_ZONE_SIZE];
+ int standardBias = - tz.getRawOffset();
+ standardBias /= 60*SECONDS;
+ setLong(tziBytes, MSFT_TIME_ZONE_BIAS_OFFSET, standardBias);
+ // If this time zone has daylight savings time, we need to do a bunch more work
+ if (tz.useDaylightTime()) {
+ long standardTransition = 0;
+ long daylightTransition = 0;
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.set(sCurrentYear, Calendar.JANUARY, 1, 0, 0, 0);
+ cal.setTimeZone(tz);
+ long startTime = cal.getTimeInMillis();
+ // Calculate rough end of year; no need to do the calculation
+ long endOfYearTime = startTime + 365*DAYS;
+ Date date = new Date(startTime);
+ boolean startInDaylightTime = tz.inDaylightTime(date);
+ // Find the first transition, and store
+ startTime = findTransition(tz, startTime, endOfYearTime, startInDaylightTime);
+ if (startInDaylightTime) {
+ standardTransition = startTime;
+ } else {
+ daylightTransition = startTime;
+ }
+ // Find the second transition, and store
+ startTime = findTransition(tz, startTime, endOfYearTime, !startInDaylightTime);
+ if (startInDaylightTime) {
+ daylightTransition = startTime;
+ } else {
+ standardTransition = startTime;
+ }
+ if (standardTransition != 0 && daylightTransition != 0) {
+ putTimeInMillisIntoSystemTime(tziBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET,
+ standardTransition);
+ putTimeInMillisIntoSystemTime(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET,
+ daylightTransition);
+ int dstOffset = tz.getDSTSavings();
+ setLong(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET, - dstOffset / MINUTES);
+ }
+ }
+ // TODO Use a more efficient Base64 API
+ byte[] tziEncodedBytes = Base64.encode(tziBytes);
+ tziString = new String(tziEncodedBytes);
+ if (Eas.USER_LOG) {
+ Log.d(TAG, "Calculated TZI String for " + tz.getDisplayName() + " in " +
+ (System.currentTimeMillis() - time) + "ms");
+ }
+ return tziString;
+ }
+
+ /**
+ * Given a String as directly read from EAS, returns a TimeZone corresponding to that String
+ * @param timeZoneString the String read from the server
+ * @return the TimeZone, or TimeZone.getDefault() if not found
+ */
+ static public TimeZone tziStringToTimeZone(String timeZoneString) {
+ // If we have this time zone cached, use that value and return
+ TimeZone timeZone = sTimeZoneCache.get(timeZoneString);
+ if (timeZone != null) {
+ if (Eas.USER_LOG) {
+ Log.d(TAG, " Using cached TimeZone " + timeZone.getDisplayName());
+ }
+ } else {
+ timeZone = tziStringToTimeZoneImpl(timeZoneString);
+ if (timeZone == null) {
+ // If we don't find a match, we just return the current TimeZone. In theory, this
+ // shouldn't be happening...
+ Log.w(TAG, "TimeZone not found using default: " + timeZoneString);
+ timeZone = TimeZone.getDefault();
+ }
+ sTimeZoneCache.put(timeZoneString, timeZone);
+ }
+ return timeZone;
+ }
+
+ /**
+ * Given a String as directly read from EAS, tries to find a TimeZone in the database of all
+ * time zones that corresponds to that String.
+ * @param timeZoneString the String read from the server
+ * @return the TimeZone, or TimeZone.getDefault() if not found
+ */
+ static public TimeZone tziStringToTimeZoneImpl(String timeZoneString) {
+ TimeZone timeZone = null;
+ // TODO Remove after we're comfortable with performance
+ long time = System.currentTimeMillis();
+ // First, we need to decode the base64 string
+ byte[] timeZoneBytes = Base64.decode(timeZoneString);
+
+ // Then, we get the bias (similar to a rawOffset); for TimeZone, we need ms
+ // but EAS gives us minutes, so do the conversion. Note that EAS is the bias that's added
+ // to the time zone to reach UTC; our library uses the time from UTC to our time zone, so
+ // we need to change the sign
+ int bias = -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_BIAS_OFFSET) * MINUTES;
+
+ // Get all of the time zones with the bias as a rawOffset; if there aren't any, we return
+ // the default time zone
+ String[] zoneIds = TimeZone.getAvailableIDs(bias);
+ if (zoneIds.length > 0) {
+ // Try to find an existing TimeZone from the data provided by EAS
+ // We start by pulling out the date that standard time begins
+ TimeZoneDate dstEnd =
+ getTimeZoneDateFromSystemTime(timeZoneBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET);
+ if (dstEnd == null) {
+ // In this case, there is no daylight savings time, so the only interesting data
+ // is the offset, and we know that all of the zoneId's match; we'll take the first
+ timeZone = TimeZone.getTimeZone(zoneIds[0]);
+ String dn = timeZone.getDisplayName();
+ sTimeZoneCache.put(timeZoneString, timeZone);
+ if (Eas.USER_LOG) {
+ Log.d(TAG, "TimeZone without DST found by offset: " + dn);
+ }
+ } else {
+ TimeZoneDate dstStart = getTimeZoneDateFromSystemTime(timeZoneBytes,
+ MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET);
+ // See comment above for bias...
+ long dstSavings =
+ -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET) * MINUTES;
+
+ // We'll go through each time zone to find one with the same DST transitions and
+ // savings length
+ for (String zoneId: zoneIds) {
+ // Get the TimeZone using the zoneId
+ timeZone = TimeZone.getTimeZone(zoneId);
+
+ // Our strategy here is to check just before and just after the transitions
+ // and see whether the check for daylight time matches the expectation
+ // If both transitions match, then we have a match for the offset and start/end
+ // of dst. That's the best we can do for now, since there's no other info
+ // provided by EAS (i.e. we can't get dynamic transitions, etc.)
+
+ int testSavingsMinutes = timeZone.getDSTSavings() / MINUTES;
+ int errorBoundsMinutes = (testSavingsMinutes * 2) + 1;
+
+ // Check start DST transition
+ GregorianCalendar testCalendar = getCheckCalendar(timeZone, dstStart);
+ testCalendar.add(GregorianCalendar.MINUTE, - errorBoundsMinutes);
+ Date before = testCalendar.getTime();
+ testCalendar.add(GregorianCalendar.MINUTE, 2*errorBoundsMinutes);
+ Date after = testCalendar.getTime();
+ if (timeZone.inDaylightTime(before)) continue;
+ if (!timeZone.inDaylightTime(after)) continue;
+
+ // Check end DST transition
+ testCalendar = getCheckCalendar(timeZone, dstEnd);
+ testCalendar.add(GregorianCalendar.MINUTE, - errorBoundsMinutes);
+ before = testCalendar.getTime();
+ testCalendar.add(GregorianCalendar.MINUTE, 2*errorBoundsMinutes);
+ after = testCalendar.getTime();
+ if (!timeZone.inDaylightTime(before)) continue;
+ if (timeZone.inDaylightTime(after)) continue;
+
+ // Check that the savings are the same
+ if (dstSavings != timeZone.getDSTSavings()) continue;
+
+ // If we're here, it's the right time zone, modulo dynamic DST
+ String dn = timeZone.getDisplayName();
+ // TODO Remove timing when we're comfortable with performance
+ if (Eas.USER_LOG) {
+ Log.d(TAG, "TimeZone found by rules: " + dn + " in " +
+ (System.currentTimeMillis() - time) + "ms");
+ }
+ break;
+ }
+ }
+ }
+ return timeZone;
+ }
+
+ /**
+ * Generate a time in milliseconds from a date string that represents a date/time in GMT
+ * @param DateTime string from Exchange server
+ * @return the time in milliseconds (since Jan 1, 1970)
+ */
+ static public long parseDateTimeToMillis(String date) {
+ // Format for calendar date strings is 20090211T180303Z
+ GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
+ Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)),
+ Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)),
+ Integer.parseInt(date.substring(13, 15)));
+ cal.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return cal.getTimeInMillis();
+ }
+
+ /**
+ * Generate a GregorianCalendar from a date string that represents a date/time in GMT
+ * @param DateTime string from Exchange server
+ * @return the GregorianCalendar
+ */
+ static public GregorianCalendar parseDateTimeToCalendar(String date) {
+ // Format for calendar date strings is 20090211T180303Z
+ GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
+ Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)),
+ Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)),
+ Integer.parseInt(date.substring(13, 15)));
+ cal.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return cal;
+ }
+
+ static String formatTwo(int num) {
+ if (num <= 12) {
+ return sTwoCharacterNumbers[num];
+ } else
+ return Integer.toString(num);
+ }
+
+ /**
+ * Generate an EAS formatted date/time string based on GMT. See below for details.
+ */
+ static public String millisToEasDateTime(long millis) {
+ return millisToEasDateTime(millis, sGmtTimeZone);
+ }
+
+ /**
+ * Generate an EAS formatted local date/time string from a time and a time zone
+ * @param millis a time in milliseconds
+ * @param tz a time zone
+ * @return an EAS formatted string indicating the date/time in the given time zone
+ */
+ static public String millisToEasDateTime(long millis, TimeZone tz) {
+ StringBuilder sb = new StringBuilder();
+ GregorianCalendar cal = new GregorianCalendar(tz);
+ cal.setTimeInMillis(millis);
+ sb.append(cal.get(Calendar.YEAR));
+ sb.append(formatTwo(cal.get(Calendar.MONTH) + 1));
+ sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH)));
+ sb.append('T');
+ sb.append(formatTwo(cal.get(Calendar.HOUR_OF_DAY)));
+ sb.append(formatTwo(cal.get(Calendar.MINUTE)));
+ sb.append(formatTwo(cal.get(Calendar.SECOND)));
+ sb.append('Z');
+ return sb.toString();
+ }
+
+ static void addByDay(StringBuilder rrule, int dow, int wom) {
+ rrule.append(";BYDAY=");
+ boolean addComma = false;
+ for (int i = 0; i < 7; i++) {
+ if ((dow & 1) == 1) {
+ if (addComma) {
+ rrule.append(',');
+ }
+ if (wom > 0) {
+ // 5 = last week -> -1
+ // So -1SU = last sunday
+ rrule.append(wom == 5 ? -1 : wom);
+ }
+ rrule.append(sDayTokens[i]);
+ addComma = true;
+ }
+ dow >>= 1;
+ }
+ }
+
+ static void addByMonthDay(StringBuilder rrule, int dom) {
+ // 127 means last day of the month
+ if (dom == 127) {
+ dom = -1;
+ }
+ rrule.append(";BYMONTHDAY=" + dom);
+ }
+
+ /**
+ * Generate the String version of the EAS integer for a given BYDAY value in an rrule
+ * @param dow the BYDAY value of the rrule
+ * @return the String version of the EAS value of these days
+ */
+ static String generateEasDayOfWeek(String dow) {
+ int bits = 0;
+ int bit = 1;
+ for (String token: sDayTokens) {
+ // If we can find the day in the dow String, add the bit to our bits value
+ if (dow.indexOf(token) >= 0) {
+ bits |= bit;
+ }
+ bit <<= 1;
+ }
+ return Integer.toString(bits);
+ }
+
+ /**
+ * Extract the value of a token in an RRULE string
+ * @param rrule an RRULE string
+ * @param token a token to look for in the RRULE
+ * @return the value of that token
+ */
+ static String tokenFromRrule(String rrule, String token) {
+ int start = rrule.indexOf(token);
+ if (start < 0) return null;
+ int len = rrule.length();
+ start += token.length();
+ int end = start;
+ char c;
+ do {
+ c = rrule.charAt(end++);
+ if (!Character.isLetterOrDigit(c) || (end == len)) {
+ if (end == len) end++;
+ return rrule.substring(start, end -1);
+ }
+ } while (true);
+ }
+
+ /**
+ * Write recurrence information to EAS based on the RRULE in CalendarProvider
+ * @param rrule the RRULE, from CalendarProvider
+ * @param startTime, the DTSTART of this Event
+ * @param s the Serializer we're using to write WBXML data
+ * @throws IOException
+ */
+ // NOTE: For the moment, we're only parsing recurrence types that are supported by the
+ // Calendar app UI, which is a small subset of possible recurrence types
+ // This code must be updated when the Calendar adds new functionality
+ static public void recurrenceFromRrule(String rrule, long startTime, Serializer s)
+ throws IOException {
+ Log.d("RRULE", "rule: " + rrule);
+ String freq = tokenFromRrule(rrule, "FREQ=");
+ // If there's no FREQ=X, then we don't write a recurrence
+ // Note that we duplicate s.start(Tags.CALENDAR_RECURRENCE); s.end(); to prevent the
+ // possibility of writing out a partial recurrence stanza
+ if (freq != null) {
+ if (freq.equals("DAILY")) {
+ s.start(Tags.CALENDAR_RECURRENCE);
+ s.data(Tags.CALENDAR_RECURRENCE_TYPE, "0");
+ s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1");
+ s.end();
+ } else if (freq.equals("WEEKLY")) {
+ s.start(Tags.CALENDAR_RECURRENCE);
+ s.data(Tags.CALENDAR_RECURRENCE_TYPE, "1");
+ s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1");
+ // Requires a day of week (whereas RRULE does not)
+ String byDay = tokenFromRrule(rrule, "BYDAY=");
+ if (byDay != null) {
+ s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay));
+ }
+ s.end();
+ } else if (freq.equals("MONTHLY")) {
+ String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
+ if (byMonthDay != null) {
+ // The nth day of the month
+ s.start(Tags.CALENDAR_RECURRENCE);
+ s.data(Tags.CALENDAR_RECURRENCE_TYPE, "2");
+ s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
+ s.end();
+ } else {
+ String byDay = tokenFromRrule(rrule, "BYDAY=");
+ String bareByDay;
+ if (byDay != null) {
+ // This can be 1WE (1st Wednesday) or -1FR (last Friday)
+ int wom = byDay.charAt(0);
+ if (wom == '-') {
+ // -1 is the only legal case (last week) Use "5" for EAS
+ wom = 5;
+ bareByDay = byDay.substring(2);
+ } else {
+ wom = wom - '0';
+ bareByDay = byDay.substring(1);
+ }
+ s.start(Tags.CALENDAR_RECURRENCE);
+ s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3");
+ s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(wom));
+ s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(bareByDay));
+ s.end();
+ }
+ }
+ } else if (freq.equals("YEARLY")) {
+ String byMonth = tokenFromRrule(rrule, "BYMONTH=");
+ String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
+ if (byMonth == null || byMonthDay == null) {
+ // Calculate the month and day from the startDate
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.setTimeInMillis(startTime);
+ cal.setTimeZone(TimeZone.getDefault());
+ byMonth = Integer.toString(cal.get(Calendar.MONTH) + 1);
+ byMonthDay = Integer.toString(cal.get(Calendar.DAY_OF_MONTH));
+ }
+ s.start(Tags.CALENDAR_RECURRENCE);
+ s.data(Tags.CALENDAR_RECURRENCE_TYPE, "5");
+ s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
+ s.data(Tags.CALENDAR_RECURRENCE_MONTHOFYEAR, byMonth);
+ s.end();
+ }
+ }
+ }
+
+ /**
+ * Build an RRULE String from EAS recurrence information
+ * @param type the type of recurrence
+ * @param occurrences how many recurrences (instances)
+ * @param interval the interval between recurrences
+ * @param dow day of the week
+ * @param dom day of the month
+ * @param wom week of the month
+ * @param moy month of the year
+ * @param until the last recurrence time
+ * @return a valid RRULE String
+ */
+ static public String rruleFromRecurrence(int type, int occurrences, int interval, int dow,
+ int dom, int wom, int moy, String until) {
+ StringBuilder rrule = new StringBuilder("FREQ=" + sTypeToFreq[type]);
+
+ // INTERVAL and COUNT
+ if (interval > 0) {
+ rrule.append(";INTERVAL=" + interval);
+ }
+ if (occurrences > 0) {
+ rrule.append(";COUNT=" + occurrences);
+ }
+
+ // Days, weeks, months, etc.
+ switch(type) {
+ case 0: // DAILY
+ case 1: // WEEKLY
+ if (dow > 0) addByDay(rrule, dow, -1);
+ break;
+ case 2: // MONTHLY
+ if (dom > 0) addByMonthDay(rrule, dom);
+ break;
+ case 3: // MONTHLY (on the nth day)
+ if (dow > 0) addByDay(rrule, dow, wom);
+ break;
+ case 5: // YEARLY
+ if (dom > 0) addByMonthDay(rrule, dom);
+ if (moy > 0) {
+ // TODO MAKE SURE WE'RE 1 BASED
+ rrule.append(";BYMONTH=" + moy);
+ }
+ break;
+ case 6: // YEARLY (on the nth day)
+ if (dow > 0) addByDay(rrule, dow, wom);
+ if (moy > 0) addByMonthDay(rrule, dow);
+ break;
+ default:
+ break;
+ }
+
+ // UNTIL comes last
+ // TODO Add UNTIL code
+ if (until != null) {
+ // *** until probably needs reformatting
+ //rrule.append(";UNTIL=" + until);
+ }
+
+ return rrule.toString();
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/utility/Duration.java b/src/com/android/exchange/utility/Duration.java
new file mode 100644
index 0000000..0ec867c
--- /dev/null
+++ b/src/com/android/exchange/utility/Duration.java
@@ -0,0 +1,128 @@
+/* Copyright 2010, The Android Open Source Project
+ **
+ ** Licensed under the Apache License, Version 2.0 (the "License");
+ ** you may not use this file except in compliance with the License.
+ ** You may obtain a copy of the License at
+ **
+ ** http://www.apache.org/licenses/LICENSE-2.0
+ **
+ ** Unless required by applicable law or agreed to in writing, software
+ ** distributed under the License is distributed on an "AS IS" BASIS,
+ ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ** See the License for the specific language governing permissions and
+ ** limitations under the License.
+ */
+
+package com.android.exchange.utility;
+
+import android.pim.DateException;
+
+import java.util.Calendar;
+
+/**
+ * Note: This class was simply copied from the class in CalendarProvider, since we don't have access
+ * to it from the Email app. I reformated some lines, but otherwise haven't altered the code.
+ */
+public class Duration {
+ public int sign; // 1 or -1
+ public int weeks;
+ public int days;
+ public int hours;
+ public int minutes;
+ public int seconds;
+
+ public Duration() {
+ sign = 1;
+ }
+
+ /**
+ * Parse according to RFC2445 ss4.3.6. (It's actually a little loose with
+ * its parsing, for better or for worse)
+ */
+ public void parse(String str) throws DateException {
+ sign = 1;
+ weeks = 0;
+ days = 0;
+ hours = 0;
+ minutes = 0;
+ seconds = 0;
+
+ int len = str.length();
+ int index = 0;
+ char c;
+
+ if (len < 1) {
+ return;
+ }
+
+ c = str.charAt(0);
+ if (c == '-') {
+ sign = -1;
+ index++;
+ } else if (c == '+') {
+ index++;
+ }
+
+ if (len < index) {
+ return;
+ }
+
+ c = str.charAt(index);
+ if (c != 'P') {
+ throw new DateException (
+ "Duration.parse(str='" + str + "') expected 'P' at index="
+ + index);
+ }
+ index++;
+
+ int n = 0;
+ for (; index < len; index++) {
+ c = str.charAt(index);
+ if (c >= '0' && c <= '9') {
+ n *= 10;
+ n += (c - '0');
+ } else if (c == 'W') {
+ weeks = n;
+ n = 0;
+ } else if (c == 'H') {
+ hours = n;
+ n = 0;
+ } else if (c == 'M') {
+ minutes = n;
+ n = 0;
+ } else if (c == 'S') {
+ seconds = n;
+ n = 0;
+ } else if (c == 'D') {
+ days = n;
+ n = 0;
+ } else if (c == 'T') {
+ } else {
+ throw new DateException (
+ "Duration.parse(str='" + str + "') unexpected char '"
+ + c + "' at index=" + index);
+ }
+ }
+ }
+
+ /**
+ * Add this to the calendar provided, in place, in the calendar.
+ */
+ public void addTo(Calendar cal) {
+ cal.add(Calendar.DAY_OF_MONTH, sign*weeks*7);
+ cal.add(Calendar.DAY_OF_MONTH, sign*days);
+ cal.add(Calendar.HOUR, sign*hours);
+ cal.add(Calendar.MINUTE, sign*minutes);
+ cal.add(Calendar.SECOND, sign*seconds);
+ }
+
+ public long addTo(long dt) {
+ return dt + getMillis();
+ }
+
+ public long getMillis() {
+ long factor = 1000 * sign;
+ return factor * ((7*24*60*60*weeks) + (24*60*60*days) + (60*60*hours) + (60*minutes) +
+ seconds);
+ }
+}
diff --git a/tests/src/com/android/exchange/EasSyncServiceTests.java b/tests/src/com/android/exchange/EasSyncServiceTests.java
index 45f4882..d4372a5 100644
--- a/tests/src/com/android/exchange/EasSyncServiceTests.java
+++ b/tests/src/com/android/exchange/EasSyncServiceTests.java
@@ -73,6 +73,4 @@
}
}
}
-
-
}
diff --git a/tests/src/com/android/exchange/SyncManagerAccountTests.java b/tests/src/com/android/exchange/SyncManagerAccountTests.java
index 9a1f24b..66c20e9 100644
--- a/tests/src/com/android/exchange/SyncManagerAccountTests.java
+++ b/tests/src/com/android/exchange/SyncManagerAccountTests.java
@@ -17,6 +17,7 @@
package com.android.exchange;
+import com.android.email.Email;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailProvider;
import com.android.email.provider.ProviderTestUtils;
@@ -63,7 +64,7 @@
}
private android.accounts.Account makeAccountManagerAccount(String username) {
- return new android.accounts.Account(username, Eas.ACCOUNT_MANAGER_TYPE);
+ return new android.accounts.Account(username, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
}
private void createAccountManagerAccount(String username) {
@@ -105,7 +106,7 @@
private void deleteTemporaryAccountManagerAccounts(Context context) {
android.accounts.Account[] accountManagerAccounts =
- AccountManager.get(context).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE);
+ AccountManager.get(context).getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
if (accountManagerAccount.name.startsWith(TEST_ACCOUNT_PREFIX) &&
accountManagerAccount.name.endsWith(TEST_ACCOUNT_SUFFIX)) {
@@ -135,7 +136,7 @@
// Check that they're set up properly
assertEquals(3, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null));
android.accounts.Account[] accountManagerAccounts =
- AccountManager.get(context).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE);
+ AccountManager.get(context).getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
assertEquals(3, accountManagerAccounts.length);
// Delete account "2" from AccountManager
@@ -145,7 +146,7 @@
// Confirm it's deleted
accountManagerAccounts =
- AccountManager.get(context).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE);
+ AccountManager.get(context).getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
assertEquals(2, accountManagerAccounts.length);
// Run the reconciler
@@ -169,8 +170,8 @@
makeSyncManagerAccountList(), accountManagerAccounts);
// There should now be only one AccountManager account
- accountManagerAccounts =
- AccountManager.get(getContext()).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE);
+ accountManagerAccounts = AccountManager.get(getContext()).getAccountsByType(
+ Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
assertEquals(1, accountManagerAccounts.length);
// ... and it should be account "3"
assertEquals(getTestAccountEmailAddress("3"), accountManagerAccounts[0].name);
diff --git a/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
new file mode 100644
index 0000000..7f380d5
--- /dev/null
+++ b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+public class CalendarSyncAdapterTests extends SyncAdapterTestCase {
+
+ public CalendarSyncAdapterTests() {
+ super();
+ }
+}
diff --git a/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java b/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
index e378b03..5f363b8 100644
--- a/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
+++ b/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2009 The Android Open Source Project
+ * Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,16 +32,14 @@
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
-import android.test.ProviderTestCase2;
import java.io.ByteArrayInputStream;
import java.io.IOException;
-import java.io.InputStream;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.TimeZone;
-public class EmailSyncAdapterTests extends ProviderTestCase2<EmailProvider> {
+public class EmailSyncAdapterTests extends SyncAdapterTestCase {
EmailProvider mProvider;
Context mMockContext;
@@ -52,54 +50,7 @@
EasEmailSyncParser mSyncParser;
public EmailSyncAdapterTests() {
- super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
- }
-
- @Override
- public void setUp() throws Exception {
- super.setUp();
- mMockContext = getMockContext();
- mMockResolver = mMockContext.getContentResolver();
- }
-
- @Override
- public void tearDown() throws Exception {
- super.tearDown();
- }
-
- /**
- * Create and return a short, simple InputStream that has at least four bytes, which is all
- * that's required to initialize an EasParser (the parent class of EasEmailSyncParser)
- * @return the InputStream
- */
- public InputStream getTestInputStream() {
- return new ByteArrayInputStream(new byte[] {0, 0, 0, 0, 0});
- }
-
- EasSyncService getTestService() {
- Account account = new Account();
- account.mId = -1;
- Mailbox mailbox = new Mailbox();
- mailbox.mId = -1;
- EasSyncService service = new EasSyncService();
- service.mContext = mMockContext;
- service.mMailbox = mailbox;
- service.mAccount = account;
- return service;
- }
-
- EasSyncService getTestService(Account account, Mailbox mailbox) {
- EasSyncService service = new EasSyncService();
- service.mContext = mMockContext;
- service.mMailbox = mailbox;
- service.mAccount = account;
- return service;
- }
-
- EmailSyncAdapter getTestSyncAdapter() {
- EasSyncService service = getTestService();
- EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
- return adapter;
+ super();
}
/**
diff --git a/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java b/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
new file mode 100644
index 0000000..311b550
--- /dev/null
+++ b/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import com.android.email.provider.EmailProvider;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.test.ProviderTestCase2;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+public class SyncAdapterTestCase extends ProviderTestCase2<EmailProvider> {
+
+ EmailProvider mProvider;
+ Context mMockContext;
+ ContentResolver mMockResolver;
+ Mailbox mMailbox;
+ Account mAccount;
+ EmailSyncAdapter mSyncAdapter;
+ EasEmailSyncParser mSyncParser;
+
+ public SyncAdapterTestCase() {
+ super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mMockContext = getMockContext();
+ mMockResolver = mMockContext.getContentResolver();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /**
+ * Create and return a short, simple InputStream that has at least four bytes, which is all
+ * that's required to initialize an EasParser (the parent class of EasEmailSyncParser)
+ * @return the InputStream
+ */
+ public InputStream getTestInputStream() {
+ return new ByteArrayInputStream(new byte[] {0, 0, 0, 0, 0});
+ }
+
+ EasSyncService getTestService() {
+ Account account = new Account();
+ account.mId = -1;
+ Mailbox mailbox = new Mailbox();
+ mailbox.mId = -1;
+ EasSyncService service = new EasSyncService();
+ service.mContext = mMockContext;
+ service.mMailbox = mailbox;
+ service.mAccount = account;
+ return service;
+ }
+
+ EasSyncService getTestService(Account account, Mailbox mailbox) {
+ EasSyncService service = new EasSyncService();
+ service.mContext = mMockContext;
+ service.mMailbox = mailbox;
+ service.mAccount = account;
+ return service;
+ }
+
+ EmailSyncAdapter getTestSyncAdapter() {
+ EasSyncService service = getTestService();
+ EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
+ return adapter;
+ }
+
+}
diff --git a/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
new file mode 100644
index 0000000..cb97e86
--- /dev/null
+++ b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.utility;
+
+import android.test.AndroidTestCase;
+
+import java.util.TimeZone;
+
+/**
+ * Tests of EAS Calendar Utilities
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.utility.CalendarUtilitiesTests email
+ *
+ * Please see RFC2445 for RRULE definition
+ * http://www.ietf.org/rfc/rfc2445.txt
+ */
+
+public class CalendarUtilitiesTests extends AndroidTestCase {
+
+ // Some prebuilt time zones, Base64 encoded (as they arrive from EAS)
+ // More time zones to be added over time
+
+ // Not all time zones are appropriate for testing. For example, ISRAEL_STANDARD_TIME cannot be
+ // used because DST is determined from year to year in a non-standard way (related to the lunar
+ // calendar); therefore, the test would only work during the year in which it was created
+ private static final String INDIA_STANDARD_TIME =
+ "tv7//0kAbgBkAGkAYQAgAFMAdABhAG4AZABhAHIAZAAgAFQAaQBtAGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEkAbgBkAGkAYQAgAEQAYQB5AGwAaQBnAGgAdAAgAFQAaQBtAGUA" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
+ private static final String PACIFIC_STANDARD_TIME =
+ "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByAGQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAYQBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkA" +
+ "bQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==";
+
+ public void testGetSet() {
+ byte[] bytes = new byte[] {0, 1, 2, 3, 4, 5, 6, 7};
+
+ // First, check that getWord/Long are properly little endian
+ assertEquals(0x0100, CalendarUtilities.getWord(bytes, 0));
+ assertEquals(0x03020100, CalendarUtilities.getLong(bytes, 0));
+ assertEquals(0x07060504, CalendarUtilities.getLong(bytes, 4));
+
+ // Set some words and longs
+ CalendarUtilities.setWord(bytes, 0, 0xDEAD);
+ CalendarUtilities.setLong(bytes, 2, 0xBEEFBEEF);
+ CalendarUtilities.setWord(bytes, 6, 0xCEDE);
+
+ // Retrieve them
+ assertEquals(0xDEAD, CalendarUtilities.getWord(bytes, 0));
+ assertEquals(0xBEEFBEEF, CalendarUtilities.getLong(bytes, 2));
+ assertEquals(0xCEDE, CalendarUtilities.getWord(bytes, 6));
+ }
+
+ public void testParseTimeZoneEndToEnd() {
+ TimeZone tz = CalendarUtilities.tziStringToTimeZone(PACIFIC_STANDARD_TIME);
+ assertEquals("Pacific Standard Time", tz.getDisplayName());
+ tz = CalendarUtilities.tziStringToTimeZone(INDIA_STANDARD_TIME);
+ assertEquals("India Standard Time", tz.getDisplayName());
+ }
+
+ public void testGenerateEasDayOfWeek() {
+ String byDay = "TU;WE;SA";
+ // TU = 4, WE = 8; SA = 64;
+ assertEquals("76", CalendarUtilities.generateEasDayOfWeek(byDay));
+ // MO = 2, TU = 4; WE = 8; TH = 16; FR = 32
+ byDay = "MO;TU;WE;TH;FR";
+ assertEquals("62", CalendarUtilities.generateEasDayOfWeek(byDay));
+ // SU = 1
+ byDay = "SU";
+ assertEquals("1", CalendarUtilities.generateEasDayOfWeek(byDay));
+ }
+
+ public void testTokenFromRrule() {
+ String rrule = "FREQ=DAILY;INTERVAL=1;BYDAY=WE,TH,SA;BYMONTHDAY=17";
+ assertEquals("DAILY", CalendarUtilities.tokenFromRrule(rrule, "FREQ="));
+ assertEquals("1", CalendarUtilities.tokenFromRrule(rrule, "INTERVAL="));
+ assertEquals("17", CalendarUtilities.tokenFromRrule(rrule, "BYMONTHDAY="));
+ assertNull(CalendarUtilities.tokenFromRrule(rrule, "UNTIL="));
+ }
+
+ // Tests in progress...
+
+// public void testTimeZoneToTziString() {
+// for (String timeZoneId: TimeZone.getAvailableIDs()) {
+// TimeZone timeZone = TimeZone.getTimeZone(timeZoneId);
+// if (timeZone != null) {
+// String tzs = CalendarUtilities.timeZoneToTziString(timeZone);
+// TimeZone newTimeZone = CalendarUtilities.tziStringToTimeZone(tzs);
+// System.err.println("In: " + timeZone.getDisplayName() + ", Out: " + newTimeZone.getDisplayName());
+// }
+// }
+// }
+// public void testParseTimeZone() {
+// GregorianCalendar cal = getTestCalendar(parsedTimeZone, dstStart);
+// cal.add(GregorianCalendar.MINUTE, -1);
+// Date b = cal.getTime();
+// cal.add(GregorianCalendar.MINUTE, 2);
+// Date a = cal.getTime();
+// if (parsedTimeZone.inDaylightTime(b) || !parsedTimeZone.inDaylightTime(a)) {
+// userLog("ERROR IN TIME ZONE CONTROL!");
+// }
+// cal = getTestCalendar(parsedTimeZone, dstEnd);
+// cal.add(GregorianCalendar.HOUR, -2);
+// b = cal.getTime();
+// cal.add(GregorianCalendar.HOUR, 2);
+// a = cal.getTime();
+// if (!parsedTimeZone.inDaylightTime(b)) userLog("ERROR IN TIME ZONE CONTROL");
+// if (parsedTimeZone.inDaylightTime(a)) userLog("ERROR IN TIME ZONE CONTROL!");
+// }
+}