Remove Exchange1 sources
* Prevents confusion
Change-Id: If698dadca64ea1c68433e03d9a37bcdcdf6d2c29
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
deleted file mode 100644
index 15ac88f..0000000
--- a/AndroidManifest.xml
+++ /dev/null
@@ -1,170 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2008 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<manifest
- xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.exchange"
- android:versionCode="410000"
- android:versionName="4.1"
- >
-
- <uses-permission
- android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
- <uses-permission
- android:name="android.permission.ACCESS_NETWORK_STATE"/>
- <uses-permission
- android:name="android.permission.INTERNET"/>
- <uses-permission
- android:name="android.permission.VIBRATE"/>
- <uses-permission
- android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
- <uses-permission
- android:name="android.permission.GET_ACCOUNTS" />
- <uses-permission
- android:name="android.permission.MANAGE_ACCOUNTS" />
- <uses-permission
- android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
- <uses-permission
- android:name="android.permission.READ_SYNC_SETTINGS" />
- <uses-permission
- android:name="android.permission.WRITE_SYNC_SETTINGS" />
-
- <uses-permission
- android:name="android.permission.READ_CONTACTS"/>
- <uses-permission
- android:name="android.permission.WRITE_CONTACTS"/>
- <uses-permission
- android:name="android.permission.READ_CALENDAR"/>
- <uses-permission
- android:name="android.permission.WRITE_CALENDAR"/>
- <uses-permission
- android:name="android.permission.USE_CREDENTIALS"/>
-
- <!-- Only required if a store implements push mail and needs to keep network open -->
- <uses-permission
- android:name="android.permission.WAKE_LOCK"/>
- <uses-permission
- android:name="android.permission.READ_PHONE_STATE"/>
-
- <uses-permission
- android:name="com.android.email.permission.READ_ATTACHMENT"/>
- <uses-permission
- android:name="com.android.email.permission.ACCESS_PROVIDER"/>
-
- <application
- android:icon="@mipmap/icon"
- android:label="@string/app_name"
- android:name="Exchange"
- android:theme="@android:style/Theme.Holo.Light"
- >
-
- <receiver
- android:name="com.android.exchange.EmailSyncAlarmReceiver"/>
- <receiver
- android:name="com.android.exchange.MailboxAlarmReceiver"/>
-
- <receiver
- android:name=".service.ExchangeBroadcastReceiver"
- android:enabled="true">
- <intent-filter>
- <action
- android:name="android.intent.action.BOOT_COMPLETED" />
- <action
- android:name="android.intent.action.DEVICE_STORAGE_LOW" />
- <action
- android:name="android.intent.action.DEVICE_STORAGE_OK" />
- <action
- android:name="android.accounts.LOGIN_ACCOUNTS_CHANGED" />
- </intent-filter>
- </receiver>
-
- <service
- android:name=".service.ExchangeBroadcastProcessorService" />
-
- <!--Required stanza to register the EAS EmailSyncAdapterService with SyncManager -->
- <service
- android:name="com.android.exchange.EmailSyncAdapterService"
- android:exported="true">
- <intent-filter>
- <action
- android:name="android.content.SyncAdapter" />
- </intent-filter>
- <meta-data android:name="android.content.SyncAdapter"
- android:resource="@xml/syncadapter_email" />
- </service>
-
- <!--Required stanza to register the EAS ContactsSyncAdapterService with SyncManager -->
- <service
- android:name="com.android.exchange.ContactsSyncAdapterService"
- android:exported="true">
- <intent-filter>
- <action
- android:name="android.content.SyncAdapter" />
- </intent-filter>
- <meta-data android:name="android.content.SyncAdapter"
- android:resource="@xml/syncadapter_contacts" />
- </service>
-
- <!--Required stanza to register the EAS CalendarSyncAdapterService with SyncManager -->
- <service
- android:name="com.android.exchange.CalendarSyncAdapterService"
- android:exported="true">
- <intent-filter>
- <action
- android:name="android.content.SyncAdapter" />
- </intent-filter>
- <meta-data android:name="android.content.SyncAdapter"
- android:resource="@xml/syncadapter_calendar" />
- </service>
-
- <!-- Add android:process=":remote" below to enable ExchangeService as a separate process -->
- <service
- android:name="com.android.exchange.ExchangeService"
- android:enabled="true"
- android:permission="com.android.email.permission.ACCESS_PROVIDER"
- >
- <intent-filter>
- <action
- android:name="com.android.email.EXCHANGE_INTENT" />
- </intent-filter>
- </service>
-
- <provider
- android:name="com.android.exchange.provider.ExchangeDirectoryProvider"
- android:authorities="com.android.exchange.directory.provider"
- android:readPermission="android.permission.READ_CONTACTS"
- android:multiprocess="false"
- >
- <meta-data
- android:name="android.content.ContactDirectory"
- android:value="true"/>
- </provider>
-
- <activity android:name=".EasCertificateRequestor">
- <intent-filter>
- <action android:name="com.android.emailcommon.REQUEST_CERT" />
- <data android:scheme="eas" android:path="/certrequest" />
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- </activity>
- <activity android:name=".SettingsRedirector">
- <intent-filter>
- <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- </activity>
- </application>
-</manifest>
diff --git a/proguard.flags b/proguard.flags
deleted file mode 100644
index 0b7da4b..0000000
--- a/proguard.flags
+++ /dev/null
@@ -1,23 +0,0 @@
-# keep names that are used by reflection.
-
-# Keep names that are used only by unit tests
--keep class ** {
- *** *ForTest(...);
-}
-
--keepclasseswithmembers class com.android.exchange.adapter.Parser {
- *** resetInput(java.io.InputStream);
-}
-
--keepclasseswithmembers class com.android.exchange.provider.GalResult {
- *** addGalData(com.android.exchange.provider.GalResult$GalData);
- *** addGalData(long, java.lang.String, java.lang.String);
-}
-
--keepclasseswithmembers class com.android.exchange.CalendarSyncEnabler {
- public <init>(android.content.Context);
-}
-
--keepclasseswithmembers class com.android.exchange.provider.MailboxUtilities {
- *** setFlagsAndChildrensParentKey(android.content.Context, java.lang.String, java.lang.String);
-}
diff --git a/src/com/android/exchange/AbstractSyncService.java b/src/com/android/exchange/AbstractSyncService.java
deleted file mode 100644
index 6ccc7c3..0000000
--- a/src/com/android/exchange/AbstractSyncService.java
+++ /dev/null
@@ -1,296 +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.emailcommon.provider.Account;
-import com.android.emailcommon.provider.HostAuth;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.exchange.utility.FileLogger;
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.NetworkInfo.DetailedState;
-import android.os.Bundle;
-import android.util.Log;
-
-import java.util.concurrent.LinkedBlockingQueue;
-
-/**
- * Base class for all protocol services SyncManager (extends Service, implements
- * Runnable) instantiates subclasses to run a sync (either timed, or push, or
- * mail placed in outbox, etc.) EasSyncService is currently implemented; my goal
- * would be to move IMAP to this structure when it comes time to introduce push
- * functionality.
- */
-public abstract class AbstractSyncService implements Runnable {
-
- public String TAG = "AbstractSyncService";
-
- public static final int SECONDS = 1000;
- public static final int MINUTES = 60*SECONDS;
- public static final int HOURS = 60*MINUTES;
- public static final int DAYS = 24*HOURS;
-
- public static final int CONNECT_TIMEOUT = 30*SECONDS;
- public static final int NETWORK_WAIT = 15*SECONDS;
-
- public static final String EAS_PROTOCOL = "eas";
- public static final int EXIT_DONE = 0;
- public static final int EXIT_IO_ERROR = 1;
- public static final int EXIT_LOGIN_FAILURE = 2;
- public static final int EXIT_EXCEPTION = 3;
- public static final int EXIT_SECURITY_FAILURE = 4;
- public static final int EXIT_ACCESS_DENIED = 5;
-
- public Mailbox mMailbox;
- protected long mMailboxId;
- protected int mExitStatus = EXIT_EXCEPTION;
- protected String mMailboxName;
- public Account mAccount;
- public Context mContext;
- public int mChangeCount = 0;
- public volatile int mSyncReason = 0;
- protected volatile boolean mStop = false;
- protected volatile Thread mThread;
- protected final Object mSynchronizer = new Object();
-
- protected volatile long mRequestTime = 0;
- protected LinkedBlockingQueue<Request> mRequestQueue = new LinkedBlockingQueue<Request>();
-
- /**
- * Sent by SyncManager to request that the service stop itself cleanly
- */
- public abstract void stop();
-
- /**
- * Sent by SyncManager to indicate that an alarm has fired for this service, and that its
- * pending (network) operation has timed out. The service is NOT automatically stopped,
- * although the behavior is service dependent.
- *
- * @return true if the operation was stopped normally; false if the thread needed to be
- * interrupted.
- */
- public abstract boolean alarm();
-
- /**
- * Sent by SyncManager to request that the service reset itself cleanly; the meaning of this
- * operation is service dependent.
- */
- public abstract void reset();
-
- /**
- * Called to validate an account; abstract to allow each protocol to do what
- * is necessary. For consistency with the Email app's original
- * functionality, success is indicated by a failure to throw an Exception
- * (ugh). Parameters are self-explanatory
- *
- * @param hostAuth
- * @return a Bundle containing a result code and, depending on the result, a PolicySet or an
- * error message
- */
- public abstract Bundle validateAccount(HostAuth hostAuth, Context context);
-
- public AbstractSyncService(Context _context, Mailbox _mailbox) {
- mContext = _context;
- mMailbox = _mailbox;
- mMailboxId = _mailbox.mId;
- mMailboxName = _mailbox.mServerId;
- mAccount = Account.restoreAccountWithId(_context, _mailbox.mAccountKey);
- }
-
- // Will be required when subclasses are instantiated by name
- public AbstractSyncService(String prefix) {
- }
-
- /**
- * The UI can call this static method to perform account validation. This method wraps each
- * protocol's validateAccount method. Arguments are self-explanatory, except where noted.
- *
- * @param klass the protocol class (EasSyncService.class for example)
- * @param hostAuth
- * @param context
- * @return a Bundle containing a result code and, depending on the result, a PolicySet or an
- * error message
- */
- public static Bundle validate(Class<? extends AbstractSyncService> klass,
- HostAuth hostAuth, Context context) {
- AbstractSyncService svc;
- try {
- svc = klass.newInstance();
- return svc.validateAccount(hostAuth, context);
- } catch (IllegalAccessException e) {
- } catch (InstantiationException e) {
- }
- return null;
- }
-
- public static class ValidationResult {
- static final int NO_FAILURE = 0;
- static final int CONNECTION_FAILURE = 1;
- static final int VALIDATION_FAILURE = 2;
- static final int EXCEPTION = 3;
-
- static final ValidationResult succeeded = new ValidationResult(true, NO_FAILURE, null);
- boolean success;
- int failure = NO_FAILURE;
- String reason = null;
- Exception exception = null;
-
- ValidationResult(boolean _success, int _failure, String _reason) {
- success = _success;
- failure = _failure;
- reason = _reason;
- }
-
- ValidationResult(boolean _success) {
- success = _success;
- }
-
- ValidationResult(Exception e) {
- success = false;
- failure = EXCEPTION;
- exception = e;
- }
-
- public boolean isSuccess() {
- return success;
- }
-
- public String getReason() {
- return reason;
- }
- }
-
- public boolean isStopped() {
- return mStop;
- }
-
- public Object getSynchronizer() {
- return mSynchronizer;
- }
-
- /**
- * Convenience methods to do user logging (i.e. connection activity). Saves a bunch of
- * repetitive code.
- */
- public void userLog(String string, int code, String string2) {
- if (Eas.USER_LOG) {
- userLog(string + code + string2);
- }
- }
-
- public void userLog(String string, int code) {
- if (Eas.USER_LOG) {
- userLog(string + code);
- }
- }
-
- public void userLog(String str, Exception e) {
- if (Eas.USER_LOG) {
- Log.e(TAG, str, e);
- } else {
- Log.e(TAG, str + e);
- }
- if (Eas.FILE_LOG) {
- FileLogger.log(e);
- }
- }
-
- /**
- * Standard logging for EAS.
- * If user logging is active, we concatenate any arguments and log them using Log.d
- * We also check for file logging, and log appropriately
- * @param strings strings to concatenate and log
- */
- public void userLog(String ...strings) {
- if (Eas.USER_LOG) {
- String logText;
- if (strings.length == 1) {
- logText = strings[0];
- } else {
- StringBuilder sb = new StringBuilder(64);
- for (String string: strings) {
- sb.append(string);
- }
- logText = sb.toString();
- }
- Log.d(TAG, logText);
- if (Eas.FILE_LOG) {
- FileLogger.log(TAG, logText);
- }
- }
- }
-
- /**
- * Error log is used for serious issues that should always be logged
- * @param str the string to log
- */
- public void errorLog(String str) {
- Log.e(TAG, str);
- if (Eas.FILE_LOG) {
- FileLogger.log(TAG, str);
- }
- }
-
- /**
- * Waits for up to 10 seconds for network connectivity; returns whether or not there is
- * network connectivity.
- *
- * @return whether there is network connectivity
- */
- public boolean hasConnectivity() {
- ConnectivityManager cm =
- (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
- int tries = 0;
- while (tries++ < 1) {
- // Use the same test as in ExchangeService#waitForConnectivity
- // TODO: Create common code for this test in emailcommon
- NetworkInfo info = cm.getActiveNetworkInfo();
- if (info != null) {
- return true;
- }
- try {
- Thread.sleep(10*SECONDS);
- } catch (InterruptedException e) {
- }
- }
- return false;
- }
-
- /**
- * Request handling (common functionality)
- * Can be overridden if desired
- */
-
- public void addRequest(Request req) {
- mRequestQueue.offer(req);
- }
-
- public void removeRequest(Request req) {
- mRequestQueue.remove(req);
- }
-
- public boolean hasPendingRequests() {
- return !mRequestQueue.isEmpty();
- }
-
- public void clearRequests() {
- mRequestQueue.clear();
- }
-}
diff --git a/src/com/android/exchange/CalendarSyncAdapterService.java b/src/com/android/exchange/CalendarSyncAdapterService.java
deleted file mode 100644
index 5ff03b3..0000000
--- a/src/com/android/exchange/CalendarSyncAdapterService.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-import android.accounts.Account;
-import android.accounts.OperationCanceledException;
-import android.app.Service;
-import android.content.AbstractThreadedSyncAdapter;
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SyncResult;
-import android.database.Cursor;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.provider.CalendarContract.Events;
-import android.util.Log;
-
-import com.android.emailcommon.provider.EmailContent;
-import com.android.emailcommon.provider.EmailContent.AccountColumns;
-import com.android.emailcommon.provider.EmailContent.MailboxColumns;
-import com.android.emailcommon.provider.Mailbox;
-
-public class CalendarSyncAdapterService extends Service {
- private static final String TAG = "EAS CalendarSyncAdapterService";
- private static SyncAdapterImpl sSyncAdapter = null;
- private static final Object sSyncAdapterLock = new Object();
-
- private static final String ACCOUNT_AND_TYPE_CALENDAR =
- MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_CALENDAR;
- private static final String DIRTY_IN_ACCOUNT =
- Events.DIRTY + "=1 AND " + Events.ACCOUNT_NAME + "=?";
- private static final String[] ID_SYNC_KEY_PROJECTION =
- new String[] {MailboxColumns.ID, MailboxColumns.SYNC_KEY};
- private static final int ID_SYNC_KEY_MAILBOX_ID = 0;
- private static final int ID_SYNC_KEY_SYNC_KEY = 1;
-
- public CalendarSyncAdapterService() {
- super();
- }
-
- private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
- private Context mContext;
-
- public SyncAdapterImpl(Context context) {
- super(context, true /* autoInitialize */);
- mContext = context;
- }
-
- @Override
- public void onPerformSync(Account account, Bundle extras,
- String authority, ContentProviderClient provider, SyncResult syncResult) {
- try {
- CalendarSyncAdapterService.performSync(mContext, account, extras,
- authority, provider, syncResult);
- } catch (OperationCanceledException e) {
- }
- }
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- synchronized (sSyncAdapterLock) {
- if (sSyncAdapter == null) {
- sSyncAdapter = new SyncAdapterImpl(getApplicationContext());
- }
- }
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return sSyncAdapter.getSyncAdapterBinder();
- }
-
- /**
- * Partial integration with system SyncManager; we tell our EAS ExchangeService to start a
- * calendar sync when we get the signal from SyncManager.
- * The missing piece at this point is integration with the push/ping mechanism in EAS; this will
- * be put in place at a later time.
- */
- private static void performSync(Context context, Account account, Bundle extras,
- String authority, ContentProviderClient provider, SyncResult syncResult)
- throws OperationCanceledException {
- ContentResolver cr = context.getContentResolver();
- boolean logging = Eas.USER_LOG;
- if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
- Cursor c = cr.query(Events.CONTENT_URI,
- new String[] {Events._ID}, DIRTY_IN_ACCOUNT, new String[] {account.name}, null);
- try {
- if (!c.moveToFirst()) {
- if (logging) {
- Log.d(TAG, "No changes for " + account.name);
- }
- return;
- }
- } finally {
- c.close();
- }
- }
-
- // Find the (EmailProvider) account associated with this email address
- Cursor accountCursor =
- cr.query(com.android.emailcommon.provider.Account.CONTENT_URI,
- EmailContent.ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?",
- new String[] {account.name}, null);
- try {
- if (accountCursor.moveToFirst()) {
- long accountId = accountCursor.getLong(0);
- // Now, find the calendar mailbox associated with the account
- Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, ID_SYNC_KEY_PROJECTION,
- ACCOUNT_AND_TYPE_CALENDAR, new String[] {Long.toString(accountId)}, null);
- try {
- if (mailboxCursor.moveToFirst()) {
- if (logging) {
- Log.d(TAG, "Upload sync requested for " + account.name);
- }
- String syncKey = mailboxCursor.getString(ID_SYNC_KEY_SYNC_KEY);
- if ((syncKey == null) || (syncKey.equals("0"))) {
- if (logging) {
- Log.d(TAG, "Can't sync; mailbox in initial state");
- }
- return;
- }
- // Ask for a sync from our sync manager
- ExchangeService.serviceRequest(mailboxCursor.getLong(
- ID_SYNC_KEY_MAILBOX_ID), ExchangeService.SYNC_UPSYNC);
- }
- } finally {
- mailboxCursor.close();
- }
- }
- } finally {
- accountCursor.close();
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/exchange/CalendarSyncEnabler.java b/src/com/android/exchange/CalendarSyncEnabler.java
deleted file mode 100644
index 5989614..0000000
--- a/src/com/android/exchange/CalendarSyncEnabler.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-import com.android.emailcommon.Logging;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.provider.CalendarContract;
-import android.util.Log;
-
-/**
- * Utility class to enable Exchange calendar sync for all existing Exchange accounts.
- *
- * <p>Exchange calendar was first supported on Froyo. It wasn't supported on Eclair, which
- * was the first version that supported Exchange email.
- *
- * <p>This class is used only once when the devices is upgraded to Froyo (or later) from Eclair,
- * to enable calendar sync for all the existing Exchange accounts.
- */
-public class CalendarSyncEnabler {
- private final Context mContext;
-
- public CalendarSyncEnabler(Context context) {
- this.mContext = context;
- }
-
- /**
- * Enable calendar sync for all the existing exchange accounts, and post a notification if any.
- */
- public final void enableEasCalendarSync() {
- String emailAddresses = enableEasCalendarSyncInternalForTest();
- if (emailAddresses.length() > 0) {
- // Exchange account(s) found.
- showNotificationForTest(emailAddresses.toString());
- }
- }
-
- /**
- * Enable calendar sync for all the existing exchange accounts
- *
- * @return email addresses of the Exchange accounts joined with spaces as delimiters,
- * or the empty string if there's no Exchange accounts.
- */
- /* package for testing */ final String enableEasCalendarSyncInternalForTest() {
- StringBuilder emailAddresses = new StringBuilder();
-
- Account[] exchangeAccounts = AccountManager.get(mContext)
- .getAccountsByType(Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
- for (Account account : exchangeAccounts) {
- final String emailAddress = account.name;
- Log.i(Logging.LOG_TAG, "Enabling Exchange calendar sync for " + emailAddress);
-
- ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1);
- ContentResolver.setSyncAutomatically(account, CalendarContract.AUTHORITY, true);
-
- // Accumulate addresses for notification.
- if (emailAddresses.length() > 0) {
- emailAddresses.append(' ');
- }
- emailAddresses.append(emailAddress);
- }
- return emailAddresses.toString();
- }
-
- // *** Taken from NotificationController
- public static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2;
-
- /**
- * Show the "Exchange calendar added" notification.
- *
- * @param emailAddresses space delimited list of email addresses of Exchange accounts. It'll
- * be shown on the notification.
- */
- /* package for testing */ void showNotificationForTest(String emailAddresses) {
- // Launch Calendar app when clicked.
- PendingIntent launchCalendarPendingIntent = PendingIntent.getActivity(mContext, 0,
- createLaunchCalendarIntent(), 0);
-
- String tickerText = mContext.getString(R.string.notification_exchange_calendar_added);
- Notification n = new Notification(R.drawable.stat_notify_calendar,
- tickerText, System.currentTimeMillis());
- n.setLatestEventInfo(mContext, tickerText, emailAddresses, launchCalendarPendingIntent);
- n.flags = Notification.FLAG_AUTO_CANCEL;
-
- NotificationManager nm =
- (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
- nm.notify(NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED, n);
- }
-
- /** @return {@link Intent} to launch the Calendar app. */
- private Intent createLaunchCalendarIntent() {
- return new Intent(Intent.ACTION_VIEW, Uri.parse("content://com.android.calendar/time"));
- }
-}
diff --git a/src/com/android/exchange/CommandStatusException.java b/src/com/android/exchange/CommandStatusException.java
deleted file mode 100644
index 80b405a..0000000
--- a/src/com/android/exchange/CommandStatusException.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-/**
- * ActiveSync command error status definitions (EAS 14.0 and later); these are in addition to the
- * command-specific errors defined for earlier protocol versions
- */
-public class CommandStatusException extends EasException {
- private static final long serialVersionUID = 1L;
-
- // A status response to an EAS account. Responses < 16 correspond to command-specific errors as
- // reported by EAS versions < 14.0; responses > 100 correspond to generic errors as reported
- // by EAS versions 14.0 and greater
- public final int mStatus;
- // If the error refers to a specific data item, that item's id (as provided by the server) is
- // stored here
- public final String mItemId;
-
- public static class CommandStatus {
- private static final long serialVersionUID = 1L;
-
- // Fatal user/provisioning issues (put on security hold)
- public static final int USER_DISABLED_FOR_SYNC = 126;
- public static final int USERS_DISABLED_FOR_SYNC = 127;
- public static final int USER_ON_LEGACY_SERVER_CANT_SYNC = 128;
- public static final int DEVICE_QUARANTINED = 129;
- public static final int ACCESS_DENIED = 130;
- public static final int USER_ACCOUNT_DISABLED = 131;
- public static final int NOT_PROVISIONABLE_PARTIAL = 139;
- public static final int NOT_PROVISIONABLE_LEGACY_DEVICE = 141;
- public static final int TOO_MANY_PARTNERSHIPS = 177;
-
- // Sync state problems (bad key, multiple client conflict, etc.)
- public static final int SYNC_STATE_LOCKED = 133;
- public static final int SYNC_STATE_CORRUPT = 134;
- public static final int SYNC_STATE_EXISTS = 135;
- public static final int SYNC_STATE_INVALID = 136;
-
- // Soft provisioning errors, we need to send Provision command
- public static final int NEEDS_PROVISIONING_WIPE = 140;
- public static final int NEEDS_PROVISIONING = 142;
- public static final int NEEDS_PROVISIONING_REFRESH = 143;
- public static final int NEEDS_PROVISIONING_INVALID = 144;
-
- // WTF issues (really shouldn't happen in our implementation)
- public static final int WTF_INVALID_COMMAND = 137;
- public static final int WTF_INVALID_PROTOCOL = 138;
- public static final int WTF_DEVICE_CLAIMS_EXTERNAL_MANAGEMENT = 145;
- public static final int WTF_UNKNOWN_ITEM_TYPE = 147;
- public static final int WTF_REQUIRES_PROXY_WITHOUT_SSL = 148;
-
- // For SmartReply/SmartForward
- public static final int ITEM_NOT_FOUND = 150;
-
- // Transient or possibly transient errors
- public static final int SERVER_ERROR_RETRY = 111;
- public static final int SYNC_STATE_NOT_FOUND = 132;
-
- // String version of error status codes (for logging only)
- private static final int STATUS_TEXT_START = 101;
- private static final int STATUS_TEXT_END = 150;
- private static final String[] STATUS_TEXT = {
- "InvalidContent", "InvalidWBXML", "InvalidXML", "InvalidDateTime", "InvalidIDCombo",
- "InvalidIDs", "InvalidMIME", "DeviceIdError", "DeviceTypeError", "ServerError",
- "ServerErrorRetry", "ADAccessDenied", "Quota", "ServerOffline", "SendQuota",
- "RecipientUnresolved", "ReplyNotAllowed", "SentPreviously", "NoRecipient", "SendFailed",
- "ReplyFailed", "AttsTooLarge", "NoMailbox", "CantBeAnonymous", "UserNotFound",
- "UserDisabled", "NewMailbox", "LegacyMailbox", "DeviceBlocked", "AccessDenied",
- "AcctDisabled", "SyncStateNF", "SyncStateLocked", "SyncStateCorrupt", "SyncStateExists",
- "SyncStateInvalid", "BadCommand", "BadVersion", "NotFullyProvisionable", "RemoteWipe",
- "LegacyDevice", "NotProvisioned", "PolicyRefresh", "BadPolicyKey", "ExternallyManaged",
- "NoRecurrence", "UnexpectedClass", "RemoteHasNoSSL", "InvalidRequest", "ItemNotFound"
- };
-
- public static boolean isNeedsProvisioning(int status) {
- return (status == CommandStatus.NEEDS_PROVISIONING ||
- status == CommandStatus.NEEDS_PROVISIONING_REFRESH ||
- status == CommandStatus.NEEDS_PROVISIONING_INVALID ||
- status == CommandStatus.NEEDS_PROVISIONING_WIPE);
- }
-
- public static boolean isBadSyncKey(int status) {
- return (status == CommandStatus.SYNC_STATE_CORRUPT ||
- status == CommandStatus.SYNC_STATE_INVALID);
- }
-
- public static boolean isDeniedAccess(int status) {
- return (status == CommandStatus.USER_DISABLED_FOR_SYNC ||
- status == CommandStatus.USERS_DISABLED_FOR_SYNC ||
- status == CommandStatus.USER_ON_LEGACY_SERVER_CANT_SYNC ||
- status == CommandStatus.DEVICE_QUARANTINED ||
- status == CommandStatus.ACCESS_DENIED ||
- status == CommandStatus.USER_ACCOUNT_DISABLED ||
- status == CommandStatus.NOT_PROVISIONABLE_LEGACY_DEVICE ||
- status == CommandStatus.NOT_PROVISIONABLE_PARTIAL ||
- status == CommandStatus.TOO_MANY_PARTNERSHIPS);
- }
-
- public static boolean isTransientError(int status) {
- return status == CommandStatus.SYNC_STATE_NOT_FOUND ||
- status == CommandStatus.SERVER_ERROR_RETRY;
- }
-
- public static String toString(int status) {
- StringBuilder sb = new StringBuilder();
- sb.append(status);
- sb.append(" (");
- if (status < STATUS_TEXT_START || status > STATUS_TEXT_END) {
- sb.append("unknown");
- } else {
- int offset = status - STATUS_TEXT_START;
- if (offset <= STATUS_TEXT.length) {
- sb.append(STATUS_TEXT[offset]);
- }
- }
- sb.append(")");
- return sb.toString();
- }
- }
-
- public CommandStatusException(int status) {
- mStatus = status;
- mItemId = null;
- }
-
- public CommandStatusException(int status, String itemId) {
- mStatus = status;
- mItemId = itemId;
- }
-}
diff --git a/src/com/android/exchange/ContactsSyncAdapterService.java b/src/com/android/exchange/ContactsSyncAdapterService.java
deleted file mode 100644
index 97706e7..0000000
--- a/src/com/android/exchange/ContactsSyncAdapterService.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-import com.android.emailcommon.provider.EmailContent.AccountColumns;
-import com.android.emailcommon.provider.EmailContent.MailboxColumns;
-import com.android.emailcommon.provider.Mailbox;
-
-import android.accounts.Account;
-import android.accounts.OperationCanceledException;
-import android.app.Service;
-import android.content.AbstractThreadedSyncAdapter;
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SyncResult;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.provider.ContactsContract.Groups;
-import android.provider.ContactsContract.RawContacts;
-import android.util.Log;
-
-public class ContactsSyncAdapterService extends Service {
- private static final String TAG = "EAS ContactsSyncAdapterService";
- private static SyncAdapterImpl sSyncAdapter = null;
- private static final Object sSyncAdapterLock = new Object();
-
- private static final String[] ID_PROJECTION = new String[] {"_id"};
- private static final String ACCOUNT_AND_TYPE_CONTACTS =
- MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_CONTACTS;
-
- public ContactsSyncAdapterService() {
- super();
- }
-
- private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
- private Context mContext;
-
- public SyncAdapterImpl(Context context) {
- super(context, true /* autoInitialize */);
- mContext = context;
- }
-
- @Override
- public void onPerformSync(Account account, Bundle extras,
- String authority, ContentProviderClient provider, SyncResult syncResult) {
- try {
- ContactsSyncAdapterService.performSync(mContext, account, extras,
- authority, provider, syncResult);
- } catch (OperationCanceledException e) {
- }
- }
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- synchronized (sSyncAdapterLock) {
- if (sSyncAdapter == null) {
- sSyncAdapter = new SyncAdapterImpl(getApplicationContext());
- }
- }
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return sSyncAdapter.getSyncAdapterBinder();
- }
-
- private static boolean hasDirtyRows(ContentResolver resolver, Uri uri, String dirtyColumn) {
- Cursor c = resolver.query(uri, ID_PROJECTION, dirtyColumn + "=1", null, null);
- try {
- return c.getCount() > 0;
- } finally {
- c.close();
- }
- }
-
- /**
- * Partial integration with system SyncManager; we tell our EAS ExchangeService to start a
- * contacts sync when we get the signal from SyncManager.
- * The missing piece at this point is integration with the push/ping mechanism in EAS; this will
- * be put in place at a later time.
- */
- private static void performSync(Context context, Account account, Bundle extras,
- String authority, ContentProviderClient provider, SyncResult syncResult)
- throws OperationCanceledException {
- ContentResolver cr = context.getContentResolver();
-
- // If we've been asked to do an upload, make sure we've got work to do
- if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
- Uri uri = RawContacts.CONTENT_URI.buildUpon()
- .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
- .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)
- .build();
- // See if we've got dirty contacts or dirty groups containing our contacts
- boolean changed = hasDirtyRows(cr, uri, RawContacts.DIRTY);
- if (!changed) {
- uri = Groups.CONTENT_URI.buildUpon()
- .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
- .appendQueryParameter(RawContacts.ACCOUNT_TYPE,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)
- .build();
- changed = hasDirtyRows(cr, uri, Groups.DIRTY);
- }
- if (!changed) {
- Log.i(TAG, "Upload sync; no changes");
- return;
- }
- }
-
- // Find the (EmailProvider) account associated with this email address
- Cursor accountCursor =
- cr.query(com.android.emailcommon.provider.Account.CONTENT_URI, ID_PROJECTION,
- AccountColumns.EMAIL_ADDRESS + "=?", new String[] {account.name}, null);
- try {
- if (accountCursor.moveToFirst()) {
- long accountId = accountCursor.getLong(0);
- // Now, find the contacts mailbox associated with the account
- Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, ID_PROJECTION,
- ACCOUNT_AND_TYPE_CONTACTS, new String[] {Long.toString(accountId)}, null);
- try {
- if (mailboxCursor.moveToFirst()) {
- Log.i(TAG, "Contact sync requested for " + account.name);
- // Ask for a sync from our sync manager
- ExchangeService.serviceRequest(mailboxCursor.getLong(0),
- ExchangeService.SYNC_UPSYNC);
- }
- } finally {
- mailboxCursor.close();
- }
- }
- } finally {
- accountCursor.close();
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/exchange/Eas.java b/src/com/android/exchange/Eas.java
deleted file mode 100644
index 9789f43..0000000
--- a/src/com/android/exchange/Eas.java
+++ /dev/null
@@ -1,123 +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 android.util.Log;
-
-import com.android.emailcommon.service.EmailServiceProxy;
-import com.android.emailcommon.service.SyncWindow;
-
-/**
- * Constants used throughout the EAS implementation are stored here.
- *
- */
-public class Eas {
- // For debugging
- public static boolean WAIT_DEBUG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE
- public static boolean DEBUG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE
-
- // The following two are for user logging (the second providing more detail)
- public static boolean USER_LOG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE
- public static boolean PARSER_LOG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE
- public static boolean FILE_LOG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE
-
- public static final String CLIENT_VERSION = "EAS-1.3";
- public static final String ACCOUNT_MAILBOX_PREFIX = "__eas";
-
- // Define our default protocol version as 2.5 (Exchange 2003)
- public static final String SUPPORTED_PROTOCOL_EX2003 = "2.5";
- public static final double SUPPORTED_PROTOCOL_EX2003_DOUBLE = 2.5;
- public static final String SUPPORTED_PROTOCOL_EX2007 = "12.0";
- public static final double SUPPORTED_PROTOCOL_EX2007_DOUBLE = 12.0;
- public static final String SUPPORTED_PROTOCOL_EX2007_SP1 = "12.1";
- public static final double SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE = 12.1;
- public static final String SUPPORTED_PROTOCOL_EX2010 = "14.0";
- public static final double SUPPORTED_PROTOCOL_EX2010_DOUBLE = 14.0;
- public static final String SUPPORTED_PROTOCOL_EX2010_SP1 = "14.1";
- public static final double SUPPORTED_PROTOCOL_EX2010_SP1_DOUBLE = 14.1;
- public static final String DEFAULT_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_EX2003;
-
- public static final String EXCHANGE_ACCOUNT_MANAGER_TYPE = "com.android.exchange";
-
- // From EAS spec
- // Mail Cal
- // 0 No filter Yes Yes
- // 1 1 day ago Yes No
- // 2 3 days ago Yes No
- // 3 1 week ago Yes No
- // 4 2 weeks ago Yes Yes
- // 5 1 month ago Yes Yes
- // 6 3 months ago No Yes
- // 7 6 months ago No Yes
-
- public static final String FILTER_AUTO = Integer.toString(SyncWindow.SYNC_WINDOW_AUTO);
- // TODO Rationalize this with SYNC_WINDOW_ALL
- public static final String FILTER_ALL = "0";
- public static final String FILTER_1_DAY = Integer.toString(SyncWindow.SYNC_WINDOW_1_DAY);
- public static final String FILTER_3_DAYS = Integer.toString(SyncWindow.SYNC_WINDOW_3_DAYS);
- public static final String FILTER_1_WEEK = Integer.toString(SyncWindow.SYNC_WINDOW_1_WEEK);
- public static final String FILTER_2_WEEKS = Integer.toString(SyncWindow.SYNC_WINDOW_2_WEEKS);
- public static final String FILTER_1_MONTH = Integer.toString(SyncWindow.SYNC_WINDOW_1_MONTH);
- public static final String FILTER_3_MONTHS = "6";
- public static final String FILTER_6_MONTHS = "7";
-
- public static final String BODY_PREFERENCE_TEXT = "1";
- public static final String BODY_PREFERENCE_HTML = "2";
-
- public static final String MIME_BODY_PREFERENCE_TEXT = "0";
- public static final String MIME_BODY_PREFERENCE_MIME = "2";
-
- // For EAS 12, we use HTML, so we want a larger size than in EAS 2.5
- public static final String EAS12_TRUNCATION_SIZE = "200000";
- // For EAS 2.5, truncation is a code; the largest is "7", which is 100k
- public static final String EAS2_5_TRUNCATION_SIZE = "7";
-
- public static final int FOLDER_STATUS_OK = 1;
- public static final int FOLDER_STATUS_INVALID_KEY = 9;
-
- public static final int EXCHANGE_ERROR_NOTIFICATION = 0x10;
-
- public static void setUserDebug(int state) {
- // DEBUG takes precedence and is never true in a user build
- if (!DEBUG) {
- USER_LOG = (state & EmailServiceProxy.DEBUG_BIT) != 0;
- PARSER_LOG = (state & EmailServiceProxy.DEBUG_VERBOSE_BIT) != 0;
- FILE_LOG = (state & EmailServiceProxy.DEBUG_FILE_BIT) != 0;
- if (FILE_LOG || PARSER_LOG) {
- USER_LOG = true;
- }
- Log.d("Eas Debug", "Logging: " + (USER_LOG ? "User " : "") +
- (PARSER_LOG ? "Parser " : "") + (FILE_LOG ? "File" : ""));
- }
- }
-
- static public Double getProtocolVersionDouble(String version) {
- if (SUPPORTED_PROTOCOL_EX2003.equals(version)) {
- return SUPPORTED_PROTOCOL_EX2003_DOUBLE;
- } else if (SUPPORTED_PROTOCOL_EX2007.equals(version)) {
- return SUPPORTED_PROTOCOL_EX2007_DOUBLE;
- } if (SUPPORTED_PROTOCOL_EX2007_SP1.equals(version)) {
- return SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE;
- } if (SUPPORTED_PROTOCOL_EX2010.equals(version)) {
- return SUPPORTED_PROTOCOL_EX2010_DOUBLE;
- } if (SUPPORTED_PROTOCOL_EX2010_SP1.equals(version)) {
- return SUPPORTED_PROTOCOL_EX2010_SP1_DOUBLE;
- }
- throw new IllegalArgumentException("illegal protocol version");
- }
-}
diff --git a/src/com/android/exchange/EasAuthenticationException.java b/src/com/android/exchange/EasAuthenticationException.java
deleted file mode 100644
index f5b14b9..0000000
--- a/src/com/android/exchange/EasAuthenticationException.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.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/EasCertificateRequestor.java b/src/com/android/exchange/EasCertificateRequestor.java
deleted file mode 100644
index ebafe44..0000000
--- a/src/com/android/exchange/EasCertificateRequestor.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package com.android.exchange;
-
-import android.security.KeyChain;
-
-import com.android.emailcommon.utility.CertificateRequestor;
-
-/**
- * A subclass of the {@link CertificateRequestor} so that the Exchange process
- * can request access to a certificate.
- *
- * They {@link KeyChain} API works in such a way that the host
- * activity requesting the certificate must be running in the process with the UID of who will
- * actually use the certificate. Since the Exchange process needs to establish connections and use
- * certificates for EAS accounts, requests for certificates must be delegated by an Activity in this
- * process.
- */
-public class EasCertificateRequestor extends CertificateRequestor {
- // Intentionally blank - no behavior overridden.
-}
diff --git a/src/com/android/exchange/EasException.java b/src/com/android/exchange/EasException.java
deleted file mode 100644
index e7613d7..0000000
--- a/src/com/android/exchange/EasException.java
+++ /dev/null
@@ -1,22 +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;
-
-public class EasException extends Exception {
- private static final long serialVersionUID = 5894556952470989968L;
-}
diff --git a/src/com/android/exchange/EasOutboxService.java b/src/com/android/exchange/EasOutboxService.java
deleted file mode 100644
index 4466925..0000000
--- a/src/com/android/exchange/EasOutboxService.java
+++ /dev/null
@@ -1,543 +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 android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.net.TrafficStats;
-import android.net.Uri;
-import android.os.RemoteException;
-import android.text.TextUtils;
-
-import com.android.emailcommon.TrafficFlags;
-import com.android.emailcommon.internet.Rfc822Output;
-import com.android.emailcommon.mail.MessagingException;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent.Body;
-import com.android.emailcommon.provider.EmailContent.BodyColumns;
-import com.android.emailcommon.provider.EmailContent.MailboxColumns;
-import com.android.emailcommon.provider.EmailContent.Message;
-import com.android.emailcommon.provider.EmailContent.MessageColumns;
-import com.android.emailcommon.provider.EmailContent.SyncColumns;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.emailcommon.service.EmailServiceStatus;
-import com.android.emailcommon.utility.Utility;
-import com.android.exchange.CommandStatusException.CommandStatus;
-import com.android.exchange.adapter.Parser;
-import com.android.exchange.adapter.Parser.EmptyStreamException;
-import com.android.exchange.adapter.Serializer;
-import com.android.exchange.adapter.Tags;
-
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpStatus;
-import org.apache.http.entity.InputStreamEntity;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-public class EasOutboxService extends EasSyncService {
-
- public static final int SEND_FAILED = 1;
- public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED =
- MessageColumns.MAILBOX_KEY + "=? and (" + SyncColumns.SERVER_ID + " is null or " +
- SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';
- public static final String[] BODY_SOURCE_PROJECTION =
- new String[] {BodyColumns.SOURCE_MESSAGE_KEY};
- public static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
-
- // This is a normal email (i.e. not one of the other types)
- public static final int MODE_NORMAL = 0;
- // This is a smart reply email
- public static final int MODE_SMART_REPLY = 1;
- // This is a smart forward email
- public static final int MODE_SMART_FORWARD = 2;
-
- // This needs to be long enough to send the longest reasonable message, without being so long
- // as to effectively "hang" sending of mail. The standard 30 second timeout isn't long enough
- // for pictures and the like. For now, we'll use 15 minutes, in the knowledge that any socket
- // failure would probably generate an Exception before timing out anyway
- public static final int SEND_MAIL_TIMEOUT = 15*MINUTES;
-
- public EasOutboxService(Context _context, Mailbox _mailbox) {
- super(_context, _mailbox);
- }
-
- /**
- * Our own HttpEntity subclass that is able to insert opaque data (in this case the MIME
- * representation of the message body as stored in a temporary file) into the serializer stream
- */
- private static class SendMailEntity extends InputStreamEntity {
- private final Context mContext;
- private final FileInputStream mFileStream;
- private final long mFileLength;
- private final int mSendTag;
- private final Message mMessage;
-
- private static final int[] MODE_TAGS = new int[] {Tags.COMPOSE_SEND_MAIL,
- Tags.COMPOSE_SMART_REPLY, Tags.COMPOSE_SMART_FORWARD};
-
- public SendMailEntity(Context context, FileInputStream instream, long length, int tag,
- Message message) {
- super(instream, length);
- mContext = context;
- mFileStream = instream;
- mFileLength = length;
- mSendTag = tag;
- mMessage = message;
- }
-
- /**
- * We always return -1 because we don't know the actual length of the POST data (this
- * causes HttpClient to send the data in "chunked" mode)
- */
- @Override
- public long getContentLength() {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- try {
- // Calculate the overhead for the WBXML data
- writeTo(baos, false);
- // Return the actual size that will be sent
- return baos.size() + mFileLength;
- } catch (IOException e) {
- // Just return -1 (unknown)
- } finally {
- try {
- baos.close();
- } catch (IOException e) {
- // Ignore
- }
- }
- return -1;
- }
-
- @Override
- public void writeTo(OutputStream outstream) throws IOException {
- writeTo(outstream, true);
- }
-
- /**
- * Write the message to the output stream
- * @param outstream the output stream to write
- * @param withData whether or not the actual data is to be written; true when sending
- * mail; false when calculating size only
- * @throws IOException
- */
- public void writeTo(OutputStream outstream, boolean withData) throws IOException {
- // Not sure if this is possible; the check is taken from the superclass
- if (outstream == null) {
- throw new IllegalArgumentException("Output stream may not be null");
- }
-
- // We'll serialize directly into the output stream
- Serializer s = new Serializer(outstream);
- // Send the appropriate initial tag
- s.start(mSendTag);
- // The Message-Id for this message (note that we cannot use the messageId stored in
- // the message, as EAS 14 limits the length to 40 chars and we use 70+)
- s.data(Tags.COMPOSE_CLIENT_ID, "SendMail-" + System.nanoTime());
- // We always save sent mail
- s.tag(Tags.COMPOSE_SAVE_IN_SENT_ITEMS);
-
- // If we're using smart reply/forward, we need info about the original message
- if (mSendTag != Tags.COMPOSE_SEND_MAIL) {
- OriginalMessageInfo info = getOriginalMessageInfo(mContext, mMessage.mId);
- if (info != null) {
- s.start(Tags.COMPOSE_SOURCE);
- // For search results, use the long id (stored in mProtocolSearchInfo); else,
- // use folder id/item id combo
- if (mMessage.mProtocolSearchInfo != null) {
- s.data(Tags.COMPOSE_LONG_ID, mMessage.mProtocolSearchInfo);
- } else {
- s.data(Tags.COMPOSE_ITEM_ID, info.mItemId);
- s.data(Tags.COMPOSE_FOLDER_ID, info.mCollectionId);
- }
- s.end(); // Tags.COMPOSE_SOURCE
- }
- }
-
- // Start the MIME tag; this is followed by "opaque" data (byte array)
- s.start(Tags.COMPOSE_MIME);
- // Send opaque data from the file stream
- if (withData) {
- s.opaque(mFileStream, (int)mFileLength);
- } else {
- s.opaqueWithoutData((int)mFileLength);
- }
- // And we're done
- s.end().end().done();
- }
- }
-
- private static class SendMailParser extends Parser {
- private final int mStartTag;
- private int mStatus;
-
- public SendMailParser(InputStream in, int startTag) throws IOException {
- super(in);
- mStartTag = startTag;
- }
-
- public int getStatus() {
- return mStatus;
- }
-
- /**
- * The only useful info in the SendMail response is the status; we capture and save it
- */
- @Override
- public boolean parse() throws IOException {
- if (nextTag(START_DOCUMENT) != mStartTag) {
- throw new IOException();
- }
- while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
- if (tag == Tags.COMPOSE_STATUS) {
- mStatus = getValueInt();
- } else {
- skipTag();
- }
- }
- return true;
- }
- }
-
- /**
- * For OriginalMessageInfo, we use the terminology of EAS for the serverId and mailboxId of the
- * original message
- */
- protected static class OriginalMessageInfo {
- final String mItemId;
- final String mCollectionId;
- final String mLongId;
-
- OriginalMessageInfo(String itemId, String collectionId, String longId) {
- mItemId = itemId;
- mCollectionId = collectionId;
- mLongId = longId;
- }
- }
-
- private void sendCallback(long msgId, String subject, int status) {
- try {
- ExchangeService.callback().sendMessageStatus(msgId, subject, status, 0);
- } catch (RemoteException e) {
- // It's all good
- }
- }
-
- /*package*/ String generateSmartSendCmd(boolean reply, OriginalMessageInfo info) {
- StringBuilder sb = new StringBuilder();
- sb.append(reply ? "SmartReply" : "SmartForward");
- if (!TextUtils.isEmpty(info.mLongId)) {
- sb.append("&LongId=");
- sb.append(Uri.encode(info.mLongId, ":"));
- } else {
- sb.append("&ItemId=");
- sb.append(Uri.encode(info.mItemId, ":"));
- sb.append("&CollectionId=");
- sb.append(Uri.encode(info.mCollectionId, ":"));
- }
- return sb.toString();
- }
-
- /**
- * Get information about the original message that is referenced by the message to be sent; this
- * information will exist for replies and forwards
- *
- * @param context the caller's context
- * @param msgId the id of the message we're sending
- * @return a data structure with the serverId and mailboxId of the original message, or null if
- * either or both of those pieces of information can't be found
- */
- private static OriginalMessageInfo getOriginalMessageInfo(Context context, long msgId) {
- // Note: itemId and collectionId are the terms used by EAS to refer to the serverId and
- // mailboxId of a Message
- String itemId = null;
- String collectionId = null;
- String longId = null;
-
- // First, we need to get the id of the reply/forward message
- String[] cols = Utility.getRowColumns(context, Body.CONTENT_URI,
- BODY_SOURCE_PROJECTION, WHERE_MESSAGE_KEY,
- new String[] {Long.toString(msgId)});
- if (cols != null) {
- long refId = Long.parseLong(cols[0]);
- // Then, we need the serverId and mailboxKey of the message
- cols = Utility.getRowColumns(context, Message.CONTENT_URI, refId,
- SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY,
- MessageColumns.PROTOCOL_SEARCH_INFO);
- if (cols != null) {
- itemId = cols[0];
- long boxId = Long.parseLong(cols[1]);
- // Then, we need the serverId of the mailbox
- cols = Utility.getRowColumns(context, Mailbox.CONTENT_URI, boxId,
- MailboxColumns.SERVER_ID);
- if (cols != null) {
- collectionId = cols[0];
- }
- }
- }
- // We need either a longId or both itemId (serverId) and collectionId (mailboxId) to process
- // a smart reply or a smart forward
- if (longId != null || (itemId != null && collectionId != null)){
- return new OriginalMessageInfo(itemId, collectionId, longId);
- }
- return null;
- }
-
- private void sendFailed(long msgId, int result) {
- ContentValues cv = new ContentValues();
- cv.put(SyncColumns.SERVER_ID, SEND_FAILED);
- Message.update(mContext, Message.CONTENT_URI, msgId, cv);
- sendCallback(msgId, null, result);
- }
-
- /**
- * Send a single message via EAS
- * Note that we mark messages SEND_FAILED when there is a permanent failure, rather than an
- * IOException, which is handled by ExchangeService with retries, backoffs, etc.
- *
- * @param cacheDir the cache directory for this context
- * @param msgId the _id of the message to send
- * @throws IOException
- */
- int sendMessage(File cacheDir, long msgId) throws IOException, MessagingException {
- // We always return SUCCESS unless the sending error is account-specific (security or
- // authentication) rather than message-specific; returning anything else will terminate
- // the Outbox sync! Message-specific errors are marked in the messages themselves.
- int result = EmailServiceStatus.SUCCESS;
- // Say we're starting to send this message
- sendCallback(msgId, null, EmailServiceStatus.IN_PROGRESS);
- // Create a temporary file (this will hold the outgoing message in RFC822 (MIME) format)
- File tmpFile = File.createTempFile("eas_", "tmp", cacheDir);
- try {
- // Get the message and fail quickly if not found
- Message msg = Message.restoreMessageWithId(mContext, msgId);
- if (msg == null) return EmailServiceStatus.MESSAGE_NOT_FOUND;
-
- // See what kind of outgoing messge this is
- int flags = msg.mFlags;
- boolean reply = (flags & Message.FLAG_TYPE_REPLY) != 0;
- boolean forward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
- boolean includeQuotedText = (flags & Message.FLAG_NOT_INCLUDE_QUOTED_TEXT) == 0;
-
- // The reference message and mailbox are called item and collection in EAS
- OriginalMessageInfo referenceInfo = null;
- // Respect the sense of the include quoted text flag
- if (includeQuotedText && (reply || forward)) {
- referenceInfo = getOriginalMessageInfo(mContext, msgId);
- }
- // Generally, we use SmartReply/SmartForward if we've got a good reference
- boolean smartSend = referenceInfo != null;
- // But we won't use SmartForward if the account isn't set up for it (currently, we only
- // use SmartForward for EAS 12.0 or later to avoid creating eml files that are
- // potentially difficult for the recipient to handle)
- if (forward && ((mAccount.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) {
- smartSend = false;
- }
-
- // Write the message to the temporary file
- FileOutputStream fileOutputStream = new FileOutputStream(tmpFile);
- Rfc822Output.writeTo(mContext, msgId, fileOutputStream, smartSend, true);
- fileOutputStream.close();
-
- // Sending via EAS14 is a whole 'nother kettle of fish
- boolean isEas14 = (Double.parseDouble(mAccount.mProtocolVersion) >=
- Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE);
-
- while (true) {
- // Get an input stream to our temporary file and create an entity with it
- FileInputStream fileStream = new FileInputStream(tmpFile);
- long fileLength = tmpFile.length();
-
- // The type of entity depends on whether we're using EAS 14
- HttpEntity inputEntity;
- // For EAS 14, we need to save the wbxml tag we're using
- int modeTag = 0;
- if (isEas14) {
- int mode =
- !smartSend ? MODE_NORMAL : reply ? MODE_SMART_REPLY : MODE_SMART_FORWARD;
- modeTag = SendMailEntity.MODE_TAGS[mode];
- inputEntity =
- new SendMailEntity(mContext, fileStream, fileLength, modeTag, msg);
- } else {
- inputEntity = new InputStreamEntity(fileStream, fileLength);
- }
- // Create the appropriate command and POST it to the server
- String cmd = "SendMail";
- if (smartSend) {
- // In EAS 14, we don't send itemId and collectionId in the command
- if (isEas14) {
- cmd = reply ? "SmartReply" : "SmartForward";
- } else {
- cmd = generateSmartSendCmd(reply, referenceInfo);
- }
- }
-
- // If we're not EAS 14, add our save-in-sent setting here
- if (!isEas14) {
- cmd += "&SaveInSent=T";
- }
- userLog("Send cmd: " + cmd);
-
- // Finally, post SendMail to the server
- EasResponse resp = sendHttpClientPost(cmd, inputEntity, SEND_MAIL_TIMEOUT);
- try {
- fileStream.close();
- int code = resp.getStatus();
- if (code == HttpStatus.SC_OK) {
- // HTTP OK before EAS 14 is a thumbs up; in EAS 14, we've got to parse
- // the reply
- if (isEas14) {
- try {
- // Try to parse the result
- SendMailParser p =
- new SendMailParser(resp.getInputStream(), modeTag);
- // If we get here, the SendMail failed; go figure
- p.parse();
- // The parser holds the status
- int status = p.getStatus();
- userLog("SendMail error, status: " + status);
- if (CommandStatus.isNeedsProvisioning(status)) {
- result = EmailServiceStatus.SECURITY_FAILURE;
- } else if (status == CommandStatus.ITEM_NOT_FOUND && smartSend) {
- // This is the retry case for EAS 14; we'll send without "smart"
- // commands next time
- resp.close();
- smartSend = false;
- continue;
- }
- sendFailed(msgId, result);
- return result;
- } catch (EmptyStreamException e) {
- // This is actually fine; an empty stream means SendMail succeeded
- }
- }
-
- // If we're here, the SendMail command succeeded
- userLog("Deleting message...");
- // Delete the message from the Outbox and send callback
- mContentResolver.delete(
- ContentUris.withAppendedId(Message.CONTENT_URI, msgId), null, null);
- sendCallback(-1, msg.mSubject, EmailServiceStatus.SUCCESS);
- break;
- } else if (code == EasSyncService.INTERNAL_SERVER_ERROR_CODE && smartSend) {
- // This is the retry case for EAS 12.1 and below; we'll send without "smart"
- // commands next time
- resp.close();
- smartSend = false;
- } else {
- userLog("Message sending failed, code: " + code);
- if (EasResponse.isAuthError(code)) {
- result = EmailServiceStatus.LOGIN_FAILED;
- } else if (EasResponse.isProvisionError(code)) {
- result = EmailServiceStatus.SECURITY_FAILURE;
- }
- sendFailed(msgId, result);
- break;
- }
- } finally {
- resp.close();
- }
- }
- } catch (IOException e) {
- // We catch this just to send the callback
- sendCallback(msgId, null, EmailServiceStatus.CONNECTION_ERROR);
- throw e;
- } finally {
- // Clean up the temporary file
- if (tmpFile.exists()) {
- tmpFile.delete();
- }
- }
- return result;
- }
-
- @Override
- public void run() {
- setupService();
- // Use SMTP flags for sending mail
- TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(mContext, mAccount));
- File cacheDir = mContext.getCacheDir();
- try {
- mDeviceId = ExchangeService.getDeviceId(mContext);
- // Get a cursor to Outbox messages
- Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI,
- Message.ID_COLUMN_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED,
- new String[] {Long.toString(mMailbox.mId)}, null);
- try {
- // Loop through the messages, sending each one
- while (c.moveToNext()) {
- long msgId = c.getLong(Message.ID_COLUMNS_ID_COLUMN);
- if (msgId != 0) {
- if (Utility.hasUnloadedAttachments(mContext, msgId)) {
- // We'll just have to wait on this...
- continue;
- }
- int result = sendMessage(cacheDir, msgId);
- // If there's an error, it should stop the service; we will distinguish
- // at least between login failures and everything else
- if (result == EmailServiceStatus.LOGIN_FAILED) {
- mExitStatus = EXIT_LOGIN_FAILURE;
- return;
- } else if (result == EmailServiceStatus.SECURITY_FAILURE) {
- mExitStatus = EXIT_SECURITY_FAILURE;
- return;
- } else if (result == EmailServiceStatus.REMOTE_EXCEPTION) {
- mExitStatus = EXIT_EXCEPTION;
- return;
- }
- }
- }
- } finally {
- c.close();
- }
- mExitStatus = EXIT_DONE;
- } catch (IOException e) {
- mExitStatus = EXIT_IO_ERROR;
- } catch (Exception e) {
- userLog("Exception caught in EasOutboxService", e);
- mExitStatus = EXIT_EXCEPTION;
- } finally {
- userLog(mMailbox.mDisplayName, ": sync finished");
- userLog("Outbox exited with status ", mExitStatus);
- ExchangeService.done(this);
- }
- }
-
- /**
- * Convenience method for adding a Message to an account's outbox
- * @param context the context of the caller
- * @param accountId the accountId for the sending account
- * @param msg the message to send
- */
- public static void sendMessage(Context context, long accountId, Message msg) {
- Mailbox mailbox = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_OUTBOX);
- if (mailbox != null) {
- msg.mMailboxKey = mailbox.mId;
- msg.mAccountKey = accountId;
- msg.save(context);
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/exchange/EasResponse.java b/src/com/android/exchange/EasResponse.java
deleted file mode 100644
index 174989e..0000000
--- a/src/com/android/exchange/EasResponse.java
+++ /dev/null
@@ -1,167 +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.emailcommon.utility.EmailClientConnectionManager;
-
-import org.apache.http.Header;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.methods.HttpUriRequest;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.zip.GZIPInputStream;
-
-/**
- * Encapsulate a response to an HTTP POST
- */
-public class EasResponse {
- // MSFT's custom HTTP result code indicating the need to provision
- static private final int HTTP_NEED_PROVISIONING = 449;
-
- final HttpResponse mResponse;
- private final HttpEntity mEntity;
- private final int mLength;
- private InputStream mInputStream;
- private boolean mClosed;
-
- /**
- * Whether or not a certificate was requested by the server and missing.
- * If this is set, it is essentially a 403 whereby the failure was due
- */
- private boolean mClientCertRequested = false;
-
- private EasResponse(HttpResponse response) {
- mResponse = response;
- mEntity = (response == null) ? null : mResponse.getEntity();
- if (mEntity != null) {
- mLength = (int) mEntity.getContentLength();
- } else {
- mLength = 0;
- }
- }
-
- public static EasResponse fromHttpRequest(
- EmailClientConnectionManager connManager, HttpClient client, HttpUriRequest request)
- throws IOException {
-
- long reqTime = System.currentTimeMillis();
- HttpResponse response = client.execute(request);
- EasResponse result = new EasResponse(response);
- if (isAuthError(response.getStatusLine().getStatusCode())
- && connManager.hasDetectedUnsatisfiedCertReq(reqTime)) {
- result.mClientCertRequested = true;
- result.mClosed = true;
- }
-
- return result;
- }
-
- /**
- * Determine whether an HTTP code represents an authentication error
- * @param code the HTTP code returned by the server
- * @return whether or not the code represents an authentication error
- */
- public static boolean isAuthError(int code) {
- return (code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN);
- }
-
- /**
- * Determine whether an HTTP code represents a provisioning error
- * @param code the HTTP code returned by the server
- * @return whether or not the code represents an provisioning error
- */
- public static boolean isProvisionError(int code) {
- return (code == HTTP_NEED_PROVISIONING) || (code == HttpStatus.SC_FORBIDDEN);
- }
-
- /**
- * Return an appropriate input stream for the response, either a GZIPInputStream, for
- * compressed data, or a generic InputStream otherwise
- * @return the input stream for the response
- */
- public InputStream getInputStream() {
- if (mInputStream != null || mClosed) {
- throw new IllegalStateException("Can't reuse stream or get closed stream");
- } else if (mEntity == null) {
- throw new IllegalStateException("Can't get input stream without entity");
- }
- InputStream is = null;
- try {
- // Get the default input stream for the entity
- is = mEntity.getContent();
- Header ceHeader = mResponse.getFirstHeader("Content-Encoding");
- if (ceHeader != null) {
- String encoding = ceHeader.getValue();
- // If we're gzip encoded, wrap appropriately
- if (encoding.toLowerCase().equals("gzip")) {
- is = new GZIPInputStream(is);
- }
- }
- } catch (IllegalStateException e1) {
- } catch (IOException e1) {
- }
- mInputStream = is;
- return is;
- }
-
- public boolean isEmpty() {
- return mLength == 0;
- }
-
- public int getStatus() {
- return mClientCertRequested
- ? HttpStatus.SC_UNAUTHORIZED
- : mResponse.getStatusLine().getStatusCode();
- }
-
- public boolean isMissingCertificate() {
- return mClientCertRequested;
- }
-
- public Header getHeader(String name) {
- return (mResponse == null) ? null : mResponse.getFirstHeader(name);
- }
-
- public int getLength() {
- return mLength;
- }
-
- public void close() {
- if (!mClosed) {
- if (mEntity != null) {
- try {
- mEntity.consumeContent();
- } catch (IOException e) {
- // No harm, no foul
- }
- }
- if (mInputStream instanceof GZIPInputStream) {
- try {
- mInputStream.close();
- } catch (IOException e) {
- // We tried
- }
- }
- mClosed = true;
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
deleted file mode 100644
index 148a348..0000000
--- a/src/com/android/exchange/EasSyncService.java
+++ /dev/null
@@ -1,2690 +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 android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Entity;
-import android.database.Cursor;
-import android.net.TrafficStats;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.RemoteException;
-import android.os.SystemClock;
-import android.provider.CalendarContract.Attendees;
-import android.provider.CalendarContract.Events;
-import android.text.TextUtils;
-import android.util.Base64;
-import android.util.Log;
-import android.util.Xml;
-
-import com.android.emailcommon.TrafficFlags;
-import com.android.emailcommon.mail.Address;
-import com.android.emailcommon.mail.MeetingInfo;
-import com.android.emailcommon.mail.MessagingException;
-import com.android.emailcommon.mail.PackedString;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent.AccountColumns;
-import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
-import com.android.emailcommon.provider.EmailContent.MailboxColumns;
-import com.android.emailcommon.provider.EmailContent.Message;
-import com.android.emailcommon.provider.EmailContent.MessageColumns;
-import com.android.emailcommon.provider.EmailContent.SyncColumns;
-import com.android.emailcommon.provider.HostAuth;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.emailcommon.provider.Policy;
-import com.android.emailcommon.provider.ProviderUnavailableException;
-import com.android.emailcommon.service.EmailServiceConstants;
-import com.android.emailcommon.service.EmailServiceProxy;
-import com.android.emailcommon.service.EmailServiceStatus;
-import com.android.emailcommon.utility.EmailClientConnectionManager;
-import com.android.emailcommon.utility.Utility;
-import com.android.exchange.CommandStatusException.CommandStatus;
-import com.android.exchange.adapter.AbstractSyncAdapter;
-import com.android.exchange.adapter.AccountSyncAdapter;
-import com.android.exchange.adapter.AttachmentLoader;
-import com.android.exchange.adapter.CalendarSyncAdapter;
-import com.android.exchange.adapter.ContactsSyncAdapter;
-import com.android.exchange.adapter.EmailSyncAdapter;
-import com.android.exchange.adapter.FolderSyncParser;
-import com.android.exchange.adapter.GalParser;
-import com.android.exchange.adapter.MeetingResponseParser;
-import com.android.exchange.adapter.MoveItemsParser;
-import com.android.exchange.adapter.Parser.EasParserException;
-import com.android.exchange.adapter.Parser.EmptyStreamException;
-import com.android.exchange.adapter.PingParser;
-import com.android.exchange.adapter.ProvisionParser;
-import com.android.exchange.adapter.Serializer;
-import com.android.exchange.adapter.SettingsParser;
-import com.android.exchange.adapter.Tags;
-import com.android.exchange.provider.GalResult;
-import com.android.exchange.provider.MailboxUtilities;
-import com.android.exchange.utility.CalendarUtilities;
-import com.google.common.annotations.VisibleForTesting;
-
-import org.apache.http.Header;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.methods.HttpOptions;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.methods.HttpRequestBase;
-import org.apache.http.entity.ByteArrayEntity;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.params.BasicHttpParams;
-import org.apache.http.params.HttpConnectionParams;
-import org.apache.http.params.HttpParams;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlPullParserFactory;
-import org.xmlpull.v1.XmlSerializer;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.Thread.State;
-import java.net.URI;
-import java.security.cert.CertificateException;
-import java.util.ArrayList;
-import java.util.HashMap;
-
-public class EasSyncService extends AbstractSyncService {
- // DO NOT CHECK IN SET TO TRUE
- public static final boolean DEBUG_GAL_SERVICE = false;
-
- private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID =
- MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?";
- private static final String WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING =
- MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
- '=' + Mailbox.CHECK_INTERVAL_PING;
- private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " +
- MailboxColumns.SYNC_INTERVAL + " IN (" + Mailbox.CHECK_INTERVAL_PING +
- ',' + Mailbox.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.TYPE + "!=\"" +
- Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + '\"';
- private static final String WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX =
- MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
- '=' + Mailbox.CHECK_INTERVAL_PUSH_HOLD;
-
- static private final String PING_COMMAND = "Ping";
- // Command timeout is the the time allowed for reading data from an open connection before an
- // IOException is thrown. After a small added allowance, our watchdog alarm goes off (allowing
- // us to detect a silently dropped connection). The allowance is defined below.
- static public final int COMMAND_TIMEOUT = 30*SECONDS;
- // Connection timeout is the time given to connect to the server before reporting an IOException
- static private final int CONNECTION_TIMEOUT = 20*SECONDS;
- // The extra time allowed beyond the COMMAND_TIMEOUT before which our watchdog alarm triggers
- static private final int WATCHDOG_TIMEOUT_ALLOWANCE = 30*SECONDS;
-
- // The amount of time the account mailbox will sleep if there are no pingable mailboxes
- // This could happen if the sync time is set to "never"; we always want to check in from time
- // to time, however, for folder list/policy changes
- static private final int ACCOUNT_MAILBOX_SLEEP_TIME = 20*MINUTES;
- static private final String ACCOUNT_MAILBOX_SLEEP_TEXT =
- "Account mailbox sleeping for " + (ACCOUNT_MAILBOX_SLEEP_TIME / MINUTES) + "m";
-
- static private final String AUTO_DISCOVER_SCHEMA_PREFIX =
- "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/";
- static private final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml";
- static private final int EAS_REDIRECT_CODE = 451;
-
- static public final int INTERNAL_SERVER_ERROR_CODE = 500;
-
- static public final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML";
- static public final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML";
-
- static public final int MESSAGE_FLAG_MOVED_MESSAGE = 1 << Message.FLAG_SYNC_ADAPTER_SHIFT;
-
- /**
- * We start with an 8 minute timeout, and increase/decrease by 3 minutes at a time. There's
- * no point having a timeout shorter than 5 minutes, I think; at that point, we can just let
- * the ping exception out. The maximum I use is 17 minutes, which is really an empirical
- * choice; too long and we risk silent connection loss and loss of push for that period. Too
- * short and we lose efficiency/battery life.
- *
- * If we ever have to drop the ping timeout, we'll never increase it again. There's no point
- * going into hysteresis; the NAT timeout isn't going to change without a change in connection,
- * which will cause the sync service to be restarted at the starting heartbeat and going through
- * the process again.
- */
- static private final int PING_MINUTES = 60; // in seconds
- static private final int PING_FUDGE_LOW = 10;
- static private final int PING_STARTING_HEARTBEAT = (8*PING_MINUTES)-PING_FUDGE_LOW;
- static private final int PING_HEARTBEAT_INCREMENT = 3*PING_MINUTES;
-
- // Maximum number of times we'll allow a sync to "loop" with MoreAvailable true before
- // forcing it to stop. This number has been determined empirically.
- static private final int MAX_LOOPING_COUNT = 100;
-
- static private final int PROTOCOL_PING_STATUS_COMPLETED = 1;
-
- // The amount of time we allow for a thread to release its post lock after receiving an alert
- static private final int POST_LOCK_TIMEOUT = 10*SECONDS;
-
- // Fallbacks (in minutes) for ping loop failures
- static private final int MAX_PING_FAILURES = 1;
- static private final int PING_FALLBACK_INBOX = 5;
- static private final int PING_FALLBACK_PIM = 25;
-
- // The EAS protocol Provision status for "we implement all of the policies"
- static private final String PROVISION_STATUS_OK = "1";
- // The EAS protocol Provision status meaning "we partially implement the policies"
- static private final String PROVISION_STATUS_PARTIAL = "2";
-
- static /*package*/ final String DEVICE_TYPE = "Android";
- static private final String USER_AGENT = DEVICE_TYPE + '/' + Build.VERSION.RELEASE + '-' +
- Eas.CLIENT_VERSION;
-
- // Reasonable default
- public String mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
- public Double mProtocolVersionDouble;
- protected String mDeviceId = null;
- @VisibleForTesting
- String mAuthString = null;
- @VisibleForTesting
- String mUserString = null;
- @VisibleForTesting
- String mBaseUriString = null;
- public String mHostAddress;
- public String mUserName;
- public String mPassword;
-
- // The parameters for the connection must be modified through setConnectionParameters
- private boolean mSsl = true;
- private boolean mTrustSsl = false;
- private String mClientCertAlias = null;
-
- public ContentResolver mContentResolver;
- private final String[] mBindArguments = new String[2];
- private ArrayList<String> mPingChangeList;
- // The HttpPost in progress
- private volatile HttpPost mPendingPost = null;
- // Our heartbeat when we are waiting for ping boxes to be ready
- /*package*/ int mPingForceHeartbeat = 2*PING_MINUTES;
- // The minimum heartbeat we will send
- /*package*/ int mPingMinHeartbeat = (5*PING_MINUTES)-PING_FUDGE_LOW;
- // The maximum heartbeat we will send
- /*package*/ int mPingMaxHeartbeat = (17*PING_MINUTES)-PING_FUDGE_LOW;
- // The ping time (in seconds)
- /*package*/ int mPingHeartbeat = PING_STARTING_HEARTBEAT;
- // The longest successful ping heartbeat
- private int mPingHighWaterMark = 0;
- // Whether we've ever lowered the heartbeat
- /*package*/ boolean mPingHeartbeatDropped = false;
- // Whether a POST was aborted due to alarm (watchdog alarm)
- private boolean mPostAborted = false;
- // Whether a POST was aborted due to reset
- private boolean mPostReset = false;
- // Whether or not the sync service is valid (usable)
- public boolean mIsValid = true;
-
- // Whether the most recent upsync failed (status 7)
- public boolean mUpsyncFailed = false;
-
- public EasSyncService(Context _context, Mailbox _mailbox) {
- super(_context, _mailbox);
- mContentResolver = _context.getContentResolver();
- if (mAccount == null) {
- mIsValid = false;
- return;
- }
- HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
- if (ha == null) {
- mIsValid = false;
- return;
- }
- mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
- mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL) != 0;
- }
-
- private EasSyncService(String prefix) {
- super(prefix);
- }
-
- public EasSyncService() {
- this("EAS Validation");
- }
-
- /**
- * Try to wake up a sync thread that is waiting on an HttpClient POST and has waited past its
- * socket timeout without having thrown an Exception
- *
- * @return true if the POST was successfully stopped; false if we've failed and interrupted
- * the thread
- */
- @Override
- public boolean alarm() {
- HttpPost post;
- if (mThread == null) return true;
- String threadName = mThread.getName();
-
- // Synchronize here so that we are guaranteed to have valid mPendingPost and mPostLock
- // executePostWithTimeout (which executes the HttpPost) also uses this lock
- synchronized(getSynchronizer()) {
- // Get a reference to the current post lock
- post = mPendingPost;
- if (post != null) {
- if (Eas.USER_LOG) {
- URI uri = post.getURI();
- if (uri != null) {
- String query = uri.getQuery();
- if (query == null) {
- query = "POST";
- }
- userLog(threadName, ": Alert, aborting ", query);
- } else {
- userLog(threadName, ": Alert, no URI?");
- }
- }
- // Abort the POST
- mPostAborted = true;
- post.abort();
- } else {
- // If there's no POST, we're done
- userLog("Alert, no pending POST");
- return true;
- }
- }
-
- // Wait for the POST to finish
- try {
- Thread.sleep(POST_LOCK_TIMEOUT);
- } catch (InterruptedException e) {
- }
-
- State s = mThread.getState();
- if (Eas.USER_LOG) {
- userLog(threadName + ": State = " + s.name());
- }
-
- synchronized (getSynchronizer()) {
- // If the thread is still hanging around and the same post is pending, let's try to
- // stop the thread with an interrupt.
- if ((s != State.TERMINATED) && (mPendingPost != null) && (mPendingPost == post)) {
- mStop = true;
- mThread.interrupt();
- userLog("Interrupting...");
- // Let the caller know we had to interrupt the thread
- return false;
- }
- }
- // Let the caller know that the alarm was handled normally
- return true;
- }
-
- @Override
- public void reset() {
- synchronized(getSynchronizer()) {
- if (mPendingPost != null) {
- URI uri = mPendingPost.getURI();
- if (uri != null) {
- String query = uri.getQuery();
- if (query.startsWith("Cmd=Ping")) {
- userLog("Reset, aborting Ping");
- mPostReset = true;
- mPendingPost.abort();
- }
- }
- }
- }
- }
-
- @Override
- public void stop() {
- mStop = true;
- synchronized(getSynchronizer()) {
- if (mPendingPost != null) {
- mPendingPost.abort();
- }
- }
- }
-
- @Override
- public void addRequest(Request request) {
- // Don't allow duplicates of requests; just refuse them
- if (mRequestQueue.contains(request)) return;
- // Add the request
- super.addRequest(request);
- }
-
- private void setupProtocolVersion(EasSyncService service, Header versionHeader)
- throws MessagingException {
- // The string is a comma separated list of EAS versions in ascending order
- // e.g. 1.0,2.0,2.5,12.0,12.1,14.0,14.1
- String supportedVersions = versionHeader.getValue();
- userLog("Server supports versions: ", supportedVersions);
- String[] supportedVersionsArray = supportedVersions.split(",");
- String ourVersion = null;
- // Find the most recent version we support
- for (String version: supportedVersionsArray) {
- if (version.equals(Eas.SUPPORTED_PROTOCOL_EX2003) ||
- version.equals(Eas.SUPPORTED_PROTOCOL_EX2007) ||
- version.equals(Eas.SUPPORTED_PROTOCOL_EX2007_SP1) ||
- version.equals(Eas.SUPPORTED_PROTOCOL_EX2010) ||
- version.equals(Eas.SUPPORTED_PROTOCOL_EX2010_SP1)) {
- ourVersion = version;
- }
- }
- // If we don't support any of the servers supported versions, throw an exception here
- // This will cause validation to fail
- if (ourVersion == null) {
- Log.w(TAG, "No supported EAS versions: " + supportedVersions);
- throw new MessagingException(MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
- } else {
- // Debug code for testing EAS 14.0; disables support for EAS 14.1
- // "adb shell setprop log.tag.Exchange14 VERBOSE"
- if (ourVersion.equals(Eas.SUPPORTED_PROTOCOL_EX2010_SP1) &&
- Log.isLoggable("Exchange14", Log.VERBOSE)) {
- ourVersion = Eas.SUPPORTED_PROTOCOL_EX2010;
- }
- service.mProtocolVersion = ourVersion;
- service.mProtocolVersionDouble = Eas.getProtocolVersionDouble(ourVersion);
- Account account = service.mAccount;
- if (account != null) {
- account.mProtocolVersion = ourVersion;
- // Fixup search flags, if they're not set
- if (service.mProtocolVersionDouble >= 12.0 &&
- (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) == 0) {
- if (account.isSaved()) {
- ContentValues cv = new ContentValues();
- account.mFlags |=
- Account.FLAGS_SUPPORTS_GLOBAL_SEARCH + Account.FLAGS_SUPPORTS_SEARCH;
- cv.put(AccountColumns.FLAGS, account.mFlags);
- account.update(service.mContext, cv);
- }
- }
- }
- }
- }
-
- /**
- * Create an EasSyncService for the specified account
- *
- * @param context the caller's context
- * @param account the account
- * @return the service, or null if the account is on hold or hasn't been initialized
- */
- public static EasSyncService setupServiceForAccount(Context context, Account account) {
- // Just return null if we're on security hold
- if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
- return null;
- }
- // If there's no protocol version, we're not initialized
- String protocolVersion = account.mProtocolVersion;
- if (protocolVersion == null) {
- return null;
- }
- EasSyncService svc = new EasSyncService("OutOfBand");
- HostAuth ha = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
- svc.mProtocolVersion = protocolVersion;
- svc.mProtocolVersionDouble = Eas.getProtocolVersionDouble(protocolVersion);
- svc.mContext = context;
- svc.mHostAddress = ha.mAddress;
- svc.mUserName = ha.mLogin;
- svc.mPassword = ha.mPassword;
- try {
- svc.setConnectionParameters(
- (ha.mFlags & HostAuth.FLAG_SSL) != 0,
- (ha.mFlags & HostAuth.FLAG_TRUST_ALL) != 0,
- ha.mClientCertAlias);
- svc.mDeviceId = ExchangeService.getDeviceId(context);
- } catch (IOException e) {
- return null;
- } catch (CertificateException e) {
- return null;
- }
- svc.mAccount = account;
- return svc;
- }
-
- /**
- * Get a redirect address and validate against it
- * @param resp the EasResponse to our POST
- * @param hostAuth the HostAuth we're using to validate
- * @return true if we have an updated HostAuth (with redirect address); false otherwise
- */
- private boolean getValidateRedirect(EasResponse resp, HostAuth hostAuth) {
- Header locHeader = resp.getHeader("X-MS-Location");
- if (locHeader != null) {
- String loc;
- try {
- loc = locHeader.getValue();
- // Reset our host address and uncache our base uri
- mHostAddress = Uri.parse(loc).getHost();
- mBaseUriString = null;
- hostAuth.mAddress = mHostAddress;
- userLog("Redirecting to: " + loc);
- return true;
- } catch (RuntimeException e) {
- // Just don't crash if the Uri is illegal
- }
- }
- return false;
- }
-
- private static final int MAX_REDIRECTS = 3;
- private int mRedirectCount = 0;
-
- @Override
- public Bundle validateAccount(HostAuth hostAuth, Context context) {
- Bundle bundle = new Bundle();
- int resultCode = MessagingException.NO_ERROR;
- try {
- userLog("Testing EAS: ", hostAuth.mAddress, ", ", hostAuth.mLogin,
- ", ssl = ", hostAuth.shouldUseSsl() ? "1" : "0");
- mContext = context;
- mHostAddress = hostAuth.mAddress;
- mUserName = hostAuth.mLogin;
- mPassword = hostAuth.mPassword;
-
- setConnectionParameters(
- hostAuth.shouldUseSsl(),
- hostAuth.shouldTrustAllServerCerts(),
- hostAuth.mClientCertAlias);
- mDeviceId = ExchangeService.getDeviceId(context);
- mAccount = new Account();
- mAccount.mEmailAddress = hostAuth.mLogin;
- EasResponse resp = sendHttpClientOptions();
- try {
- int code = resp.getStatus();
- userLog("Validation (OPTIONS) response: " + code);
- if (code == HttpStatus.SC_OK) {
- // No exception means successful validation
- Header commands = resp.getHeader("MS-ASProtocolCommands");
- Header versions = resp.getHeader("ms-asprotocolversions");
- // Make sure we've got the right protocol version set up
- try {
- if (commands == null || versions == null) {
- userLog("OPTIONS response without commands or versions");
- // We'll treat this as a protocol exception
- throw new MessagingException(0);
- }
- setupProtocolVersion(this, versions);
- } catch (MessagingException e) {
- bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE,
- MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
- return bundle;
- }
-
- // Run second test here for provisioning failures using FolderSync
- userLog("Try folder sync");
- // Send "0" as the sync key for new accounts; otherwise, use the current key
- String syncKey = "0";
- Account existingAccount = Utility.findExistingAccount(
- context, -1L, hostAuth.mAddress, hostAuth.mLogin);
- if (existingAccount != null && existingAccount.mSyncKey != null) {
- syncKey = existingAccount.mSyncKey;
- }
- Serializer s = new Serializer();
- s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text(syncKey)
- .end().end().done();
- resp = sendHttpClientPost("FolderSync", s.toByteArray());
- code = resp.getStatus();
- // Handle HTTP error responses accordingly
- if (code == HttpStatus.SC_FORBIDDEN) {
- // For validation only, we take 403 as ACCESS_DENIED (the account isn't
- // authorized, possibly due to device type)
- resultCode = MessagingException.ACCESS_DENIED;
- } else if (EasResponse.isProvisionError(code)) {
- // The device needs to have security policies enforced
- throw new CommandStatusException(CommandStatus.NEEDS_PROVISIONING);
- } else if (code == HttpStatus.SC_NOT_FOUND) {
- // We get a 404 from OWA addresses (which are NOT EAS addresses)
- resultCode = MessagingException.PROTOCOL_VERSION_UNSUPPORTED;
- } else if (code == HttpStatus.SC_UNAUTHORIZED) {
- resultCode = resp.isMissingCertificate()
- ? MessagingException.CLIENT_CERTIFICATE_REQUIRED
- : MessagingException.AUTHENTICATION_FAILED;
- } else if (code != HttpStatus.SC_OK) {
- if ((code == EAS_REDIRECT_CODE) && (mRedirectCount++ < MAX_REDIRECTS) &&
- getValidateRedirect(resp, hostAuth)) {
- return validateAccount(hostAuth, context);
- }
- // Fail generically with anything other than success
- userLog("Unexpected response for FolderSync: ", code);
- resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
- } else {
- // We need to parse the result to see if we've got a provisioning issue
- // (EAS 14.0 only)
- if (!resp.isEmpty()) {
- InputStream is = resp.getInputStream();
- // Create the parser with statusOnly set to true; we only care about
- // seeing if a CommandStatusException is thrown (indicating a
- // provisioning failure)
- new FolderSyncParser(is, new AccountSyncAdapter(this), true).parse();
- }
- userLog("Validation successful");
- }
- } else if (EasResponse.isAuthError(code)) {
- userLog("Authentication failed");
- resultCode = resp.isMissingCertificate()
- ? MessagingException.CLIENT_CERTIFICATE_REQUIRED
- : MessagingException.AUTHENTICATION_FAILED;
- } else if (code == INTERNAL_SERVER_ERROR_CODE) {
- // For Exchange 2003, this could mean an authentication failure OR server error
- userLog("Internal server error");
- resultCode = MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR;
- } else {
- if ((code == EAS_REDIRECT_CODE) && (mRedirectCount++ < MAX_REDIRECTS) &&
- getValidateRedirect(resp, hostAuth)) {
- return validateAccount(hostAuth, context);
- }
- // TODO Need to catch other kinds of errors (e.g. policy) For now, report code.
- userLog("Validation failed, reporting I/O error: ", code);
- resultCode = MessagingException.IOERROR;
- }
- } catch (CommandStatusException e) {
- int status = e.mStatus;
- if (CommandStatus.isNeedsProvisioning(status)) {
- // Get the policies and see if we are able to support them
- ProvisionParser pp = canProvision();
- if (pp != null && pp.hasSupportablePolicySet()) {
- // Set the proper result code and save the PolicySet in our Bundle
- resultCode = MessagingException.SECURITY_POLICIES_REQUIRED;
- bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET,
- pp.getPolicy());
- if (mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
- mAccount.mSecuritySyncKey = pp.getSecuritySyncKey();
- if (!sendSettings()) {
- userLog("Denied access: ", CommandStatus.toString(status));
- resultCode = MessagingException.ACCESS_DENIED;
- }
- }
- } else
- // If not, set the proper code (the account will not be created)
- resultCode = MessagingException.SECURITY_POLICIES_UNSUPPORTED;
- bundle.putStringArray(
- EmailServiceProxy.VALIDATE_BUNDLE_UNSUPPORTED_POLICIES,
- ((pp == null) ? null : pp.getUnsupportedPolicies()));
- } else if (CommandStatus.isDeniedAccess(status)) {
- userLog("Denied access: ", CommandStatus.toString(status));
- resultCode = MessagingException.ACCESS_DENIED;
- } else if (CommandStatus.isTransientError(status)) {
- userLog("Transient error: ", CommandStatus.toString(status));
- resultCode = MessagingException.IOERROR;
- } else {
- userLog("Unexpected response: ", CommandStatus.toString(status));
- resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
- }
- } finally {
- resp.close();
- }
- } catch (IOException e) {
- Throwable cause = e.getCause();
- if (cause != null && cause instanceof CertificateException) {
- // This could be because the server's certificate failed to validate.
- userLog("CertificateException caught: ", e.getMessage());
- resultCode = MessagingException.GENERAL_SECURITY;
- }
- userLog("IOException caught: ", e.getMessage());
- resultCode = MessagingException.IOERROR;
- } catch (CertificateException e) {
- // This occurs if the client certificate the user specified is invalid/inaccessible.
- userLog("CertificateException caught: ", e.getMessage());
- resultCode = MessagingException.CLIENT_CERTIFICATE_ERROR;
- }
- bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, resultCode);
- return bundle;
- }
-
- /**
- * Gets the redirect location from the HTTP headers and uses that to modify the HttpPost so that
- * it can be reused
- *
- * @param resp the HttpResponse that indicates a redirect (451)
- * @param post the HttpPost that was originally sent to the server
- * @return the HttpPost, updated with the redirect location
- */
- private HttpPost getRedirect(HttpResponse resp, HttpPost post) {
- Header locHeader = resp.getFirstHeader("X-MS-Location");
- if (locHeader != null) {
- String loc = locHeader.getValue();
- // If we've gotten one and it shows signs of looking like an address, we try
- // sending our request there
- if (loc != null && loc.startsWith("http")) {
- post.setURI(URI.create(loc));
- return post;
- }
- }
- return null;
- }
-
- /**
- * Send the POST command to the autodiscover server, handling a redirect, if necessary, and
- * return the HttpResponse. If we get a 401 (unauthorized) error and we're using the
- * full email address, try the bare user name instead (e.g. foo instead of foo@bar.com)
- *
- * @param client the HttpClient to be used for the request
- * @param post the HttpPost we're going to send
- * @param canRetry whether we can retry using the bare name on an authentication failure (401)
- * @return an HttpResponse from the original or redirect server
- * @throws IOException on any IOException within the HttpClient code
- * @throws MessagingException
- */
- private EasResponse postAutodiscover(HttpClient client, HttpPost post, boolean canRetry)
- throws IOException, MessagingException {
- userLog("Posting autodiscover to: " + post.getURI());
- EasResponse resp = executePostWithTimeout(client, post, COMMAND_TIMEOUT);
- int code = resp.getStatus();
- // On a redirect, try the new location
- if (code == EAS_REDIRECT_CODE) {
- post = getRedirect(resp.mResponse, post);
- if (post != null) {
- userLog("Posting autodiscover to redirect: " + post.getURI());
- return executePostWithTimeout(client, post, COMMAND_TIMEOUT);
- }
- // 401 (Unauthorized) is for true auth errors when used in Autodiscover
- } else if (code == HttpStatus.SC_UNAUTHORIZED) {
- if (canRetry && mUserName.contains("@")) {
- // Try again using the bare user name
- int atSignIndex = mUserName.indexOf('@');
- mUserName = mUserName.substring(0, atSignIndex);
- cacheAuthUserAndBaseUriStrings();
- userLog("401 received; trying username: ", mUserName);
- // Recreate the basic authentication string and reset the header
- post.removeHeaders("Authorization");
- post.setHeader("Authorization", mAuthString);
- return postAutodiscover(client, post, false);
- }
- throw new MessagingException(MessagingException.AUTHENTICATION_FAILED);
- // 403 (and others) we'll just punt on
- } else if (code != HttpStatus.SC_OK) {
- // We'll try the next address if this doesn't work
- userLog("Code: " + code + ", throwing IOException");
- throw new IOException();
- }
- return resp;
- }
-
- /**
- * Use the Exchange 2007 AutoDiscover feature to try to retrieve server information using
- * only an email address and the password
- *
- * @param userName the user's email address
- * @param password the user's password
- * @return a HostAuth ready to be saved in an Account or null (failure)
- */
- public Bundle tryAutodiscover(String userName, String password) throws RemoteException {
- XmlSerializer s = Xml.newSerializer();
- ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
- HostAuth hostAuth = new HostAuth();
- Bundle bundle = new Bundle();
- bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
- MessagingException.NO_ERROR);
- try {
- // Build the XML document that's sent to the autodiscover server(s)
- s.setOutput(os, "UTF-8");
- s.startDocument("UTF-8", false);
- s.startTag(null, "Autodiscover");
- s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006");
- s.startTag(null, "Request");
- s.startTag(null, "EMailAddress").text(userName).endTag(null, "EMailAddress");
- s.startTag(null, "AcceptableResponseSchema");
- s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006");
- s.endTag(null, "AcceptableResponseSchema");
- s.endTag(null, "Request");
- s.endTag(null, "Autodiscover");
- s.endDocument();
- String req = os.toString();
-
- // Initialize the user name and password
- mUserName = userName;
- mPassword = password;
- // Make sure the authentication string is recreated and cached
- cacheAuthUserAndBaseUriStrings();
-
- // Split out the domain name
- int amp = userName.indexOf('@');
- // The UI ensures that userName is a valid email address
- if (amp < 0) {
- throw new RemoteException();
- }
- String domain = userName.substring(amp + 1);
-
- // There are up to four attempts here; the two URLs that we're supposed to try per the
- // specification, and up to one redirect for each (handled in postAutodiscover)
- // Note: The expectation is that, of these four attempts, only a single server will
- // actually be identified as the autodiscover server. For the identified server,
- // we may also try a 2nd connection with a different format (bare name).
-
- // Try the domain first and see if we can get a response
- HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE);
- setHeaders(post, false);
- post.setHeader("Content-Type", "text/xml");
- post.setEntity(new StringEntity(req));
- HttpClient client = getHttpClient(COMMAND_TIMEOUT);
- EasResponse resp;
- try {
- resp = postAutodiscover(client, post, true /*canRetry*/);
- } catch (IOException e1) {
- userLog("IOException in autodiscover; trying alternate address");
- // We catch the IOException here because we have an alternate address to try
- post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE));
- // If we fail here, we're out of options, so we let the outer try catch the
- // IOException and return null
- resp = postAutodiscover(client, post, true /*canRetry*/);
- }
-
- try {
- // Get the "final" code; if it's not 200, just return null
- int code = resp.getStatus();
- userLog("Code: " + code);
- if (code != HttpStatus.SC_OK) return null;
-
- InputStream is = resp.getInputStream();
- // The response to Autodiscover is regular XML (not WBXML)
- // If we ever get an error in this process, we'll just punt and return null
- XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
- XmlPullParser parser = factory.newPullParser();
- parser.setInput(is, "UTF-8");
- int type = parser.getEventType();
- if (type == XmlPullParser.START_DOCUMENT) {
- type = parser.next();
- if (type == XmlPullParser.START_TAG) {
- String name = parser.getName();
- if (name.equals("Autodiscover")) {
- hostAuth = new HostAuth();
- parseAutodiscover(parser, hostAuth);
- // On success, we'll have a server address and login
- if (hostAuth.mAddress != null) {
- // Fill in the rest of the HostAuth
- // We use the user name and password that were successful during
- // the autodiscover process
- hostAuth.mLogin = mUserName;
- hostAuth.mPassword = mPassword;
- // Note: there is no way we can auto-discover the proper client
- // SSL certificate to use, if one is needed.
- hostAuth.mPort = 443;
- hostAuth.mProtocol = "eas";
- hostAuth.mFlags =
- HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
- bundle.putParcelable(
- EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, hostAuth);
- } else {
- bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
- MessagingException.UNSPECIFIED_EXCEPTION);
- }
- }
- }
- }
- } catch (XmlPullParserException e1) {
- // This would indicate an I/O error of some sort
- // We will simply return null and user can configure manually
- } finally {
- resp.close();
- }
- // There's no reason at all for exceptions to be thrown, and it's ok if so.
- // We just won't do auto-discover; user can configure manually
- } catch (IllegalArgumentException e) {
- bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
- MessagingException.UNSPECIFIED_EXCEPTION);
- } catch (IllegalStateException e) {
- bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
- MessagingException.UNSPECIFIED_EXCEPTION);
- } catch (IOException e) {
- userLog("IOException in Autodiscover", e);
- bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
- MessagingException.IOERROR);
- } catch (MessagingException e) {
- bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
- MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED);
- }
- return bundle;
- }
-
- void parseServer(XmlPullParser parser, HostAuth hostAuth)
- throws XmlPullParserException, IOException {
- boolean mobileSync = false;
- while (true) {
- int type = parser.next();
- if (type == XmlPullParser.END_TAG && parser.getName().equals("Server")) {
- break;
- } else if (type == XmlPullParser.START_TAG) {
- String name = parser.getName();
- if (name.equals("Type")) {
- if (parser.nextText().equals("MobileSync")) {
- mobileSync = true;
- }
- } else if (mobileSync && name.equals("Url")) {
- String url = parser.nextText().toLowerCase();
- // This will look like https://<server address>/Microsoft-Server-ActiveSync
- // We need to extract the <server address>
- if (url.startsWith("https://") &&
- url.endsWith("/microsoft-server-activesync")) {
- int lastSlash = url.lastIndexOf('/');
- hostAuth.mAddress = url.substring(8, lastSlash);
- userLog("Autodiscover, server: " + hostAuth.mAddress);
- }
- }
- }
- }
- }
-
- void parseSettings(XmlPullParser parser, HostAuth hostAuth)
- throws XmlPullParserException, IOException {
- while (true) {
- int type = parser.next();
- if (type == XmlPullParser.END_TAG && parser.getName().equals("Settings")) {
- break;
- } else if (type == XmlPullParser.START_TAG) {
- String name = parser.getName();
- if (name.equals("Server")) {
- parseServer(parser, hostAuth);
- }
- }
- }
- }
-
- void parseAction(XmlPullParser parser, HostAuth hostAuth)
- throws XmlPullParserException, IOException {
- while (true) {
- int type = parser.next();
- if (type == XmlPullParser.END_TAG && parser.getName().equals("Action")) {
- break;
- } else if (type == XmlPullParser.START_TAG) {
- String name = parser.getName();
- if (name.equals("Error")) {
- // Should parse the error
- } else if (name.equals("Redirect")) {
- Log.d(TAG, "Redirect: " + parser.nextText());
- } else if (name.equals("Settings")) {
- parseSettings(parser, hostAuth);
- }
- }
- }
- }
-
- void parseUser(XmlPullParser parser, HostAuth hostAuth)
- throws XmlPullParserException, IOException {
- while (true) {
- int type = parser.next();
- if (type == XmlPullParser.END_TAG && parser.getName().equals("User")) {
- break;
- } else if (type == XmlPullParser.START_TAG) {
- String name = parser.getName();
- if (name.equals("EMailAddress")) {
- String addr = parser.nextText();
- userLog("Autodiscover, email: " + addr);
- } else if (name.equals("DisplayName")) {
- String dn = parser.nextText();
- userLog("Autodiscover, user: " + dn);
- }
- }
- }
- }
-
- void parseResponse(XmlPullParser parser, HostAuth hostAuth)
- throws XmlPullParserException, IOException {
- while (true) {
- int type = parser.next();
- if (type == XmlPullParser.END_TAG && parser.getName().equals("Response")) {
- break;
- } else if (type == XmlPullParser.START_TAG) {
- String name = parser.getName();
- if (name.equals("User")) {
- parseUser(parser, hostAuth);
- } else if (name.equals("Action")) {
- parseAction(parser, hostAuth);
- }
- }
- }
- }
-
- void parseAutodiscover(XmlPullParser parser, HostAuth hostAuth)
- throws XmlPullParserException, IOException {
- while (true) {
- int type = parser.nextTag();
- if (type == XmlPullParser.END_TAG && parser.getName().equals("Autodiscover")) {
- break;
- } else if (type == XmlPullParser.START_TAG && parser.getName().equals("Response")) {
- parseResponse(parser, hostAuth);
- }
- }
- }
-
- /**
- * Contact the GAL and obtain a list of matching accounts
- * @param context caller's context
- * @param accountId the account Id to search
- * @param filter the characters entered so far
- * @return a result record or null for no data
- *
- * TODO: shorter timeout for interactive lookup
- * TODO: make watchdog actually work (it doesn't understand our service w/Mailbox == 0)
- * TODO: figure out why sendHttpClientPost() hangs - possibly pool exhaustion
- */
- static public GalResult searchGal(Context context, long accountId, String filter, int limit) {
- Account acct = Account.restoreAccountWithId(context, accountId);
- if (acct != null) {
- EasSyncService svc = setupServiceForAccount(context, acct);
- if (svc == null) return null;
- try {
- Serializer s = new Serializer();
- s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE);
- s.data(Tags.SEARCH_NAME, "GAL").data(Tags.SEARCH_QUERY, filter);
- s.start(Tags.SEARCH_OPTIONS);
- s.data(Tags.SEARCH_RANGE, "0-" + Integer.toString(limit - 1));
- s.end().end().end().done();
- EasResponse resp = svc.sendHttpClientPost("Search", s.toByteArray());
- try {
- int code = resp.getStatus();
- if (code == HttpStatus.SC_OK) {
- InputStream is = resp.getInputStream();
- try {
- GalParser gp = new GalParser(is, svc);
- if (gp.parse()) {
- return gp.getGalResult();
- }
- } finally {
- is.close();
- }
- } else {
- svc.userLog("GAL lookup returned " + code);
- }
- } finally {
- resp.close();
- }
- } catch (IOException e) {
- // GAL is non-critical; we'll just go on
- svc.userLog("GAL lookup exception " + e);
- }
- }
- return null;
- }
- /**
- * Send an email responding to a Message that has been marked as a meeting request. The message
- * will consist a little bit of event information and an iCalendar attachment
- * @param msg the meeting request email
- */
- private void sendMeetingResponseMail(Message msg, int response) {
- // Get the meeting information; we'd better have some...
- if (msg.mMeetingInfo == null) return;
- PackedString meetingInfo = new PackedString(msg.mMeetingInfo);
-
- // This will come as "First Last" <box@server.blah>, so we use Address to
- // parse it into parts; we only need the email address part for the ics file
- Address[] addrs = Address.parse(meetingInfo.get(MeetingInfo.MEETING_ORGANIZER_EMAIL));
- // It shouldn't be possible, but handle it anyway
- if (addrs.length != 1) return;
- String organizerEmail = addrs[0].getAddress();
-
- String dtStamp = meetingInfo.get(MeetingInfo.MEETING_DTSTAMP);
- String dtStart = meetingInfo.get(MeetingInfo.MEETING_DTSTART);
- String dtEnd = meetingInfo.get(MeetingInfo.MEETING_DTEND);
-
- // What we're doing here is to create an Entity that looks like an Event as it would be
- // stored by CalendarProvider
- ContentValues entityValues = new ContentValues();
- Entity entity = new Entity(entityValues);
-
- // Fill in times, location, title, and organizer
- entityValues.put("DTSTAMP",
- CalendarUtilities.convertEmailDateTimeToCalendarDateTime(dtStamp));
- entityValues.put(Events.DTSTART, Utility.parseEmailDateTimeToMillis(dtStart));
- entityValues.put(Events.DTEND, Utility.parseEmailDateTimeToMillis(dtEnd));
- entityValues.put(Events.EVENT_LOCATION, meetingInfo.get(MeetingInfo.MEETING_LOCATION));
- entityValues.put(Events.TITLE, meetingInfo.get(MeetingInfo.MEETING_TITLE));
- entityValues.put(Events.ORGANIZER, organizerEmail);
-
- // Add ourselves as an attendee, using our account email address
- ContentValues attendeeValues = new ContentValues();
- attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP,
- Attendees.RELATIONSHIP_ATTENDEE);
- attendeeValues.put(Attendees.ATTENDEE_EMAIL, mAccount.mEmailAddress);
- entity.addSubValue(Attendees.CONTENT_URI, attendeeValues);
-
- // Add the organizer
- ContentValues organizerValues = new ContentValues();
- organizerValues.put(Attendees.ATTENDEE_RELATIONSHIP,
- Attendees.RELATIONSHIP_ORGANIZER);
- organizerValues.put(Attendees.ATTENDEE_EMAIL, organizerEmail);
- entity.addSubValue(Attendees.CONTENT_URI, organizerValues);
-
- // Create a message from the Entity we've built. The message will have fields like
- // to, subject, date, and text filled in. There will also be an "inline" attachment
- // which is in iCalendar format
- int flag;
- switch(response) {
- case EmailServiceConstants.MEETING_REQUEST_ACCEPTED:
- flag = Message.FLAG_OUTGOING_MEETING_ACCEPT;
- break;
- case EmailServiceConstants.MEETING_REQUEST_DECLINED:
- flag = Message.FLAG_OUTGOING_MEETING_DECLINE;
- break;
- case EmailServiceConstants.MEETING_REQUEST_TENTATIVE:
- default:
- flag = Message.FLAG_OUTGOING_MEETING_TENTATIVE;
- break;
- }
- Message outgoingMsg =
- CalendarUtilities.createMessageForEntity(mContext, entity, flag,
- meetingInfo.get(MeetingInfo.MEETING_UID), mAccount);
- // Assuming we got a message back (we might not if the event has been deleted), send it
- if (outgoingMsg != null) {
- EasOutboxService.sendMessage(mContext, mAccount.mId, outgoingMsg);
- }
- }
-
- /**
- * Responds to a move request. The MessageMoveRequest is basically our
- * wrapper for the MoveItems service call
- * @param req the request (message id and "to" mailbox id)
- * @throws IOException
- */
- protected void messageMoveRequest(MessageMoveRequest req) throws IOException {
- // Retrieve the message and mailbox; punt if either are null
- Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
- if (msg == null) return;
- Cursor c = mContentResolver.query(ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI,
- msg.mId), new String[] {MessageColumns.MAILBOX_KEY}, null, null, null);
- if (c == null) throw new ProviderUnavailableException();
- Mailbox srcMailbox = null;
- try {
- if (!c.moveToNext()) return;
- srcMailbox = Mailbox.restoreMailboxWithId(mContext, c.getLong(0));
- } finally {
- c.close();
- }
- if (srcMailbox == null) return;
- Mailbox dstMailbox = Mailbox.restoreMailboxWithId(mContext, req.mMailboxId);
- if (dstMailbox == null) return;
- Serializer s = new Serializer();
- s.start(Tags.MOVE_MOVE_ITEMS).start(Tags.MOVE_MOVE);
- s.data(Tags.MOVE_SRCMSGID, msg.mServerId);
- s.data(Tags.MOVE_SRCFLDID, srcMailbox.mServerId);
- s.data(Tags.MOVE_DSTFLDID, dstMailbox.mServerId);
- s.end().end().done();
- EasResponse resp = sendHttpClientPost("MoveItems", s.toByteArray());
- try {
- int status = resp.getStatus();
- if (status == HttpStatus.SC_OK) {
- if (!resp.isEmpty()) {
- InputStream is = resp.getInputStream();
- MoveItemsParser p = new MoveItemsParser(is, this);
- p.parse();
- int statusCode = p.getStatusCode();
- ContentValues cv = new ContentValues();
- if (statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
- // Restore the old mailbox id
- cv.put(MessageColumns.MAILBOX_KEY, srcMailbox.mServerId);
- mContentResolver.update(
- ContentUris.withAppendedId(Message.CONTENT_URI, req.mMessageId),
- cv, null, null);
- } else if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS) {
- // Update with the new server id
- cv.put(SyncColumns.SERVER_ID, p.getNewServerId());
- cv.put(Message.FLAGS, msg.mFlags | MESSAGE_FLAG_MOVED_MESSAGE);
- mContentResolver.update(
- ContentUris.withAppendedId(Message.CONTENT_URI, req.mMessageId),
- cv, null, null);
- }
- if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS
- || statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
- // If we revert or succeed, we no longer need the update information
- // OR the now-duplicate email (the new copy will be synced down)
- mContentResolver.delete(ContentUris.withAppendedId(
- Message.UPDATED_CONTENT_URI, req.mMessageId), null, null);
- } else {
- // In this case, we're retrying, so do nothing. The request will be
- // handled next sync
- }
- }
- } else if (EasResponse.isAuthError(status)) {
- throw new EasAuthenticationException();
- } else {
- userLog("Move items request failed, code: " + status);
- throw new IOException();
- }
- } finally {
- resp.close();
- }
- }
-
- /**
- * Responds to a meeting request. The MeetingResponseRequest is basically our
- * wrapper for the meetingResponse service call
- * @param req the request (message id and response code)
- * @throws IOException
- */
- protected void sendMeetingResponse(MeetingResponseRequest req) throws IOException {
- // Retrieve the message and mailbox; punt if either are null
- Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
- if (msg == null) return;
- Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, msg.mMailboxKey);
- if (mailbox == null) return;
- Serializer s = new Serializer();
- s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST);
- s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(req.mResponse));
- s.data(Tags.MREQ_COLLECTION_ID, mailbox.mServerId);
- s.data(Tags.MREQ_REQ_ID, msg.mServerId);
- s.end().end().done();
- EasResponse resp = sendHttpClientPost("MeetingResponse", s.toByteArray());
- try {
- int status = resp.getStatus();
- if (status == HttpStatus.SC_OK) {
- if (!resp.isEmpty()) {
- InputStream is = resp.getInputStream();
- new MeetingResponseParser(is, this).parse();
- String meetingInfo = msg.mMeetingInfo;
- if (meetingInfo != null) {
- String responseRequested = new PackedString(meetingInfo).get(
- MeetingInfo.MEETING_RESPONSE_REQUESTED);
- // If there's no tag, or a non-zero tag, we send the response mail
- if ("0".equals(responseRequested)) {
- return;
- }
- }
- sendMeetingResponseMail(msg, req.mResponse);
- }
- } else if (EasResponse.isAuthError(status)) {
- throw new EasAuthenticationException();
- } else {
- userLog("Meeting response request failed, code: " + status);
- throw new IOException();
- }
- } finally {
- resp.close();
- }
- }
-
- /**
- * Using mUserName and mPassword, lazily create the strings that are commonly used in our HTTP
- * POSTs, including the authentication header string, the base URI we use to communicate with
- * EAS, and the user information string (user, deviceId, and deviceType)
- */
- private void cacheAuthUserAndBaseUriStrings() {
- if (mAuthString == null || mUserString == null || mBaseUriString == null) {
- String safeUserName = Uri.encode(mUserName);
- String cs = mUserName + ':' + mPassword;
- mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP);
- mUserString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId +
- "&DeviceType=" + DEVICE_TYPE;
- String scheme =
- EmailClientConnectionManager.makeScheme(mSsl, mTrustSsl, mClientCertAlias);
- mBaseUriString = scheme + "://" + mHostAddress + "/Microsoft-Server-ActiveSync";
- }
- }
-
- @VisibleForTesting
- String makeUriString(String cmd, String extra) {
- cacheAuthUserAndBaseUriStrings();
- String uriString = mBaseUriString;
- if (cmd != null) {
- uriString += "?Cmd=" + cmd + mUserString;
- }
- if (extra != null) {
- uriString += extra;
- }
- return uriString;
- }
-
- /**
- * Set standard HTTP headers, using a policy key if required
- * @param method the method we are going to send
- * @param usePolicyKey whether or not a policy key should be sent in the headers
- */
- /*package*/ void setHeaders(HttpRequestBase method, boolean usePolicyKey) {
- method.setHeader("Authorization", mAuthString);
- method.setHeader("MS-ASProtocolVersion", mProtocolVersion);
- method.setHeader("User-Agent", USER_AGENT);
- method.setHeader("Accept-Encoding", "gzip");
- if (usePolicyKey) {
- // If there's an account in existence, use its key; otherwise (we're creating the
- // account), send "0". The server will respond with code 449 if there are policies
- // to be enforced
- String key = "0";
- if (mAccount != null) {
- String accountKey = mAccount.mSecuritySyncKey;
- if (!TextUtils.isEmpty(accountKey)) {
- key = accountKey;
- }
- }
- method.setHeader("X-MS-PolicyKey", key);
- }
- }
-
- protected void setConnectionParameters(
- boolean useSsl, boolean trustAllServerCerts, String clientCertAlias)
- throws CertificateException {
-
- EmailClientConnectionManager connManager = getClientConnectionManager();
-
- mSsl = useSsl;
- mTrustSsl = trustAllServerCerts;
- mClientCertAlias = clientCertAlias;
-
- // Register the new alias, if needed.
- if (mClientCertAlias != null) {
- // Ensure that the connection manager knows to use the proper client certificate
- // when establishing connections for this service.
- connManager.registerClientCert(mContext, mClientCertAlias, mTrustSsl);
- }
- }
-
- private EmailClientConnectionManager getClientConnectionManager() {
- return ExchangeService.getClientConnectionManager();
- }
-
- private HttpClient getHttpClient(int timeout) {
- HttpParams params = new BasicHttpParams();
- HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
- HttpConnectionParams.setSoTimeout(params, timeout);
- HttpConnectionParams.setSocketBufferSize(params, 8192);
- HttpClient client = new DefaultHttpClient(getClientConnectionManager(), params);
- return client;
- }
-
- public EasResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException {
- return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), COMMAND_TIMEOUT);
- }
-
- protected EasResponse sendHttpClientPost(String cmd, HttpEntity entity) throws IOException {
- return sendHttpClientPost(cmd, entity, COMMAND_TIMEOUT);
- }
-
- protected EasResponse sendPing(byte[] bytes, int heartbeat) throws IOException {
- Thread.currentThread().setName(mAccount.mDisplayName + ": Ping");
- if (Eas.USER_LOG) {
- userLog("Send ping, timeout: " + heartbeat + "s, high: " + mPingHighWaterMark + 's');
- }
- return sendHttpClientPost(PING_COMMAND, new ByteArrayEntity(bytes), (heartbeat+5)*SECONDS);
- }
-
- /**
- * Convenience method for executePostWithTimeout for use other than with the Ping command
- */
- protected EasResponse executePostWithTimeout(HttpClient client, HttpPost method, int timeout)
- throws IOException {
- return executePostWithTimeout(client, method, timeout, false);
- }
-
- /**
- * Handle executing an HTTP POST command with proper timeout, watchdog, and ping behavior
- * @param client the HttpClient
- * @param method the HttpPost
- * @param timeout the timeout before failure, in ms
- * @param isPingCommand whether the POST is for the Ping command (requires wakelock logic)
- * @return the HttpResponse
- * @throws IOException
- */
- protected EasResponse executePostWithTimeout(HttpClient client, HttpPost method, int timeout,
- boolean isPingCommand) throws IOException {
- synchronized(getSynchronizer()) {
- mPendingPost = method;
- long alarmTime = timeout + WATCHDOG_TIMEOUT_ALLOWANCE;
- if (isPingCommand) {
- ExchangeService.runAsleep(mMailboxId, alarmTime);
- } else {
- ExchangeService.setWatchdogAlarm(mMailboxId, alarmTime);
- }
- }
- try {
- return EasResponse.fromHttpRequest(getClientConnectionManager(), client, method);
- } finally {
- synchronized(getSynchronizer()) {
- if (isPingCommand) {
- ExchangeService.runAwake(mMailboxId);
- } else {
- ExchangeService.clearWatchdogAlarm(mMailboxId);
- }
- mPendingPost = null;
- }
- }
- }
-
- public EasResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout)
- throws IOException {
- HttpClient client = getHttpClient(timeout);
- boolean isPingCommand = cmd.equals(PING_COMMAND);
-
- // Split the mail sending commands
- String extra = null;
- boolean msg = false;
- if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) {
- int cmdLength = cmd.indexOf('&');
- extra = cmd.substring(cmdLength);
- cmd = cmd.substring(0, cmdLength);
- msg = true;
- } else if (cmd.startsWith("SendMail&")) {
- msg = true;
- }
-
- String us = makeUriString(cmd, extra);
- HttpPost method = new HttpPost(URI.create(us));
- // Send the proper Content-Type header; it's always wbxml except for messages when
- // the EAS protocol version is < 14.0
- // If entity is null (e.g. for attachments), don't set this header
- if (msg && (mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE)) {
- method.setHeader("Content-Type", "message/rfc822");
- } else if (entity != null) {
- method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml");
- }
- setHeaders(method, !isPingCommand);
- // NOTE
- // The next lines are added at the insistence of $VENDOR, who is seeing inappropriate
- // network activity related to the Ping command on some networks with some servers.
- // This code should be removed when the underlying issue is resolved
- if (isPingCommand) {
- method.setHeader("Connection", "close");
- }
- method.setEntity(entity);
- return executePostWithTimeout(client, method, timeout, isPingCommand);
- }
-
- protected EasResponse sendHttpClientOptions() throws IOException {
- cacheAuthUserAndBaseUriStrings();
- // For OPTIONS, just use the base string and the single header
- String uriString = mBaseUriString;
- HttpOptions method = new HttpOptions(URI.create(uriString));
- method.setHeader("Authorization", mAuthString);
- method.setHeader("User-Agent", USER_AGENT);
- HttpClient client = getHttpClient(COMMAND_TIMEOUT);
- return EasResponse.fromHttpRequest(getClientConnectionManager(), client, method);
- }
-
- private String getTargetCollectionClassFromCursor(Cursor c) {
- int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
- if (type == Mailbox.TYPE_CONTACTS) {
- return "Contacts";
- } else if (type == Mailbox.TYPE_CALENDAR) {
- return "Calendar";
- } else {
- return "Email";
- }
- }
-
- /**
- * Negotiate provisioning with the server. First, get policies form the server and see if
- * the policies are supported by the device. Then, write the policies to the account and
- * tell SecurityPolicy that we have policies in effect. Finally, see if those policies are
- * active; if so, acknowledge the policies to the server and get a final policy key that we
- * use in future EAS commands and write this key to the account.
- * @return whether or not provisioning has been successful
- * @throws IOException
- */
- private boolean tryProvision() throws IOException {
- // First, see if provisioning is even possible, i.e. do we support the policies required
- // by the server
- ProvisionParser pp = canProvision();
- if (pp != null && pp.hasSupportablePolicySet()) {
- // Get the policies from ProvisionParser
- Policy policy = pp.getPolicy();
- Policy oldPolicy = null;
- // Grab the old policy (if any)
- if (mAccount.mPolicyKey > 0) {
- oldPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
- }
- // Update the account with a null policyKey (the key we've gotten is
- // temporary and cannot be used for syncing)
- Policy.setAccountPolicy(mContext, mAccount, policy, null);
- // Make sure mAccount is current (with latest policy key)
- mAccount.refresh(mContext);
- // Make sure that SecurityPolicy is up-to-date
- SecurityPolicyDelegate.policiesUpdated(mContext, mAccount.mId);
- if (pp.getRemoteWipe()) {
- // We've gotten a remote wipe command
- ExchangeService.alwaysLog("!!! Remote wipe request received");
- // Start by setting the account to security hold
- SecurityPolicyDelegate.setAccountHoldFlag(mContext, mAccount, true);
- // Force a stop to any running syncs for this account (except this one)
- ExchangeService.stopNonAccountMailboxSyncsForAccount(mAccount.mId);
-
- // If we're not the admin, we can't do the wipe, so just return
- if (!SecurityPolicyDelegate.isActiveAdmin(mContext)) {
- ExchangeService.alwaysLog("!!! Not device admin; can't wipe");
- return false;
- }
-
- // First, we've got to acknowledge it, but wrap the wipe in try/catch so that
- // we wipe the device regardless of any errors in acknowledgment
- try {
- ExchangeService.alwaysLog("!!! Acknowledging remote wipe to server");
- acknowledgeRemoteWipe(pp.getSecuritySyncKey());
- } catch (Exception e) {
- // Because remote wipe is such a high priority task, we don't want to
- // circumvent it if there's an exception in acknowledgment
- }
- // Then, tell SecurityPolicy to wipe the device
- ExchangeService.alwaysLog("!!! Executing remote wipe");
- SecurityPolicyDelegate.remoteWipe(mContext);
- return false;
- } else if (SecurityPolicyDelegate.isActive(mContext, policy)) {
- // See if the required policies are in force; if they are, acknowledge the policies
- // to the server and get the final policy key
- // NOTE: For EAS 14.0, we already have the acknowledgment in the ProvisionParser
- String securitySyncKey;
- if (mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
- securitySyncKey = pp.getSecuritySyncKey();
- } else {
- securitySyncKey = acknowledgeProvision(pp.getSecuritySyncKey(),
- PROVISION_STATUS_OK);
- }
- if (securitySyncKey != null) {
- // If attachment policies have changed, fix up any affected attachment records
- if (oldPolicy != null) {
- if ((oldPolicy.mDontAllowAttachments != policy.mDontAllowAttachments) ||
- (oldPolicy.mMaxAttachmentSize != policy.mMaxAttachmentSize)) {
- Policy.setAttachmentFlagsForNewPolicy(mContext, mAccount, policy);
- }
- }
- // Write the final policy key to the Account and say we've been successful
- Policy.setAccountPolicy(mContext, mAccount, policy, securitySyncKey);
- // Release any mailboxes that might be in a security hold
- ExchangeService.releaseSecurityHold(mAccount);
- return true;
- }
- } else {
- // Notify that we are blocked because of policies
- // TODO: Indicate unsupported policies here?
- SecurityPolicyDelegate.policiesRequired(mContext, mAccount.mId);
- }
- }
- return false;
- }
-
- private String getPolicyType() {
- return (mProtocolVersionDouble >=
- Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) ? EAS_12_POLICY_TYPE : EAS_2_POLICY_TYPE;
- }
-
- /**
- * Obtain a set of policies from the server and determine whether those policies are supported
- * by the device.
- * @return the ProvisionParser (holds policies and key) if we receive policies; null otherwise
- * @throws IOException
- */
- private ProvisionParser canProvision() throws IOException {
- Serializer s = new Serializer();
- s.start(Tags.PROVISION_PROVISION);
- if (mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_SP1_DOUBLE) {
- // Send settings information in 14.1 and greater
- s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
- s.data(Tags.SETTINGS_MODEL, Build.MODEL);
- //s.data(Tags.SETTINGS_IMEI, "");
- //s.data(Tags.SETTINGS_FRIENDLY_NAME, "Friendly Name");
- s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
- //s.data(Tags.SETTINGS_OS_LANGUAGE, "");
- //s.data(Tags.SETTINGS_PHONE_NUMBER, "");
- //s.data(Tags.SETTINGS_MOBILE_OPERATOR, "");
- s.data(Tags.SETTINGS_USER_AGENT, USER_AGENT);
- s.end().end(); // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION
- }
- s.start(Tags.PROVISION_POLICIES);
- s.start(Tags.PROVISION_POLICY).data(Tags.PROVISION_POLICY_TYPE, getPolicyType()).end();
- s.end(); // PROVISION_POLICIES
- s.end().done(); // PROVISION_PROVISION
- EasResponse resp = sendHttpClientPost("Provision", s.toByteArray());
- try {
- int code = resp.getStatus();
- if (code == HttpStatus.SC_OK) {
- InputStream is = resp.getInputStream();
- ProvisionParser pp = new ProvisionParser(is, this);
- if (pp.parse()) {
- // The PolicySet in the ProvisionParser will have the requirements for all KNOWN
- // policies. If others are required, hasSupportablePolicySet will be false
- if (pp.hasSupportablePolicySet() &&
- mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
- // In EAS 14.0, we need the final security key in order to use the settings
- // command
- String policyKey = acknowledgeProvision(pp.getSecuritySyncKey(),
- PROVISION_STATUS_OK);
- if (policyKey != null) {
- pp.setSecuritySyncKey(policyKey);
- }
- } else if (!pp.hasSupportablePolicySet()) {
- // Try to acknowledge using the "partial" status (i.e. we can partially
- // accommodate the required policies). The server will agree to this if the
- // "allow non-provisionable devices" setting is enabled on the server
- ExchangeService.log("PolicySet is NOT fully supportable");
- String policyKey = acknowledgeProvision(pp.getSecuritySyncKey(),
- PROVISION_STATUS_PARTIAL);
- // Return either the parser (success) or null (failure)
- if (policyKey != null) {
- pp.clearUnsupportedPolicies();
- }
- }
- return pp;
- }
- }
- } finally {
- resp.close();
- }
-
- // On failures, simply return null
- return null;
- }
-
- /**
- * Acknowledge that we support the policies provided by the server, and that these policies
- * are in force.
- * @param tempKey the initial (temporary) policy key sent by the server
- * @return the final policy key, which can be used for syncing
- * @throws IOException
- */
- private void acknowledgeRemoteWipe(String tempKey) throws IOException {
- acknowledgeProvisionImpl(tempKey, PROVISION_STATUS_OK, true);
- }
-
- private String acknowledgeProvision(String tempKey, String result) throws IOException {
- return acknowledgeProvisionImpl(tempKey, result, false);
- }
-
- private String acknowledgeProvisionImpl(String tempKey, String status,
- boolean remoteWipe) throws IOException {
- Serializer s = new Serializer();
- s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES);
- s.start(Tags.PROVISION_POLICY);
-
- // Use the proper policy type, depending on EAS version
- s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType());
-
- s.data(Tags.PROVISION_POLICY_KEY, tempKey);
- s.data(Tags.PROVISION_STATUS, status);
- s.end().end(); // PROVISION_POLICY, PROVISION_POLICIES
- if (remoteWipe) {
- s.start(Tags.PROVISION_REMOTE_WIPE);
- s.data(Tags.PROVISION_STATUS, PROVISION_STATUS_OK);
- s.end();
- }
- s.end().done(); // PROVISION_PROVISION
- EasResponse resp = sendHttpClientPost("Provision", s.toByteArray());
- try {
- int code = resp.getStatus();
- if (code == HttpStatus.SC_OK) {
- InputStream is = resp.getInputStream();
- ProvisionParser pp = new ProvisionParser(is, this);
- if (pp.parse()) {
- // Return the final policy key from the ProvisionParser
- ExchangeService.log("Provision confirmation received for " +
- (PROVISION_STATUS_PARTIAL.equals(status) ? "PART" : "FULL") + " set");
- return pp.getSecuritySyncKey();
- }
- }
- } finally {
- resp.close();
- }
- // On failures, log issue and return null
- ExchangeService.log("Provision confirmation failed for" +
- (PROVISION_STATUS_PARTIAL.equals(status) ? "PART" : "FULL") + " set");
- return null;
- }
-
- private boolean sendSettings() throws IOException {
- Serializer s = new Serializer();
- s.start(Tags.SETTINGS_SETTINGS);
- s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
- s.data(Tags.SETTINGS_MODEL, Build.MODEL);
- s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
- s.data(Tags.SETTINGS_USER_AGENT, USER_AGENT);
- s.end().end().end().done(); // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION, SETTINGS_SETTINGS
- EasResponse resp = sendHttpClientPost("Settings", s.toByteArray());
- try {
- int code = resp.getStatus();
- if (code == HttpStatus.SC_OK) {
- InputStream is = resp.getInputStream();
- SettingsParser sp = new SettingsParser(is, this);
- return sp.parse();
- }
- } finally {
- resp.close();
- }
- // On failures, simply return false
- return false;
- }
-
- /**
- * Translate exit status code to service status code (used in callbacks)
- * @param exitStatus the service's exit status
- * @return the corresponding service status
- */
- private int exitStatusToServiceStatus(int exitStatus) {
- switch(exitStatus) {
- case EXIT_SECURITY_FAILURE:
- return EmailServiceStatus.SECURITY_FAILURE;
- case EXIT_LOGIN_FAILURE:
- return EmailServiceStatus.LOGIN_FAILED;
- default:
- return EmailServiceStatus.SUCCESS;
- }
- }
-
- /**
- * If possible, update the account to the new server address; report result
- * @param resp the EasResponse from the current POST
- * @return whether or not the redirect is handled and the POST should be retried
- */
- private boolean canHandleAccountMailboxRedirect(EasResponse resp) {
- userLog("AccountMailbox redirect error");
- HostAuth ha =
- HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
- if (ha != null && getValidateRedirect(resp, ha)) {
- // Update the account's HostAuth with new values
- ContentValues haValues = new ContentValues();
- haValues.put(HostAuthColumns.ADDRESS, ha.mAddress);
- ha.update(mContext, haValues);
- return true;
- }
- return false;
- }
-
- /**
- * Performs FolderSync
- *
- * @throws IOException
- * @throws EasParserException
- */
- public void runAccountMailbox() throws IOException, EasParserException {
- // Check that the account's mailboxes are consistent
- MailboxUtilities.checkMailboxConsistency(mContext, mAccount.mId);
- // Initialize exit status to success
- mExitStatus = EXIT_DONE;
- try {
- try {
- ExchangeService.callback()
- .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0);
- } catch (RemoteException e1) {
- // Don't care if this fails
- }
-
- if (mAccount.mSyncKey == null) {
- mAccount.mSyncKey = "0";
- userLog("Account syncKey INIT to 0");
- ContentValues cv = new ContentValues();
- cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
- mAccount.update(mContext, cv);
- }
-
- boolean firstSync = mAccount.mSyncKey.equals("0");
- if (firstSync) {
- userLog("Initial FolderSync");
- }
-
- // When we first start up, change all mailboxes to push.
- ContentValues cv = new ContentValues();
- cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
- if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
- WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING,
- new String[] {Long.toString(mAccount.mId)}) > 0) {
- ExchangeService.kick("change ping boxes to push");
- }
-
- // Determine our protocol version, if we haven't already and save it in the Account
- // Also re-check protocol version at least once a day (in case of upgrade)
- if (mAccount.mProtocolVersion == null || firstSync ||
- ((System.currentTimeMillis() - mMailbox.mSyncTime) > DAYS)) {
- userLog("Determine EAS protocol version");
- EasResponse resp = sendHttpClientOptions();
- try {
- int code = resp.getStatus();
- userLog("OPTIONS response: ", code);
- if (code == HttpStatus.SC_OK) {
- Header header = resp.getHeader("MS-ASProtocolCommands");
- userLog(header.getValue());
- header = resp.getHeader("ms-asprotocolversions");
- try {
- setupProtocolVersion(this, header);
- } catch (MessagingException e) {
- // Since we've already validated, this can't really happen
- // But if it does, we'll rethrow this...
- throw new IOException();
- }
- // Save the protocol version
- cv.clear();
- // Save the protocol version in the account; if we're using 12.0 or greater,
- // set the flag for support of SmartForward
- cv.put(Account.PROTOCOL_VERSION, mProtocolVersion);
- mAccount.update(mContext, cv);
- cv.clear();
- // Save the sync time of the account mailbox to current time
- cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
- mMailbox.update(mContext, cv);
- } else if (code == EAS_REDIRECT_CODE && canHandleAccountMailboxRedirect(resp)) {
- // Cause this to re-run
- throw new IOException("Will retry after a brief hold...");
- } else {
- errorLog("OPTIONS command failed; throwing IOException");
- throw new IOException();
- }
- } finally {
- resp.close();
- }
- }
-
- // Make sure we've upgraded flags for ICS if we're using v12.0 or later
- if (mProtocolVersionDouble >= 12.0 &&
- (mAccount.mFlags & Account.FLAGS_SUPPORTS_SEARCH) == 0) {
- cv.clear();
- mAccount.mFlags = mAccount.mFlags | Account.FLAGS_SUPPORTS_SMART_FORWARD |
- Account.FLAGS_SUPPORTS_SEARCH | Account.FLAGS_SUPPORTS_GLOBAL_SEARCH;
- cv.put(AccountColumns.FLAGS, mAccount.mFlags);
- mAccount.update(mContext, cv);
- }
-
- // Change all pushable boxes to push when we start the account mailbox
- if (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) {
- cv.clear();
- cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
- if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
- ExchangeService.WHERE_IN_ACCOUNT_AND_PUSHABLE,
- new String[] {Long.toString(mAccount.mId)}) > 0) {
- userLog("Push account; set pushable boxes to push...");
- }
- }
-
- while (!mStop) {
- // If we're not allowed to sync (e.g. roaming policy), leave now
- if (!ExchangeService.canAutoSync(mAccount)) return;
- userLog("Sending Account syncKey: ", mAccount.mSyncKey);
- Serializer s = new Serializer();
- s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY)
- .text(mAccount.mSyncKey).end().end().done();
- EasResponse resp = sendHttpClientPost("FolderSync", s.toByteArray());
- try {
- if (mStop) break;
- int code = resp.getStatus();
- if (code == HttpStatus.SC_OK) {
- if (!resp.isEmpty()) {
- InputStream is = resp.getInputStream();
- // Returns true if we need to sync again
- if (new FolderSyncParser(is, new AccountSyncAdapter(this)).parse()) {
- continue;
- }
- }
- } else if (EasResponse.isProvisionError(code)) {
- userLog("FolderSync provisioning error: ", code);
- throw new CommandStatusException(CommandStatus.NEEDS_PROVISIONING);
- } else if (EasResponse.isAuthError(code)) {
- userLog("FolderSync auth error: ", code);
- mExitStatus = EXIT_LOGIN_FAILURE;
- return;
- } else if (code == EAS_REDIRECT_CODE && canHandleAccountMailboxRedirect(resp)) {
- // This will cause a retry of the FolderSync
- continue;
- } else {
- userLog("FolderSync response error: ", code);
- }
- } finally {
- resp.close();
- }
-
- // Change all push/hold boxes to push
- cv.clear();
- cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH);
- if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
- WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX,
- new String[] {Long.toString(mAccount.mId)}) > 0) {
- userLog("Set push/hold boxes to push...");
- }
-
- try {
- ExchangeService.callback()
- .syncMailboxListStatus(mAccount.mId, exitStatusToServiceStatus(mExitStatus),
- 0);
- } catch (RemoteException e1) {
- // Don't care if this fails
- }
-
- // Before each run of the pingLoop, if this Account has a PolicySet, make sure it's
- // active; otherwise, clear out the key/flag. This should cause a provisioning
- // error on the next POST, and start the security sequence over again
- String key = mAccount.mSecuritySyncKey;
- if (!TextUtils.isEmpty(key)) {
- Policy policy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
- if ((policy != null) && !SecurityPolicyDelegate.isActive(mContext, policy)) {
- resetSecurityPolicies();
- }
- }
-
- // Wait for push notifications.
- String threadName = Thread.currentThread().getName();
- try {
- runPingLoop();
- } catch (StaleFolderListException e) {
- // We break out if we get told about a stale folder list
- userLog("Ping interrupted; folder list requires sync...");
- } catch (IllegalHeartbeatException e) {
- // If we're sending an illegal heartbeat, reset either the min or the max to
- // that heartbeat
- resetHeartbeats(e.mLegalHeartbeat);
- } finally {
- Thread.currentThread().setName(threadName);
- }
- }
- } catch (CommandStatusException e) {
- // If the sync error is a provisioning failure (perhaps policies changed),
- // let's try the provisioning procedure
- // Provisioning must only be attempted for the account mailbox - trying to
- // provision any other mailbox may result in race conditions and the
- // creation of multiple policy keys.
- int status = e.mStatus;
- if (CommandStatus.isNeedsProvisioning(status)) {
- if (!tryProvision()) {
- // Set the appropriate failure status
- mExitStatus = EXIT_SECURITY_FAILURE;
- return;
- }
- } else if (CommandStatus.isDeniedAccess(status)) {
- mExitStatus = EXIT_ACCESS_DENIED;
- try {
- ExchangeService.callback().syncMailboxListStatus(mAccount.mId,
- EmailServiceStatus.ACCESS_DENIED, 0);
- } catch (RemoteException e1) {
- // Don't care if this fails
- }
- return;
- } else {
- userLog("Unexpected status: " + CommandStatus.toString(status));
- mExitStatus = EXIT_EXCEPTION;
- }
- } catch (IOException e) {
- // We catch this here to send the folder sync status callback
- // A folder sync failed callback will get sent from run()
- try {
- if (!mStop) {
- // NOTE: The correct status is CONNECTION_ERROR, but the UI displays this, and
- // it's not really appropriate for EAS as this is not unexpected for a ping and
- // connection errors are retried in any case
- ExchangeService.callback()
- .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.SUCCESS, 0);
- }
- } catch (RemoteException e1) {
- // Don't care if this fails
- }
- throw e;
- }
- }
-
- /**
- * Reset either our minimum or maximum ping heartbeat to a heartbeat known to be legal
- * @param legalHeartbeat a known legal heartbeat (from the EAS server)
- */
- /*package*/ void resetHeartbeats(int legalHeartbeat) {
- userLog("Resetting min/max heartbeat, legal = " + legalHeartbeat);
- // We are here because the current heartbeat (mPingHeartbeat) is invalid. Depending on
- // whether the argument is above or below the current heartbeat, we can infer the need to
- // change either the minimum or maximum heartbeat
- if (legalHeartbeat > mPingHeartbeat) {
- // The legal heartbeat is higher than the ping heartbeat; therefore, our minimum was
- // too low. We respond by raising either or both of the minimum heartbeat or the
- // force heartbeat to the argument value
- if (mPingMinHeartbeat < legalHeartbeat) {
- mPingMinHeartbeat = legalHeartbeat;
- }
- if (mPingForceHeartbeat < legalHeartbeat) {
- mPingForceHeartbeat = legalHeartbeat;
- }
- // If our minimum is now greater than the max, bring them together
- if (mPingMinHeartbeat > mPingMaxHeartbeat) {
- mPingMaxHeartbeat = legalHeartbeat;
- }
- } else if (legalHeartbeat < mPingHeartbeat) {
- // The legal heartbeat is lower than the ping heartbeat; therefore, our maximum was
- // too high. We respond by lowering the maximum to the argument value
- mPingMaxHeartbeat = legalHeartbeat;
- // If our maximum is now less than the minimum, bring them together
- if (mPingMaxHeartbeat < mPingMinHeartbeat) {
- mPingMinHeartbeat = legalHeartbeat;
- }
- }
- // Set current heartbeat to the legal heartbeat
- mPingHeartbeat = legalHeartbeat;
- // Allow the heartbeat logic to run
- mPingHeartbeatDropped = false;
- }
-
- private void pushFallback(long mailboxId) {
- Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
- if (mailbox == null) {
- return;
- }
- ContentValues cv = new ContentValues();
- int mins = PING_FALLBACK_PIM;
- if (mailbox.mType == Mailbox.TYPE_INBOX) {
- mins = PING_FALLBACK_INBOX;
- }
- cv.put(Mailbox.SYNC_INTERVAL, mins);
- mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
- cv, null, null);
- errorLog("*** PING ERROR LOOP: Set " + mailbox.mDisplayName + " to " + mins + " min sync");
- ExchangeService.kick("push fallback");
- }
-
- /**
- * Simplistic attempt to determine a NAT timeout, based on experience with various carriers
- * and networks. The string "reset by peer" is very common in these situations, so we look for
- * that specifically. We may add additional tests here as more is learned.
- * @param message
- * @return whether this message is likely associated with a NAT failure
- */
- private boolean isLikelyNatFailure(String message) {
- if (message == null) return false;
- if (message.contains("reset by peer")) {
- return true;
- }
- return false;
- }
-
- private void runPingLoop() throws IOException, StaleFolderListException,
- IllegalHeartbeatException, CommandStatusException {
- int pingHeartbeat = mPingHeartbeat;
- userLog("runPingLoop");
- // Do push for all sync services here
- long endTime = System.currentTimeMillis() + (30*MINUTES);
- HashMap<String, Integer> pingErrorMap = new HashMap<String, Integer>();
- ArrayList<String> readyMailboxes = new ArrayList<String>();
- ArrayList<String> notReadyMailboxes = new ArrayList<String>();
- int pingWaitCount = 0;
- long inboxId = -1;
-
- while ((System.currentTimeMillis() < endTime) && !mStop) {
- // Count of pushable mailboxes
- int pushCount = 0;
- // Count of mailboxes that can be pushed right now
- int canPushCount = 0;
- // Count of uninitialized boxes
- int uninitCount = 0;
-
- Serializer s = new Serializer();
- Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
- MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId +
- AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null);
- if (c == null) throw new ProviderUnavailableException();
- notReadyMailboxes.clear();
- readyMailboxes.clear();
- // Look for an inbox, and remember its id
- if (inboxId == -1) {
- inboxId = Mailbox.findMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_INBOX);
- }
- try {
- // Loop through our pushed boxes seeing what is available to push
- while (c.moveToNext()) {
- pushCount++;
- // Two requirements for push:
- // 1) ExchangeService tells us the mailbox is syncable (not running/not stopped)
- // 2) The syncKey isn't "0" (i.e. it's synced at least once)
- long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
- int pingStatus = ExchangeService.pingStatus(mailboxId);
- String mailboxName = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
- if (pingStatus == ExchangeService.PING_STATUS_OK) {
- String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN);
- if ((syncKey == null) || syncKey.equals("0")) {
- // We can't push until the initial sync is done
- pushCount--;
- uninitCount++;
- continue;
- }
-
- if (canPushCount++ == 0) {
- // Initialize the Ping command
- s.start(Tags.PING_PING)
- .data(Tags.PING_HEARTBEAT_INTERVAL,
- Integer.toString(pingHeartbeat))
- .start(Tags.PING_FOLDERS);
- }
-
- String folderClass = getTargetCollectionClassFromCursor(c);
- s.start(Tags.PING_FOLDER)
- .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN))
- .data(Tags.PING_CLASS, folderClass)
- .end();
- readyMailboxes.add(mailboxName);
- } else if ((pingStatus == ExchangeService.PING_STATUS_RUNNING) ||
- (pingStatus == ExchangeService.PING_STATUS_WAITING)) {
- notReadyMailboxes.add(mailboxName);
- } else if (pingStatus == ExchangeService.PING_STATUS_UNABLE) {
- pushCount--;
- userLog(mailboxName, " in error state; ignore");
- continue;
- }
- }
- } finally {
- c.close();
- }
-
- if (Eas.USER_LOG) {
- if (!notReadyMailboxes.isEmpty()) {
- userLog("Ping not ready for: " + notReadyMailboxes);
- }
- if (!readyMailboxes.isEmpty()) {
- userLog("Ping ready for: " + readyMailboxes);
- }
- }
-
- // If we've waited 10 seconds or more, just ping with whatever boxes are ready
- // But use a shorter than normal heartbeat
- boolean forcePing = !notReadyMailboxes.isEmpty() && (pingWaitCount > 5);
-
- if ((canPushCount > 0) && ((canPushCount == pushCount) || forcePing)) {
- // If all pingable boxes are ready for push, send Ping to the server
- s.end().end().done();
- pingWaitCount = 0;
- mPostReset = false;
- mPostAborted = false;
-
- // If we've been stopped, this is a good time to return
- if (mStop) return;
-
- long pingTime = SystemClock.elapsedRealtime();
- try {
- // Send the ping, wrapped by appropriate timeout/alarm
- if (forcePing) {
- userLog("Forcing ping after waiting for all boxes to be ready");
- }
- EasResponse resp =
- sendPing(s.toByteArray(), forcePing ? mPingForceHeartbeat : pingHeartbeat);
-
- try {
- int code = resp.getStatus();
- userLog("Ping response: ", code);
-
- // If we're not allowed to sync (e.g. roaming policy), terminate gracefully
- // now; otherwise we might start a sync based on the response
- if (!ExchangeService.canAutoSync(mAccount)) {
- mStop = true;
- }
-
- // Return immediately if we've been asked to stop during the ping
- if (mStop) {
- userLog("Stopping pingLoop");
- return;
- }
-
- if (code == HttpStatus.SC_OK) {
- // Make sure to clear out any pending sync errors
- ExchangeService.removeFromSyncErrorMap(mMailboxId);
- if (!resp.isEmpty()) {
- InputStream is = resp.getInputStream();
- int pingResult = parsePingResult(is, mContentResolver,
- pingErrorMap);
- // If our ping completed (status = 1), and wasn't forced and we're
- // not at the maximum, try increasing timeout by two minutes
- if (pingResult == PROTOCOL_PING_STATUS_COMPLETED && !forcePing) {
- if (pingHeartbeat > mPingHighWaterMark) {
- mPingHighWaterMark = pingHeartbeat;
- userLog("Setting high water mark at: ", mPingHighWaterMark);
- }
- if ((pingHeartbeat < mPingMaxHeartbeat) &&
- !mPingHeartbeatDropped) {
- pingHeartbeat += PING_HEARTBEAT_INCREMENT;
- if (pingHeartbeat > mPingMaxHeartbeat) {
- pingHeartbeat = mPingMaxHeartbeat;
- }
- userLog("Increase ping heartbeat to ", pingHeartbeat, "s");
- }
- }
- } else {
- userLog("Ping returned empty result; throwing IOException");
- throw new IOException();
- }
- } else if (EasResponse.isAuthError(code)) {
- mExitStatus = EXIT_LOGIN_FAILURE;
- userLog("Authorization error during Ping: ", code);
- throw new IOException();
- } else if (EasResponse.isProvisionError(code)) {
- userLog("Provisioning required during Ping: ", code);
- throw new CommandStatusException(CommandStatus.NEEDS_PROVISIONING);
- }
- } finally {
- resp.close();
- }
- } catch (IOException e) {
- String message = e.getMessage();
- // If we get the exception that is indicative of a NAT timeout and if we
- // haven't yet "fixed" the timeout, back off by two minutes and "fix" it
- boolean hasMessage = message != null;
- userLog("IOException runPingLoop: " + (hasMessage ? message : "[no message]"));
- if (mPostReset) {
- // Nothing to do in this case; this is ExchangeService telling us to try
- // another ping.
- } else if (mPostAborted || isLikelyNatFailure(message)) {
- long pingLength = SystemClock.elapsedRealtime() - pingTime;
- if ((pingHeartbeat > mPingMinHeartbeat) &&
- (pingHeartbeat > mPingHighWaterMark)) {
- pingHeartbeat -= PING_HEARTBEAT_INCREMENT;
- mPingHeartbeatDropped = true;
- if (pingHeartbeat < mPingMinHeartbeat) {
- pingHeartbeat = mPingMinHeartbeat;
- }
- userLog("Decreased ping heartbeat to ", pingHeartbeat, "s");
- } else if (mPostAborted) {
- // There's no point in throwing here; this can happen in two cases
- // 1) An alarm, which indicates minutes without activity; no sense
- // backing off
- // 2) ExchangeService abort, due to sync of mailbox. Again, we want to
- // keep on trying to ping
- userLog("Ping aborted; retry");
- } else if (pingLength < 2000) {
- userLog("Abort or NAT type return < 2 seconds; throwing IOException");
- throw e;
- } else {
- userLog("NAT type IOException");
- }
- } else if (hasMessage && message.contains("roken pipe")) {
- // The "broken pipe" error (uppercase or lowercase "b") seems to be an
- // internal error, so let's not throw an exception (which leads to delays)
- // but rather simply run through the loop again
- } else {
- throw e;
- }
- }
- } else if (forcePing) {
- // In this case, there aren't any boxes that are pingable, but there are boxes
- // waiting (for IOExceptions)
- userLog("pingLoop waiting 60s for any pingable boxes");
- sleep(60*SECONDS, true);
- } else if (pushCount > 0) {
- // If we want to Ping, but can't just yet, wait a little bit
- // TODO Change sleep to wait and use notify from ExchangeService when a sync ends
- sleep(2*SECONDS, false);
- pingWaitCount++;
- //userLog("pingLoop waited 2s for: ", (pushCount - canPushCount), " box(es)");
- } else if (uninitCount > 0) {
- // In this case, we're doing an initial sync of at least one mailbox. Since this
- // is typically a one-time case, I'm ok with trying again every 10 seconds until
- // we're in one of the other possible states.
- userLog("pingLoop waiting for initial sync of ", uninitCount, " box(es)");
- sleep(10*SECONDS, true);
- } else if (inboxId == -1) {
- // In this case, we're still syncing mailboxes, so sleep for only a short time
- sleep(45*SECONDS, true);
- } else {
- // We've got nothing to do, so we'll check again in 20 minutes at which time
- // we'll update the folder list, check for policy changes and/or remote wipe, etc.
- // Let the device sleep in the meantime...
- userLog(ACCOUNT_MAILBOX_SLEEP_TEXT);
- sleep(ACCOUNT_MAILBOX_SLEEP_TIME, true);
- }
- }
-
- // Save away the current heartbeat
- mPingHeartbeat = pingHeartbeat;
- }
-
- private void sleep(long ms, boolean runAsleep) {
- if (runAsleep) {
- ExchangeService.runAsleep(mMailboxId, ms+(5*SECONDS));
- }
- try {
- Thread.sleep(ms);
- } catch (InterruptedException e) {
- // Doesn't matter whether we stop early; it's the thought that counts
- } finally {
- if (runAsleep) {
- ExchangeService.runAwake(mMailboxId);
- }
- }
- }
-
- private int parsePingResult(InputStream is, ContentResolver cr,
- HashMap<String, Integer> errorMap)
- throws IOException, StaleFolderListException, IllegalHeartbeatException,
- CommandStatusException {
- PingParser pp = new PingParser(is, this);
- if (pp.parse()) {
- // True indicates some mailboxes need syncing...
- // syncList has the serverId's of the mailboxes...
- mBindArguments[0] = Long.toString(mAccount.mId);
- mPingChangeList = pp.getSyncList();
- for (String serverId: mPingChangeList) {
- mBindArguments[1] = serverId;
- Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
- WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null);
- if (c == null) throw new ProviderUnavailableException();
- try {
- if (c.moveToFirst()) {
-
- /**
- * Check the boxes reporting changes to see if there really were any...
- * We do this because bugs in various Exchange servers can put us into a
- * looping behavior by continually reporting changes in a mailbox, even when
- * there aren't any.
- *
- * This behavior is seemingly random, and therefore we must code defensively
- * by backing off of push behavior when it is detected.
- *
- * One known cause, on certain Exchange 2003 servers, is acknowledged by
- * Microsoft, and the server hotfix for this case can be found at
- * http://support.microsoft.com/kb/923282
- */
-
- // Check the status of the last sync
- String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN);
- int type = ExchangeService.getStatusType(status);
- // This check should always be true...
- if (type == ExchangeService.SYNC_PING) {
- int changeCount = ExchangeService.getStatusChangeCount(status);
- if (changeCount > 0) {
- errorMap.remove(serverId);
- } else if (changeCount == 0) {
- // This means that a ping reported changes in error; we keep a count
- // of consecutive errors of this kind
- String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
- Integer failures = errorMap.get(serverId);
- if (failures == null) {
- userLog("Last ping reported changes in error for: ", name);
- errorMap.put(serverId, 1);
- } else if (failures > MAX_PING_FAILURES) {
- // We'll back off of push for this box
- pushFallback(c.getLong(Mailbox.CONTENT_ID_COLUMN));
- continue;
- } else {
- userLog("Last ping reported changes in error for: ", name);
- errorMap.put(serverId, failures + 1);
- }
- }
- }
-
- // If there were no problems with previous sync, we'll start another one
- ExchangeService.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN),
- ExchangeService.SYNC_PING, null);
- }
- } finally {
- c.close();
- }
- }
- }
- return pp.getSyncStatus();
- }
-
- /**
- * Common code to sync E+PIM data
- *
- * @param target an EasMailbox, EasContacts, or EasCalendar object
- */
- public void sync(AbstractSyncAdapter target) throws IOException {
- Mailbox mailbox = target.mMailbox;
-
- boolean moreAvailable = true;
- int loopingCount = 0;
- while (!mStop && (moreAvailable || hasPendingRequests())) {
- // If we have no connectivity, just exit cleanly. ExchangeService will start us up again
- // when connectivity has returned
- if (!hasConnectivity()) {
- userLog("No connectivity in sync; finishing sync");
- mExitStatus = EXIT_DONE;
- return;
- }
-
- // Every time through the loop we check to see if we're still syncable
- if (!target.isSyncable()) {
- mExitStatus = EXIT_DONE;
- return;
- }
-
- // Now, handle various requests
- while (true) {
- Request req = null;
-
- if (mRequestQueue.isEmpty()) {
- break;
- } else {
- req = mRequestQueue.peek();
- }
-
- // Our two request types are PartRequest (loading attachment) and
- // MeetingResponseRequest (respond to a meeting request)
- if (req instanceof PartRequest) {
- TrafficStats.setThreadStatsTag(
- TrafficFlags.getAttachmentFlags(mContext, mAccount));
- new AttachmentLoader(this, (PartRequest)req).loadAttachment();
- TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, mAccount));
- } else if (req instanceof MeetingResponseRequest) {
- sendMeetingResponse((MeetingResponseRequest)req);
- } else if (req instanceof MessageMoveRequest) {
- messageMoveRequest((MessageMoveRequest)req);
- }
-
- // If there's an exception handling the request, we'll throw it
- // Otherwise, we remove the request
- mRequestQueue.remove();
- }
-
- // Don't sync if we've got nothing to do
- if (!moreAvailable) {
- continue;
- }
-
- Serializer s = new Serializer();
-
- String className = target.getCollectionName();
- String syncKey = target.getSyncKey();
- userLog("sync, sending ", className, " syncKey: ", syncKey);
- s.start(Tags.SYNC_SYNC)
- .start(Tags.SYNC_COLLECTIONS)
- .start(Tags.SYNC_COLLECTION);
- // The "Class" element is removed in EAS 12.1 and later versions
- if (mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
- s.data(Tags.SYNC_CLASS, className);
- }
- s.data(Tags.SYNC_SYNC_KEY, syncKey)
- .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId);
-
- // Start with the default timeout
- int timeout = COMMAND_TIMEOUT;
- if (!syncKey.equals("0")) {
- // EAS doesn't allow GetChanges in an initial sync; sending other options
- // appears to cause the server to delay its response in some cases, and this delay
- // can be long enough to result in an IOException and total failure to sync.
- // Therefore, we don't send any options with the initial sync.
- // Set the truncation amount, body preference, lookback, etc.
- target.sendSyncOptions(mProtocolVersionDouble, s);
- } else {
- // Use enormous timeout for initial sync, which empirically can take a while longer
- timeout = 120*SECONDS;
- }
- // Send our changes up to the server
- if (mUpsyncFailed) {
- if (Eas.USER_LOG) {
- Log.d(TAG, "Inhibiting upsync this cycle");
- }
- } else {
- target.sendLocalChanges(s);
- }
-
- s.end().end().end().done();
- EasResponse resp = sendHttpClientPost("Sync", new ByteArrayEntity(s.toByteArray()),
- timeout);
- try {
- int code = resp.getStatus();
- if (code == HttpStatus.SC_OK) {
- // In EAS 12.1, we can get "empty" sync responses, which indicate that there are
- // no changes in the mailbox; handle that case here
- // There are two cases here; if we get back a compressed stream (GZIP), we won't
- // know until we try to parse it (and generate an EmptyStreamException). If we
- // get uncompressed data, the response will be empty (i.e. have zero length)
- boolean emptyStream = false;
- if (!resp.isEmpty()) {
- InputStream is = resp.getInputStream();
- try {
- moreAvailable = target.parse(is);
- // If we inhibited upsync, we need yet another sync
- if (mUpsyncFailed) {
- moreAvailable = true;
- }
-
- if (target.isLooping()) {
- loopingCount++;
- userLog("** Looping: " + loopingCount);
- // After the maximum number of loops, we'll set moreAvailable to
- // false and allow the sync loop to terminate
- if (moreAvailable && (loopingCount > MAX_LOOPING_COUNT)) {
- userLog("** Looping force stopped");
- moreAvailable = false;
- }
- } else {
- loopingCount = 0;
- }
-
- // Cleanup clears out the updated/deleted tables, and we don't want to
- // do that if our upsync failed; clear the flag otherwise
- if (!mUpsyncFailed) {
- target.cleanup();
- } else {
- mUpsyncFailed = false;
- }
- } catch (EmptyStreamException e) {
- userLog("Empty stream detected in GZIP response");
- emptyStream = true;
- } catch (CommandStatusException e) {
- // TODO 14.1
- int status = e.mStatus;
- if (CommandStatus.isNeedsProvisioning(status)) {
- mExitStatus = EXIT_SECURITY_FAILURE;
- } else if (CommandStatus.isDeniedAccess(status)) {
- mExitStatus = EXIT_ACCESS_DENIED;
- } else if (CommandStatus.isTransientError(status)) {
- mExitStatus = EXIT_IO_ERROR;
- } else {
- mExitStatus = EXIT_EXCEPTION;
- }
- return;
- }
- } else {
- emptyStream = true;
- }
-
- if (emptyStream) {
- // If this happens, exit cleanly, and change the interval from push to ping
- // if necessary
- userLog("Empty sync response; finishing");
- if (mMailbox.mSyncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
- userLog("Changing mailbox from push to ping");
- ContentValues cv = new ContentValues();
- cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PING);
- mContentResolver.update(
- ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId),
- cv, null, null);
- }
- if (mRequestQueue.isEmpty()) {
- mExitStatus = EXIT_DONE;
- return;
- } else {
- continue;
- }
- }
- } else {
- userLog("Sync response error: ", code);
- if (EasResponse.isProvisionError(code)) {
- mExitStatus = EXIT_SECURITY_FAILURE;
- } else if (EasResponse.isAuthError(code)) {
- mExitStatus = EXIT_LOGIN_FAILURE;
- } else {
- mExitStatus = EXIT_IO_ERROR;
- }
- return;
- }
- } finally {
- resp.close();
- }
- }
- mExitStatus = EXIT_DONE;
- }
-
- protected boolean setupService() {
- synchronized(getSynchronizer()) {
- mThread = Thread.currentThread();
- android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
- TAG = mThread.getName();
- }
- // Make sure account and mailbox are always the latest from the database
- mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
- if (mAccount == null) return false;
- mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId);
- if (mMailbox == null) return false;
- HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
- if (ha == null) return false;
- mHostAddress = ha.mAddress;
- mUserName = ha.mLogin;
- mPassword = ha.mPassword;
-
- try {
- setConnectionParameters(
- (ha.mFlags & HostAuth.FLAG_SSL) != 0,
- (ha.mFlags & HostAuth.FLAG_TRUST_ALL) != 0,
- ha.mClientCertAlias);
- } catch (CertificateException e) {
- userLog("Couldn't retrieve certificate for connection");
- try {
- ExchangeService.callback().syncMailboxStatus(mMailboxId,
- EmailServiceStatus.CLIENT_CERTIFICATE_ERROR, 0);
- } catch (RemoteException e1) {
- // Don't care if this fails.
- }
- return false;
- }
-
- // Set up our protocol version from the Account
- mProtocolVersion = mAccount.mProtocolVersion;
- // If it hasn't been set up, start with default version
- if (mProtocolVersion == null) {
- mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
- }
- mProtocolVersionDouble = Eas.getProtocolVersionDouble(mProtocolVersion);
-
- // Do checks to address historical policy sets.
- Policy policy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
- if ((policy != null) && policy.mRequireEncryptionExternal) {
- // External storage encryption is not supported at this time. In a previous release,
- // prior to the system supporting true removable storage on Honeycomb, we accepted
- // this since we emulated external storage on partitions that could be encrypted.
- // If that was set before, we must clear it out now that the system supports true
- // removable storage (which can't be encrypted).
- resetSecurityPolicies();
- }
- return true;
- }
-
- /**
- * Clears out the security policies associated with the account, forcing a provision error
- * and a re-sync of the policy information for the account.
- */
- private void resetSecurityPolicies() {
- ContentValues cv = new ContentValues();
- cv.put(AccountColumns.SECURITY_FLAGS, 0);
- cv.putNull(AccountColumns.SECURITY_SYNC_KEY);
- long accountId = mAccount.mId;
- mContentResolver.update(ContentUris.withAppendedId(
- Account.CONTENT_URI, accountId), cv, null, null);
- SecurityPolicyDelegate.policiesRequired(mContext, accountId);
- }
-
- @Override
- public void run() {
- try {
- // Make sure account and mailbox are still valid
- if (!setupService()) return;
- // If we've been stopped, we're done
- if (mStop) return;
-
- // Whether or not we're the account mailbox
- try {
- mDeviceId = ExchangeService.getDeviceId(mContext);
- int trafficFlags = TrafficFlags.getSyncFlags(mContext, mAccount);
- if ((mMailbox == null) || (mAccount == null)) {
- return;
- } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
- TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_EMAIL);
- runAccountMailbox();
- } else {
- AbstractSyncAdapter target;
- if (mMailbox.mType == Mailbox.TYPE_CONTACTS) {
- TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_CONTACTS);
- target = new ContactsSyncAdapter( this);
- } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) {
- TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_CALENDAR);
- target = new CalendarSyncAdapter(this);
- } else {
- TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_EMAIL);
- target = new EmailSyncAdapter(this);
- }
- // We loop because someone might have put a request in while we were syncing
- // and we've missed that opportunity...
- do {
- if (mRequestTime != 0) {
- userLog("Looping for user request...");
- mRequestTime = 0;
- }
- String syncKey = target.getSyncKey();
- if (mSyncReason >= ExchangeService.SYNC_CALLBACK_START ||
- "0".equals(syncKey)) {
- try {
- ExchangeService.callback().syncMailboxStatus(mMailboxId,
- EmailServiceStatus.IN_PROGRESS, 0);
- } catch (RemoteException e1) {
- // Don't care if this fails
- }
- }
- sync(target);
- } while (mRequestTime != 0);
- }
- } catch (EasAuthenticationException e) {
- userLog("Caught authentication error");
- mExitStatus = EXIT_LOGIN_FAILURE;
- } catch (IOException e) {
- String message = e.getMessage();
- userLog("Caught IOException: ", (message == null) ? "No message" : message);
- mExitStatus = EXIT_IO_ERROR;
- } catch (Exception e) {
- userLog("Uncaught exception in EasSyncService", e);
- } finally {
- int status;
- ExchangeService.done(this);
- if (!mStop) {
- userLog("Sync finished");
- switch (mExitStatus) {
- case EXIT_IO_ERROR:
- status = EmailServiceStatus.CONNECTION_ERROR;
- break;
- case EXIT_DONE:
- status = EmailServiceStatus.SUCCESS;
- ContentValues cv = new ContentValues();
- cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
- String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount;
- cv.put(Mailbox.SYNC_STATUS, s);
- mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI,
- mMailboxId), cv, null, null);
- break;
- case EXIT_LOGIN_FAILURE:
- status = EmailServiceStatus.LOGIN_FAILED;
- break;
- case EXIT_SECURITY_FAILURE:
- status = EmailServiceStatus.SECURITY_FAILURE;
- // Ask for a new folder list. This should wake up the account mailbox; a
- // security error in account mailbox should start provisioning
- ExchangeService.reloadFolderList(mContext, mAccount.mId, true);
- break;
- case EXIT_ACCESS_DENIED:
- status = EmailServiceStatus.ACCESS_DENIED;
- break;
- default:
- status = EmailServiceStatus.REMOTE_EXCEPTION;
- errorLog("Sync ended due to an exception.");
- break;
- }
- } else {
- userLog("Stopped sync finished.");
- status = EmailServiceStatus.SUCCESS;
- }
-
- // Send a callback (doesn't matter how the sync was started)
- try {
- // Unless the user specifically asked for a sync, we don't want to report
- // connection issues, as they are likely to be transient. In this case, we
- // simply report success, so that the progress indicator terminates without
- // putting up an error banner
- if (mSyncReason != ExchangeService.SYNC_UI_REQUEST &&
- status == EmailServiceStatus.CONNECTION_ERROR) {
- status = EmailServiceStatus.SUCCESS;
- }
- ExchangeService.callback().syncMailboxStatus(mMailboxId, status, 0);
- } catch (RemoteException e1) {
- // Don't care if this fails
- }
-
- // Make sure ExchangeService knows about this
- ExchangeService.kick("sync finished");
- }
- } catch (ProviderUnavailableException e) {
- Log.e(TAG, "EmailProvider unavailable; sync ended prematurely");
- }
- }
-}
diff --git a/src/com/android/exchange/EmailSyncAdapterService.java b/src/com/android/exchange/EmailSyncAdapterService.java
deleted file mode 100644
index f91ee13..0000000
--- a/src/com/android/exchange/EmailSyncAdapterService.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-import com.android.emailcommon.provider.EmailContent;
-import com.android.emailcommon.provider.EmailContent.AccountColumns;
-import com.android.emailcommon.provider.EmailContent.MailboxColumns;
-import com.android.emailcommon.provider.Mailbox;
-
-import android.accounts.Account;
-import android.accounts.OperationCanceledException;
-import android.app.Service;
-import android.content.AbstractThreadedSyncAdapter;
-import android.content.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SyncResult;
-import android.database.Cursor;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.util.Log;
-
-public class EmailSyncAdapterService extends Service {
- private static final String TAG = "EAS EmailSyncAdapterService";
- private static SyncAdapterImpl sSyncAdapter = null;
- private static final Object sSyncAdapterLock = new Object();
-
- private static final String[] ID_PROJECTION = new String[] {EmailContent.RECORD_ID};
- private static final String ACCOUNT_AND_TYPE_INBOX =
- MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_INBOX;
-
- public EmailSyncAdapterService() {
- super();
- }
-
- private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
- private Context mContext;
-
- public SyncAdapterImpl(Context context) {
- super(context, true /* autoInitialize */);
- mContext = context;
- }
-
- @Override
- public void onPerformSync(Account account, Bundle extras,
- String authority, ContentProviderClient provider, SyncResult syncResult) {
- try {
- EmailSyncAdapterService.performSync(mContext, account, extras,
- authority, provider, syncResult);
- } catch (OperationCanceledException e) {
- }
- }
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- synchronized (sSyncAdapterLock) {
- if (sSyncAdapter == null) {
- sSyncAdapter = new SyncAdapterImpl(getApplicationContext());
- }
- }
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return sSyncAdapter.getSyncAdapterBinder();
- }
-
- /**
- * Partial integration with system SyncManager; we tell our EAS ExchangeService to start an
- * inbox sync when we get the signal from the system SyncManager.
- */
- private static void performSync(Context context, Account account, Bundle extras,
- String authority, ContentProviderClient provider, SyncResult syncResult)
- throws OperationCanceledException {
- ContentResolver cr = context.getContentResolver();
- Log.i(TAG, "performSync");
-
- // Find the (EmailProvider) account associated with this email address
- Cursor accountCursor =
- cr.query(com.android.emailcommon.provider.Account.CONTENT_URI,
- ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?", new String[] {account.name},
- null);
- try {
- if (accountCursor.moveToFirst()) {
- long accountId = accountCursor.getLong(0);
- // Now, find the inbox associated with the account
- Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, ID_PROJECTION,
- ACCOUNT_AND_TYPE_INBOX, new String[] {Long.toString(accountId)}, null);
- try {
- if (mailboxCursor.moveToFirst()) {
- Log.i(TAG, "Mail sync requested for " + account.name);
- // Ask for a sync from our sync manager
- ExchangeService.serviceRequest(mailboxCursor.getLong(0),
- ExchangeService.SYNC_KICK);
- }
- } finally {
- mailboxCursor.close();
- }
- }
- } finally {
- accountCursor.close();
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/exchange/EmailSyncAlarmReceiver.java b/src/com/android/exchange/EmailSyncAlarmReceiver.java
deleted file mode 100644
index 8006016..0000000
--- a/src/com/android/exchange/EmailSyncAlarmReceiver.java
+++ /dev/null
@@ -1,114 +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 android.content.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.util.Log;
-
-import com.android.emailcommon.provider.EmailContent.Message;
-import com.android.emailcommon.provider.EmailContent.MessageColumns;
-import com.android.emailcommon.provider.ProviderUnavailableException;
-
-import java.util.ArrayList;
-
-/**
- * EmailSyncAlarmReceiver (USAR) is used by the SyncManager to start up-syncs of user-modified data
- * back to the Exchange server.
- *
- * Here's how this works for Email, for example:
- *
- * 1) User modifies or deletes an email from the UI.
- * 2) SyncManager, which has a ContentObserver watching the Message class, is alerted to a change
- * 3) SyncManager sets an alarm (to be received by USAR) for a few seconds in the
- * future (currently 15), the delay preventing excess syncing (think of it as a debounce mechanism).
- * 4) ESAR Receiver's onReceive method is called
- * 5) ESAR goes through all change and deletion records and compiles a list of mailboxes which have
- * changes to be uploaded.
- * 6) ESAR calls SyncManager to start syncs of those mailboxes
- *
- * If EmailProvider isn't available, the upsyncs will happen the next time ExchangeService starts
- *
- */
-public class EmailSyncAlarmReceiver extends BroadcastReceiver {
- final String[] MAILBOX_DATA_PROJECTION = {MessageColumns.MAILBOX_KEY};
-
- @Override
- public void onReceive(final Context context, Intent intent) {
- new Thread(new Runnable() {
- public void run() {
- handleReceive(context);
- }
- }).start();
- }
-
- private void handleReceive(Context context) {
- ArrayList<Long> mailboxesToNotify = new ArrayList<Long>();
- ContentResolver cr = context.getContentResolver();
- int messageCount = 0;
-
- // Get a selector for EAS accounts (we don't want to sync on changes to POP/IMAP messages)
- String selector = ExchangeService.getEasAccountSelector();
-
- try {
- // Find all of the deletions
- Cursor c = cr.query(Message.DELETED_CONTENT_URI, MAILBOX_DATA_PROJECTION, selector,
- null, null);
- if (c == null) throw new ProviderUnavailableException();
- try {
- // Keep track of which mailboxes to notify; we'll only notify each one once
- while (c.moveToNext()) {
- messageCount++;
- long mailboxId = c.getLong(0);
- if (!mailboxesToNotify.contains(mailboxId)) {
- mailboxesToNotify.add(mailboxId);
- }
- }
- } finally {
- c.close();
- }
-
- // Now, find changed messages
- c = cr.query(Message.UPDATED_CONTENT_URI, MAILBOX_DATA_PROJECTION, selector,
- null, null);
- if (c == null) throw new ProviderUnavailableException();
- try {
- // Keep track of which mailboxes to notify; we'll only notify each one once
- while (c.moveToNext()) {
- messageCount++;
- long mailboxId = c.getLong(0);
- if (!mailboxesToNotify.contains(mailboxId)) {
- mailboxesToNotify.add(mailboxId);
- }
- }
- } finally {
- c.close();
- }
-
- // Request service from the mailbox
- for (Long mailboxId: mailboxesToNotify) {
- ExchangeService.serviceRequest(mailboxId, ExchangeService.SYNC_UPSYNC);
- }
- } catch (ProviderUnavailableException e) {
- Log.e("EmailSyncAlarmReceiver", "EmailProvider unavailable; aborting alarm receiver");
- }
- }
-}
diff --git a/src/com/android/exchange/Exchange.java b/src/com/android/exchange/Exchange.java
deleted file mode 100644
index 496e1f5..0000000
--- a/src/com/android/exchange/Exchange.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-import android.app.Application;
-
-public class Exchange extends Application {
- // TODO Investigate whether this class is needed
-}
diff --git a/src/com/android/exchange/ExchangeService.java b/src/com/android/exchange/ExchangeService.java
deleted file mode 100644
index 833e0c4..0000000
--- a/src/com/android/exchange/ExchangeService.java
+++ /dev/null
@@ -1,2583 +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 android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.net.NetworkInfo.State;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Debug;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.PowerManager;
-import android.os.PowerManager.WakeLock;
-import android.os.Process;
-import android.os.RemoteCallbackList;
-import android.os.RemoteException;
-import android.provider.CalendarContract;
-import android.provider.CalendarContract.Calendars;
-import android.provider.CalendarContract.Events;
-import android.provider.ContactsContract;
-import android.util.Log;
-
-import com.android.emailcommon.Api;
-import com.android.emailcommon.TempDirectory;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent;
-import com.android.emailcommon.provider.EmailContent.Attachment;
-import com.android.emailcommon.provider.EmailContent.HostAuthColumns;
-import com.android.emailcommon.provider.EmailContent.MailboxColumns;
-import com.android.emailcommon.provider.EmailContent.Message;
-import com.android.emailcommon.provider.EmailContent.SyncColumns;
-import com.android.emailcommon.provider.HostAuth;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.emailcommon.provider.Policy;
-import com.android.emailcommon.provider.ProviderUnavailableException;
-import com.android.emailcommon.service.AccountServiceProxy;
-import com.android.emailcommon.service.EmailServiceProxy;
-import com.android.emailcommon.service.EmailServiceStatus;
-import com.android.emailcommon.service.IEmailService;
-import com.android.emailcommon.service.IEmailServiceCallback;
-import com.android.emailcommon.service.PolicyServiceProxy;
-import com.android.emailcommon.service.SearchParams;
-import com.android.emailcommon.utility.EmailAsyncTask;
-import com.android.emailcommon.utility.EmailClientConnectionManager;
-import com.android.emailcommon.utility.Utility;
-import com.android.exchange.adapter.CalendarSyncAdapter;
-import com.android.exchange.adapter.ContactsSyncAdapter;
-import com.android.exchange.adapter.Search;
-import com.android.exchange.provider.MailboxUtilities;
-import com.android.exchange.utility.FileLogger;
-
-import org.apache.http.conn.params.ConnManagerPNames;
-import org.apache.http.conn.params.ConnPerRoute;
-import org.apache.http.conn.routing.HttpRoute;
-import org.apache.http.params.BasicHttpParams;
-import org.apache.http.params.HttpParams;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * The ExchangeService handles all aspects of starting, maintaining, and stopping the various sync
- * adapters used by Exchange. However, it is capable of handing any kind of email sync, and it
- * would be appropriate to use for IMAP push, when that functionality is added to the Email
- * application.
- *
- * The Email application communicates with EAS sync adapters via ExchangeService's binder interface,
- * which exposes UI-related functionality to the application (see the definitions below)
- *
- * ExchangeService uses ContentObservers to detect changes to accounts, mailboxes, and messages in
- * order to maintain proper 2-way syncing of data. (More documentation to follow)
- *
- */
-public class ExchangeService extends Service implements Runnable {
-
- private static final String TAG = "ExchangeService";
-
- // The ExchangeService's mailbox "id"
- public static final int EXTRA_MAILBOX_ID = -1;
- public static final int EXCHANGE_SERVICE_MAILBOX_ID = 0;
-
- private static final int SECONDS = 1000;
- private static final int MINUTES = 60*SECONDS;
- private static final int ONE_DAY_MINUTES = 1440;
-
- private static final int EXCHANGE_SERVICE_HEARTBEAT_TIME = 15*MINUTES;
- private static final int CONNECTIVITY_WAIT_TIME = 10*MINUTES;
-
- // Sync hold constants for services with transient errors
- private static final int HOLD_DELAY_MAXIMUM = 4*MINUTES;
-
- // Reason codes when ExchangeService.kick is called (mainly for debugging)
- // UI has changed data, requiring an upsync of changes
- public static final int SYNC_UPSYNC = 0;
- // A scheduled sync (when not using push)
- public static final int SYNC_SCHEDULED = 1;
- // Mailbox was marked push
- public static final int SYNC_PUSH = 2;
- // A ping (EAS push signal) was received
- public static final int SYNC_PING = 3;
- // Misc.
- public static final int SYNC_KICK = 4;
- // A part request (attachment load, for now) was sent to ExchangeService
- public static final int SYNC_SERVICE_PART_REQUEST = 5;
-
- // Requests >= SYNC_CALLBACK_START generate callbacks to the UI
- public static final int SYNC_CALLBACK_START = 6;
- // startSync was requested of ExchangeService (other than due to user request)
- public static final int SYNC_SERVICE_START_SYNC = SYNC_CALLBACK_START + 0;
- // startSync was requested of ExchangeService (due to user request)
- public static final int SYNC_UI_REQUEST = SYNC_CALLBACK_START + 1;
-
- private static final String WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX =
- MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.TYPE + "!=" +
- Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + " and " + MailboxColumns.SYNC_INTERVAL +
- " IN (" + Mailbox.CHECK_INTERVAL_PING + ',' + Mailbox.CHECK_INTERVAL_PUSH + ')';
- protected static final String WHERE_IN_ACCOUNT_AND_PUSHABLE =
- MailboxColumns.ACCOUNT_KEY + "=? and type in (" + Mailbox.TYPE_INBOX + ','
- + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + ',' + Mailbox.TYPE_CONTACTS + ','
- + Mailbox.TYPE_CALENDAR + ')';
- protected static final String WHERE_IN_ACCOUNT_AND_TYPE_INBOX =
- MailboxColumns.ACCOUNT_KEY + "=? and type = " + Mailbox.TYPE_INBOX ;
- private static final String WHERE_MAILBOX_KEY = Message.MAILBOX_KEY + "=?";
- private static final String WHERE_PROTOCOL_EAS = HostAuthColumns.PROTOCOL + "=\"" +
- AbstractSyncService.EAS_PROTOCOL + "\"";
- private static final String WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN =
- "(" + MailboxColumns.TYPE + '=' + Mailbox.TYPE_OUTBOX
- + " or " + MailboxColumns.SYNC_INTERVAL + "!=" + Mailbox.CHECK_INTERVAL_NEVER + ')'
- + " and " + MailboxColumns.ACCOUNT_KEY + " in (";
- private static final String ACCOUNT_KEY_IN = MailboxColumns.ACCOUNT_KEY + " in (";
- private static final String WHERE_CALENDAR_ID = Events.CALENDAR_ID + "=?";
-
- // Offsets into the syncStatus data for EAS that indicate type, exit status, and change count
- // The format is S<type_char>:<exit_char>:<change_count>
- public static final int STATUS_TYPE_CHAR = 1;
- public static final int STATUS_EXIT_CHAR = 3;
- public static final int STATUS_CHANGE_COUNT_OFFSET = 5;
-
- // Ready for ping
- public static final int PING_STATUS_OK = 0;
- // Service already running (can't ping)
- public static final int PING_STATUS_RUNNING = 1;
- // Service waiting after I/O error (can't ping)
- public static final int PING_STATUS_WAITING = 2;
- // Service had a fatal error; can't run
- public static final int PING_STATUS_UNABLE = 3;
-
- private static final int MAX_CLIENT_CONNECTION_MANAGER_SHUTDOWNS = 1;
-
- // We synchronize on this for all actions affecting the service and error maps
- private static final Object sSyncLock = new Object();
- // All threads can use this lock to wait for connectivity
- public static final Object sConnectivityLock = new Object();
- public static boolean sConnectivityHold = false;
-
- // Keeps track of running services (by mailbox id)
- private final HashMap<Long, AbstractSyncService> mServiceMap =
- new HashMap<Long, AbstractSyncService>();
- // Keeps track of services whose last sync ended with an error (by mailbox id)
- /*package*/ ConcurrentHashMap<Long, SyncError> mSyncErrorMap =
- new ConcurrentHashMap<Long, SyncError>();
- // Keeps track of which services require a wake lock (by mailbox id)
- private final HashMap<Long, Boolean> mWakeLocks = new HashMap<Long, Boolean>();
- // Keeps track of PendingIntents for mailbox alarms (by mailbox id)
- private final HashMap<Long, PendingIntent> mPendingIntents = new HashMap<Long, PendingIntent>();
- // The actual WakeLock obtained by ExchangeService
- private WakeLock mWakeLock = null;
- // Keep our cached list of active Accounts here
- public final AccountList mAccountList = new AccountList();
-
- // Observers that we use to look for changed mail-related data
- private final Handler mHandler = new Handler();
- private AccountObserver mAccountObserver;
- private MailboxObserver mMailboxObserver;
- private SyncedMessageObserver mSyncedMessageObserver;
-
- // Concurrent because CalendarSyncAdapter can modify the map during a wipe
- private final ConcurrentHashMap<Long, CalendarObserver> mCalendarObservers =
- new ConcurrentHashMap<Long, CalendarObserver>();
-
- private ContentResolver mResolver;
-
- // The singleton ExchangeService object, with its thread and stop flag
- protected static ExchangeService INSTANCE;
- private static Thread sServiceThread = null;
- // Cached unique device id
- private static String sDeviceId = null;
- // ConnectionManager that all EAS threads can use
- private static EmailClientConnectionManager sClientConnectionManager = null;
- // Count of ClientConnectionManager shutdowns
- private static volatile int sClientConnectionManagerShutdownCount = 0;
-
- private static volatile boolean sStartingUp = false;
- private static volatile boolean sStop = false;
-
- // The reason for ExchangeService's next wakeup call
- private String mNextWaitReason;
- // Whether we have an unsatisfied "kick" pending
- private boolean mKicked = false;
-
- // Receiver of connectivity broadcasts
- private ConnectivityReceiver mConnectivityReceiver = null;
- private ConnectivityReceiver mBackgroundDataSettingReceiver = null;
- private volatile boolean mBackgroundData = true;
- // The most current NetworkInfo (from ConnectivityManager)
- private NetworkInfo mNetworkInfo;
-
- // Callbacks as set up via setCallback
- private final RemoteCallbackList<IEmailServiceCallback> mCallbackList =
- new RemoteCallbackList<IEmailServiceCallback>();
-
- private interface ServiceCallbackWrapper {
- public void call(IEmailServiceCallback cb) throws RemoteException;
- }
-
- /**
- * Proxy that can be used by various sync adapters to tie into ExchangeService's callback system
- * Used this way: ExchangeService.callback().callbackMethod(args...);
- * The proxy wraps checking for existence of a ExchangeService instance
- * Failures of these callbacks can be safely ignored.
- */
- static private final IEmailServiceCallback.Stub sCallbackProxy =
- new IEmailServiceCallback.Stub() {
-
- /**
- * Broadcast a callback to the everyone that's registered
- *
- * @param wrapper the ServiceCallbackWrapper used in the broadcast
- */
- private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) {
- RemoteCallbackList<IEmailServiceCallback> callbackList =
- (INSTANCE == null) ? null: INSTANCE.mCallbackList;
- if (callbackList != null) {
- // Call everyone on our callback list
- int count = callbackList.beginBroadcast();
- try {
- for (int i = 0; i < count; i++) {
- try {
- wrapper.call(callbackList.getBroadcastItem(i));
- } catch (RemoteException e) {
- // Safe to ignore
- } catch (RuntimeException e) {
- // We don't want an exception in one call to prevent other calls, so
- // we'll just log this and continue
- Log.e(TAG, "Caught RuntimeException in broadcast", e);
- }
- }
- } finally {
- // No matter what, we need to finish the broadcast
- callbackList.finishBroadcast();
- }
- }
- }
-
- public void loadAttachmentStatus(final long messageId, final long attachmentId,
- final int status, final int progress) {
- broadcastCallback(new ServiceCallbackWrapper() {
- @Override
- public void call(IEmailServiceCallback cb) throws RemoteException {
- cb.loadAttachmentStatus(messageId, attachmentId, status, progress);
- }
- });
- }
-
- public void sendMessageStatus(final long messageId, final String subject, final int status,
- final int progress) {
- broadcastCallback(new ServiceCallbackWrapper() {
- @Override
- public void call(IEmailServiceCallback cb) throws RemoteException {
- cb.sendMessageStatus(messageId, subject, status, progress);
- }
- });
- }
-
- public void syncMailboxListStatus(final long accountId, final int status,
- final int progress) {
- broadcastCallback(new ServiceCallbackWrapper() {
- @Override
- public void call(IEmailServiceCallback cb) throws RemoteException {
- cb.syncMailboxListStatus(accountId, status, progress);
- }
- });
- }
-
- public void syncMailboxStatus(final long mailboxId, final int status,
- final int progress) {
- broadcastCallback(new ServiceCallbackWrapper() {
- @Override
- public void call(IEmailServiceCallback cb) throws RemoteException {
- cb.syncMailboxStatus(mailboxId, status, progress);
- }
- });
- }
- };
-
- /**
- * Create our EmailService implementation here.
- */
- private final IEmailService.Stub mBinder = new IEmailService.Stub() {
-
- public int getApiLevel() {
- return Api.LEVEL;
- }
-
- public Bundle validate(HostAuth hostAuth) throws RemoteException {
- return AbstractSyncService.validate(EasSyncService.class,
- hostAuth, ExchangeService.this);
- }
-
- public Bundle autoDiscover(String userName, String password) throws RemoteException {
- return new EasSyncService().tryAutodiscover(userName, password);
- }
-
- public void startSync(long mailboxId, boolean userRequest) throws RemoteException {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return;
- checkExchangeServiceServiceRunning();
- Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
- if (m == null) return;
- Account acct = Account.restoreAccountWithId(exchangeService, m.mAccountKey);
- if (acct == null) return;
- // If this is a user request and we're being held, release the hold; this allows us to
- // try again (the hold might have been specific to this account and released already)
- if (userRequest) {
- if (onSyncDisabledHold(acct)) {
- releaseSyncHolds(exchangeService, AbstractSyncService.EXIT_ACCESS_DENIED, acct);
- log("User requested sync of account in sync disabled hold; releasing");
- } else if (onSecurityHold(acct)) {
- releaseSyncHolds(exchangeService, AbstractSyncService.EXIT_SECURITY_FAILURE,
- acct);
- log("User requested sync of account in security hold; releasing");
- }
- if (sConnectivityHold) {
- try {
- // UI is expecting the callbacks....
- sCallbackProxy.syncMailboxStatus(mailboxId, EmailServiceStatus.IN_PROGRESS,
- 0);
- sCallbackProxy.syncMailboxStatus(mailboxId,
- EmailServiceStatus.CONNECTION_ERROR, 0);
- } catch (RemoteException ignore) {
- }
- return;
- }
- }
- if (m.mType == Mailbox.TYPE_OUTBOX) {
- // We're using SERVER_ID to indicate an error condition (it has no other use for
- // sent mail) Upon request to sync the Outbox, we clear this so that all messages
- // are candidates for sending.
- ContentValues cv = new ContentValues();
- cv.put(SyncColumns.SERVER_ID, 0);
- exchangeService.getContentResolver().update(Message.CONTENT_URI,
- cv, WHERE_MAILBOX_KEY, new String[] {Long.toString(mailboxId)});
- // Clear the error state; the Outbox sync will be started from checkMailboxes
- exchangeService.mSyncErrorMap.remove(mailboxId);
- kick("start outbox");
- // Outbox can't be synced in EAS
- return;
- } else if (!isSyncable(m)) {
- try {
- // UI may be expecting the callbacks, so send them
- sCallbackProxy.syncMailboxStatus(mailboxId, EmailServiceStatus.IN_PROGRESS, 0);
- sCallbackProxy.syncMailboxStatus(mailboxId, EmailServiceStatus.SUCCESS, 0);
- } catch (RemoteException ignore) {
- // We tried
- }
- return;
- }
- startManualSync(mailboxId, userRequest ? ExchangeService.SYNC_UI_REQUEST :
- ExchangeService.SYNC_SERVICE_START_SYNC, null);
- }
-
- public void stopSync(long mailboxId) throws RemoteException {
- stopManualSync(mailboxId);
- }
-
- public void loadAttachment(long attachmentId, boolean background) throws RemoteException {
- Attachment att = Attachment.restoreAttachmentWithId(ExchangeService.this, attachmentId);
- log("loadAttachment " + attachmentId + ": " + att.mFileName);
- sendMessageRequest(new PartRequest(att, null, null));
- }
-
- public void updateFolderList(long accountId) throws RemoteException {
- reloadFolderList(ExchangeService.this, accountId, false);
- }
-
- public void hostChanged(long accountId) throws RemoteException {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return;
- ConcurrentHashMap<Long, SyncError> syncErrorMap = exchangeService.mSyncErrorMap;
- // Go through the various error mailboxes
- for (long mailboxId: syncErrorMap.keySet()) {
- SyncError error = syncErrorMap.get(mailboxId);
- // If it's a login failure, look a little harder
- Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
- // If it's for the account whose host has changed, clear the error
- // If the mailbox is no longer around, remove the entry in the map
- if (m == null) {
- syncErrorMap.remove(mailboxId);
- } else if (error != null && m.mAccountKey == accountId) {
- error.fatal = false;
- error.holdEndTime = 0;
- }
- }
- // Stop any running syncs
- exchangeService.stopAccountSyncs(accountId, true);
- // Kick ExchangeService
- kick("host changed");
- }
-
- public void setLogging(int flags) throws RemoteException {
- Eas.setUserDebug(flags);
- }
-
- public void sendMeetingResponse(long messageId, int response) throws RemoteException {
- sendMessageRequest(new MeetingResponseRequest(messageId, response));
- }
-
- public void loadMore(long messageId) throws RemoteException {
- }
-
- // The following three methods are not implemented in this version
- public boolean createFolder(long accountId, String name) throws RemoteException {
- return false;
- }
-
- public boolean deleteFolder(long accountId, String name) throws RemoteException {
- return false;
- }
-
- public boolean renameFolder(long accountId, String oldName, String newName)
- throws RemoteException {
- return false;
- }
-
- public void setCallback(IEmailServiceCallback cb) throws RemoteException {
- mCallbackList.register(cb);
- }
-
- /**
- * Delete PIM (calendar, contacts) data for the specified account
- *
- * @param accountId the account whose data should be deleted
- * @throws RemoteException
- */
- public void deleteAccountPIMData(long accountId) throws RemoteException {
- // Stop any running syncs
- ExchangeService.stopAccountSyncs(accountId);
- // Delete the data
- ExchangeService.deleteAccountPIMData(accountId);
- }
-
- public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return 0;
- return Search.searchMessages(exchangeService, accountId, searchParams,
- destMailboxId);
- }
- };
-
- /**
- * Return a list of all Accounts in EmailProvider. Because the result of this call may be used
- * in account reconciliation, an exception is thrown if the result cannot be guaranteed accurate
- * @param context the caller's context
- * @param accounts a list that Accounts will be added into
- * @return the list of Accounts
- * @throws ProviderUnavailableException if the list of Accounts cannot be guaranteed valid
- */
- private static AccountList collectEasAccounts(Context context, AccountList accounts) {
- ContentResolver resolver = context.getContentResolver();
- Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION, null, null,
- null);
- // We must throw here; callers might use the information we provide for reconciliation, etc.
- if (c == null) throw new ProviderUnavailableException();
- try {
- ContentValues cv = new ContentValues();
- while (c.moveToNext()) {
- long hostAuthId = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
- if (hostAuthId > 0) {
- HostAuth ha = HostAuth.restoreHostAuthWithId(context, hostAuthId);
- if (ha != null && ha.mProtocol.equals("eas")) {
- Account account = new Account();
- account.restore(c);
- // Cache the HostAuth
- account.mHostAuthRecv = ha;
- accounts.add(account);
- // Fixup flags for inbox (should accept moved mail)
- Mailbox inbox = Mailbox.restoreMailboxOfType(context, account.mId,
- Mailbox.TYPE_INBOX);
- if (inbox != null &&
- ((inbox.mFlags & Mailbox.FLAG_ACCEPTS_MOVED_MAIL) == 0)) {
- cv.put(MailboxColumns.FLAGS,
- inbox.mFlags | Mailbox.FLAG_ACCEPTS_MOVED_MAIL);
- resolver.update(
- ContentUris.withAppendedId(Mailbox.CONTENT_URI, inbox.mId), cv,
- null, null);
- }
- }
- }
- }
- } finally {
- c.close();
- }
- return accounts;
- }
-
- static class AccountList extends ArrayList<Account> {
- private static final long serialVersionUID = 1L;
-
- @Override
- public boolean add(Account account) {
- // Cache the account manager account
- account.mAmAccount = new android.accounts.Account(account.mEmailAddress,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
- super.add(account);
- return true;
- }
-
- public boolean contains(long id) {
- for (Account account : this) {
- if (account.mId == id) {
- return true;
- }
- }
- return false;
- }
-
- public Account getById(long id) {
- for (Account account : this) {
- if (account.mId == id) {
- return account;
- }
- }
- return null;
- }
-
- public Account getByName(String accountName) {
- for (Account account : this) {
- if (account.mEmailAddress.equalsIgnoreCase(accountName)) {
- return account;
- }
- }
- return null;
- }
- }
-
- public static void deleteAccountPIMData(long accountId) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return;
- Mailbox mailbox =
- Mailbox.restoreMailboxOfType(exchangeService, accountId, Mailbox.TYPE_CONTACTS);
- if (mailbox != null) {
- EasSyncService service = new EasSyncService(exchangeService, mailbox);
- ContactsSyncAdapter adapter = new ContactsSyncAdapter(service);
- adapter.wipe();
- }
- mailbox =
- Mailbox.restoreMailboxOfType(exchangeService, accountId, Mailbox.TYPE_CALENDAR);
- if (mailbox != null) {
- EasSyncService service = new EasSyncService(exchangeService, mailbox);
- CalendarSyncAdapter adapter = new CalendarSyncAdapter(service);
- adapter.wipe();
- }
- }
-
- private boolean onSecurityHold(Account account) {
- return (account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0;
- }
-
- private boolean onSyncDisabledHold(Account account) {
- return (account.mFlags & Account.FLAGS_SYNC_DISABLED) != 0;
- }
-
- class AccountObserver extends ContentObserver {
- String mSyncableEasMailboxSelector = null;
- String mEasAccountSelector = null;
-
- // Runs when ExchangeService first starts
- public AccountObserver(Handler handler) {
- super(handler);
- // At startup, we want to see what EAS accounts exist and cache them
- // TODO: Move database work out of UI thread
- Context context = getContext();
- synchronized (mAccountList) {
- try {
- collectEasAccounts(context, mAccountList);
- } catch (ProviderUnavailableException e) {
- // Just leave if EmailProvider is unavailable
- return;
- }
- // Create an account mailbox for any account without one
- for (Account account : mAccountList) {
- int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, "accountKey="
- + account.mId, null);
- if (cnt == 0) {
- // This case handles a newly created account
- addAccountMailbox(account.mId);
- }
- }
- }
- // Run through accounts and update account hold information
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- synchronized (mAccountList) {
- for (Account account : mAccountList) {
- if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
- // If we're in a security hold, and our policies are active, release
- // the hold; otherwise, ping PolicyService that this account's
- // policies are required
- if (PolicyServiceProxy.isActive(ExchangeService.this, null)) {
- PolicyServiceProxy.setAccountHoldFlag(ExchangeService.this,
- account, false);
- log("isActive true; release hold for " + account.mDisplayName);
- } else {
- PolicyServiceProxy.policiesRequired(ExchangeService.this,
- account.mId);
- }
- }
- }
- }
- }});
- }
-
- /**
- * Returns a String suitable for appending to a where clause that selects for all syncable
- * mailboxes in all eas accounts
- * @return a complex selection string that is not to be cached
- */
- public String getSyncableEasMailboxWhere() {
- if (mSyncableEasMailboxSelector == null) {
- StringBuilder sb = new StringBuilder(WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN);
- boolean first = true;
- synchronized (mAccountList) {
- for (Account account : mAccountList) {
- if (!first) {
- sb.append(',');
- } else {
- first = false;
- }
- sb.append(account.mId);
- }
- }
- sb.append(')');
- mSyncableEasMailboxSelector = sb.toString();
- }
- return mSyncableEasMailboxSelector;
- }
-
- /**
- * Returns a String suitable for appending to a where clause that selects for all eas
- * accounts.
- * @return a String in the form "accountKey in (a, b, c...)" that is not to be cached
- */
- public String getAccountKeyWhere() {
- if (mEasAccountSelector == null) {
- StringBuilder sb = new StringBuilder(ACCOUNT_KEY_IN);
- boolean first = true;
- synchronized (mAccountList) {
- for (Account account : mAccountList) {
- if (!first) {
- sb.append(',');
- } else {
- first = false;
- }
- sb.append(account.mId);
- }
- }
- sb.append(')');
- mEasAccountSelector = sb.toString();
- }
- return mEasAccountSelector;
- }
-
- private void onAccountChanged() {
- try {
- maybeStartExchangeServiceThread();
- Context context = getContext();
-
- // A change to the list requires us to scan for deletions (stop running syncs)
- // At startup, we want to see what accounts exist and cache them
- AccountList currentAccounts = new AccountList();
- try {
- collectEasAccounts(context, currentAccounts);
- } catch (ProviderUnavailableException e) {
- // Just leave if EmailProvider is unavailable
- return;
- }
- synchronized (mAccountList) {
- for (Account account : mAccountList) {
- boolean accountIncomplete =
- (account.mFlags & Account.FLAGS_INCOMPLETE) != 0;
- // If the current list doesn't include this account and the account wasn't
- // incomplete, then this is a deletion
- if (!currentAccounts.contains(account.mId) && !accountIncomplete) {
- // The implication is that the account has been deleted; let's find out
- alwaysLog("Observer found deleted account: " + account.mDisplayName);
- // Run the reconciler (the reconciliation itself runs in the Email app)
- runAccountReconcilerSync(ExchangeService.this);
- // See if the account is still around
- Account deletedAccount =
- Account.restoreAccountWithId(context, account.mId);
- if (deletedAccount != null) {
- // It is; add it to our account list
- alwaysLog("Account still in provider: " + account.mDisplayName);
- currentAccounts.add(account);
- } else {
- // It isn't; stop syncs and clear our selectors
- alwaysLog("Account deletion confirmed: " + account.mDisplayName);
- stopAccountSyncs(account.mId, true);
- mSyncableEasMailboxSelector = null;
- mEasAccountSelector = null;
- }
- } else {
- // Get the newest version of this account
- Account updatedAccount =
- Account.restoreAccountWithId(context, account.mId);
- if (updatedAccount == null) continue;
- if (account.mSyncInterval != updatedAccount.mSyncInterval
- || account.mSyncLookback != updatedAccount.mSyncLookback) {
- // Set the inbox interval to the interval of the Account
- // This setting should NOT affect other boxes
- ContentValues cv = new ContentValues();
- cv.put(MailboxColumns.SYNC_INTERVAL, updatedAccount.mSyncInterval);
- getContentResolver().update(Mailbox.CONTENT_URI, cv,
- WHERE_IN_ACCOUNT_AND_TYPE_INBOX, new String[] {
- Long.toString(account.mId)
- });
- // Stop all current syncs; the appropriate ones will restart
- log("Account " + account.mDisplayName + " changed; stop syncs");
- stopAccountSyncs(account.mId, true);
- }
-
- // See if this account is no longer on security hold
- if (onSecurityHold(account) && !onSecurityHold(updatedAccount)) {
- releaseSyncHolds(ExchangeService.this,
- AbstractSyncService.EXIT_SECURITY_FAILURE, account);
- }
-
- // Put current values into our cached account
- account.mSyncInterval = updatedAccount.mSyncInterval;
- account.mSyncLookback = updatedAccount.mSyncLookback;
- account.mFlags = updatedAccount.mFlags;
- }
- }
- // Look for new accounts
- for (Account account : currentAccounts) {
- if (!mAccountList.contains(account.mId)) {
- // Don't forget to cache the HostAuth
- HostAuth ha = HostAuth.restoreHostAuthWithId(getContext(),
- account.mHostAuthKeyRecv);
- if (ha == null) continue;
- account.mHostAuthRecv = ha;
- // This is an addition; create our magic hidden mailbox...
- log("Account observer found new account: " + account.mDisplayName);
- addAccountMailbox(account.mId);
- mAccountList.add(account);
- mSyncableEasMailboxSelector = null;
- mEasAccountSelector = null;
- }
- }
- // Finally, make sure our account list is up to date
- mAccountList.clear();
- mAccountList.addAll(currentAccounts);
- }
-
- // See if there's anything to do...
- kick("account changed");
- } catch (ProviderUnavailableException e) {
- alwaysLog("Observer failed; provider unavailable");
- }
- }
-
- @Override
- public void onChange(boolean selfChange) {
- new Thread(new Runnable() {
- public void run() {
- onAccountChanged();
- }}, "Account Observer").start();
- }
-
- private void addAccountMailbox(long acctId) {
- Account acct = Account.restoreAccountWithId(getContext(), acctId);
- Mailbox main = new Mailbox();
- main.mDisplayName = Eas.ACCOUNT_MAILBOX_PREFIX;
- main.mServerId = Eas.ACCOUNT_MAILBOX_PREFIX + System.nanoTime();
- main.mAccountKey = acct.mId;
- main.mType = Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
- main.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH;
- main.mFlagVisible = false;
- main.save(getContext());
- log("Initializing account: " + acct.mDisplayName);
- }
-
- }
-
- /**
- * Register a specific Calendar's data observer; we need to recognize when the SYNC_EVENTS
- * column has changed (when sync has turned off or on)
- * @param account the Account whose Calendar we're observing
- */
- private void registerCalendarObserver(Account account) {
- // Get a new observer
- CalendarObserver observer = new CalendarObserver(mHandler, account);
- if (observer.mCalendarId != 0) {
- // If we find the Calendar (and we'd better) register it and store it in the map
- mCalendarObservers.put(account.mId, observer);
- mResolver.registerContentObserver(
- ContentUris.withAppendedId(Calendars.CONTENT_URI, observer.mCalendarId), false,
- observer);
- }
- }
-
- /**
- * Unregister all CalendarObserver's
- */
- static public void unregisterCalendarObservers() {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return;
- ContentResolver resolver = exchangeService.mResolver;
- for (CalendarObserver observer: exchangeService.mCalendarObservers.values()) {
- resolver.unregisterContentObserver(observer);
- }
- exchangeService.mCalendarObservers.clear();
- }
-
- /**
- * Return the syncable state of an account's calendar, as determined by the sync_events column
- * of our Calendar (from CalendarProvider2)
- * Note that the current state of sync_events is cached in our CalendarObserver
- * @param accountId the id of the account whose calendar we are checking
- * @return whether or not syncing of events is enabled
- */
- private boolean isCalendarEnabled(long accountId) {
- CalendarObserver observer = mCalendarObservers.get(accountId);
- if (observer != null) {
- return (observer.mSyncEvents == 1);
- }
- // If there's no observer, there's no Calendar in CalendarProvider2, so we return true
- // to allow Calendar creation
- return true;
- }
-
- private class CalendarObserver extends ContentObserver {
- long mAccountId;
- long mCalendarId;
- long mSyncEvents;
- String mAccountName;
-
- public CalendarObserver(Handler handler, Account account) {
- super(handler);
- mAccountId = account.mId;
- mAccountName = account.mEmailAddress;
-
- // Find the Calendar for this account
- Cursor c = mResolver.query(Calendars.CONTENT_URI,
- new String[] {Calendars._ID, Calendars.SYNC_EVENTS},
- CalendarSyncAdapter.CALENDAR_SELECTION,
- new String[] {account.mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE},
- null);
- if (c != null) {
- // Save its id and its sync events status
- try {
- if (c.moveToFirst()) {
- mCalendarId = c.getLong(0);
- mSyncEvents = c.getLong(1);
- }
- } finally {
- c.close();
- }
- }
- }
-
- @Override
- public synchronized void onChange(boolean selfChange) {
- // See if the user has changed syncing of our calendar
- if (!selfChange) {
- new Thread(new Runnable() {
- public void run() {
- try {
- Cursor c = mResolver.query(Calendars.CONTENT_URI,
- new String[] {Calendars.SYNC_EVENTS}, Calendars._ID + "=?",
- new String[] {Long.toString(mCalendarId)}, null);
- if (c == null) return;
- // Get its sync events; if it's changed, we've got work to do
- try {
- if (c.moveToFirst()) {
- long newSyncEvents = c.getLong(0);
- if (newSyncEvents != mSyncEvents) {
- log("_sync_events changed for calendar in " + mAccountName);
- Mailbox mailbox = Mailbox.restoreMailboxOfType(INSTANCE,
- mAccountId, Mailbox.TYPE_CALENDAR);
- // Sanity check for mailbox deletion
- if (mailbox == null) return;
- ContentValues cv = new ContentValues();
- if (newSyncEvents == 0) {
- // When sync is disabled, we're supposed to delete
- // all events in the calendar
- log("Deleting events and setting syncKey to 0 for " +
- mAccountName);
- // First, stop any sync that's ongoing
- stopManualSync(mailbox.mId);
- // Set the syncKey to 0 (reset)
- EasSyncService service =
- new EasSyncService(INSTANCE, mailbox);
- CalendarSyncAdapter adapter =
- new CalendarSyncAdapter(service);
- try {
- adapter.setSyncKey("0", false);
- } catch (IOException e) {
- // The provider can't be reached; nothing to be done
- }
- // Reset the sync key locally and stop syncing
- cv.put(Mailbox.SYNC_KEY, "0");
- cv.put(Mailbox.SYNC_INTERVAL,
- Mailbox.CHECK_INTERVAL_NEVER);
- mResolver.update(ContentUris.withAppendedId(
- Mailbox.CONTENT_URI, mailbox.mId), cv, null,
- null);
- // Delete all events using the sync adapter
- // parameter so that the deletion is only local
- Uri eventsAsSyncAdapter =
- CalendarSyncAdapter.asSyncAdapter(
- Events.CONTENT_URI,
- mAccountName,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
- mResolver.delete(eventsAsSyncAdapter, WHERE_CALENDAR_ID,
- new String[] {Long.toString(mCalendarId)});
- } else {
- // Make this a push mailbox and kick; this will start
- // a resync of the Calendar; the account mailbox will
- // ping on this during the next cycle of the ping loop
- cv.put(Mailbox.SYNC_INTERVAL,
- Mailbox.CHECK_INTERVAL_PUSH);
- mResolver.update(ContentUris.withAppendedId(
- Mailbox.CONTENT_URI, mailbox.mId), cv, null,
- null);
- kick("calendar sync changed");
- }
-
- // Save away the new value
- mSyncEvents = newSyncEvents;
- }
- }
- } finally {
- c.close();
- }
- } catch (ProviderUnavailableException e) {
- Log.w(TAG, "Observer failed; provider unavailable");
- }
- }}, "Calendar Observer").start();
- }
- }
- }
-
- private class MailboxObserver extends ContentObserver {
- public MailboxObserver(Handler handler) {
- super(handler);
- }
-
- @Override
- public void onChange(boolean selfChange) {
- // See if there's anything to do...
- if (!selfChange) {
- kick("mailbox changed");
- }
- }
- }
-
- private class SyncedMessageObserver extends ContentObserver {
- Intent syncAlarmIntent = new Intent(INSTANCE, EmailSyncAlarmReceiver.class);
- PendingIntent syncAlarmPendingIntent =
- PendingIntent.getBroadcast(INSTANCE, 0, syncAlarmIntent, 0);
- AlarmManager alarmManager = (AlarmManager)INSTANCE.getSystemService(Context.ALARM_SERVICE);
-
- public SyncedMessageObserver(Handler handler) {
- super(handler);
- }
-
- @Override
- public void onChange(boolean selfChange) {
- alarmManager.set(AlarmManager.RTC_WAKEUP,
- System.currentTimeMillis() + 10*SECONDS, syncAlarmPendingIntent);
- }
- }
-
- static public IEmailServiceCallback callback() {
- return sCallbackProxy;
- }
-
- static public Account getAccountById(long accountId) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService != null) {
- AccountList accountList = exchangeService.mAccountList;
- synchronized (accountList) {
- return accountList.getById(accountId);
- }
- }
- return null;
- }
-
- static public Account getAccountByName(String accountName) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService != null) {
- AccountList accountList = exchangeService.mAccountList;
- synchronized (accountList) {
- return accountList.getByName(accountName);
- }
- }
- return null;
- }
-
- static public String getEasAccountSelector() {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService != null && exchangeService.mAccountObserver != null) {
- return exchangeService.mAccountObserver.getAccountKeyWhere();
- }
- return null;
- }
-
- public class SyncStatus {
- static public final int NOT_RUNNING = 0;
- static public final int DIED = 1;
- static public final int SYNC = 2;
- static public final int IDLE = 3;
- }
-
- /*package*/ class SyncError {
- int reason;
- boolean fatal = false;
- long holdDelay = 15*SECONDS;
- long holdEndTime = System.currentTimeMillis() + holdDelay;
-
- SyncError(int _reason, boolean _fatal) {
- reason = _reason;
- fatal = _fatal;
- }
-
- /**
- * We double the holdDelay from 15 seconds through 4 mins
- */
- void escalate() {
- if (holdDelay < HOLD_DELAY_MAXIMUM) {
- holdDelay *= 2;
- }
- holdEndTime = System.currentTimeMillis() + holdDelay;
- }
- }
-
- private void logSyncHolds() {
- if (Eas.USER_LOG) {
- log("Sync holds:");
- long time = System.currentTimeMillis();
- for (long mailboxId : mSyncErrorMap.keySet()) {
- Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
- if (m == null) {
- log("Mailbox " + mailboxId + " no longer exists");
- } else {
- SyncError error = mSyncErrorMap.get(mailboxId);
- if (error != null) {
- log("Mailbox " + m.mDisplayName + ", error = " + error.reason
- + ", fatal = " + error.fatal);
- if (error.holdEndTime > 0) {
- log("Hold ends in " + ((error.holdEndTime - time) / 1000) + "s");
- }
- }
- }
- }
- }
- }
-
- /**
- * Release security holds for the specified account
- * @param account the account whose Mailboxes should be released from security hold
- */
- static public void releaseSecurityHold(Account account) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService != null) {
- exchangeService.releaseSyncHolds(INSTANCE, AbstractSyncService.EXIT_SECURITY_FAILURE,
- account);
- }
- }
-
- /**
- * Release a specific type of hold (the reason) for the specified Account; if the account
- * is null, mailboxes from all accounts with the specified hold will be released
- * @param reason the reason for the SyncError (AbstractSyncService.EXIT_XXX)
- * @param account an Account whose mailboxes should be released (or all if null)
- * @return whether or not any mailboxes were released
- */
- /*package*/ boolean releaseSyncHolds(Context context, int reason, Account account) {
- boolean holdWasReleased = releaseSyncHoldsImpl(context, reason, account);
- kick("security release");
- return holdWasReleased;
- }
-
- private boolean releaseSyncHoldsImpl(Context context, int reason, Account account) {
- boolean holdWasReleased = false;
- for (long mailboxId: mSyncErrorMap.keySet()) {
- if (account != null) {
- Mailbox m = Mailbox.restoreMailboxWithId(context, mailboxId);
- if (m == null) {
- mSyncErrorMap.remove(mailboxId);
- } else if (m.mAccountKey != account.mId) {
- continue;
- }
- }
- SyncError error = mSyncErrorMap.get(mailboxId);
- if (error != null && error.reason == reason) {
- mSyncErrorMap.remove(mailboxId);
- holdWasReleased = true;
- }
- }
- return holdWasReleased;
- }
-
- /**
- * Reconcile Exchange accounts with AccountManager (asynchronous)
- * @param context the caller's Context
- */
- public static void reconcileAccounts(final Context context) {
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService != null) {
- exchangeService.runAccountReconcilerSync(context);
- }
- }});
- }
-
- /**
- * Blocking call to the account reconciler
- */
- public static void runAccountReconcilerSync(Context context) {
- alwaysLog("Reconciling accounts...");
- new AccountServiceProxy(context).reconcileAccounts(
- HostAuth.SCHEME_EAS, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
- }
-
- public static void log(String str) {
- log(TAG, str);
- }
-
- public static void log(String tag, String str) {
- if (Eas.USER_LOG) {
- Log.d(tag, str);
- if (Eas.FILE_LOG) {
- FileLogger.log(tag, str);
- }
- }
- }
-
- public static void alwaysLog(String str) {
- if (!Eas.USER_LOG) {
- Log.d(TAG, str);
- } else {
- log(str);
- }
- }
-
- /**
- * EAS requires a unique device id, so that sync is possible from a variety of different
- * devices (e.g. the syncKey is specific to a device) If we're on an emulator or some other
- * device that doesn't provide one, we can create it as "device".
- * This would work on a real device as well, but it would be better to use the "real" id if
- * it's available
- */
- static public String getDeviceId(Context context) throws IOException {
- if (sDeviceId == null) {
- sDeviceId = new AccountServiceProxy(context).getDeviceId();
- alwaysLog("Received deviceId from Email app: " + sDeviceId);
- }
- return sDeviceId;
- }
-
- @Override
- public IBinder onBind(Intent arg0) {
- return mBinder;
- }
-
- static public ConnPerRoute sConnPerRoute = new ConnPerRoute() {
- public int getMaxForRoute(HttpRoute route) {
- return 8;
- }
- };
-
- static public synchronized EmailClientConnectionManager getClientConnectionManager() {
- if (sClientConnectionManager == null) {
- // After two tries, kill the process. Most likely, this will happen in the background
- // The service will restart itself after about 5 seconds
- if (sClientConnectionManagerShutdownCount > MAX_CLIENT_CONNECTION_MANAGER_SHUTDOWNS) {
- alwaysLog("Shutting down process to unblock threads");
- Process.killProcess(Process.myPid());
- }
- HttpParams params = new BasicHttpParams();
- params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 25);
- params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, sConnPerRoute);
- sClientConnectionManager = EmailClientConnectionManager.newInstance(params);
- }
- // Null is a valid return result if we get an exception
- return sClientConnectionManager;
- }
-
- static private synchronized void shutdownConnectionManager() {
- if (sClientConnectionManager != null) {
- log("Shutting down ClientConnectionManager");
- sClientConnectionManager.shutdown();
- sClientConnectionManagerShutdownCount++;
- sClientConnectionManager = null;
- }
- }
-
- public static void stopAccountSyncs(long acctId) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService != null) {
- exchangeService.stopAccountSyncs(acctId, true);
- }
- }
-
- private void stopAccountSyncs(long acctId, boolean includeAccountMailbox) {
- synchronized (sSyncLock) {
- List<Long> deletedBoxes = new ArrayList<Long>();
- for (Long mid : mServiceMap.keySet()) {
- Mailbox box = Mailbox.restoreMailboxWithId(this, mid);
- if (box != null) {
- if (box.mAccountKey == acctId) {
- if (!includeAccountMailbox &&
- box.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
- AbstractSyncService svc = mServiceMap.get(mid);
- if (svc != null) {
- svc.stop();
- }
- continue;
- }
- AbstractSyncService svc = mServiceMap.get(mid);
- if (svc != null) {
- svc.stop();
- Thread t = svc.mThread;
- if (t != null) {
- t.interrupt();
- }
- }
- deletedBoxes.add(mid);
- }
- }
- }
- for (Long mid : deletedBoxes) {
- releaseMailbox(mid);
- }
- }
- }
-
- static private void reloadFolderListFailed(long accountId) {
- try {
- callback().syncMailboxListStatus(accountId,
- EmailServiceStatus.ACCOUNT_UNINITIALIZED, 0);
- } catch (RemoteException e1) {
- // Don't care if this fails
- }
- }
-
- static public void reloadFolderList(Context context, long accountId, boolean force) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return;
- Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
- Mailbox.CONTENT_PROJECTION, MailboxColumns.ACCOUNT_KEY + "=? AND " +
- MailboxColumns.TYPE + "=?",
- new String[] {Long.toString(accountId),
- Long.toString(Mailbox.TYPE_EAS_ACCOUNT_MAILBOX)}, null);
- try {
- if (c.moveToFirst()) {
- synchronized(sSyncLock) {
- Mailbox mailbox = new Mailbox();
- mailbox.restore(c);
- Account acct = Account.restoreAccountWithId(context, accountId);
- if (acct == null) {
- reloadFolderListFailed(accountId);
- return;
- }
- String syncKey = acct.mSyncKey;
- // No need to reload the list if we don't have one
- if (!force && (syncKey == null || syncKey.equals("0"))) {
- reloadFolderListFailed(accountId);
- return;
- }
-
- // Change all ping/push boxes to push/hold
- ContentValues cv = new ContentValues();
- cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH_HOLD);
- context.getContentResolver().update(Mailbox.CONTENT_URI, cv,
- WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX,
- new String[] {Long.toString(accountId)});
- log("Set push/ping boxes to push/hold");
-
- long id = mailbox.mId;
- AbstractSyncService svc = exchangeService.mServiceMap.get(id);
- // Tell the service we're done
- if (svc != null) {
- synchronized (svc.getSynchronizer()) {
- svc.stop();
- // Interrupt the thread so that it can stop
- Thread thread = svc.mThread;
- if (thread != null) {
- thread.setName(thread.getName() + " (Stopped)");
- thread.interrupt();
- }
- }
- // Abandon the service
- exchangeService.releaseMailbox(id);
- // And have it start naturally
- kick("reload folder list");
- }
- }
- }
- } finally {
- c.close();
- }
- }
-
- /**
- * Informs ExchangeService that an account has a new folder list; as a result, any existing
- * folder might have become invalid. Therefore, we act as if the account has been deleted, and
- * then we reinitialize it.
- *
- * @param acctId
- */
- static public void stopNonAccountMailboxSyncsForAccount(long acctId) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService != null) {
- exchangeService.stopAccountSyncs(acctId, false);
- kick("reload folder list");
- }
- }
-
- private void acquireWakeLock(long id) {
- synchronized (mWakeLocks) {
- Boolean lock = mWakeLocks.get(id);
- if (lock == null) {
- if (mWakeLock == null) {
- PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
- mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MAIL_SERVICE");
- mWakeLock.acquire();
- //log("+WAKE LOCK ACQUIRED");
- }
- mWakeLocks.put(id, true);
- }
- }
- }
-
- private void releaseWakeLock(long id) {
- synchronized (mWakeLocks) {
- Boolean lock = mWakeLocks.get(id);
- if (lock != null) {
- mWakeLocks.remove(id);
- if (mWakeLocks.isEmpty()) {
- if (mWakeLock != null) {
- mWakeLock.release();
- }
- mWakeLock = null;
- //log("+WAKE LOCK RELEASED");
- } else {
- }
- }
- }
- }
-
- static public String alarmOwner(long id) {
- if (id == EXTRA_MAILBOX_ID) {
- return "ExchangeService";
- } else {
- String name = Long.toString(id);
- if (Eas.USER_LOG && INSTANCE != null) {
- Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, id);
- if (m != null) {
- name = m.mDisplayName + '(' + m.mAccountKey + ')';
- }
- }
- return "Mailbox " + name;
- }
- }
-
- private void clearAlarm(long id) {
- synchronized (mPendingIntents) {
- PendingIntent pi = mPendingIntents.get(id);
- if (pi != null) {
- AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
- alarmManager.cancel(pi);
- //log("+Alarm cleared for " + alarmOwner(id));
- mPendingIntents.remove(id);
- }
- }
- }
-
- private void setAlarm(long id, long millis) {
- synchronized (mPendingIntents) {
- PendingIntent pi = mPendingIntents.get(id);
- if (pi == null) {
- Intent i = new Intent(this, MailboxAlarmReceiver.class);
- i.putExtra("mailbox", id);
- i.setData(Uri.parse("Box" + id));
- pi = PendingIntent.getBroadcast(this, 0, i, 0);
- mPendingIntents.put(id, pi);
-
- AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
- alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + millis, pi);
- //log("+Alarm set for " + alarmOwner(id) + ", " + millis/1000 + "s");
- }
- }
- }
-
- private void clearAlarms() {
- AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
- synchronized (mPendingIntents) {
- for (PendingIntent pi : mPendingIntents.values()) {
- alarmManager.cancel(pi);
- }
- mPendingIntents.clear();
- }
- }
-
- static public void runAwake(long id) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService != null) {
- exchangeService.acquireWakeLock(id);
- exchangeService.clearAlarm(id);
- }
- }
-
- static public void runAsleep(long id, long millis) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService != null) {
- exchangeService.setAlarm(id, millis);
- exchangeService.releaseWakeLock(id);
- }
- }
-
- static public void clearWatchdogAlarm(long id) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService != null) {
- exchangeService.clearAlarm(id);
- }
- }
-
- static public void setWatchdogAlarm(long id, long millis) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService != null) {
- exchangeService.setAlarm(id, millis);
- }
- }
-
- static public void alert(Context context, final long id) {
- final ExchangeService exchangeService = INSTANCE;
- checkExchangeServiceServiceRunning();
- if (id < 0) {
- log("ExchangeService alert");
- kick("ping ExchangeService");
- } else if (exchangeService == null) {
- context.startService(new Intent(context, ExchangeService.class));
- } else {
- final AbstractSyncService service = exchangeService.mServiceMap.get(id);
- if (service != null) {
- // Handle alerts in a background thread, as we are typically called from a
- // broadcast receiver, and are therefore running in the UI thread
- String threadName = "ExchangeService Alert: ";
- if (service.mMailbox != null) {
- threadName += service.mMailbox.mDisplayName;
- }
- new Thread(new Runnable() {
- public void run() {
- Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, id);
- if (m != null) {
- // We ignore drafts completely (doesn't sync). Changes in Outbox are
- // handled in the checkMailboxes loop, so we can ignore these pings.
- if (Eas.USER_LOG) {
- Log.d(TAG, "Alert for mailbox " + id + " (" + m.mDisplayName + ")");
- }
- if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) {
- String[] args = new String[] {Long.toString(m.mId)};
- ContentResolver resolver = INSTANCE.mResolver;
- resolver.delete(Message.DELETED_CONTENT_URI, WHERE_MAILBOX_KEY,
- args);
- resolver.delete(Message.UPDATED_CONTENT_URI, WHERE_MAILBOX_KEY,
- args);
- return;
- }
- service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey);
- service.mMailbox = m;
- // Send the alarm to the sync service
- if (!service.alarm()) {
- // A false return means that we were forced to interrupt the thread
- // In this case, we release the mailbox so that we can start another
- // thread to do the work
- log("Alarm failed; releasing mailbox");
- synchronized(sSyncLock) {
- exchangeService.releaseMailbox(id);
- }
- // Shutdown the connection manager; this should close all of our
- // sockets and generate IOExceptions all around.
- ExchangeService.shutdownConnectionManager();
- }
- }
- }}, threadName).start();
- }
- }
- }
-
- public class ConnectivityReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
- Bundle b = intent.getExtras();
- if (b != null) {
- NetworkInfo a = (NetworkInfo)b.get(ConnectivityManager.EXTRA_NETWORK_INFO);
- String info = "Connectivity alert for " + a.getTypeName();
- State state = a.getState();
- if (state == State.CONNECTED) {
- info += " CONNECTED";
- log(info);
- synchronized (sConnectivityLock) {
- sConnectivityLock.notifyAll();
- }
- kick("connected");
- } else if (state == State.DISCONNECTED) {
- info += " DISCONNECTED";
- log(info);
- kick("disconnected");
- }
- }
- } else if (intent.getAction().equals(
- ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED)) {
- ConnectivityManager cm =
- (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
- mBackgroundData = cm.getBackgroundDataSetting();
- // If background data is now on, we want to kick ExchangeService
- if (mBackgroundData) {
- kick("background data on");
- log("Background data on; restart syncs");
- // Otherwise, stop all syncs
- } else {
- log("Background data off: stop all syncs");
- EmailAsyncTask.runAsyncParallel(new Runnable() {
- @Override
- public void run() {
- synchronized (mAccountList) {
- for (Account account : mAccountList)
- ExchangeService.stopAccountSyncs(account.mId);
- }
- }});
- }
- }
- }
- }
-
- /**
- * Starts a service thread and enters it into the service map
- * This is the point of instantiation of all sync threads
- * @param service the service to start
- * @param m the Mailbox on which the service will operate
- */
- private void startServiceThread(AbstractSyncService service, Mailbox m) {
- if (m == null) return;
- synchronized (sSyncLock) {
- String mailboxName = m.mDisplayName;
- String accountName = service.mAccount.mDisplayName;
- Thread thread = new Thread(service, mailboxName + "[" + accountName + "]");
- log("Starting thread for " + mailboxName + " in account " + accountName);
- thread.start();
- mServiceMap.put(m.mId, service);
- runAwake(m.mId);
- if ((m.mServerId != null) && !m.mServerId.startsWith(Eas.ACCOUNT_MAILBOX_PREFIX)) {
- stopPing(m.mAccountKey);
- }
- }
- }
-
- /**
- * Stop any ping in progress for the given account
- * @param accountId
- */
- private void stopPing(long accountId) {
- // Go through our active mailboxes looking for the right one
- synchronized (sSyncLock) {
- for (long mailboxId: mServiceMap.keySet()) {
- Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
- if (m != null) {
- String serverId = m.mServerId;
- if (m.mAccountKey == accountId && serverId != null &&
- serverId.startsWith(Eas.ACCOUNT_MAILBOX_PREFIX)) {
- // Here's our account mailbox; reset him (stopping pings)
- AbstractSyncService svc = mServiceMap.get(mailboxId);
- svc.reset();
- }
- }
- }
- }
- }
-
- private void requestSync(Mailbox m, int reason, Request req) {
- // Don't sync if there's no connectivity
- if (sConnectivityHold || (m == null) || sStop) {
- if (reason >= SYNC_CALLBACK_START) {
- try {
- sCallbackProxy.syncMailboxStatus(m.mId, EmailServiceStatus.CONNECTION_ERROR, 0);
- } catch (RemoteException e) {
- // We tried...
- }
- }
- return;
- }
- synchronized (sSyncLock) {
- Account acct = Account.restoreAccountWithId(this, m.mAccountKey);
- if (acct != null) {
- // Always make sure there's not a running instance of this service
- AbstractSyncService service = mServiceMap.get(m.mId);
- if (service == null) {
- service = new EasSyncService(this, m);
- if (!((EasSyncService)service).mIsValid) return;
- service.mSyncReason = reason;
- if (req != null) {
- service.addRequest(req);
- }
- startServiceThread(service, m);
- }
- }
- }
- }
-
- private void stopServiceThreads() {
- synchronized (sSyncLock) {
- ArrayList<Long> toStop = new ArrayList<Long>();
-
- // Keep track of which services to stop
- for (Long mailboxId : mServiceMap.keySet()) {
- toStop.add(mailboxId);
- }
-
- // Shut down all of those running services
- for (Long mailboxId : toStop) {
- AbstractSyncService svc = mServiceMap.get(mailboxId);
- if (svc != null) {
- log("Stopping " + svc.mAccount.mDisplayName + '/' + svc.mMailbox.mDisplayName);
- svc.stop();
- if (svc.mThread != null) {
- svc.mThread.interrupt();
- }
- }
- releaseWakeLock(mailboxId);
- }
- }
- }
-
- private void waitForConnectivity() {
- boolean waiting = false;
- ConnectivityManager cm =
- (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
- while (!sStop) {
- NetworkInfo info = cm.getActiveNetworkInfo();
- if (info != null) {
- mNetworkInfo = info;
- // We're done if there's an active network
- if (waiting) {
- // If we've been waiting, release any I/O error holds
- releaseSyncHolds(this, AbstractSyncService.EXIT_IO_ERROR, null);
- // And log what's still being held
- logSyncHolds();
- }
- return;
- } else {
- // If this is our first time through the loop, shut down running service threads
- if (!waiting) {
- waiting = true;
- stopServiceThreads();
- }
- // Wait until a network is connected (or 10 mins), but let the device sleep
- // We'll set an alarm just in case we don't get notified (bugs happen)
- synchronized (sConnectivityLock) {
- runAsleep(EXTRA_MAILBOX_ID, CONNECTIVITY_WAIT_TIME+5*SECONDS);
- try {
- log("Connectivity lock...");
- sConnectivityHold = true;
- sConnectivityLock.wait(CONNECTIVITY_WAIT_TIME);
- log("Connectivity lock released...");
- } catch (InterruptedException e) {
- // This is fine; we just go around the loop again
- } finally {
- sConnectivityHold = false;
- }
- runAwake(EXTRA_MAILBOX_ID);
- }
- }
- }
- }
-
- /**
- * Note that there are two ways the EAS ExchangeService service can be created:
- *
- * 1) as a background service instantiated via startService (which happens on boot, when the
- * first EAS account is created, etc), in which case the service thread is spun up, mailboxes
- * sync, etc. and
- * 2) to execute an RPC call from the UI, in which case the background service will already be
- * running most of the time (unless we're creating a first EAS account)
- *
- * If the running background service detects that there are no EAS accounts (on boot, if none
- * were created, or afterward if the last remaining EAS account is deleted), it will call
- * stopSelf() to terminate operation.
- *
- * The goal is to ensure that the background service is running at all times when there is at
- * least one EAS account in existence
- *
- * Because there are edge cases in which our process can crash (typically, this has been seen
- * in UI crashes, ANR's, etc.), it's possible for the UI to start up again without the
- * background service having been started. We explicitly try to start the service in Welcome
- * (to handle the case of the app having been reloaded). We also start the service on any
- * startSync call (if it isn't already running)
- */
- @Override
- public void onCreate() {
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- // Quick checks first, before getting the lock
- if (sStartingUp) return;
- synchronized (sSyncLock) {
- alwaysLog("!!! EAS ExchangeService, onCreate");
- // Try to start up properly; we might be coming back from a crash that the Email
- // application isn't aware of.
- startService(new Intent(EmailServiceProxy.EXCHANGE_INTENT));
- if (sStop) {
- return;
- }
- }
- }});
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- alwaysLog("!!! EAS ExchangeService, onStartCommand, startingUp = " + sStartingUp +
- ", running = " + (INSTANCE != null));
- if (!sStartingUp && INSTANCE == null) {
- sStartingUp = true;
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- try {
- synchronized (sSyncLock) {
- // ExchangeService cannot start unless we can connect to AccountService
- if (!new AccountServiceProxy(ExchangeService.this).test()) {
- alwaysLog("!!! Email application not found; stopping self");
- stopSelf();
- }
- if (sDeviceId == null) {
- try {
- String deviceId = getDeviceId(ExchangeService.this);
- if (deviceId != null) {
- sDeviceId = deviceId;
- }
- } catch (IOException e) {
- }
- if (sDeviceId == null) {
- alwaysLog("!!! deviceId unknown; stopping self and retrying");
- stopSelf();
- // Try to restart ourselves in a few seconds
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- try {
- Thread.sleep(5000);
- } catch (InterruptedException e) {
- }
- startService(new Intent(
- EmailServiceProxy.EXCHANGE_INTENT));
- }});
- return;
- }
- }
- // Run the reconciler and clean up mismatched accounts - if we weren't
- // running when accounts were deleted, it won't have been called.
- runAccountReconcilerSync(ExchangeService.this);
- // Update other services depending on final account configuration
- maybeStartExchangeServiceThread();
- if (sServiceThread == null) {
- log("!!! EAS ExchangeService, stopping self");
- stopSelf();
- } else if (sStop) {
- // If we were trying to stop, attempt a restart in 5 secs
- setAlarm(EXCHANGE_SERVICE_MAILBOX_ID, 5*SECONDS);
- }
- }
- } finally {
- sStartingUp = false;
- }
- }});
- }
- return Service.START_STICKY;
- }
-
- @Override
- public void onDestroy() {
- log("!!! EAS ExchangeService, onDestroy");
- // Handle shutting down off the UI thread
- Utility.runAsync(new Runnable() {
- @Override
- public void run() {
- // Quick checks first, before getting the lock
- if (INSTANCE == null || sServiceThread == null) return;
- synchronized(sSyncLock) {
- // Stop the sync manager thread and return
- if (sServiceThread != null) {
- sStop = true;
- sServiceThread.interrupt();
- }
- }
- }});
- }
-
- void maybeStartExchangeServiceThread() {
- // Start our thread...
- // See if there are any EAS accounts; otherwise, just go away
- if (sServiceThread == null || !sServiceThread.isAlive()) {
- if (EmailContent.count(this, HostAuth.CONTENT_URI, WHERE_PROTOCOL_EAS, null) > 0) {
- log(sServiceThread == null ? "Starting thread..." : "Restarting thread...");
- sServiceThread = new Thread(this, "ExchangeService");
- INSTANCE = this;
- sServiceThread.start();
- }
- }
- }
-
- /**
- * Start up the ExchangeService service if it's not already running
- * This is a stopgap for cases in which ExchangeService died (due to a crash somewhere in
- * com.android.email) and hasn't been restarted. See the comment for onCreate for details
- */
- static void checkExchangeServiceServiceRunning() {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return;
- if (sServiceThread == null) {
- log("!!! checkExchangeServiceServiceRunning; starting service...");
- exchangeService.startService(new Intent(exchangeService, ExchangeService.class));
- }
- }
-
- public void run() {
- sStop = false;
- alwaysLog("ExchangeService thread running");
- // If we're really debugging, turn on all logging
- if (Eas.DEBUG) {
- Eas.USER_LOG = true;
- Eas.PARSER_LOG = true;
- Eas.FILE_LOG = true;
- }
-
- TempDirectory.setTempDirectory(this);
-
- // If we need to wait for the debugger, do so
- if (Eas.WAIT_DEBUG) {
- Debug.waitForDebugger();
- }
-
- // Synchronize here to prevent a shutdown from happening while we initialize our observers
- // and receivers
- synchronized (sSyncLock) {
- if (INSTANCE != null) {
- mResolver = getContentResolver();
-
- // Set up our observers; we need them to know when to start/stop various syncs based
- // on the insert/delete/update of mailboxes and accounts
- // We also observe synced messages to trigger upsyncs at the appropriate time
- mAccountObserver = new AccountObserver(mHandler);
- mResolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver);
- mMailboxObserver = new MailboxObserver(mHandler);
- mResolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver);
- mSyncedMessageObserver = new SyncedMessageObserver(mHandler);
- mResolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true,
- mSyncedMessageObserver);
-
- // Set up receivers for connectivity and background data setting
- mConnectivityReceiver = new ConnectivityReceiver();
- registerReceiver(mConnectivityReceiver, new IntentFilter(
- ConnectivityManager.CONNECTIVITY_ACTION));
-
- mBackgroundDataSettingReceiver = new ConnectivityReceiver();
- registerReceiver(mBackgroundDataSettingReceiver, new IntentFilter(
- ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED));
- // Save away the current background data setting; we'll keep track of it with the
- // receiver we just registered
- ConnectivityManager cm = (ConnectivityManager)getSystemService(
- Context.CONNECTIVITY_SERVICE);
- mBackgroundData = cm.getBackgroundDataSetting();
-
- // Do any required work to clean up our Mailboxes (this serves to upgrade
- // mailboxes that existed prior to EmailProvider database version 17)
- MailboxUtilities.fixupUninitializedParentKeys(this, getEasAccountSelector());
- }
- }
-
- try {
- // Loop indefinitely until we're shut down
- while (!sStop) {
- runAwake(EXTRA_MAILBOX_ID);
- waitForConnectivity();
- mNextWaitReason = null;
- long nextWait = checkMailboxes();
- try {
- synchronized (this) {
- if (!mKicked) {
- if (nextWait < 0) {
- log("Negative wait? Setting to 1s");
- nextWait = 1*SECONDS;
- }
- if (nextWait > 10*SECONDS) {
- if (mNextWaitReason != null) {
- log("Next awake " + nextWait / 1000 + "s: " + mNextWaitReason);
- }
- runAsleep(EXTRA_MAILBOX_ID, nextWait + (3*SECONDS));
- }
- wait(nextWait);
- }
- }
- } catch (InterruptedException e) {
- // Needs to be caught, but causes no problem
- log("ExchangeService interrupted");
- } finally {
- synchronized (this) {
- if (mKicked) {
- //log("Wait deferred due to kick");
- mKicked = false;
- }
- }
- }
- }
- log("Shutdown requested");
- } catch (ProviderUnavailableException pue) {
- // Shutdown cleanly in this case
- // NOTE: Sync adapters will also crash with this error, but that is already handled
- // in the adapters themselves, i.e. they return cleanly via done(). When the Email
- // process starts running again, the Exchange process will be started again in due
- // course, assuming there is at least one existing EAS account.
- Log.e(TAG, "EmailProvider unavailable; shutting down");
- // Ask for our service to be restarted; this should kick-start the Email process as well
- startService(new Intent(this, ExchangeService.class));
- } catch (RuntimeException e) {
- // Crash; this is a completely unexpected runtime error
- Log.e(TAG, "RuntimeException in ExchangeService", e);
- throw e;
- } finally {
- shutdown();
- }
- }
-
- private void shutdown() {
- synchronized (sSyncLock) {
- // If INSTANCE is null, we've already been shut down
- if (INSTANCE != null) {
- log("ExchangeService shutting down...");
-
- // Stop our running syncs
- stopServiceThreads();
-
- // Stop receivers
- if (mConnectivityReceiver != null) {
- unregisterReceiver(mConnectivityReceiver);
- }
- if (mBackgroundDataSettingReceiver != null) {
- unregisterReceiver(mBackgroundDataSettingReceiver);
- }
-
- // Unregister observers
- ContentResolver resolver = getContentResolver();
- if (mSyncedMessageObserver != null) {
- resolver.unregisterContentObserver(mSyncedMessageObserver);
- mSyncedMessageObserver = null;
- }
- if (mAccountObserver != null) {
- resolver.unregisterContentObserver(mAccountObserver);
- mAccountObserver = null;
- }
- if (mMailboxObserver != null) {
- resolver.unregisterContentObserver(mMailboxObserver);
- mMailboxObserver = null;
- }
- unregisterCalendarObservers();
-
- // Clear pending alarms and associated Intents
- clearAlarms();
-
- // Release our wake lock, if we have one
- synchronized (mWakeLocks) {
- if (mWakeLock != null) {
- mWakeLock.release();
- mWakeLock = null;
- }
- }
-
- INSTANCE = null;
- sServiceThread = null;
- sStop = false;
- log("Goodbye");
- }
- }
- }
-
- /**
- * Release a mailbox from the service map and release its wake lock.
- * NOTE: This method MUST be called while holding sSyncLock!
- *
- * @param mailboxId the id of the mailbox to be released
- */
- private void releaseMailbox(long mailboxId) {
- mServiceMap.remove(mailboxId);
- releaseWakeLock(mailboxId);
- }
-
- /**
- * Check whether an Outbox (referenced by a Cursor) has any messages that can be sent
- * @param c the cursor to an Outbox
- * @return true if there is mail to be sent
- */
- private boolean hasSendableMessages(Cursor outboxCursor) {
- Cursor c = mResolver.query(Message.CONTENT_URI, Message.ID_COLUMN_PROJECTION,
- EasOutboxService.MAILBOX_KEY_AND_NOT_SEND_FAILED,
- new String[] {Long.toString(outboxCursor.getLong(Mailbox.CONTENT_ID_COLUMN))},
- null);
- try {
- while (c.moveToNext()) {
- if (!Utility.hasUnloadedAttachments(this, c.getLong(Message.CONTENT_ID_COLUMN))) {
- return true;
- }
- }
- } finally {
- if (c != null) {
- c.close();
- }
- }
- return false;
- }
-
- /**
- * Determine whether the account is allowed to sync automatically, as opposed to manually, based
- * on whether the "require manual sync when roaming" policy is in force and applicable
- * @param account the account
- * @return whether or not the account can sync automatically
- */
- /*package*/ static boolean canAutoSync(Account account) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) {
- return false;
- }
- NetworkInfo networkInfo = exchangeService.mNetworkInfo;
-
- // Enforce manual sync only while roaming here
- long policyKey = account.mPolicyKey;
- // Quick exit from this check
- if ((policyKey != 0) && (networkInfo != null) &&
- (ConnectivityManager.isNetworkTypeMobile(networkInfo.getType()))) {
- // We'll cache the Policy data here
- Policy policy = account.mPolicy;
- if (policy == null) {
- policy = Policy.restorePolicyWithId(INSTANCE, policyKey);
- account.mPolicy = policy;
- }
- if (policy != null && policy.mRequireManualSyncWhenRoaming && networkInfo.isRoaming()) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Convenience method to determine whether Email sync is enabled for a given account
- * @param account the Account in question
- * @return whether Email sync is enabled
- */
- private boolean canSyncEmail(android.accounts.Account account) {
- return ContentResolver.getSyncAutomatically(account, EmailContent.AUTHORITY);
- }
-
- /**
- * Determine whether a mailbox of a given type in a given account can be synced automatically
- * by ExchangeService. This is an increasingly complex determination, taking into account
- * security policies and user settings (both within the Email application and in the Settings
- * application)
- *
- * @param account the Account that the mailbox is in
- * @param type the type of the Mailbox
- * @return whether or not to start a sync
- */
- private boolean isMailboxSyncable(Account account, int type) {
- // This 'if' statement performs checks to see whether or not a mailbox is a
- // candidate for syncing based on policies, user settings, & other restrictions
- if (type == Mailbox.TYPE_OUTBOX || type == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
- // Outbox and account mailbox are always syncable
- return true;
- } else if (type == Mailbox.TYPE_CONTACTS || type == Mailbox.TYPE_CALENDAR) {
- // Contacts/Calendar obey this setting from ContentResolver
- if (!ContentResolver.getMasterSyncAutomatically()) {
- return false;
- }
- // Get the right authority for the mailbox
- String authority;
- if (type == Mailbox.TYPE_CONTACTS) {
- authority = ContactsContract.AUTHORITY;
- } else {
- authority = CalendarContract.AUTHORITY;
- if (!mCalendarObservers.containsKey(account.mId)){
- // Make sure we have an observer for this Calendar, as
- // we need to be able to detect sync state changes, sigh
- registerCalendarObserver(account);
- }
- }
- // See if "sync automatically" is set; if not, punt
- if (!ContentResolver.getSyncAutomatically(account.mAmAccount, authority)) {
- return false;
- // See if the calendar is enabled from the Calendar app UI; if not, punt
- } else if ((type == Mailbox.TYPE_CALENDAR) && !isCalendarEnabled(account.mId)) {
- return false;
- }
- // Never automatically sync trash
- } else if (type == Mailbox.TYPE_TRASH) {
- return false;
- // For non-outbox, non-account mail, we do three checks:
- // 1) are we restricted by policy (i.e. manual sync only),
- // 2) has the user checked the "Sync Email" box in Account Settings, and
- // 3) does the user have the master "background data" box checked in Settings
- } else if (!canAutoSync(account) || !canSyncEmail(account.mAmAccount) || !mBackgroundData) {
- return false;
- }
- return true;
- }
-
- private long checkMailboxes () {
- // First, see if any running mailboxes have been deleted
- ArrayList<Long> deletedMailboxes = new ArrayList<Long>();
- synchronized (sSyncLock) {
- for (long mailboxId: mServiceMap.keySet()) {
- Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
- if (m == null) {
- deletedMailboxes.add(mailboxId);
- }
- }
- // If so, stop them or remove them from the map
- for (Long mailboxId: deletedMailboxes) {
- AbstractSyncService svc = mServiceMap.get(mailboxId);
- if (svc == null || svc.mThread == null) {
- releaseMailbox(mailboxId);
- continue;
- } else {
- boolean alive = svc.mThread.isAlive();
- log("Deleted mailbox: " + svc.mMailboxName);
- if (alive) {
- stopManualSync(mailboxId);
- } else {
- log("Removing from serviceMap");
- releaseMailbox(mailboxId);
- }
- }
- }
- }
-
- long nextWait = EXCHANGE_SERVICE_HEARTBEAT_TIME;
- long now = System.currentTimeMillis();
-
- // Start up threads that need it; use a query which finds eas mailboxes where the
- // the sync interval is not "never". This is the set of mailboxes that we control
- if (mAccountObserver == null) {
- log("mAccountObserver null; service died??");
- return nextWait;
- }
-
- Cursor c = getContentResolver().query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
- mAccountObserver.getSyncableEasMailboxWhere(), null, null);
- if (c == null) throw new ProviderUnavailableException();
- try {
- while (c.moveToNext()) {
- long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
- AbstractSyncService service = null;
- synchronized (sSyncLock) {
- service = mServiceMap.get(mailboxId);
- }
- if (service == null) {
- // Get the cached account
- Account account = getAccountById(c.getInt(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN));
- if (account == null) continue;
-
- // We handle a few types of mailboxes specially
- int mailboxType = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
- if (!isMailboxSyncable(account, mailboxType)) {
- continue;
- }
-
- // Check whether we're in a hold (temporary or permanent)
- SyncError syncError = mSyncErrorMap.get(mailboxId);
- if (syncError != null) {
- // Nothing we can do about fatal errors
- if (syncError.fatal) continue;
- if (now < syncError.holdEndTime) {
- // If release time is earlier than next wait time,
- // move next wait time up to the release time
- if (syncError.holdEndTime < now + nextWait) {
- nextWait = syncError.holdEndTime - now;
- mNextWaitReason = "Release hold";
- }
- continue;
- } else {
- // Keep the error around, but clear the end time
- syncError.holdEndTime = 0;
- }
- }
-
- // Otherwise, we use the sync interval
- long syncInterval = c.getInt(Mailbox.CONTENT_SYNC_INTERVAL_COLUMN);
- if (syncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
- Mailbox m = EmailContent.getContent(c, Mailbox.class);
- requestSync(m, SYNC_PUSH, null);
- } else if (mailboxType == Mailbox.TYPE_OUTBOX) {
- if (hasSendableMessages(c)) {
- Mailbox m = EmailContent.getContent(c, Mailbox.class);
- startServiceThread(new EasOutboxService(this, m), m);
- }
- } else if (syncInterval > 0 && syncInterval <= ONE_DAY_MINUTES) {
- long lastSync = c.getLong(Mailbox.CONTENT_SYNC_TIME_COLUMN);
- long sinceLastSync = now - lastSync;
- long toNextSync = syncInterval*MINUTES - sinceLastSync;
- String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
- if (toNextSync <= 0) {
- Mailbox m = EmailContent.getContent(c, Mailbox.class);
- requestSync(m, SYNC_SCHEDULED, null);
- } else if (toNextSync < nextWait) {
- nextWait = toNextSync;
- if (Eas.USER_LOG) {
- log("Next sync for " + name + " in " + nextWait/1000 + "s");
- }
- mNextWaitReason = "Scheduled sync, " + name;
- } else if (Eas.USER_LOG) {
- log("Next sync for " + name + " in " + toNextSync/1000 + "s");
- }
- }
- } else {
- Thread thread = service.mThread;
- // Look for threads that have died and remove them from the map
- if (thread != null && !thread.isAlive()) {
- if (Eas.USER_LOG) {
- log("Dead thread, mailbox released: " +
- c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN));
- }
- releaseMailbox(mailboxId);
- // Restart this if necessary
- if (nextWait > 3*SECONDS) {
- nextWait = 3*SECONDS;
- mNextWaitReason = "Clean up dead thread(s)";
- }
- } else {
- long requestTime = service.mRequestTime;
- if (requestTime > 0) {
- long timeToRequest = requestTime - now;
- if (timeToRequest <= 0) {
- service.mRequestTime = 0;
- service.alarm();
- } else if (requestTime > 0 && timeToRequest < nextWait) {
- if (timeToRequest < 11*MINUTES) {
- nextWait = timeToRequest < 250 ? 250 : timeToRequest;
- mNextWaitReason = "Sync data change";
- } else {
- log("Illegal timeToRequest: " + timeToRequest);
- }
- }
- }
- }
- }
- }
- } finally {
- c.close();
- }
- return nextWait;
- }
-
- static public void serviceRequest(long mailboxId, int reason) {
- serviceRequest(mailboxId, 5*SECONDS, reason);
- }
-
- /**
- * Return a boolean indicating whether the mailbox can be synced
- * @param m the mailbox
- * @return whether or not the mailbox can be synced
- */
- public static boolean isSyncable(Mailbox m) {
- return m.loadsFromServer(HostAuth.SCHEME_EAS);
- }
-
- static public void serviceRequest(long mailboxId, long ms, int reason) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return;
- Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
- if (m == null || !isSyncable(m)) return;
- try {
- AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId);
- if (service != null) {
- service.mRequestTime = System.currentTimeMillis() + ms;
- kick("service request");
- } else {
- startManualSync(mailboxId, reason, null);
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- static public void serviceRequestImmediate(long mailboxId) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return;
- AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId);
- if (service != null) {
- service.mRequestTime = System.currentTimeMillis();
- Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
- if (m != null) {
- service.mAccount = Account.restoreAccountWithId(exchangeService, m.mAccountKey);
- service.mMailbox = m;
- kick("service request immediate");
- }
- }
- }
-
- static public void sendMessageRequest(Request req) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return;
- Message msg = Message.restoreMessageWithId(exchangeService, req.mMessageId);
- if (msg == null) {
- return;
- }
- long mailboxId = msg.mMailboxKey;
- AbstractSyncService service = exchangeService.mServiceMap.get(mailboxId);
-
- if (service == null) {
- startManualSync(mailboxId, SYNC_SERVICE_PART_REQUEST, req);
- kick("part request");
- } else {
- service.addRequest(req);
- }
- }
-
- /**
- * Determine whether a given Mailbox can be synced, i.e. is not already syncing and is not in
- * an error state
- *
- * @param mailboxId
- * @return whether or not the Mailbox is available for syncing (i.e. is a valid push target)
- */
- static public int pingStatus(long mailboxId) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return PING_STATUS_OK;
- // Already syncing...
- if (exchangeService.mServiceMap.get(mailboxId) != null) {
- return PING_STATUS_RUNNING;
- }
- // No errors or a transient error, don't ping...
- SyncError error = exchangeService.mSyncErrorMap.get(mailboxId);
- if (error != null) {
- if (error.fatal) {
- return PING_STATUS_UNABLE;
- } else if (error.holdEndTime > 0) {
- return PING_STATUS_WAITING;
- }
- }
- return PING_STATUS_OK;
- }
-
- static public void startManualSync(long mailboxId, int reason, Request req) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return;
- synchronized (sSyncLock) {
- AbstractSyncService svc = exchangeService.mServiceMap.get(mailboxId);
- if (svc == null) {
- exchangeService.mSyncErrorMap.remove(mailboxId);
- Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
- if (m != null) {
- log("Starting sync for " + m.mDisplayName);
- exchangeService.requestSync(m, reason, req);
- }
- } else {
- // If this is a ui request, set the sync reason for the service
- if (reason >= SYNC_CALLBACK_START) {
- svc.mSyncReason = reason;
- }
- }
- }
- }
-
- // DO NOT CALL THIS IN A LOOP ON THE SERVICEMAP
- static public void stopManualSync(long mailboxId) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return;
- synchronized (sSyncLock) {
- AbstractSyncService svc = exchangeService.mServiceMap.get(mailboxId);
- if (svc != null) {
- log("Stopping sync for " + svc.mMailboxName);
- svc.stop();
- svc.mThread.interrupt();
- exchangeService.releaseWakeLock(mailboxId);
- }
- }
- }
-
- /**
- * Wake up ExchangeService to check for mailboxes needing service
- */
- static public void kick(String reason) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService != null) {
- synchronized (exchangeService) {
- //INSTANCE.log("Kick: " + reason);
- exchangeService.mKicked = true;
- exchangeService.notify();
- }
- }
- if (sConnectivityLock != null) {
- synchronized (sConnectivityLock) {
- sConnectivityLock.notify();
- }
- }
- }
-
- static public void accountUpdated(long acctId) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return;
- synchronized (sSyncLock) {
- for (AbstractSyncService svc : exchangeService.mServiceMap.values()) {
- if (svc.mAccount.mId == acctId) {
- svc.mAccount = Account.restoreAccountWithId(exchangeService, acctId);
- }
- }
- }
- }
-
- /**
- * Tell ExchangeService to remove the mailbox from the map of mailboxes with sync errors
- * @param mailboxId the id of the mailbox
- */
- static public void removeFromSyncErrorMap(long mailboxId) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService != null) {
- exchangeService.mSyncErrorMap.remove(mailboxId);
- }
- }
-
- private boolean isRunningInServiceThread(long mailboxId) {
- AbstractSyncService syncService = mServiceMap.get(mailboxId);
- Thread thisThread = Thread.currentThread();
- return syncService != null && syncService.mThread != null &&
- thisThread == syncService.mThread;
- }
-
- /**
- * Sent by services indicating that their thread is finished; action depends on the exitStatus
- * of the service.
- *
- * @param svc the service that is finished
- */
- static public void done(AbstractSyncService svc) {
- ExchangeService exchangeService = INSTANCE;
- if (exchangeService == null) return;
- synchronized(sSyncLock) {
- long mailboxId = svc.mMailboxId;
- // If we're no longer the syncing thread for the mailbox, just return
- if (!exchangeService.isRunningInServiceThread(mailboxId)) {
- return;
- }
- exchangeService.releaseMailbox(mailboxId);
-
- ConcurrentHashMap<Long, SyncError> errorMap = exchangeService.mSyncErrorMap;
- SyncError syncError = errorMap.get(mailboxId);
-
- int exitStatus = svc.mExitStatus;
- Mailbox m = Mailbox.restoreMailboxWithId(exchangeService, mailboxId);
- if (m == null) return;
-
- if (exitStatus != AbstractSyncService.EXIT_LOGIN_FAILURE) {
- long accountId = m.mAccountKey;
- Account account = Account.restoreAccountWithId(exchangeService, accountId);
- if (account == null) return;
- if (exchangeService.releaseSyncHolds(exchangeService,
- AbstractSyncService.EXIT_LOGIN_FAILURE, account)) {
- new AccountServiceProxy(exchangeService).notifyLoginSucceeded(accountId);
- }
- }
-
- switch (exitStatus) {
- case AbstractSyncService.EXIT_DONE:
- if (svc.hasPendingRequests()) {
- // TODO Handle this case
- }
- errorMap.remove(mailboxId);
- // If we've had a successful sync, clear the shutdown count
- synchronized (ExchangeService.class) {
- sClientConnectionManagerShutdownCount = 0;
- }
- break;
- // I/O errors get retried at increasing intervals
- case AbstractSyncService.EXIT_IO_ERROR:
- if (syncError != null) {
- syncError.escalate();
- log(m.mDisplayName + " held for " + syncError.holdDelay + "ms");
- } else {
- errorMap.put(mailboxId, exchangeService.new SyncError(exitStatus, false));
- log(m.mDisplayName + " added to syncErrorMap, hold for 15s");
- }
- break;
- // These errors are not retried automatically
- case AbstractSyncService.EXIT_LOGIN_FAILURE:
- new AccountServiceProxy(exchangeService).notifyLoginFailed(m.mAccountKey);
- // Fall through
- case AbstractSyncService.EXIT_SECURITY_FAILURE:
- case AbstractSyncService.EXIT_ACCESS_DENIED:
- case AbstractSyncService.EXIT_EXCEPTION:
- errorMap.put(mailboxId, exchangeService.new SyncError(exitStatus, true));
- break;
- }
- kick("sync completed");
- }
- }
-
- /**
- * Given the status string from a Mailbox, return the type code for the last sync
- * @param status the syncStatus column of a Mailbox
- * @return
- */
- static public int getStatusType(String status) {
- if (status == null) {
- return -1;
- } else {
- return status.charAt(STATUS_TYPE_CHAR) - '0';
- }
- }
-
- /**
- * Given the status string from a Mailbox, return the change count for the last sync
- * The change count is the number of adds + deletes + changes in the last sync
- * @param status the syncStatus column of a Mailbox
- * @return
- */
- static public int getStatusChangeCount(String status) {
- try {
- String s = status.substring(STATUS_CHANGE_COUNT_OFFSET);
- return Integer.parseInt(s);
- } catch (RuntimeException e) {
- return -1;
- }
- }
-
- static public Context getContext() {
- return INSTANCE;
- }
-}
diff --git a/src/com/android/exchange/IllegalHeartbeatException.java b/src/com/android/exchange/IllegalHeartbeatException.java
deleted file mode 100644
index e480aa7..0000000
--- a/src/com/android/exchange/IllegalHeartbeatException.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- * Licensed to The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-public class IllegalHeartbeatException extends EasException {
- private static final long serialVersionUID = 1L;
- public final int mLegalHeartbeat;
-
- public IllegalHeartbeatException(int legalHeartbeat) {
- mLegalHeartbeat = legalHeartbeat;
- }
-}
diff --git a/src/com/android/exchange/MailboxAlarmReceiver.java b/src/com/android/exchange/MailboxAlarmReceiver.java
deleted file mode 100644
index 7958c03..0000000
--- a/src/com/android/exchange/MailboxAlarmReceiver.java
+++ /dev/null
@@ -1,41 +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 android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-/**
- * MailboxAlarmReceiver is used to "wake up" the ExchangeService at the appropriate time(s). It may
- * also be used for individual sync adapters, but this isn't implemented at the present time.
- *
- */
-public class MailboxAlarmReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- long mailboxId = intent.getLongExtra("mailbox", ExchangeService.EXTRA_MAILBOX_ID);
- // EXCHANGE_SERVICE_MAILBOX_ID tells us that the service is asking to be started
- if (mailboxId == ExchangeService.EXCHANGE_SERVICE_MAILBOX_ID) {
- context.startService(new Intent(context, ExchangeService.class));
- } else {
- ExchangeService.alert(context, mailboxId);
- }
- }
-}
-
diff --git a/src/com/android/exchange/MeetingResponseRequest.java b/src/com/android/exchange/MeetingResponseRequest.java
deleted file mode 100644
index ea769e2..0000000
--- a/src/com/android/exchange/MeetingResponseRequest.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-/**
- * MeetingResponseRequest is the EAS wrapper for responding to meeting requests.
- */
-public class MeetingResponseRequest extends Request {
- public final int mResponse;
-
- MeetingResponseRequest(long messageId, int response) {
- super(messageId);
- mResponse = response;
- }
-
- // MeetingResponseRequests are unique by their message id (i.e. there's only one response to
- // a given message)
- public boolean equals(Object o) {
- if (!(o instanceof MeetingResponseRequest)) return false;
- return ((MeetingResponseRequest)o).mMessageId == mMessageId;
- }
-
- public int hashCode() {
- return (int)mMessageId;
- }
-}
diff --git a/src/com/android/exchange/MessageMoveRequest.java b/src/com/android/exchange/MessageMoveRequest.java
deleted file mode 100644
index a884bda..0000000
--- a/src/com/android/exchange/MessageMoveRequest.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-/**
- * MessageMoveRequest is the EAS wrapper for requesting a "move to folder"
- */
-public class MessageMoveRequest extends Request {
- public final long mMailboxId;
-
- public MessageMoveRequest(long messageId, long mailboxId) {
- super(messageId);
- mMailboxId = mailboxId;
- }
-
- // MessageMoveRequests are unique by their message id (i.e. it's meaningless to have two
- // separate message moves queued at the same time)
- public boolean equals(Object o) {
- if (!(o instanceof MessageMoveRequest)) return false;
- return ((MessageMoveRequest)o).mMessageId == mMessageId;
- }
-
- public int hashCode() {
- return (int)mMessageId;
- }
-}
diff --git a/src/com/android/exchange/MockParserStream.java b/src/com/android/exchange/MockParserStream.java
deleted file mode 100644
index a518667..0000000
--- a/src/com/android/exchange/MockParserStream.java
+++ /dev/null
@@ -1,55 +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 java.io.IOException;
-import java.io.InputStream;
-
-/**
- * MockParserStream is an InputStream that feeds pre-generated data into various EasParser
- * subclasses.
- *
- * After parsing is done, the result can be obtained with getResult
- *
- */
-public class MockParserStream extends InputStream {
- int[] array;
- int pos = 0;
- Object value;
-
- MockParserStream (int[] _array) {
- array = _array;
- }
-
- @Override
- public int read() throws IOException {
- try {
- return array[pos++];
- } catch (IndexOutOfBoundsException e) {
- throw new IOException("End of stream");
- }
- }
-
- public void setResult(Object _value) {
- value = _value;
- }
-
- public Object getResult() {
- return value;
- }
-}
diff --git a/src/com/android/exchange/PartRequest.java b/src/com/android/exchange/PartRequest.java
deleted file mode 100644
index 23b4add..0000000
--- a/src/com/android/exchange/PartRequest.java
+++ /dev/null
@@ -1,51 +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.emailcommon.provider.EmailContent.Attachment;
-
-/**
- * PartRequest is the EAS wrapper for attachment loading requests. In addition to information about
- * the attachment to be loaded, it also contains the callback to be used for status/progress
- * updates to the UI.
- */
-public class PartRequest extends Request {
- public final Attachment mAttachment;
- public final String mDestination;
- public final String mContentUriString;
- public final String mLocation;
-
- public PartRequest(Attachment _att, String _destination, String _contentUriString) {
- super(_att.mMessageKey);
- mAttachment = _att;
- mLocation = mAttachment.mLocation;
- mDestination = _destination;
- mContentUriString = _contentUriString;
- }
-
- // PartRequests are unique by their attachment id (i.e. multiple attachments might be queued
- // for a particular message, but any individual attachment can only be loaded once)
- public boolean equals(Object o) {
- if (!(o instanceof PartRequest)) return false;
- return ((PartRequest)o).mAttachment.mId == mAttachment.mId;
- }
-
- public int hashCode() {
- return (int)mAttachment.mId;
- }
-}
diff --git a/src/com/android/exchange/Request.java b/src/com/android/exchange/Request.java
deleted file mode 100644
index 280a084..0000000
--- a/src/com/android/exchange/Request.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-/**
- * Requests for mailbox actions are handled by subclasses of this abstract class.
- * Three subclasses are now defined: PartRequest (attachment load), MeetingResponseRequest
- * (respond to a meeting invitation), and MessageMoveRequest (move a message to another folder)
- */
-public abstract class Request {
- public final long mTimeStamp = System.currentTimeMillis();
- public final long mMessageId;
-
- public Request(long messageId) {
- mMessageId = messageId;
- }
-
- // Subclasses of Request may have different semantics regarding equality; therefore,
- // we force them to implement the equals method
- public abstract boolean equals(Object o);
- public abstract int hashCode();
-}
diff --git a/src/com/android/exchange/SecurityPolicyDelegate.java b/src/com/android/exchange/SecurityPolicyDelegate.java
deleted file mode 100644
index 5025c8f..0000000
--- a/src/com/android/exchange/SecurityPolicyDelegate.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.Policy;
-import com.android.emailcommon.service.PolicyServiceProxy;
-
-import android.content.Context;
-import android.os.RemoteException;
-
-public class SecurityPolicyDelegate {
-
- public static boolean isActive(Context context, Policy policy) {
- try {
- return new PolicyServiceProxy(context).isActive(policy);
- } catch (RemoteException e) {
- }
- return false;
- }
-
- public static void policiesRequired(Context context, long accountId) {
- try {
- new PolicyServiceProxy(context).policiesRequired(accountId);
- } catch (RemoteException e) {
- throw new IllegalStateException("PolicyService transaction failed");
- }
- }
-
- public static void policiesUpdated(Context context, long accountId) {
- try {
- new PolicyServiceProxy(context).policiesUpdated(accountId);
- } catch (RemoteException e) {
- throw new IllegalStateException("PolicyService transaction failed");
- }
- }
-
- public static void setAccountHoldFlag(Context context, Account account, boolean newState) {
- try {
- new PolicyServiceProxy(context).setAccountHoldFlag(account.mId, newState);
- } catch (RemoteException e) {
- throw new IllegalStateException("PolicyService transaction failed");
- }
- }
-
- public static boolean isActiveAdmin(Context context) {
- try {
- return new PolicyServiceProxy(context).isActiveAdmin();
- } catch (RemoteException e) {
- }
- return false;
- }
-
- public static void remoteWipe(Context context) {
- try {
- new PolicyServiceProxy(context).remoteWipe();
- } catch (RemoteException e) {
- throw new IllegalStateException("PolicyService transaction failed");
- }
- }
-
- public static boolean isSupported(Context context, Policy policy) {
- try {
- return new PolicyServiceProxy(context).isSupported(policy);
- } catch (RemoteException e) {
- }
- return false;
- }
-
- public static Policy clearUnsupportedPolicies(Context context, Policy policy) {
- try {
- return new PolicyServiceProxy(context).clearUnsupportedPolicies(policy);
- } catch (RemoteException e) {
- }
- throw new IllegalStateException("PolicyService transaction failed");
- }
-}
diff --git a/src/com/android/exchange/SettingsRedirector.java b/src/com/android/exchange/SettingsRedirector.java
deleted file mode 100644
index 5a50825..0000000
--- a/src/com/android/exchange/SettingsRedirector.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package com.android.exchange;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-
-import com.android.emailcommon.utility.IntentUtilities;
-
-/**
- * An empty {@link Activity} that simply redirects to the proper settings editor for the Email
- * application.
- * This is needed since the Exchange service runs as a separate UID and is therefore tracked as
- * a separate entity in the framework for things such as data usage. Links from those places to
- * Exchange should really go to Email where the real settings are.
- */
-public class SettingsRedirector extends Activity {
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Intent intent = getIntent();
- Intent redirect = new Intent(
- Intent.ACTION_EDIT,
- IntentUtilities.createActivityIntentUrlBuilder("settings").build());
- redirect.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
- startActivity(redirect);
- finish();
- }
-}
diff --git a/src/com/android/exchange/StaleFolderListException.java b/src/com/android/exchange/StaleFolderListException.java
deleted file mode 100644
index 70ac032..0000000
--- a/src/com/android/exchange/StaleFolderListException.java
+++ /dev/null
@@ -1,22 +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;
-
-public class StaleFolderListException extends EasException {
- private static final long serialVersionUID = 1L;
-}
diff --git a/src/com/android/exchange/adapter/AbstractSyncAdapter.java b/src/com/android/exchange/adapter/AbstractSyncAdapter.java
deleted file mode 100644
index 52400c4..0000000
--- a/src/com/android/exchange/adapter/AbstractSyncAdapter.java
+++ /dev/null
@@ -1,349 +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.adapter;
-
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.exchange.CommandStatusException;
-import com.android.exchange.Eas;
-import com.android.exchange.EasSyncService;
-import com.google.common.annotations.VisibleForTesting;
-
-import android.content.ContentProviderOperation;
-import android.content.ContentProviderResult;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.OperationApplicationException;
-import android.net.Uri;
-import android.os.RemoteException;
-import android.os.TransactionTooLargeException;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-
-/**
- * Parent class of all sync adapters (EasMailbox, EasCalendar, and EasContacts)
- *
- */
-public abstract class AbstractSyncAdapter {
-
- public static final int SECONDS = 1000;
- public static final int MINUTES = SECONDS*60;
- public static final int HOURS = MINUTES*60;
- public static final int DAYS = HOURS*24;
- public static final int WEEKS = DAYS*7;
-
- protected static final String PIM_WINDOW_SIZE = "4";
-
- private static final long SEPARATOR_ID = Long.MAX_VALUE;
-
- public Mailbox mMailbox;
- public EasSyncService mService;
- public Context mContext;
- public Account mAccount;
- public final ContentResolver mContentResolver;
- public final android.accounts.Account mAccountManagerAccount;
-
- // Create the data for local changes that need to be sent up to the server
- public abstract boolean sendLocalChanges(Serializer s) throws IOException;
- // Parse incoming data from the EAS server, creating, modifying, and deleting objects as
- // required through the EmailProvider
- public abstract boolean parse(InputStream is) throws IOException, CommandStatusException;
- // The name used to specify the collection type of the target (Email, Calendar, or Contacts)
- public abstract String getCollectionName();
- public abstract void cleanup();
- public abstract boolean isSyncable();
- // Add sync options (filter, body type - html vs plain, and truncation)
- public abstract void sendSyncOptions(Double protocolVersion, Serializer s) throws IOException;
- /**
- * Delete all records of this class in this account
- */
- public abstract void wipe();
-
- public boolean isLooping() {
- return false;
- }
-
- public AbstractSyncAdapter(EasSyncService service) {
- mService = service;
- mMailbox = service.mMailbox;
- mContext = service.mContext;
- mAccount = service.mAccount;
- mAccountManagerAccount = new android.accounts.Account(mAccount.mEmailAddress,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
- mContentResolver = mContext.getContentResolver();
- }
-
- public void userLog(String ...strings) {
- mService.userLog(strings);
- }
-
- public void incrementChangeCount() {
- mService.mChangeCount++;
- }
-
- /**
- * Set sync options common to PIM's (contacts and calendar)
- * @param protocolVersion the protocol version under which we're syncing
- * @param the filter to use (or null)
- * @param s the Serializer
- * @throws IOException
- */
- protected void setPimSyncOptions(Double protocolVersion, String filter, Serializer s)
- throws IOException {
- s.tag(Tags.SYNC_DELETES_AS_MOVES);
- s.tag(Tags.SYNC_GET_CHANGES);
- s.data(Tags.SYNC_WINDOW_SIZE, PIM_WINDOW_SIZE);
- s.start(Tags.SYNC_OPTIONS);
- // Set the filter (lookback), if provided
- if (filter != null) {
- s.data(Tags.SYNC_FILTER_TYPE, filter);
- }
- // Set the truncation amount and body type
- if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
- s.start(Tags.BASE_BODY_PREFERENCE);
- // Plain text
- s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT);
- s.data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE);
- s.end();
- } else {
- s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
- }
- s.end();
- }
-
- /**
- * Returns the current SyncKey; override if the SyncKey is stored elsewhere (as for Contacts)
- * @return the current SyncKey for the Mailbox
- * @throws IOException
- */
- public String getSyncKey() throws IOException {
- if (mMailbox.mSyncKey == null) {
- userLog("Reset SyncKey to 0");
- mMailbox.mSyncKey = "0";
- }
- return mMailbox.mSyncKey;
- }
-
- public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
- mMailbox.mSyncKey = syncKey;
- }
-
- /**
- * Operation is our binder-safe ContentProviderOperation (CPO) construct; an Operation can
- * be created from a CPO, a CPO Builder, or a CPO Builder with a "back reference" column name
- * and offset (that might be used in Builder.withValueBackReference). The CPO is not actually
- * built until it is ready to be executed (with applyBatch); this allows us to recalculate
- * back reference offsets if we are required to re-send a large batch in smaller chunks.
- *
- * NOTE: A failed binder transaction is something of an emergency case, and shouldn't happen
- * with any frequency. When it does, and we are forced to re-send the data to the content
- * provider in smaller chunks, we DO lose the sync-window atomicity, and thereby add another
- * small risk to the data. Of course, this is far, far better than dropping the data on the
- * floor, as was done before the framework implemented TransactionTooLargeException
- */
- protected static class Operation {
- final ContentProviderOperation mOp;
- final ContentProviderOperation.Builder mBuilder;
- final String mColumnName;
- final int mOffset;
- // Is this Operation a separator? (a good place to break up a large transaction)
- boolean mSeparator = false;
-
- // For toString()
- final String[] TYPES = new String[] {"???", "Ins", "Upd", "Del", "Assert"};
-
- Operation(ContentProviderOperation.Builder builder, String columnName, int offset) {
- mOp = null;
- mBuilder = builder;
- mColumnName = columnName;
- mOffset = offset;
- }
-
- Operation(ContentProviderOperation.Builder builder) {
- mOp = null;
- mBuilder = builder;
- mColumnName = null;
- mOffset = 0;
- }
-
- Operation(ContentProviderOperation op) {
- mOp = op;
- mBuilder = null;
- mColumnName = null;
- mOffset = 0;
- }
-
- public String toString() {
- StringBuilder sb = new StringBuilder("Op: ");
- ContentProviderOperation op = operationToContentProviderOperation(this, 0);
- int type = 0;
- //DO NOT SHIP WITH THE FOLLOWING LINE (the API is hidden!)
- //type = op.getType();
- sb.append(TYPES[type]);
- Uri uri = op.getUri();
- sb.append(' ');
- sb.append(uri.getPath());
- if (mColumnName != null) {
- sb.append(" Back value of " + mColumnName + ": " + mOffset);
- }
- return sb.toString();
- }
- }
-
- /**
- * We apply the batch of CPO's here. We synchronize on the service to avoid thread-nasties,
- * and we just return quickly if the service has already been stopped.
- */
- private ContentProviderResult[] execute(String authority,
- ArrayList<ContentProviderOperation> ops)
- throws RemoteException, OperationApplicationException {
- synchronized (mService.getSynchronizer()) {
- if (!mService.isStopped()) {
- if (!ops.isEmpty()) {
- ContentProviderResult[] result = mContentResolver.applyBatch(authority, ops);
- mService.userLog("Results: " + result.length);
- return result;
- }
- }
- }
- return new ContentProviderResult[0];
- }
-
- /**
- * Convert an Operation to a CPO; if the Operation has a back reference, apply it with the
- * passed-in offset
- */
- @VisibleForTesting
- static ContentProviderOperation operationToContentProviderOperation(Operation op, int offset) {
- if (op.mOp != null) {
- return op.mOp;
- } else if (op.mBuilder == null) {
- throw new IllegalArgumentException("Operation must have CPO.Builder");
- }
- ContentProviderOperation.Builder builder = op.mBuilder;
- if (op.mColumnName != null) {
- builder.withValueBackReference(op.mColumnName, op.mOffset - offset);
- }
- return builder.build();
- }
-
- /**
- * Create a list of CPOs from a list of Operations, and then apply them in a batch
- */
- private ContentProviderResult[] applyBatch(String authority, ArrayList<Operation> ops,
- int offset) throws RemoteException, OperationApplicationException {
- // Handle the empty case
- if (ops.isEmpty()) {
- return new ContentProviderResult[0];
- }
- ArrayList<ContentProviderOperation> cpos = new ArrayList<ContentProviderOperation>();
- for (Operation op: ops) {
- cpos.add(operationToContentProviderOperation(op, offset));
- }
- return execute(authority, cpos);
- }
-
- /**
- * Apply the list of CPO's in the provider and copy the "mini" result into our full result array
- */
- private void applyAndCopyResults(String authority, ArrayList<Operation> mini,
- ContentProviderResult[] result, int offset) throws RemoteException {
- // Empty lists are ok; we just ignore them
- if (mini.isEmpty()) return;
- try {
- ContentProviderResult[] miniResult = applyBatch(authority, mini, offset);
- // Copy the results from this mini-batch into our results array
- System.arraycopy(miniResult, 0, result, offset, miniResult.length);
- } catch (OperationApplicationException e) {
- // Not possible since we're building the ops ourselves
- }
- }
-
- /**
- * Called by a sync adapter to execute a list of Operations in the ContentProvider handling
- * the passed-in authority. If the attempt to apply the batch fails due to a too-large
- * binder transaction, we split the Operations as directed by separators. If any of the
- * "mini" batches fails due to a too-large transaction, we're screwed, but this would be
- * vanishingly rare. Other, possibly transient, errors are handled by throwing a
- * RemoteException, which the caller will likely re-throw as an IOException so that the sync
- * can be attempted again.
- *
- * Callers MAY leave a dangling separator at the end of the list; note that the separators
- * themselves are only markers and are not sent to the provider.
- */
- protected ContentProviderResult[] safeExecute(String authority, ArrayList<Operation> ops)
- throws RemoteException {
- mService.userLog("Try to execute ", ops.size(), " CPO's for " + authority);
- ContentProviderResult[] result = null;
- try {
- // Try to execute the whole thing
- return applyBatch(authority, ops, 0);
- } catch (TransactionTooLargeException e) {
- // Nope; split into smaller chunks, demarcated by the separator operation
- mService.userLog("Transaction too large; spliting!");
- ArrayList<Operation> mini = new ArrayList<Operation>();
- // Build a result array with the total size we're sending
- result = new ContentProviderResult[ops.size()];
- int count = 0;
- int offset = 0;
- for (Operation op: ops) {
- if (op.mSeparator) {
- try {
- mService.userLog("Try mini-batch of ", mini.size(), " CPO's");
- applyAndCopyResults(authority, mini, result, offset);
- mini.clear();
- // Save away the offset here; this will need to be subtracted out of the
- // value originally set by the adapter
- offset = count + 1; // Remember to add 1 for the separator!
- } catch (TransactionTooLargeException e1) {
- throw new RuntimeException("Can't send transaction; sync stopped.");
- } catch (RemoteException e1) {
- throw e1;
- }
- } else {
- mini.add(op);
- }
- count++;
- }
- // Check out what's left; if it's more than just a separator, apply the batch
- int miniSize = mini.size();
- if ((miniSize > 0) && !(miniSize == 1 && mini.get(0).mSeparator)) {
- applyAndCopyResults(authority, mini, result, offset);
- }
- } catch (RemoteException e) {
- throw e;
- } catch (OperationApplicationException e) {
- // Not possible since we're building the ops ourselves
- }
- return result;
- }
-
- /**
- * Called by a sync adapter to indicate a relatively safe place to split a batch of CPO's
- */
- protected void addSeparatorOperation(ArrayList<Operation> ops, Uri uri) {
- Operation op = new Operation(
- ContentProviderOperation.newDelete(ContentUris.withAppendedId(uri, SEPARATOR_ID)));
- op.mSeparator = true;
- ops.add(op);
- }
-}
-
diff --git a/src/com/android/exchange/adapter/AbstractSyncParser.java b/src/com/android/exchange/adapter/AbstractSyncParser.java
deleted file mode 100644
index 1162b66..0000000
--- a/src/com/android/exchange/adapter/AbstractSyncParser.java
+++ /dev/null
@@ -1,244 +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.adapter;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent.MailboxColumns;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.exchange.CommandStatusException;
-import com.android.exchange.CommandStatusException.CommandStatus;
-import com.android.exchange.EasSyncService;
-import com.android.exchange.ExchangeService;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Base class for the Email and PIM sync parsers
- * Handles the basic flow of syncKeys, looping to get more data, handling errors, etc.
- * Each subclass must implement a handful of methods that relate specifically to the data type
- *
- */
-public abstract class AbstractSyncParser extends Parser {
-
- protected EasSyncService mService;
- protected Mailbox mMailbox;
- protected Account mAccount;
- protected Context mContext;
- protected ContentResolver mContentResolver;
- protected AbstractSyncAdapter mAdapter;
-
- private boolean mLooping;
-
- public AbstractSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
- super(in);
- init(adapter);
- }
-
- public AbstractSyncParser(Parser p, AbstractSyncAdapter adapter) throws IOException {
- super(p);
- init(adapter);
- }
-
- private void init(AbstractSyncAdapter adapter) {
- mAdapter = adapter;
- mService = adapter.mService;
- mContext = mService.mContext;
- mContentResolver = mContext.getContentResolver();
- mMailbox = mService.mMailbox;
- mAccount = mService.mAccount;
- }
-
- /**
- * Read, parse, and act on incoming commands from the Exchange server
- * @throws IOException if the connection is broken
- * @throws CommandStatusException
- */
- public abstract void commandsParser() throws IOException, CommandStatusException;
-
- /**
- * Read, parse, and act on server responses
- * @throws IOException
- */
- public abstract void responsesParser() throws IOException;
-
- /**
- * Commit any changes found during parsing
- * @throws IOException
- */
- public abstract void commit() throws IOException;
-
- public boolean isLooping() {
- return mLooping;
- }
-
- /**
- * Skip through tags until we reach the specified end tag
- * @param endTag the tag we end with
- * @throws IOException
- */
- public void skipParser(int endTag) throws IOException {
- while (nextTag(endTag) != END) {
- skipTag();
- }
- }
-
- /**
- * Loop through the top-level structure coming from the Exchange server
- * Sync keys and the more available flag are handled here, whereas specific data parsing
- * is handled by abstract methods implemented for each data class (e.g. Email, Contacts, etc.)
- * @throws CommandStatusException
- */
- @Override
- public boolean parse() throws IOException, CommandStatusException {
- int status;
- boolean moreAvailable = false;
- boolean newSyncKey = false;
- int interval = mMailbox.mSyncInterval;
- mLooping = false;
- // If we're not at the top of the xml tree, throw an exception
- if (nextTag(START_DOCUMENT) != Tags.SYNC_SYNC) {
- throw new EasParserException();
- }
-
- boolean mailboxUpdated = false;
- ContentValues cv = new ContentValues();
-
- // Loop here through the remaining xml
- while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
- if (tag == Tags.SYNC_COLLECTION || tag == Tags.SYNC_COLLECTIONS) {
- // Ignore these tags, since we've only got one collection syncing in this loop
- } else if (tag == Tags.SYNC_STATUS) {
- // Status = 1 is success; everything else is a failure
- status = getValueInt();
- if (status != 1) {
- mService.errorLog("Sync failed: " + CommandStatus.toString(status));
- if (status == 3 || CommandStatus.isBadSyncKey(status)) {
- // Must delete all of the data and start over with syncKey of "0"
- mAdapter.setSyncKey("0", false);
- // Make this a push box through the first sync
- // TODO Make frequency conditional on user settings!
- mMailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH;
- mService.errorLog("Bad sync key; RESET and delete data");
- mAdapter.wipe();
- // Indicate there's more so that we'll start syncing again
- moreAvailable = true;
- } else if (status == 16 || status == 5) {
- // Status 16 indicates a transient server error (indeterminate state)
- // Status 5 indicates "server error"; this tends to loop for a while so
- // throwing IOException will at least provide backoff behavior
- throw new IOException();
- } else if (status == 8 || status == 12) {
- // Status 8 is Bad; it means the server doesn't recognize the serverId it
- // sent us. 12 means that we're being asked to refresh the folder list.
- // We'll do that with 8 also...
- ExchangeService.reloadFolderList(mContext, mAccount.mId, true);
- // We don't have any provision for telling the user "wait a minute while
- // we sync folders"...
- throw new IOException();
- } else if (status == 7) {
- mService.mUpsyncFailed = true;
- moreAvailable = true;
- } else {
- // Access, provisioning, transient, etc.
- throw new CommandStatusException(status);
- }
- }
- } else if (tag == Tags.SYNC_COMMANDS) {
- commandsParser();
- } else if (tag == Tags.SYNC_RESPONSES) {
- responsesParser();
- } else if (tag == Tags.SYNC_MORE_AVAILABLE) {
- moreAvailable = true;
- } else if (tag == Tags.SYNC_SYNC_KEY) {
- if (mAdapter.getSyncKey().equals("0")) {
- moreAvailable = true;
- }
- String newKey = getValue();
- userLog("Parsed key for ", mMailbox.mDisplayName, ": ", newKey);
- if (!newKey.equals(mMailbox.mSyncKey)) {
- mAdapter.setSyncKey(newKey, true);
- cv.put(MailboxColumns.SYNC_KEY, newKey);
- mailboxUpdated = true;
- newSyncKey = true;
- }
- // If we were pushing (i.e. auto-start), now we'll become ping-triggered
- if (mMailbox.mSyncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
- mMailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_PING;
- }
- } else {
- skipTag();
- }
- }
-
- // If we don't have a new sync key, ignore moreAvailable (or we'll loop)
- if (moreAvailable && !newSyncKey) {
- mLooping = true;
- }
-
- // Commit any changes
- commit();
-
- boolean abortSyncs = false;
-
- // If the sync interval has changed, we need to save it
- if (mMailbox.mSyncInterval != interval) {
- cv.put(MailboxColumns.SYNC_INTERVAL, mMailbox.mSyncInterval);
- mailboxUpdated = true;
- // If there are changes, and we were bounced from push/ping, try again
- } else if (mService.mChangeCount > 0 &&
- mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH &&
- mMailbox.mSyncInterval > 0) {
- userLog("Changes found to ping loop mailbox ", mMailbox.mDisplayName, ": will ping.");
- cv.put(MailboxColumns.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PING);
- mailboxUpdated = true;
- abortSyncs = true;
- }
-
- if (mailboxUpdated) {
- synchronized (mService.getSynchronizer()) {
- if (!mService.isStopped()) {
- mMailbox.update(mContext, cv);
- }
- }
- }
-
- if (abortSyncs) {
- userLog("Aborting account syncs due to mailbox change to ping...");
- ExchangeService.stopAccountSyncs(mAccount.mId);
- }
-
- // Let the caller know that there's more to do
- if (moreAvailable) {
- userLog("MoreAvailable");
- }
- return moreAvailable;
- }
-
- void userLog(String ...strings) {
- mService.userLog(strings);
- }
-
- void userLog(String string, int num, String string2) {
- mService.userLog(string, num, string2);
- }
-}
diff --git a/src/com/android/exchange/adapter/AccountSyncAdapter.java b/src/com/android/exchange/adapter/AccountSyncAdapter.java
deleted file mode 100644
index cf54ed5..0000000
--- a/src/com/android/exchange/adapter/AccountSyncAdapter.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.android.exchange.adapter;
-
-import com.android.exchange.EasSyncService;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-public class AccountSyncAdapter extends AbstractSyncAdapter {
-
- public AccountSyncAdapter(EasSyncService service) {
- super(service);
- }
-
- @Override
- public void cleanup() {
- }
-
- @Override
- public void wipe() {
- }
-
- @Override
- public String getCollectionName() {
- return null;
- }
-
- @Override
- public boolean parse(InputStream is) throws IOException {
- return false;
- }
-
- @Override
- public boolean sendLocalChanges(Serializer s) throws IOException {
- return false;
- }
-
- @Override
- public boolean isSyncable() {
- return true;
- }
-
- @Override
- public void sendSyncOptions(Double protocolVersion, Serializer s) throws IOException {
- }
-}
diff --git a/src/com/android/exchange/adapter/AttachmentLoader.java b/src/com/android/exchange/adapter/AttachmentLoader.java
deleted file mode 100644
index 4d14934..0000000
--- a/src/com/android/exchange/adapter/AttachmentLoader.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/* Copyright (C) 2011 The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.adapter;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.net.Uri;
-import android.os.RemoteException;
-
-import com.android.emailcommon.provider.EmailContent.Attachment;
-import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
-import com.android.emailcommon.provider.EmailContent.Message;
-import com.android.emailcommon.service.EmailServiceStatus;
-import com.android.emailcommon.utility.AttachmentUtilities;
-import com.android.exchange.Eas;
-import com.android.exchange.EasResponse;
-import com.android.exchange.EasSyncService;
-import com.android.exchange.ExchangeService;
-import com.android.exchange.PartRequest;
-import com.android.exchange.utility.UriCodec;
-import com.google.common.annotations.VisibleForTesting;
-
-import org.apache.http.HttpStatus;
-
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * Handle EAS attachment loading, regardless of protocol version
- */
-public class AttachmentLoader {
- static private final int CHUNK_SIZE = 16*1024;
-
- private final EasSyncService mService;
- private final Context mContext;
- private final ContentResolver mResolver;
- private final Attachment mAttachment;
- private final long mAttachmentId;
- private final int mAttachmentSize;
- private final long mMessageId;
- private final Message mMessage;
- private final long mAccountId;
- private final Uri mAttachmentUri;
-
- public AttachmentLoader(EasSyncService service, PartRequest req) {
- mService = service;
- mContext = service.mContext;
- mResolver = service.mContentResolver;
- mAttachment = req.mAttachment;
- mAttachmentId = mAttachment.mId;
- mAttachmentSize = (int)mAttachment.mSize;
- mAccountId = mAttachment.mAccountKey;
- mMessageId = mAttachment.mMessageKey;
- mMessage = Message.restoreMessageWithId(mContext, mMessageId);
- mAttachmentUri = AttachmentUtilities.getAttachmentUri(mAccountId, mAttachmentId);
- }
-
- private void doStatusCallback(int status) {
- try {
- ExchangeService.callback().loadAttachmentStatus(mMessageId, mAttachmentId, status, 0);
- } catch (RemoteException e) {
- // No danger if the client is no longer around
- }
- }
-
- private void doProgressCallback(int progress) {
- try {
- ExchangeService.callback().loadAttachmentStatus(mMessageId, mAttachmentId,
- EmailServiceStatus.IN_PROGRESS, progress);
- } catch (RemoteException e) {
- // No danger if the client is no longer around
- }
- }
-
- /**
- * Save away the contentUri for this Attachment and notify listeners
- */
- private void finishLoadAttachment() {
- ContentValues cv = new ContentValues();
- cv.put(AttachmentColumns.CONTENT_URI, mAttachmentUri.toString());
- mAttachment.update(mContext, cv);
- doStatusCallback(EmailServiceStatus.SUCCESS);
- }
-
- /**
- * Read the attachment data in chunks and write the data back out to our attachment file
- * @param inputStream the InputStream we're reading the attachment from
- * @param outputStream the OutputStream the attachment will be written to
- * @param len the number of expected bytes we're going to read
- * @throws IOException
- */
- public void readChunked(InputStream inputStream, OutputStream outputStream, int len)
- throws IOException {
- byte[] bytes = new byte[CHUNK_SIZE];
- int length = len;
- // Loop terminates 1) when EOF is reached or 2) IOException occurs
- // One of these is guaranteed to occur
- int totalRead = 0;
- int lastCallbackPct = -1;
- int lastCallbackTotalRead = 0;
- mService.userLog("Expected attachment length: ", len);
- while (true) {
- int read = inputStream.read(bytes, 0, CHUNK_SIZE);
- if (read < 0) {
- // -1 means EOF
- mService.userLog("Attachment load reached EOF, totalRead: ", totalRead);
- break;
- }
-
- // Keep track of how much we've read for progress callback
- totalRead += read;
- // Write these bytes out
- outputStream.write(bytes, 0, read);
-
- // We can't report percentage if data is chunked; the length of incoming data is unknown
- if (length > 0) {
- int pct = (totalRead * 100) / length;
- // Callback only if we've read at least 1% more and have read more than CHUNK_SIZE
- // We don't want to spam the Email app
- if ((pct > lastCallbackPct) && (totalRead > (lastCallbackTotalRead + CHUNK_SIZE))) {
- // Report progress back to the UI
- doProgressCallback(pct);
- lastCallbackTotalRead = totalRead;
- lastCallbackPct = pct;
- }
- }
- }
- if (totalRead > length) {
- // Apparently, the length, as reported by EAS, isn't always accurate; let's log it
- mService.userLog("Read more than expected: ", totalRead);
- }
- }
-
- @VisibleForTesting
- static String encodeForExchange2003(String str) {
- AttachmentNameEncoder enc = new AttachmentNameEncoder();
- StringBuilder sb = new StringBuilder(str.length() + 16);
- enc.appendPartiallyEncoded(sb, str);
- return sb.toString();
- }
-
- /**
- * Encoder for Exchange 2003 attachment names. They come from the server partially encoded,
- * but there are still possible characters that need to be encoded (Why, MSFT, why?)
- */
- private static class AttachmentNameEncoder extends UriCodec {
- @Override protected boolean isRetained(char c) {
- // These four characters are commonly received in EAS 2.5 attachment names and are
- // valid (verified by testing); we won't encode them
- return c == '_' || c == ':' || c == '/' || c == '.';
- }
- }
-
- /**
- * Loads an attachment, based on the PartRequest passed in the constructor
- * @throws IOException
- */
- public void loadAttachment() throws IOException {
- if (mMessage == null) {
- doStatusCallback(EmailServiceStatus.MESSAGE_NOT_FOUND);
- return;
- }
- // Say we've started loading the attachment
- doProgressCallback(0);
-
- EasResponse resp;
- boolean eas14 = mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE;
- // The method of attachment loading is different in EAS 14.0 than in earlier versions
- if (eas14) {
- Serializer s = new Serializer();
- s.start(Tags.ITEMS_ITEMS).start(Tags.ITEMS_FETCH);
- s.data(Tags.ITEMS_STORE, "Mailbox");
- s.data(Tags.BASE_FILE_REFERENCE, mAttachment.mLocation);
- s.end().end().done(); // ITEMS_FETCH, ITEMS_ITEMS
- resp = mService.sendHttpClientPost("ItemOperations", s.toByteArray());
- } else {
- String location = mAttachment.mLocation;
- // For Exchange 2003 (EAS 2.5), we have to look for illegal characters in the file name
- // that EAS sent to us!
- if (mService.mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
- location = encodeForExchange2003(location);
- }
- String cmd = "GetAttachment&AttachmentName=" + location;
- resp = mService.sendHttpClientPost(cmd, null, EasSyncService.COMMAND_TIMEOUT);
- }
-
- try {
- int status = resp.getStatus();
- if (status == HttpStatus.SC_OK) {
- if (!resp.isEmpty()) {
- InputStream is = resp.getInputStream();
- OutputStream os = null;
- try {
- os = mResolver.openOutputStream(mAttachmentUri);
- if (eas14) {
- ItemOperationsParser p = new ItemOperationsParser(this, is, os,
- mAttachmentSize);
- p.parse();
- if (p.getStatusCode() == 1 /* Success */) {
- finishLoadAttachment();
- return;
- }
- } else {
- int len = resp.getLength();
- if (len != 0) {
- // len > 0 means that Content-Length was set in the headers
- // len < 0 means "chunked" transfer-encoding
- readChunked(is, os, (len < 0) ? mAttachmentSize : len);
- finishLoadAttachment();
- return;
- }
- }
- } catch (FileNotFoundException e) {
- mService.errorLog("Can't get attachment; write file not found?");
- } finally {
- if (os != null) {
- os.flush();
- os.close();
- }
- }
- }
- }
- } finally {
- resp.close();
- }
-
- // All errors lead here...
- doStatusCallback(EmailServiceStatus.ATTACHMENT_NOT_FOUND);
- }
-}
diff --git a/src/com/android/exchange/adapter/Base64InputStream.java b/src/com/android/exchange/adapter/Base64InputStream.java
deleted file mode 100644
index 5b78c4c..0000000
--- a/src/com/android/exchange/adapter/Base64InputStream.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/****************************************************************
- * Licensed to the Apache Software Foundation (ASF) under one *
- * or more contributor license agreements. See the NOTICE file *
- * distributed with this work for additional information *
- * regarding copyright ownership. The ASF licenses this file *
- * to you under the Apache License, Version 2.0 (the *
- * "License"); you may not use this file except in compliance *
- * with the License. You may obtain a copy of the License at *
- * *
- * http://www.apache.org/licenses/LICENSE-2.0 *
- * *
- * Unless required by applicable law or agreed to in writing, *
- * software distributed under the License is distributed on an *
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
- * KIND, either express or implied. See the License for the *
- * specific language governing permissions and limitations *
- * under the License. *
- ****************************************************************/
-
-/**
- * Modified for Exchange attachment decoding 5/12/11. Changes are bracketed with START EAS CHANGES
- * and END EAS CHANGES
- *
- * Without the included changes, the final bytes of the input stream will be read here and thrown
- * away; in that case, the WBXML parser will lose information necessary to determine that the
- * entire stream has been processed correctly. Since inline WBXML text is terminated with a zero
- * byte, and since zero is not valid Base64, we terminate reading when we find a zero byte, leaving
- * the remainder of the stream untouched (to be read by the Parser that created the
- * Base64InputStream.
- */
-
-package com.android.exchange.adapter;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Performs Base-64 decoding on an underlying stream.
- *
- *
- * @version $Id: Base64InputStream.java,v 1.3 2004/11/29 13:15:47 ntherning Exp $
- */
-public class Base64InputStream extends InputStream {
- private final InputStream s;
- private int outCount = 0;
- private int outIndex = 0;
- private final int[] outputBuffer = new int[3];
- private final byte[] inputBuffer = new byte[4];
- private boolean done = false;
- // START EAS CHANGES
- private boolean padSeen = false;
- // END EAS CHANGES
-
- public Base64InputStream(InputStream s) {
- this.s = s;
- }
-
- /**
- * Closes the underlying stream.
- *
- * @throws IOException on I/O errors.
- */
- @Override
- public void close() throws IOException {
- s.close();
- }
-
- @Override
- public int read() throws IOException {
- if (outIndex == outCount) {
- fillBuffer();
- if (outIndex == outCount) {
- return -1;
- }
- }
-
- return outputBuffer[outIndex++];
- }
-
- /**
- * Retrieve data from the underlying stream, decode it,
- * and put the results in the byteq.
- * @throws IOException
- */
- private void fillBuffer() throws IOException {
- outCount = 0;
- outIndex = 0;
- int inCount = 0;
-
- int i;
- // "done" is needed for the two successive '=' at the end
- while (!done) {
- switch (i = s.read()) {
- // START EAS CHANGES
- case 0:
- // In EAS, a zero will indicate the end of the (inline) base64 string
- // Stop reading at this point, so that we can continue with WBXML
- done = true;
- return;
- // END EAS CHANGES
- case -1:
- // No more input - just return, let outputBuffer drain out, and be done
- return;
- case '=':
- // START EAS CHANGES
- // Allow for a second '=' before we're really done (we should get a zero next)
- if (padSeen) {
- return;
- }
- // We've seen a (first) end padding character, flush what's in the buffer
- padSeen = true;
- // END EAS CHANGES
- // PRE-EAS LINE COMMENTED OUT
- //done = true;
- decodeAndEnqueue(inCount);
- return;
- default:
- byte sX = TRANSLATION[i];
- if (sX < 0) continue;
- inputBuffer[inCount++] = sX;
- if (inCount == 4) {
- decodeAndEnqueue(inCount);
- return;
- }
- break;
- }
- }
- }
-
- private void decodeAndEnqueue(int len) {
- int accum = 0;
- accum |= inputBuffer[0] << 18;
- accum |= inputBuffer[1] << 12;
- accum |= inputBuffer[2] << 6;
- accum |= inputBuffer[3];
-
- // There's a bit of duplicated code here because we want to have straight-through operation
- // for the most common case of len==4
- if (len == 4) {
- outputBuffer[0] = (accum >> 16) & 0xFF;
- outputBuffer[1] = (accum >> 8) & 0xFF;
- outputBuffer[2] = (accum) & 0xFF;
- outCount = 3;
- return;
- } else if (len == 3) {
- outputBuffer[0] = (accum >> 16) & 0xFF;
- outputBuffer[1] = (accum >> 8) & 0xFF;
- outCount = 2;
- return;
- } else { // len == 2
- outputBuffer[0] = (accum >> 16) & 0xFF;
- outCount = 1;
- return;
- }
- }
-
- private static byte[] TRANSLATION = {
- -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x00 */
- -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x10 */
- -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, /* 0x20 */
- 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, /* 0x30 */
- -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 0x40 */
- 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, /* 0x50 */
- -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* 0x60 */
- 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, /* 0x70 */
- -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x80 */
- -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x90 */
- -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xA0 */
- -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xB0 */
- -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xC0 */
- -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xD0 */
- -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xE0 */
- -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 /* 0xF0 */
- };
-}
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
deleted file mode 100644
index c8ab766..0000000
--- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ /dev/null
@@ -1,2163 +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.adapter;
-
-import android.content.ContentProviderClient;
-import android.content.ContentProviderOperation;
-import android.content.ContentProviderResult;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Entity;
-import android.content.Entity.NamedContentValues;
-import android.content.EntityIterator;
-import android.database.Cursor;
-import android.database.DatabaseUtils;
-import android.net.Uri;
-import android.os.RemoteException;
-import android.provider.CalendarContract;
-import android.provider.CalendarContract.Attendees;
-import android.provider.CalendarContract.Calendars;
-import android.provider.CalendarContract.Events;
-import android.provider.CalendarContract.EventsEntity;
-import android.provider.CalendarContract.ExtendedProperties;
-import android.provider.CalendarContract.Reminders;
-import android.provider.CalendarContract.SyncState;
-import android.provider.ContactsContract.RawContacts;
-import android.provider.SyncStateContract;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.emailcommon.AccountManagerTypes;
-import com.android.emailcommon.provider.EmailContent;
-import com.android.emailcommon.provider.EmailContent.Message;
-import com.android.emailcommon.utility.Utility;
-import com.android.exchange.CommandStatusException;
-import com.android.exchange.Eas;
-import com.android.exchange.EasOutboxService;
-import com.android.exchange.EasSyncService;
-import com.android.exchange.ExchangeService;
-import com.android.exchange.utility.CalendarUtilities;
-import com.android.exchange.utility.Duration;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.GregorianCalendar;
-import java.util.Map.Entry;
-import java.util.StringTokenizer;
-import java.util.TimeZone;
-import java.util.UUID;
-
-/**
- * Sync adapter class for EAS calendars
- *
- */
-public class CalendarSyncAdapter extends AbstractSyncAdapter {
-
- private static final String TAG = "EasCalendarSyncAdapter";
-
- private static final String EVENT_SAVED_TIMEZONE_COLUMN = Events.SYNC_DATA1;
- /**
- * Used to keep track of exception vs parent event dirtiness.
- */
- private static final String EVENT_SYNC_MARK = Events.SYNC_DATA8;
- private static final String EVENT_SYNC_VERSION = Events.SYNC_DATA4;
- // Since exceptions will have the same _SYNC_ID as the original event we have to check that
- // there's no original event when finding an item by _SYNC_ID
- private static final String SERVER_ID_AND_CALENDAR_ID = Events._SYNC_ID + "=? AND " +
- Events.ORIGINAL_SYNC_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?";
- private static final String EVENT_ID_AND_CALENDAR_ID = Events._ID + "=? AND " +
- Events.ORIGINAL_SYNC_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?";
- private static final String DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR = "(" + Events.DIRTY
- + "=1 OR " + EVENT_SYNC_MARK + "= 1) AND " +
- Events.ORIGINAL_ID + " ISNULL AND " + Events.CALENDAR_ID + "=?";
- private static final String DIRTY_EXCEPTION_IN_CALENDAR =
- Events.DIRTY + "=1 AND " + Events.ORIGINAL_ID + " NOTNULL AND " +
- Events.CALENDAR_ID + "=?";
- private static final String CLIENT_ID_SELECTION = Events.SYNC_DATA2 + "=?";
- private static final String ORIGINAL_EVENT_AND_CALENDAR =
- Events.ORIGINAL_SYNC_ID + "=? AND " + Events.CALENDAR_ID + "=?";
- private static final String ATTENDEES_EXCEPT_ORGANIZER = Attendees.EVENT_ID + "=? AND " +
- Attendees.ATTENDEE_RELATIONSHIP + "!=" + Attendees.RELATIONSHIP_ORGANIZER;
- private static final String[] ID_PROJECTION = new String[] {Events._ID};
- private static final String[] ORIGINAL_EVENT_PROJECTION =
- new String[] {Events.ORIGINAL_ID, Events._ID};
- private static final String EVENT_ID_AND_NAME =
- ExtendedProperties.EVENT_ID + "=? AND " + ExtendedProperties.NAME + "=?";
-
- // Note that we use LIKE below for its case insensitivity
- private static final String EVENT_AND_EMAIL =
- Attendees.EVENT_ID + "=? AND "+ Attendees.ATTENDEE_EMAIL + " LIKE ?";
- private static final int ATTENDEE_STATUS_COLUMN_STATUS = 0;
- private static final String[] ATTENDEE_STATUS_PROJECTION =
- new String[] {Attendees.ATTENDEE_STATUS};
-
- public static final String CALENDAR_SELECTION =
- Calendars.ACCOUNT_NAME + "=? AND " + Calendars.ACCOUNT_TYPE + "=?";
- private static final int CALENDAR_SELECTION_ID = 0;
-
- private static final String[] EXTENDED_PROPERTY_PROJECTION =
- new String[] {ExtendedProperties._ID};
- private static final int EXTENDED_PROPERTY_ID = 0;
-
- private static final String CATEGORY_TOKENIZER_DELIMITER = "\\";
- private static final String ATTENDEE_TOKENIZER_DELIMITER = CATEGORY_TOKENIZER_DELIMITER;
-
- private static final String EXTENDED_PROPERTY_USER_ATTENDEE_STATUS = "userAttendeeStatus";
- private static final String EXTENDED_PROPERTY_ATTENDEES = "attendees";
- private static final String EXTENDED_PROPERTY_DTSTAMP = "dtstamp";
- private static final String EXTENDED_PROPERTY_MEETING_STATUS = "meeting_status";
- private static final String EXTENDED_PROPERTY_CATEGORIES = "categories";
- // Used to indicate that we removed the attendee list because it was too large
- private static final String EXTENDED_PROPERTY_ATTENDEES_REDACTED = "attendeesRedacted";
- // Used to indicate that upsyncs aren't allowed (we catch this in sendLocalChanges)
- private static final String EXTENDED_PROPERTY_UPSYNC_PROHIBITED = "upsyncProhibited";
-
- private static final Operation PLACEHOLDER_OPERATION =
- new Operation(ContentProviderOperation.newInsert(Uri.EMPTY));
-
- private static final Object sSyncKeyLock = new Object();
-
- private static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
- private final TimeZone mLocalTimeZone = TimeZone.getDefault();
-
-
- // Maximum number of allowed attendees; above this number, we mark the Event with the
- // attendeesRedacted extended property and don't allow the event to be upsynced to the server
- private static final int MAX_SYNCED_ATTENDEES = 50;
- // We set the organizer to this when the user is the organizer and we've redacted the
- // attendee list. By making the meeting organizer OTHER than the user, we cause the UI to
- // prevent edits to this event (except local changes like reminder).
- private static final String BOGUS_ORGANIZER_EMAIL = "upload_disallowed@uploadisdisallowed.aaa";
- // Maximum number of CPO's before we start redacting attendees in exceptions
- // The number 500 has been determined empirically; 1500 CPOs appears to be the limit before
- // binder failures occur, but we need room at any point for additional events/exceptions so
- // we set our limit at 1/3 of the apparent maximum for extra safety
- // TODO Find a better solution to this workaround
- private static final int MAX_OPS_BEFORE_EXCEPTION_ATTENDEE_REDACTION = 500;
-
- private long mCalendarId = -1;
- private String mCalendarIdString;
- private String[] mCalendarIdArgument;
- /*package*/ String mEmailAddress;
-
- private ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
- private ArrayList<Long> mUploadedIdList = new ArrayList<Long>();
- private ArrayList<Long> mSendCancelIdList = new ArrayList<Long>();
- private ArrayList<Message> mOutgoingMailList = new ArrayList<Message>();
-
- private final Uri mAsSyncAdapterAttendees;
- private final Uri mAsSyncAdapterEvents;
- private final Uri mAsSyncAdapterReminders;
- private final Uri mAsSyncAdapterExtendedProperties;
-
- public CalendarSyncAdapter(EasSyncService service) {
- super(service);
- mEmailAddress = mAccount.mEmailAddress;
-
- String amType = Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE;
- mAsSyncAdapterAttendees =
- asSyncAdapter(Attendees.CONTENT_URI, mEmailAddress, amType);
- mAsSyncAdapterEvents =
- asSyncAdapter(Events.CONTENT_URI, mEmailAddress, amType);
- mAsSyncAdapterReminders =
- asSyncAdapter(Reminders.CONTENT_URI, mEmailAddress, amType);
- mAsSyncAdapterExtendedProperties =
- asSyncAdapter(ExtendedProperties.CONTENT_URI, mEmailAddress, amType);
-
- Cursor c = mService.mContentResolver.query(Calendars.CONTENT_URI,
- new String[] {Calendars._ID}, CALENDAR_SELECTION,
- new String[] {mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE}, null);
- if (c == null) return;
- try {
- if (c.moveToFirst()) {
- mCalendarId = c.getLong(CALENDAR_SELECTION_ID);
- } else {
- mCalendarId = CalendarUtilities.createCalendar(mService, mAccount, mMailbox);
- }
- mCalendarIdString = Long.toString(mCalendarId);
- mCalendarIdArgument = new String[] {mCalendarIdString};
- } finally {
- c.close();
- }
- }
-
- @Override
- public String getCollectionName() {
- return "Calendar";
- }
-
- @Override
- public void cleanup() {
- }
-
- @Override
- public void wipe() {
- // Delete the calendar associated with this account
- // CalendarProvider2 does NOT handle selection arguments in deletions
- mContentResolver.delete(
- asSyncAdapter(Calendars.CONTENT_URI, mEmailAddress,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
- Calendars.ACCOUNT_NAME + "=" + DatabaseUtils.sqlEscapeString(mEmailAddress)
- + " AND " + Calendars.ACCOUNT_TYPE + "="
- + DatabaseUtils.sqlEscapeString(AccountManagerTypes.TYPE_EXCHANGE), null);
- // Invalidate our calendar observers
- ExchangeService.unregisterCalendarObservers();
- }
-
- @Override
- public void sendSyncOptions(Double protocolVersion, Serializer s) throws IOException {
- setPimSyncOptions(protocolVersion, Eas.FILTER_2_WEEKS, s);
- }
-
- @Override
- public boolean isSyncable() {
- return ContentResolver.getSyncAutomatically(mAccountManagerAccount,
- CalendarContract.AUTHORITY);
- }
-
- @Override
- public boolean parse(InputStream is) throws IOException, CommandStatusException {
- EasCalendarSyncParser p = new EasCalendarSyncParser(is, this);
- return p.parse();
- }
-
- public static Uri asSyncAdapter(Uri uri, String account, String accountType) {
- return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
- .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
- .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
- }
-
- /**
- * Generate the uri for the data row associated with this NamedContentValues object
- * @param ncv the NamedContentValues object
- * @return a uri that can be used to refer to this row
- */
- public Uri dataUriFromNamedContentValues(NamedContentValues ncv) {
- long id = ncv.values.getAsLong(RawContacts._ID);
- Uri dataUri = ContentUris.withAppendedId(ncv.uri, id);
- return dataUri;
- }
-
- /**
- * We get our SyncKey from CalendarProvider. If there's not one, we set it to "0" (the reset
- * state) and save that away.
- */
- @Override
- public String getSyncKey() throws IOException {
- synchronized (sSyncKeyLock) {
- ContentProviderClient client = mService.mContentResolver
- .acquireContentProviderClient(CalendarContract.CONTENT_URI);
- try {
- byte[] data = SyncStateContract.Helpers.get(
- client,
- asSyncAdapter(SyncState.CONTENT_URI, mEmailAddress,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mAccountManagerAccount);
- if (data == null || data.length == 0) {
- // Initialize the SyncKey
- setSyncKey("0", false);
- return "0";
- } else {
- String syncKey = new String(data);
- userLog("SyncKey retrieved as ", syncKey, " from CalendarProvider");
- return syncKey;
- }
- } catch (RemoteException e) {
- throw new IOException("Can't get SyncKey from CalendarProvider");
- }
- }
- }
-
- /**
- * We only need to set this when we're forced to make the SyncKey "0" (a reset). In all other
- * cases, the SyncKey is set within Calendar
- */
- @Override
- public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
- synchronized (sSyncKeyLock) {
- if ("0".equals(syncKey) || !inCommands) {
- ContentProviderClient client = mService.mContentResolver
- .acquireContentProviderClient(CalendarContract.CONTENT_URI);
- try {
- SyncStateContract.Helpers.set(
- client,
- asSyncAdapter(SyncState.CONTENT_URI, mEmailAddress,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mAccountManagerAccount,
- syncKey.getBytes());
- userLog("SyncKey set to ", syncKey, " in CalendarProvider");
- } catch (RemoteException e) {
- throw new IOException("Can't set SyncKey in CalendarProvider");
- }
- }
- mMailbox.mSyncKey = syncKey;
- }
- }
-
- public class EasCalendarSyncParser extends AbstractSyncParser {
-
- String[] mBindArgument = new String[1];
- Uri mAccountUri;
- CalendarOperations mOps = new CalendarOperations();
-
- public EasCalendarSyncParser(InputStream in, CalendarSyncAdapter adapter)
- throws IOException {
- super(in, adapter);
- setLoggingTag("CalendarParser");
- mAccountUri = Events.CONTENT_URI;
- }
-
- private void addOrganizerToAttendees(CalendarOperations ops, long eventId,
- String organizerName, String organizerEmail) {
- // Handle the organizer (who IS an attendee on device, but NOT in EAS)
- if (organizerName != null || organizerEmail != null) {
- ContentValues attendeeCv = new ContentValues();
- if (organizerName != null) {
- attendeeCv.put(Attendees.ATTENDEE_NAME, organizerName);
- }
- if (organizerEmail != null) {
- attendeeCv.put(Attendees.ATTENDEE_EMAIL, organizerEmail);
- }
- attendeeCv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
- attendeeCv.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
- attendeeCv.put(Attendees.ATTENDEE_STATUS, Attendees.ATTENDEE_STATUS_ACCEPTED);
- if (eventId < 0) {
- ops.newAttendee(attendeeCv);
- } else {
- ops.updatedAttendee(attendeeCv, eventId);
- }
- }
- }
-
- /**
- * Set DTSTART, DTEND, DURATION and EVENT_TIMEZONE as appropriate for the given Event
- * The follow rules are enforced by CalendarProvider2:
- * Events that aren't exceptions MUST have either 1) a DTEND or 2) a DURATION
- * Recurring events (i.e. events with RRULE) must have a DURATION
- * All-day recurring events MUST have a DURATION that is in the form P<n>D
- * Other events MAY have a DURATION in any valid form (we use P<n>M)
- * All-day events MUST have hour, minute, and second = 0; in addition, they must have
- * the EVENT_TIMEZONE set to UTC
- * Also, exceptions to all-day events need to have an ORIGINAL_INSTANCE_TIME that has
- * hour, minute, and second = 0 and be set in UTC
- * @param cv the ContentValues for the Event
- * @param startTime the start time for the Event
- * @param endTime the end time for the Event
- * @param allDayEvent whether this is an all day event (1) or not (0)
- */
- /*package*/ void setTimeRelatedValues(ContentValues cv, long startTime, long endTime,
- int allDayEvent) {
- // If there's no startTime, the event will be found to be invalid, so return
- if (startTime < 0) return;
- // EAS events can arrive without an end time, but CalendarProvider requires them
- // so we'll default to 30 minutes; this will be superceded if this is an all-day event
- if (endTime < 0) endTime = startTime + (30*MINUTES);
-
- // If this is an all-day event, set hour, minute, and second to zero, and use UTC
- if (allDayEvent != 0) {
- startTime = CalendarUtilities.getUtcAllDayCalendarTime(startTime, mLocalTimeZone);
- endTime = CalendarUtilities.getUtcAllDayCalendarTime(endTime, mLocalTimeZone);
- String originalTimeZone = cv.getAsString(Events.EVENT_TIMEZONE);
- cv.put(EVENT_SAVED_TIMEZONE_COLUMN, originalTimeZone);
- cv.put(Events.EVENT_TIMEZONE, UTC_TIMEZONE.getID());
- }
-
- // If this is an exception, and the original was an all-day event, make sure the
- // original instance time has hour, minute, and second set to zero, and is in UTC
- if (cv.containsKey(Events.ORIGINAL_INSTANCE_TIME) &&
- cv.containsKey(Events.ORIGINAL_ALL_DAY)) {
- Integer ade = cv.getAsInteger(Events.ORIGINAL_ALL_DAY);
- if (ade != null && ade != 0) {
- long exceptionTime = cv.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
- GregorianCalendar cal = new GregorianCalendar(UTC_TIMEZONE);
- cal.setTimeInMillis(exceptionTime);
- cal.set(GregorianCalendar.HOUR_OF_DAY, 0);
- cal.set(GregorianCalendar.MINUTE, 0);
- cal.set(GregorianCalendar.SECOND, 0);
- cv.put(Events.ORIGINAL_INSTANCE_TIME, cal.getTimeInMillis());
- }
- }
-
- // Always set DTSTART
- cv.put(Events.DTSTART, startTime);
- // For recurring events, set DURATION. Use P<n>D format for all day events
- if (cv.containsKey(Events.RRULE)) {
- if (allDayEvent != 0) {
- cv.put(Events.DURATION, "P" + ((endTime - startTime) / DAYS) + "D");
- }
- else {
- cv.put(Events.DURATION, "P" + ((endTime - startTime) / MINUTES) + "M");
- }
- // For other events, set DTEND and LAST_DATE
- } else {
- cv.put(Events.DTEND, endTime);
- cv.put(Events.LAST_DATE, endTime);
- }
- }
-
- public void addEvent(CalendarOperations ops, String serverId, boolean update)
- throws IOException {
- ContentValues cv = new ContentValues();
- cv.put(Events.CALENDAR_ID, mCalendarId);
- cv.put(Events._SYNC_ID, serverId);
- cv.put(Events.HAS_ATTENDEE_DATA, 1);
- cv.put(Events.SYNC_DATA2, "0");
-
- int allDayEvent = 0;
- String organizerName = null;
- String organizerEmail = null;
- int eventOffset = -1;
- int deleteOffset = -1;
- int busyStatus = CalendarUtilities.BUSY_STATUS_TENTATIVE;
- int responseType = CalendarUtilities.RESPONSE_TYPE_NONE;
-
- boolean firstTag = true;
- long eventId = -1;
- long startTime = -1;
- long endTime = -1;
- TimeZone timeZone = null;
-
- // Keep track of the attendees; exceptions will need them
- ArrayList<ContentValues> attendeeValues = new ArrayList<ContentValues>();
- int reminderMins = -1;
- String dtStamp = null;
- boolean organizerAdded = false;
-
- while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
- if (update && firstTag) {
- // Find the event that's being updated
- Cursor c = getServerIdCursor(serverId);
- long id = -1;
- try {
- if (c != null && c.moveToFirst()) {
- id = c.getLong(0);
- }
- } finally {
- if (c != null) c.close();
- }
- if (id > 0) {
- // DTSTAMP can come first, and we simply need to track it
- if (tag == Tags.CALENDAR_DTSTAMP) {
- dtStamp = getValue();
- continue;
- } else if (tag == Tags.CALENDAR_ATTENDEES) {
- // This is an attendees-only update; just
- // delete/re-add attendees
- mBindArgument[0] = Long.toString(id);
- ops.add(new Operation(ContentProviderOperation
- .newDelete(mAsSyncAdapterAttendees)
- .withSelection(ATTENDEES_EXCEPT_ORGANIZER, mBindArgument)));
- eventId = id;
- } else {
- // Otherwise, delete the original event and recreate it
- userLog("Changing (delete/add) event ", serverId);
- deleteOffset = ops.newDelete(id, serverId);
- // Add a placeholder event so that associated tables can reference
- // this as a back reference. We add the event at the end of the method
- eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
- }
- } else {
- // The changed item isn't found. We'll treat this as a new item
- eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
- userLog(TAG, "Changed item not found; treating as new.");
- }
- } else if (firstTag) {
- // Add a placeholder event so that associated tables can reference
- // this as a back reference. We add the event at the end of the method
- eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
- }
- firstTag = false;
- switch (tag) {
- case Tags.CALENDAR_ALL_DAY_EVENT:
- allDayEvent = getValueInt();
- if (allDayEvent != 0 && timeZone != null) {
- // If the event doesn't start at midnight local time, we won't consider
- // this an all-day event in the local time zone (this is what OWA does)
- GregorianCalendar cal = new GregorianCalendar(mLocalTimeZone);
- cal.setTimeInMillis(startTime);
- userLog("All-day event arrived in: " + timeZone.getID());
- if (cal.get(GregorianCalendar.HOUR_OF_DAY) != 0 ||
- cal.get(GregorianCalendar.MINUTE) != 0) {
- allDayEvent = 0;
- userLog("Not an all-day event locally: " + mLocalTimeZone.getID());
- }
- }
- cv.put(Events.ALL_DAY, allDayEvent);
- break;
- case Tags.CALENDAR_ATTACHMENTS:
- attachmentsParser();
- break;
- case Tags.CALENDAR_ATTENDEES:
- // If eventId >= 0, this is an update; otherwise, a new Event
- attendeeValues = attendeesParser(ops, eventId);
- break;
- case Tags.BASE_BODY:
- cv.put(Events.DESCRIPTION, bodyParser());
- break;
- case Tags.CALENDAR_BODY:
- cv.put(Events.DESCRIPTION, getValue());
- break;
- case Tags.CALENDAR_TIME_ZONE:
- timeZone = CalendarUtilities.tziStringToTimeZone(getValue());
- if (timeZone == null) {
- timeZone = mLocalTimeZone;
- }
- cv.put(Events.EVENT_TIMEZONE, timeZone.getID());
- break;
- case Tags.CALENDAR_START_TIME:
- startTime = Utility.parseDateTimeToMillis(getValue());
- break;
- case Tags.CALENDAR_END_TIME:
- endTime = Utility.parseDateTimeToMillis(getValue());
- break;
- case Tags.CALENDAR_EXCEPTIONS:
- // For exceptions to show the organizer, the organizer must be added before
- // we call exceptionsParser
- addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail);
- organizerAdded = true;
- exceptionsParser(ops, cv, attendeeValues, reminderMins, busyStatus,
- startTime, endTime);
- break;
- case Tags.CALENDAR_LOCATION:
- cv.put(Events.EVENT_LOCATION, getValue());
- break;
- case Tags.CALENDAR_RECURRENCE:
- String rrule = recurrenceParser();
- if (rrule != null) {
- cv.put(Events.RRULE, rrule);
- }
- break;
- case Tags.CALENDAR_ORGANIZER_EMAIL:
- organizerEmail = getValue();
- cv.put(Events.ORGANIZER, organizerEmail);
- break;
- case Tags.CALENDAR_SUBJECT:
- cv.put(Events.TITLE, getValue());
- break;
- case Tags.CALENDAR_SENSITIVITY:
- cv.put(Events.ACCESS_LEVEL, encodeVisibility(getValueInt()));
- break;
- case Tags.CALENDAR_ORGANIZER_NAME:
- organizerName = getValue();
- break;
- case Tags.CALENDAR_REMINDER_MINS_BEFORE:
- // Save away whether this tag has content; Exchange 2010 sends an empty tag
- // rather than not sending one (as with Ex07 and Ex03)
- boolean hasContent = !noContent;
- reminderMins = getValueInt();
- if (hasContent) {
- ops.newReminder(reminderMins);
- cv.put(Events.HAS_ALARM, 1);
- }
- break;
- // The following are fields we should save (for changes), though they don't
- // relate to data used by CalendarProvider at this point
- case Tags.CALENDAR_UID:
- cv.put(Events.SYNC_DATA2, getValue());
- break;
- case Tags.CALENDAR_DTSTAMP:
- dtStamp = getValue();
- break;
- case Tags.CALENDAR_MEETING_STATUS:
- ops.newExtendedProperty(EXTENDED_PROPERTY_MEETING_STATUS, getValue());
- break;
- case Tags.CALENDAR_BUSY_STATUS:
- // We'll set the user's status in the Attendees table below
- // Don't set selfAttendeeStatus or CalendarProvider will create a duplicate
- // attendee!
- busyStatus = getValueInt();
- break;
- case Tags.CALENDAR_RESPONSE_TYPE:
- // EAS 14+ uses this for the user's response status; we'll use this instead
- // of busy status, if it appears
- responseType = getValueInt();
- break;
- case Tags.CALENDAR_CATEGORIES:
- String categories = categoriesParser(ops);
- if (categories.length() > 0) {
- ops.newExtendedProperty(EXTENDED_PROPERTY_CATEGORIES, categories);
- }
- break;
- default:
- skipTag();
- }
- }
-
- // Enforce CalendarProvider required properties
- setTimeRelatedValues(cv, startTime, endTime, allDayEvent);
-
- // If we haven't added the organizer to attendees, do it now
- if (!organizerAdded) {
- addOrganizerToAttendees(ops, eventId, organizerName, organizerEmail);
- }
-
- // Note that organizerEmail can be null with a DTSTAMP only change from the server
- boolean selfOrganizer = (mEmailAddress.equals(organizerEmail));
-
- // Store email addresses of attendees (in a tokenizable string) in ExtendedProperties
- // If the user is an attendee, set the attendee status using busyStatus (note that the
- // busyStatus is inherited from the parent unless it's specified in the exception)
- // Add the insert/update operation for each attendee (based on whether it's add/change)
- int numAttendees = attendeeValues.size();
- if (numAttendees > MAX_SYNCED_ATTENDEES) {
- // Indicate that we've redacted attendees. If we're the organizer, disable edit
- // by setting organizerEmail to a bogus value and by setting the upsync prohibited
- // extended properly.
- // Note that we don't set ANY attendees if we're in this branch; however, the
- // organizer has already been included above, and WILL show up (which is good)
- if (eventId < 0) {
- ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "1");
- if (selfOrganizer) {
- ops.newExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "1");
- }
- } else {
- ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "1", eventId);
- if (selfOrganizer) {
- ops.updatedExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "1",
- eventId);
- }
- }
- if (selfOrganizer) {
- organizerEmail = BOGUS_ORGANIZER_EMAIL;
- cv.put(Events.ORGANIZER, organizerEmail);
- }
- // Tell UI that we don't have any attendees
- cv.put(Events.HAS_ATTENDEE_DATA, "0");
- mService.userLog("Maximum number of attendees exceeded; redacting");
- } else if (numAttendees > 0) {
- StringBuilder sb = new StringBuilder();
- for (ContentValues attendee: attendeeValues) {
- String attendeeEmail = attendee.getAsString(Attendees.ATTENDEE_EMAIL);
- sb.append(attendeeEmail);
- sb.append(ATTENDEE_TOKENIZER_DELIMITER);
- if (mEmailAddress.equalsIgnoreCase(attendeeEmail)) {
- int attendeeStatus;
- // We'll use the response type (EAS 14), if we've got one; otherwise, we'll
- // try to infer it from busy status
- if (responseType != CalendarUtilities.RESPONSE_TYPE_NONE) {
- attendeeStatus =
- CalendarUtilities.attendeeStatusFromResponseType(responseType);
- } else if (!update) {
- // For new events in EAS < 14, we have no idea what the busy status
- // means, so we show "none", allowing the user to select an option.
- attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
- } else {
- // For updated events, we'll try to infer the attendee status from the
- // busy status
- attendeeStatus =
- CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus);
- }
- attendee.put(Attendees.ATTENDEE_STATUS, attendeeStatus);
- // If we're an attendee, save away our initial attendee status in the
- // event's ExtendedProperties (we look for differences between this and
- // the user's current attendee status to determine whether an email needs
- // to be sent to the organizer)
- // organizerEmail will be null in the case that this is an attendees-only
- // change from the server
- if (organizerEmail == null ||
- !organizerEmail.equalsIgnoreCase(attendeeEmail)) {
- if (eventId < 0) {
- ops.newExtendedProperty(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS,
- Integer.toString(attendeeStatus));
- } else {
- ops.updatedExtendedProperty(EXTENDED_PROPERTY_USER_ATTENDEE_STATUS,
- Integer.toString(attendeeStatus), eventId);
-
- }
- }
- }
- if (eventId < 0) {
- ops.newAttendee(attendee);
- } else {
- ops.updatedAttendee(attendee, eventId);
- }
- }
- if (eventId < 0) {
- ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES, sb.toString());
- ops.newExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "0");
- ops.newExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "0");
- } else {
- ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES, sb.toString(),
- eventId);
- ops.updatedExtendedProperty(EXTENDED_PROPERTY_ATTENDEES_REDACTED, "0", eventId);
- ops.updatedExtendedProperty(EXTENDED_PROPERTY_UPSYNC_PROHIBITED, "0", eventId);
- }
- }
-
- // Put the real event in the proper place in the ops ArrayList
- if (eventOffset >= 0) {
- // Store away the DTSTAMP here
- if (dtStamp != null) {
- ops.newExtendedProperty(EXTENDED_PROPERTY_DTSTAMP, dtStamp);
- }
-
- if (isValidEventValues(cv)) {
- ops.set(eventOffset,
- new Operation(ContentProviderOperation
- .newInsert(mAsSyncAdapterEvents).withValues(cv)));
- } else {
- // If we can't add this event (it's invalid), remove all of the inserts
- // we've built for it
- int cnt = ops.mCount - eventOffset;
- userLog(TAG, "Removing " + cnt + " inserts from mOps");
- for (int i = 0; i < cnt; i++) {
- ops.remove(eventOffset);
- }
- ops.mCount = eventOffset;
- // If this is a change, we need to also remove the deletion that comes
- // before the addition
- if (deleteOffset >= 0) {
- // Remove the deletion
- ops.remove(deleteOffset);
- // And the deletion of exceptions
- ops.remove(deleteOffset);
- userLog(TAG, "Removing deletion ops from mOps");
- ops.mCount = deleteOffset;
- }
- }
- }
- // Mark the end of the event
- addSeparatorOperation(ops, Events.CONTENT_URI);
- }
-
- private void logEventColumns(ContentValues cv, String reason) {
- if (Eas.USER_LOG) {
- StringBuilder sb =
- new StringBuilder("Event invalid, " + reason + ", skipping: Columns = ");
- for (Entry<String, Object> entry: cv.valueSet()) {
- sb.append(entry.getKey());
- sb.append('/');
- }
- userLog(TAG, sb.toString());
- }
- }
-
- /*package*/ boolean isValidEventValues(ContentValues cv) {
- boolean isException = cv.containsKey(Events.ORIGINAL_INSTANCE_TIME);
- // All events require DTSTART
- if (!cv.containsKey(Events.DTSTART)) {
- logEventColumns(cv, "DTSTART missing");
- return false;
- // If we're a top-level event, we must have _SYNC_DATA (uid)
- } else if (!isException && !cv.containsKey(Events.SYNC_DATA2)) {
- logEventColumns(cv, "_SYNC_DATA missing");
- return false;
- // We must also have DTEND or DURATION if we're not an exception
- } else if (!isException && !cv.containsKey(Events.DTEND) &&
- !cv.containsKey(Events.DURATION)) {
- logEventColumns(cv, "DTEND/DURATION missing");
- return false;
- // Exceptions require DTEND
- } else if (isException && !cv.containsKey(Events.DTEND)) {
- logEventColumns(cv, "Exception missing DTEND");
- return false;
- // If this is a recurrence, we need a DURATION (in days if an all-day event)
- } else if (cv.containsKey(Events.RRULE)) {
- String duration = cv.getAsString(Events.DURATION);
- if (duration == null) return false;
- if (cv.containsKey(Events.ALL_DAY)) {
- Integer ade = cv.getAsInteger(Events.ALL_DAY);
- if (ade != null && ade != 0 && !duration.endsWith("D")) {
- return false;
- }
- }
- }
- return true;
- }
-
- public String recurrenceParser() throws IOException {
- // Turn this information into an RRULE
- int type = -1;
- int occurrences = -1;
- int interval = -1;
- int dow = -1;
- int dom = -1;
- int wom = -1;
- int moy = -1;
- String until = null;
-
- while (nextTag(Tags.CALENDAR_RECURRENCE) != END) {
- switch (tag) {
- case Tags.CALENDAR_RECURRENCE_TYPE:
- type = getValueInt();
- break;
- case Tags.CALENDAR_RECURRENCE_INTERVAL:
- interval = getValueInt();
- break;
- case Tags.CALENDAR_RECURRENCE_OCCURRENCES:
- occurrences = getValueInt();
- break;
- case Tags.CALENDAR_RECURRENCE_DAYOFWEEK:
- dow = getValueInt();
- break;
- case Tags.CALENDAR_RECURRENCE_DAYOFMONTH:
- dom = getValueInt();
- break;
- case Tags.CALENDAR_RECURRENCE_WEEKOFMONTH:
- wom = getValueInt();
- break;
- case Tags.CALENDAR_RECURRENCE_MONTHOFYEAR:
- moy = getValueInt();
- break;
- case Tags.CALENDAR_RECURRENCE_UNTIL:
- until = getValue();
- break;
- default:
- skipTag();
- }
- }
-
- return CalendarUtilities.rruleFromRecurrence(type, occurrences, interval,
- dow, dom, wom, moy, until);
- }
-
- private void exceptionParser(CalendarOperations ops, ContentValues parentCv,
- ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus,
- long startTime, long endTime) throws IOException {
- ContentValues cv = new ContentValues();
- cv.put(Events.CALENDAR_ID, mCalendarId);
-
- // It appears that these values have to be copied from the parent if they are to appear
- // Note that they can be overridden below
- cv.put(Events.ORGANIZER, parentCv.getAsString(Events.ORGANIZER));
- cv.put(Events.TITLE, parentCv.getAsString(Events.TITLE));
- cv.put(Events.DESCRIPTION, parentCv.getAsString(Events.DESCRIPTION));
- cv.put(Events.ORIGINAL_ALL_DAY, parentCv.getAsInteger(Events.ALL_DAY));
- cv.put(Events.EVENT_LOCATION, parentCv.getAsString(Events.EVENT_LOCATION));
- cv.put(Events.ACCESS_LEVEL, parentCv.getAsString(Events.ACCESS_LEVEL));
- cv.put(Events.EVENT_TIMEZONE, parentCv.getAsString(Events.EVENT_TIMEZONE));
- // Exceptions should always have this set to zero, since EAS has no concept of
- // separate attendee lists for exceptions; if we fail to do this, then the UI will
- // allow the user to change attendee data, and this change would never get reflected
- // on the server.
- cv.put(Events.HAS_ATTENDEE_DATA, 0);
-
- int allDayEvent = 0;
-
- // This column is the key that links the exception to the serverId
- cv.put(Events.ORIGINAL_SYNC_ID, parentCv.getAsString(Events._SYNC_ID));
-
- String exceptionStartTime = "_noStartTime";
- while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
- switch (tag) {
- case Tags.CALENDAR_ATTACHMENTS:
- attachmentsParser();
- break;
- case Tags.CALENDAR_EXCEPTION_START_TIME:
- exceptionStartTime = getValue();
- cv.put(Events.ORIGINAL_INSTANCE_TIME,
- Utility.parseDateTimeToMillis(exceptionStartTime));
- break;
- case Tags.CALENDAR_EXCEPTION_IS_DELETED:
- if (getValueInt() == 1) {
- cv.put(Events.STATUS, Events.STATUS_CANCELED);
- }
- break;
- case Tags.CALENDAR_ALL_DAY_EVENT:
- allDayEvent = getValueInt();
- cv.put(Events.ALL_DAY, allDayEvent);
- break;
- case Tags.BASE_BODY:
- cv.put(Events.DESCRIPTION, bodyParser());
- break;
- case Tags.CALENDAR_BODY:
- cv.put(Events.DESCRIPTION, getValue());
- break;
- case Tags.CALENDAR_START_TIME:
- startTime = Utility.parseDateTimeToMillis(getValue());
- break;
- case Tags.CALENDAR_END_TIME:
- endTime = Utility.parseDateTimeToMillis(getValue());
- break;
- case Tags.CALENDAR_LOCATION:
- cv.put(Events.EVENT_LOCATION, getValue());
- break;
- case Tags.CALENDAR_RECURRENCE:
- String rrule = recurrenceParser();
- if (rrule != null) {
- cv.put(Events.RRULE, rrule);
- }
- break;
- case Tags.CALENDAR_SUBJECT:
- cv.put(Events.TITLE, getValue());
- break;
- case Tags.CALENDAR_SENSITIVITY:
- cv.put(Events.ACCESS_LEVEL, encodeVisibility(getValueInt()));
- break;
- case Tags.CALENDAR_BUSY_STATUS:
- busyStatus = getValueInt();
- // Don't set selfAttendeeStatus or CalendarProvider will create a duplicate
- // attendee!
- break;
- // TODO How to handle these items that are linked to event id!
-// case Tags.CALENDAR_DTSTAMP:
-// ops.newExtendedProperty("dtstamp", getValue());
-// break;
-// case Tags.CALENDAR_REMINDER_MINS_BEFORE:
-// ops.newReminder(getValueInt());
-// break;
- default:
- skipTag();
- }
- }
-
- // We need a _sync_id, but it can't be the parent's id, so we generate one
- cv.put(Events._SYNC_ID, parentCv.getAsString(Events._SYNC_ID) + '_' +
- exceptionStartTime);
-
- // Enforce CalendarProvider required properties
- setTimeRelatedValues(cv, startTime, endTime, allDayEvent);
-
- // Don't insert an invalid exception event
- if (!isValidEventValues(cv)) return;
-
- // Add the exception insert
- int exceptionStart = ops.mCount;
- ops.newException(cv);
- // Also add the attendees, because they need to be copied over from the parent event
- boolean attendeesRedacted = false;
- if (attendeeValues != null) {
- for (ContentValues attValues: attendeeValues) {
- // If this is the user, use his busy status for attendee status
- String attendeeEmail = attValues.getAsString(Attendees.ATTENDEE_EMAIL);
- // Note that the exception at which we surpass the redaction limit might have
- // any number of attendees shown; since this is an edge case and a workaround,
- // it seems to be an acceptable implementation
- if (mEmailAddress.equalsIgnoreCase(attendeeEmail)) {
- attValues.put(Attendees.ATTENDEE_STATUS,
- CalendarUtilities.attendeeStatusFromBusyStatus(busyStatus));
- ops.newAttendee(attValues, exceptionStart);
- } else if (ops.size() < MAX_OPS_BEFORE_EXCEPTION_ATTENDEE_REDACTION) {
- ops.newAttendee(attValues, exceptionStart);
- } else {
- attendeesRedacted = true;
- }
- }
- }
- // And add the parent's reminder value
- if (reminderMins > 0) {
- ops.newReminder(reminderMins, exceptionStart);
- }
- if (attendeesRedacted) {
- mService.userLog("Attendees redacted in this exception");
- }
- }
-
- private int encodeVisibility(int easVisibility) {
- int visibility = 0;
- switch(easVisibility) {
- case 0:
- visibility = Events.ACCESS_DEFAULT;
- break;
- case 1:
- visibility = Events.ACCESS_PUBLIC;
- break;
- case 2:
- visibility = Events.ACCESS_PRIVATE;
- break;
- case 3:
- visibility = Events.ACCESS_CONFIDENTIAL;
- break;
- }
- return visibility;
- }
-
- private void exceptionsParser(CalendarOperations ops, ContentValues cv,
- ArrayList<ContentValues> attendeeValues, int reminderMins, int busyStatus,
- long startTime, long endTime) throws IOException {
- while (nextTag(Tags.CALENDAR_EXCEPTIONS) != END) {
- switch (tag) {
- case Tags.CALENDAR_EXCEPTION:
- exceptionParser(ops, cv, attendeeValues, reminderMins, busyStatus,
- startTime, endTime);
- break;
- default:
- skipTag();
- }
- }
- }
-
- private String categoriesParser(CalendarOperations ops) throws IOException {
- StringBuilder categories = new StringBuilder();
- while (nextTag(Tags.CALENDAR_CATEGORIES) != END) {
- switch (tag) {
- case Tags.CALENDAR_CATEGORY:
- // TODO Handle categories (there's no similar concept for gdata AFAIK)
- // We need to save them and spit them back when we update the event
- categories.append(getValue());
- categories.append(CATEGORY_TOKENIZER_DELIMITER);
- break;
- default:
- skipTag();
- }
- }
- return categories.toString();
- }
-
- /**
- * For now, we ignore (but still have to parse) event attachments; these are new in EAS 14
- */
- private void attachmentsParser() throws IOException {
- while (nextTag(Tags.CALENDAR_ATTACHMENTS) != END) {
- switch (tag) {
- case Tags.CALENDAR_ATTACHMENT:
- skipParser(Tags.CALENDAR_ATTACHMENT);
- break;
- default:
- skipTag();
- }
- }
- }
-
- private ArrayList<ContentValues> attendeesParser(CalendarOperations ops, long eventId)
- throws IOException {
- int attendeeCount = 0;
- ArrayList<ContentValues> attendeeValues = new ArrayList<ContentValues>();
- while (nextTag(Tags.CALENDAR_ATTENDEES) != END) {
- switch (tag) {
- case Tags.CALENDAR_ATTENDEE:
- ContentValues cv = attendeeParser(ops, eventId);
- // If we're going to redact these attendees anyway, let's avoid unnecessary
- // memory pressure, and not keep them around
- // We still need to parse them all, however
- attendeeCount++;
- // Allow one more than MAX_ATTENDEES, so that the check for "too many" will
- // succeed in addEvent
- if (attendeeCount <= (MAX_SYNCED_ATTENDEES+1)) {
- attendeeValues.add(cv);
- }
- break;
- default:
- skipTag();
- }
- }
- return attendeeValues;
- }
-
- private ContentValues attendeeParser(CalendarOperations ops, long eventId)
- throws IOException {
- ContentValues cv = new ContentValues();
- while (nextTag(Tags.CALENDAR_ATTENDEE) != END) {
- switch (tag) {
- case Tags.CALENDAR_ATTENDEE_EMAIL:
- cv.put(Attendees.ATTENDEE_EMAIL, getValue());
- break;
- case Tags.CALENDAR_ATTENDEE_NAME:
- cv.put(Attendees.ATTENDEE_NAME, getValue());
- break;
- case Tags.CALENDAR_ATTENDEE_STATUS:
- int status = getValueInt();
- cv.put(Attendees.ATTENDEE_STATUS,
- (status == 2) ? Attendees.ATTENDEE_STATUS_TENTATIVE :
- (status == 3) ? Attendees.ATTENDEE_STATUS_ACCEPTED :
- (status == 4) ? Attendees.ATTENDEE_STATUS_DECLINED :
- (status == 5) ? Attendees.ATTENDEE_STATUS_INVITED :
- Attendees.ATTENDEE_STATUS_NONE);
- break;
- case Tags.CALENDAR_ATTENDEE_TYPE:
- int type = Attendees.TYPE_NONE;
- // EAS types: 1 = req'd, 2 = opt, 3 = resource
- switch (getValueInt()) {
- case 1:
- type = Attendees.TYPE_REQUIRED;
- break;
- case 2:
- type = Attendees.TYPE_OPTIONAL;
- break;
- }
- cv.put(Attendees.ATTENDEE_TYPE, type);
- break;
- default:
- skipTag();
- }
- }
- cv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
- return cv;
- }
-
- private String bodyParser() throws IOException {
- String body = null;
- while (nextTag(Tags.BASE_BODY) != END) {
- switch (tag) {
- case Tags.BASE_DATA:
- body = getValue();
- break;
- default:
- skipTag();
- }
- }
-
- // Handle null data without error
- if (body == null) return "";
- // Remove \r's from any body text
- return body.replace("\r\n", "\n");
- }
-
- public void addParser(CalendarOperations ops) throws IOException {
- String serverId = null;
- while (nextTag(Tags.SYNC_ADD) != END) {
- switch (tag) {
- case Tags.SYNC_SERVER_ID: // same as
- serverId = getValue();
- break;
- case Tags.SYNC_APPLICATION_DATA:
- addEvent(ops, serverId, false);
- break;
- default:
- skipTag();
- }
- }
- }
-
- private Cursor getServerIdCursor(String serverId) {
- return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_AND_CALENDAR_ID,
- new String[] {serverId, mCalendarIdString}, null);
- }
-
- private Cursor getClientIdCursor(String clientId) {
- mBindArgument[0] = clientId;
- return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION,
- mBindArgument, null);
- }
-
- public void deleteParser(CalendarOperations ops) throws IOException {
- while (nextTag(Tags.SYNC_DELETE) != END) {
- switch (tag) {
- case Tags.SYNC_SERVER_ID:
- String serverId = getValue();
- // Find the event with the given serverId
- Cursor c = getServerIdCursor(serverId);
- try {
- if (c.moveToFirst()) {
- userLog("Deleting ", serverId);
- ops.delete(c.getLong(0), serverId);
- }
- } finally {
- c.close();
- }
- break;
- default:
- skipTag();
- }
- }
- }
-
- /**
- * A change is handled as a delete (including all exceptions) and an add
- * This isn't as efficient as attempting to traverse the original and all of its exceptions,
- * but changes happen infrequently and this code is both simpler and easier to maintain
- * @param ops the array of pending ContactProviderOperations.
- * @throws IOException
- */
- public void changeParser(CalendarOperations ops) throws IOException {
- String serverId = null;
- while (nextTag(Tags.SYNC_CHANGE) != END) {
- switch (tag) {
- case Tags.SYNC_SERVER_ID:
- serverId = getValue();
- break;
- case Tags.SYNC_APPLICATION_DATA:
- userLog("Changing " + serverId);
- addEvent(ops, serverId, true);
- break;
- default:
- skipTag();
- }
- }
- }
-
- @Override
- public void commandsParser() throws IOException {
- while (nextTag(Tags.SYNC_COMMANDS) != END) {
- if (tag == Tags.SYNC_ADD) {
- addParser(mOps);
- incrementChangeCount();
- } else if (tag == Tags.SYNC_DELETE) {
- deleteParser(mOps);
- incrementChangeCount();
- } else if (tag == Tags.SYNC_CHANGE) {
- changeParser(mOps);
- incrementChangeCount();
- } else
- skipTag();
- }
- }
-
- @Override
- public void commit() throws IOException {
- userLog("Calendar SyncKey saved as: ", mMailbox.mSyncKey);
- // Save the syncKey here, using the Helper provider by Calendar provider
- mOps.add(new Operation(SyncStateContract.Helpers.newSetOperation(
- asSyncAdapter(SyncState.CONTENT_URI, mEmailAddress,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
- mAccountManagerAccount,
- mMailbox.mSyncKey.getBytes())));
-
- // We need to send cancellations now, because the Event won't exist after the commit
- for (long eventId: mSendCancelIdList) {
- EmailContent.Message msg;
- try {
- msg = CalendarUtilities.createMessageForEventId(mContext, eventId,
- EmailContent.Message.FLAG_OUTGOING_MEETING_CANCEL, null,
- mAccount);
- } catch (RemoteException e) {
- // Nothing to do here; the Event may no longer exist
- continue;
- }
- if (msg != null) {
- EasOutboxService.sendMessage(mContext, mAccount.mId, msg);
- }
- }
-
- // Execute our CPO's safely
- try {
- mOps.mResults = safeExecute(CalendarContract.AUTHORITY, mOps);
- } catch (RemoteException e) {
- throw new IOException("Remote exception caught; will retry");
- }
-
- if (mOps.mResults != null) {
- // Clear dirty and mark flags for updates sent to server
- if (!mUploadedIdList.isEmpty()) {
- ContentValues cv = new ContentValues();
- cv.put(Events.DIRTY, 0);
- cv.put(EVENT_SYNC_MARK, "0");
- for (long eventId : mUploadedIdList) {
- mContentResolver.update(
- asSyncAdapter(
- ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
- mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv,
- null, null);
- }
- }
- // Delete events marked for deletion
- if (!mDeletedIdList.isEmpty()) {
- for (long eventId : mDeletedIdList) {
- mContentResolver.delete(
- asSyncAdapter(
- ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
- mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null,
- null);
- }
- }
- // Send any queued up email (invitations replies, etc.)
- for (Message msg: mOutgoingMailList) {
- EasOutboxService.sendMessage(mContext, mAccount.mId, msg);
- }
- }
- }
-
- public void addResponsesParser() throws IOException {
- String serverId = null;
- String clientId = null;
- int status = -1;
- ContentValues cv = new ContentValues();
- while (nextTag(Tags.SYNC_ADD) != END) {
- switch (tag) {
- case Tags.SYNC_SERVER_ID:
- serverId = getValue();
- break;
- case Tags.SYNC_CLIENT_ID:
- clientId = getValue();
- break;
- case Tags.SYNC_STATUS:
- status = getValueInt();
- if (status != 1) {
- userLog("Attempt to add event failed with status: " + status);
- }
- break;
- default:
- skipTag();
- }
- }
-
- if (clientId == null) return;
- if (serverId == null) {
- // TODO Reconsider how to handle this
- serverId = "FAIL:" + status;
- }
-
- Cursor c = getClientIdCursor(clientId);
- try {
- if (c.moveToFirst()) {
- cv.put(Events._SYNC_ID, serverId);
- cv.put(Events.SYNC_DATA2, clientId);
- long id = c.getLong(0);
- // Write the serverId into the Event
- mOps.add(new Operation(ContentProviderOperation
- .newUpdate(ContentUris.withAppendedId(mAsSyncAdapterEvents, id))
- .withValues(cv)));
- userLog("New event " + clientId + " was given serverId: " + serverId);
- }
- } finally {
- c.close();
- }
- }
-
- public void changeResponsesParser() throws IOException {
- String serverId = null;
- String status = null;
- while (nextTag(Tags.SYNC_CHANGE) != END) {
- switch (tag) {
- case Tags.SYNC_SERVER_ID:
- serverId = getValue();
- break;
- case Tags.SYNC_STATUS:
- status = getValue();
- break;
- default:
- skipTag();
- }
- }
- if (serverId != null && status != null) {
- userLog("Changed event " + serverId + " failed with status: " + status);
- }
- }
-
-
- @Override
- public void responsesParser() throws IOException {
- // Handle server responses here (for Add and Change)
- while (nextTag(Tags.SYNC_RESPONSES) != END) {
- if (tag == Tags.SYNC_ADD) {
- addResponsesParser();
- } else if (tag == Tags.SYNC_CHANGE) {
- changeResponsesParser();
- } else
- skipTag();
- }
- }
- }
-
- protected class CalendarOperations extends ArrayList<Operation> {
- private static final long serialVersionUID = 1L;
- public int mCount = 0;
- private ContentProviderResult[] mResults = null;
- private int mEventStart = 0;
-
- @Override
- public boolean add(Operation op) {
- super.add(op);
- mCount++;
- return true;
- }
-
- public int newEvent(Operation op) {
- mEventStart = mCount;
- add(op);
- return mEventStart;
- }
-
- public int newDelete(long id, String serverId) {
- int offset = mCount;
- delete(id, serverId);
- return offset;
- }
-
- public void newAttendee(ContentValues cv) {
- newAttendee(cv, mEventStart);
- }
-
- public void newAttendee(ContentValues cv, int eventStart) {
- add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterAttendees)
- .withValues(cv),
- Attendees.EVENT_ID,
- eventStart));
- }
-
- public void updatedAttendee(ContentValues cv, long id) {
- cv.put(Attendees.EVENT_ID, id);
- add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterAttendees)
- .withValues(cv)));
- }
-
- public void newException(ContentValues cv) {
- add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterEvents)
- .withValues(cv)));
- }
-
- public void newExtendedProperty(String name, String value) {
- add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterExtendedProperties)
- .withValue(ExtendedProperties.NAME, name)
- .withValue(ExtendedProperties.VALUE, value),
- ExtendedProperties.EVENT_ID,
- mEventStart));
- }
-
- public void updatedExtendedProperty(String name, String value, long id) {
- // Find an existing ExtendedProperties row for this event and property name
- Cursor c = mService.mContentResolver.query(ExtendedProperties.CONTENT_URI,
- EXTENDED_PROPERTY_PROJECTION, EVENT_ID_AND_NAME,
- new String[] {Long.toString(id), name}, null);
- long extendedPropertyId = -1;
- // If there is one, capture its _id
- if (c != null) {
- try {
- if (c.moveToFirst()) {
- extendedPropertyId = c.getLong(EXTENDED_PROPERTY_ID);
- }
- } finally {
- c.close();
- }
- }
- // Either do an update or an insert, depending on whether one
- // already exists
- if (extendedPropertyId >= 0) {
- add(new Operation(ContentProviderOperation
- .newUpdate(
- ContentUris.withAppendedId(mAsSyncAdapterExtendedProperties,
- extendedPropertyId))
- .withValue(ExtendedProperties.VALUE, value)));
- } else {
- newExtendedProperty(name, value);
- }
- }
-
- public void newReminder(int mins, int eventStart) {
- add(new Operation(ContentProviderOperation.newInsert(mAsSyncAdapterReminders)
- .withValue(Reminders.MINUTES, mins)
- .withValue(Reminders.METHOD, Reminders.METHOD_ALERT),
- ExtendedProperties.EVENT_ID,
- eventStart));
- }
-
- public void newReminder(int mins) {
- newReminder(mins, mEventStart);
- }
-
- public void delete(long id, String syncId) {
- add(new Operation(ContentProviderOperation.newDelete(
- ContentUris.withAppendedId(mAsSyncAdapterEvents, id))));
- // Delete the exceptions for this Event (CalendarProvider doesn't do this)
- add(new Operation(ContentProviderOperation
- .newDelete(mAsSyncAdapterEvents)
- .withSelection(Events.ORIGINAL_SYNC_ID + "=?", new String[] {syncId})));
- }
- }
-
- private String decodeVisibility(int visibility) {
- int easVisibility = 0;
- switch(visibility) {
- case Events.ACCESS_DEFAULT:
- easVisibility = 0;
- break;
- case Events.ACCESS_PUBLIC:
- easVisibility = 1;
- break;
- case Events.ACCESS_PRIVATE:
- easVisibility = 2;
- break;
- case Events.ACCESS_CONFIDENTIAL:
- easVisibility = 3;
- break;
- }
- return Integer.toString(easVisibility);
- }
-
- private int getInt(ContentValues cv, String column) {
- Integer i = cv.getAsInteger(column);
- if (i == null) return 0;
- return i;
- }
-
- private void sendEvent(Entity entity, String clientId, Serializer s)
- throws IOException {
- // Serialize for EAS here
- // Set uid with the client id we created
- // 1) Serialize the top-level event
- // 2) Serialize attendees and reminders from subvalues
- // 3) Look for exceptions and serialize with the top-level event
- ContentValues entityValues = entity.getEntityValues();
- final boolean isException = (clientId == null);
- boolean hasAttendees = false;
- final boolean isChange = entityValues.containsKey(Events._SYNC_ID);
- final Double version = mService.mProtocolVersionDouble;
- final boolean allDay =
- CalendarUtilities.getIntegerValueAsBoolean(entityValues, Events.ALL_DAY);
-
- // NOTE: Exchange 2003 (EAS 2.5) seems to require the "exception deleted" and "exception
- // start time" data before other data in exceptions. Failure to do so results in a
- // status 6 error during sync
- if (isException) {
- // Send exception deleted flag if necessary
- Integer deleted = entityValues.getAsInteger(Events.DELETED);
- boolean isDeleted = deleted != null && deleted == 1;
- Integer eventStatus = entityValues.getAsInteger(Events.STATUS);
- boolean isCanceled = eventStatus != null && eventStatus.equals(Events.STATUS_CANCELED);
- if (isDeleted || isCanceled) {
- s.data(Tags.CALENDAR_EXCEPTION_IS_DELETED, "1");
- // If we're deleted, the UI will continue to show this exception until we mark
- // it canceled, so we'll do that here...
- if (isDeleted && !isCanceled) {
- final long eventId = entityValues.getAsLong(Events._ID);
- ContentValues cv = new ContentValues();
- cv.put(Events.STATUS, Events.STATUS_CANCELED);
- mService.mContentResolver.update(
- asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
- mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv, null,
- null);
- }
- } else {
- s.data(Tags.CALENDAR_EXCEPTION_IS_DELETED, "0");
- }
-
- // TODO Add reminders to exceptions (allow them to be specified!)
- Long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
- if (originalTime != null) {
- final boolean originalAllDay =
- CalendarUtilities.getIntegerValueAsBoolean(entityValues,
- Events.ORIGINAL_ALL_DAY);
- if (originalAllDay) {
- // For all day events, we need our local all-day time
- originalTime =
- CalendarUtilities.getLocalAllDayCalendarTime(originalTime, mLocalTimeZone);
- }
- s.data(Tags.CALENDAR_EXCEPTION_START_TIME,
- CalendarUtilities.millisToEasDateTime(originalTime));
- } else {
- // Illegal; what should we do?
- }
- }
-
- // Get the event's time zone
- String timeZoneName =
- entityValues.getAsString(allDay ? EVENT_SAVED_TIMEZONE_COLUMN : Events.EVENT_TIMEZONE);
- if (timeZoneName == null) {
- timeZoneName = mLocalTimeZone.getID();
- }
- TimeZone eventTimeZone = TimeZone.getTimeZone(timeZoneName);
-
- if (!isException) {
- // A time zone is required in all EAS events; we'll use the default if none is set
- // Exchange 2003 seems to require this first... :-)
- String timeZone = CalendarUtilities.timeZoneToTziString(eventTimeZone);
- s.data(Tags.CALENDAR_TIME_ZONE, timeZone);
- }
-
- s.data(Tags.CALENDAR_ALL_DAY_EVENT, allDay ? "1" : "0");
-
- // DTSTART is always supplied
- long startTime = entityValues.getAsLong(Events.DTSTART);
- // Determine endTime; it's either provided as DTEND or we calculate using DURATION
- // If no DURATION is provided, we default to one hour
- long endTime;
- if (entityValues.containsKey(Events.DTEND)) {
- endTime = entityValues.getAsLong(Events.DTEND);
- } else {
- long durationMillis = HOURS;
- if (entityValues.containsKey(Events.DURATION)) {
- Duration duration = new Duration();
- try {
- duration.parse(entityValues.getAsString(Events.DURATION));
- durationMillis = duration.getMillis();
- } catch (ParseException e) {
- // Can't do much about this; use the default (1 hour)
- }
- }
- endTime = startTime + durationMillis;
- }
- if (allDay) {
- TimeZone tz = mLocalTimeZone;
- startTime = CalendarUtilities.getLocalAllDayCalendarTime(startTime, tz);
- endTime = CalendarUtilities.getLocalAllDayCalendarTime(endTime, tz);
- }
- s.data(Tags.CALENDAR_START_TIME, CalendarUtilities.millisToEasDateTime(startTime));
- s.data(Tags.CALENDAR_END_TIME, CalendarUtilities.millisToEasDateTime(endTime));
-
- s.data(Tags.CALENDAR_DTSTAMP,
- CalendarUtilities.millisToEasDateTime(System.currentTimeMillis()));
-
- String loc = entityValues.getAsString(Events.EVENT_LOCATION);
- if (!TextUtils.isEmpty(loc)) {
- if (version < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
- // EAS 2.5 doesn't like bare line feeds
- loc = Utility.replaceBareLfWithCrlf(loc);
- }
- s.data(Tags.CALENDAR_LOCATION, loc);
- }
- s.writeStringValue(entityValues, Events.TITLE, Tags.CALENDAR_SUBJECT);
-
- if (version >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
- s.start(Tags.BASE_BODY);
- s.data(Tags.BASE_TYPE, "1");
- s.writeStringValue(entityValues, Events.DESCRIPTION, Tags.BASE_DATA);
- s.end();
- } else {
- // EAS 2.5 doesn't like bare line feeds
- s.writeStringValue(entityValues, Events.DESCRIPTION, Tags.CALENDAR_BODY);
- }
-
- if (!isException) {
- // For Exchange 2003, only upsync if the event is new
- if ((version >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) || !isChange) {
- s.writeStringValue(entityValues, Events.ORGANIZER, Tags.CALENDAR_ORGANIZER_EMAIL);
- }
-
- String rrule = entityValues.getAsString(Events.RRULE);
- if (rrule != null) {
- CalendarUtilities.recurrenceFromRrule(rrule, startTime, s);
- }
-
- // Handle associated data EXCEPT for attendees, which have to be grouped
- ArrayList<NamedContentValues> subValues = entity.getSubValues();
- // The earliest of the reminders for this Event; we can only send one reminder...
- int earliestReminder = -1;
- for (NamedContentValues ncv: subValues) {
- Uri ncvUri = ncv.uri;
- ContentValues ncvValues = ncv.values;
- if (ncvUri.equals(ExtendedProperties.CONTENT_URI)) {
- String propertyName =
- ncvValues.getAsString(ExtendedProperties.NAME);
- String propertyValue =
- ncvValues.getAsString(ExtendedProperties.VALUE);
- if (TextUtils.isEmpty(propertyValue)) {
- continue;
- }
- if (propertyName.equals(EXTENDED_PROPERTY_CATEGORIES)) {
- // Send all the categories back to the server
- // We've saved them as a String of delimited tokens
- StringTokenizer st =
- new StringTokenizer(propertyValue, CATEGORY_TOKENIZER_DELIMITER);
- if (st.countTokens() > 0) {
- s.start(Tags.CALENDAR_CATEGORIES);
- while (st.hasMoreTokens()) {
- String category = st.nextToken();
- s.data(Tags.CALENDAR_CATEGORY, category);
- }
- s.end();
- }
- }
- } else if (ncvUri.equals(Reminders.CONTENT_URI)) {
- Integer mins = ncvValues.getAsInteger(Reminders.MINUTES);
- if (mins != null) {
- // -1 means "default", which for Exchange, is 30
- if (mins < 0) {
- mins = 30;
- }
- // Save this away if it's the earliest reminder (greatest minutes)
- if (mins > earliestReminder) {
- earliestReminder = mins;
- }
- }
- }
- }
-
- // If we have a reminder, send it to the server
- if (earliestReminder >= 0) {
- s.data(Tags.CALENDAR_REMINDER_MINS_BEFORE, Integer.toString(earliestReminder));
- }
-
- // We've got to send a UID, unless this is an exception. If the event is new, we've
- // generated one; if not, we should have gotten one from extended properties.
- if (clientId != null) {
- s.data(Tags.CALENDAR_UID, clientId);
- }
-
- // Handle attendee data here; keep track of organizer and stream it afterward
- String organizerName = null;
- String organizerEmail = null;
- for (NamedContentValues ncv: subValues) {
- Uri ncvUri = ncv.uri;
- ContentValues ncvValues = ncv.values;
- if (ncvUri.equals(Attendees.CONTENT_URI)) {
- Integer relationship = ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
- // If there's no relationship, we can't create this for EAS
- // Similarly, we need an attendee email for each invitee
- if (relationship != null && ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
- // Organizer isn't among attendees in EAS
- if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
- organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
- organizerEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
- continue;
- }
- if (!hasAttendees) {
- s.start(Tags.CALENDAR_ATTENDEES);
- hasAttendees = true;
- }
- s.start(Tags.CALENDAR_ATTENDEE);
- String attendeeEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
- String attendeeName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
- if (attendeeName == null) {
- attendeeName = attendeeEmail;
- }
- s.data(Tags.CALENDAR_ATTENDEE_NAME, attendeeName);
- s.data(Tags.CALENDAR_ATTENDEE_EMAIL, attendeeEmail);
- if (version >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
- s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1"); // Required
- }
- s.end(); // Attendee
- }
- }
- }
- if (hasAttendees) {
- s.end(); // Attendees
- }
-
- // Get busy status from Attendees table
- long eventId = entityValues.getAsLong(Events._ID);
- int busyStatus = CalendarUtilities.BUSY_STATUS_TENTATIVE;
- Cursor c = mService.mContentResolver.query(
- asSyncAdapter(Attendees.CONTENT_URI, mEmailAddress,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
- ATTENDEE_STATUS_PROJECTION, EVENT_AND_EMAIL,
- new String[] {Long.toString(eventId), mEmailAddress}, null);
- if (c != null) {
- try {
- if (c.moveToFirst()) {
- busyStatus = CalendarUtilities.busyStatusFromAttendeeStatus(
- c.getInt(ATTENDEE_STATUS_COLUMN_STATUS));
- }
- } finally {
- c.close();
- }
- }
- s.data(Tags.CALENDAR_BUSY_STATUS, Integer.toString(busyStatus));
-
- // Meeting status, 0 = appointment, 1 = meeting, 3 = attendee
- if (mEmailAddress.equalsIgnoreCase(organizerEmail)) {
- s.data(Tags.CALENDAR_MEETING_STATUS, hasAttendees ? "1" : "0");
- } else {
- s.data(Tags.CALENDAR_MEETING_STATUS, "3");
- }
-
- // For Exchange 2003, only upsync if the event is new
- if (((version >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) || !isChange) &&
- organizerName != null) {
- s.data(Tags.CALENDAR_ORGANIZER_NAME, organizerName);
- }
-
- // NOTE: Sensitivity must NOT be sent to the server for exceptions in Exchange 2003
- // The result will be a status 6 failure during sync
- Integer visibility = entityValues.getAsInteger(Events.ACCESS_LEVEL);
- if (visibility != null) {
- s.data(Tags.CALENDAR_SENSITIVITY, decodeVisibility(visibility));
- } else {
- // Default to private if not set
- s.data(Tags.CALENDAR_SENSITIVITY, "1");
- }
- }
- }
-
- /**
- * Convenience method for sending an email to the organizer declining the meeting
- * @param entity
- * @param clientId
- */
- private void sendDeclinedEmail(Entity entity, String clientId) {
- Message msg =
- CalendarUtilities.createMessageForEntity(mContext, entity,
- Message.FLAG_OUTGOING_MEETING_DECLINE, clientId, mAccount);
- if (msg != null) {
- userLog("Queueing declined response to " + msg.mTo);
- mOutgoingMailList.add(msg);
- }
- }
-
- @Override
- public boolean sendLocalChanges(Serializer s) throws IOException {
- ContentResolver cr = mService.mContentResolver;
-
- if (getSyncKey().equals("0")) {
- return false;
- }
-
- try {
- // We've got to handle exceptions as part of the parent when changes occur, so we need
- // to find new/changed exceptions and mark the parent dirty
- ArrayList<Long> orphanedExceptions = new ArrayList<Long>();
- Cursor c = cr.query(Events.CONTENT_URI, ORIGINAL_EVENT_PROJECTION,
- DIRTY_EXCEPTION_IN_CALENDAR, mCalendarIdArgument, null);
- try {
- ContentValues cv = new ContentValues();
- // We use _sync_mark here to distinguish dirty parents from parents with dirty
- // exceptions
- cv.put(EVENT_SYNC_MARK, "1");
- while (c.moveToNext()) {
- // Mark the parents of dirty exceptions
- long parentId = c.getLong(0);
- int cnt = cr.update(
- asSyncAdapter(Events.CONTENT_URI, mEmailAddress,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv,
- EVENT_ID_AND_CALENDAR_ID, new String[] {
- Long.toString(parentId), mCalendarIdString
- });
- // Keep track of any orphaned exceptions
- if (cnt == 0) {
- orphanedExceptions.add(c.getLong(1));
- }
- }
- } finally {
- c.close();
- }
-
- // Delete any orphaned exceptions
- for (long orphan : orphanedExceptions) {
- userLog(TAG, "Deleted orphaned exception: " + orphan);
- cr.delete(
- asSyncAdapter(ContentUris.withAppendedId(Events.CONTENT_URI, orphan),
- mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null, null);
- }
- orphanedExceptions.clear();
-
- // Now we can go through dirty/marked top-level events and send them
- // back to the server
- EntityIterator eventIterator = EventsEntity.newEntityIterator(cr.query(
- asSyncAdapter(Events.CONTENT_URI, mEmailAddress,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null,
- DIRTY_OR_MARKED_TOP_LEVEL_IN_CALENDAR, mCalendarIdArgument, null), cr);
- ContentValues cidValues = new ContentValues();
-
- try {
- boolean first = true;
- while (eventIterator.hasNext()) {
- Entity entity = eventIterator.next();
-
- // For each of these entities, create the change commands
- ContentValues entityValues = entity.getEntityValues();
- String serverId = entityValues.getAsString(Events._SYNC_ID);
-
- // We first need to check whether we can upsync this event; our test for this
- // is currently the value of EXTENDED_PROPERTY_ATTENDEES_REDACTED
- // If this is set to "1", we can't upsync the event
- for (NamedContentValues ncv: entity.getSubValues()) {
- if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
- ContentValues ncvValues = ncv.values;
- if (ncvValues.getAsString(ExtendedProperties.NAME).equals(
- EXTENDED_PROPERTY_UPSYNC_PROHIBITED)) {
- if ("1".equals(ncvValues.getAsString(ExtendedProperties.VALUE))) {
- // Make sure we mark this to clear the dirty flag
- mUploadedIdList.add(entityValues.getAsLong(Events._ID));
- continue;
- }
- }
- }
- }
-
- // Find our uid in the entity; otherwise create one
- String clientId = entityValues.getAsString(Events.SYNC_DATA2);
- if (clientId == null) {
- clientId = UUID.randomUUID().toString();
- }
-
- // EAS 2.5 needs: BusyStatus DtStamp EndTime Sensitivity StartTime TimeZone UID
- // We can generate all but what we're testing for below
- String organizerEmail = entityValues.getAsString(Events.ORGANIZER);
- boolean selfOrganizer = organizerEmail.equalsIgnoreCase(mEmailAddress);
-
- if (!entityValues.containsKey(Events.DTSTART)
- || (!entityValues.containsKey(Events.DURATION) &&
- !entityValues.containsKey(Events.DTEND))
- || organizerEmail == null) {
- continue;
- }
-
- if (first) {
- s.start(Tags.SYNC_COMMANDS);
- userLog("Sending Calendar changes to the server");
- first = false;
- }
- long eventId = entityValues.getAsLong(Events._ID);
- if (serverId == null) {
- // This is a new event; create a clientId
- userLog("Creating new event with clientId: ", clientId);
- s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
- // And save it in the Event as the local id
- cidValues.put(Events.SYNC_DATA2, clientId);
- cidValues.put(EVENT_SYNC_VERSION, "0");
- cr.update(
- asSyncAdapter(
- ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
- mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
- cidValues, null, null);
- } else {
- if (entityValues.getAsInteger(Events.DELETED) == 1) {
- userLog("Deleting event with serverId: ", serverId);
- s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
- mDeletedIdList.add(eventId);
- if (selfOrganizer) {
- mSendCancelIdList.add(eventId);
- } else {
- sendDeclinedEmail(entity, clientId);
- }
- continue;
- }
- userLog("Upsync change to event with serverId: " + serverId);
- // Get the current version
- String version = entityValues.getAsString(EVENT_SYNC_VERSION);
- // This should never be null, but catch this error anyway
- // Version should be "0" when we create the event, so use that
- if (version == null) {
- version = "0";
- } else {
- // Increment and save
- try {
- version = Integer.toString((Integer.parseInt(version) + 1));
- } catch (Exception e) {
- // Handle the case in which someone writes a non-integer here;
- // shouldn't happen, but we don't want to kill the sync for his
- version = "0";
- }
- }
- cidValues.put(EVENT_SYNC_VERSION, version);
- // Also save in entityValues so that we send it this time around
- entityValues.put(EVENT_SYNC_VERSION, version);
- cr.update(
- asSyncAdapter(
- ContentUris.withAppendedId(Events.CONTENT_URI, eventId),
- mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
- cidValues, null, null);
- s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
- }
- s.start(Tags.SYNC_APPLICATION_DATA);
-
- sendEvent(entity, clientId, s);
-
- // Now, the hard part; find exceptions for this event
- if (serverId != null) {
- EntityIterator exIterator = EventsEntity.newEntityIterator(cr.query(
- asSyncAdapter(Events.CONTENT_URI, mEmailAddress,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), null,
- ORIGINAL_EVENT_AND_CALENDAR, new String[] {
- serverId, mCalendarIdString
- }, null), cr);
- boolean exFirst = true;
- while (exIterator.hasNext()) {
- Entity exEntity = exIterator.next();
- if (exFirst) {
- s.start(Tags.CALENDAR_EXCEPTIONS);
- exFirst = false;
- }
- s.start(Tags.CALENDAR_EXCEPTION);
- sendEvent(exEntity, null, s);
- ContentValues exValues = exEntity.getEntityValues();
- if (getInt(exValues, Events.DIRTY) == 1) {
- // This is a new/updated exception, so we've got to notify our
- // attendees about it
- long exEventId = exValues.getAsLong(Events._ID);
- int flag;
-
- // Copy subvalues into the exception; otherwise, we won't see the
- // attendees when preparing the message
- for (NamedContentValues ncv: entity.getSubValues()) {
- exEntity.addSubValue(ncv.uri, ncv.values);
- }
-
- if ((getInt(exValues, Events.DELETED) == 1) ||
- (getInt(exValues, Events.STATUS) ==
- Events.STATUS_CANCELED)) {
- flag = Message.FLAG_OUTGOING_MEETING_CANCEL;
- if (!selfOrganizer) {
- // Send a cancellation notice to the organizer
- // Since CalendarProvider2 sets the organizer of exceptions
- // to the user, we have to reset it first to the original
- // organizer
- exValues.put(Events.ORGANIZER,
- entityValues.getAsString(Events.ORGANIZER));
- sendDeclinedEmail(exEntity, clientId);
- }
- } else {
- flag = Message.FLAG_OUTGOING_MEETING_INVITE;
- }
- // Add the eventId of the exception to the uploaded id list, so that
- // the dirty/mark bits are cleared
- mUploadedIdList.add(exEventId);
-
- // Copy version so the ics attachment shows the proper sequence #
- exValues.put(EVENT_SYNC_VERSION,
- entityValues.getAsString(EVENT_SYNC_VERSION));
- // Copy location so that it's included in the outgoing email
- if (entityValues.containsKey(Events.EVENT_LOCATION)) {
- exValues.put(Events.EVENT_LOCATION,
- entityValues.getAsString(Events.EVENT_LOCATION));
- }
-
- if (selfOrganizer) {
- Message msg =
- CalendarUtilities.createMessageForEntity(mContext,
- exEntity, flag, clientId, mAccount);
- if (msg != null) {
- userLog("Queueing exception update to " + msg.mTo);
- mOutgoingMailList.add(msg);
- }
- }
- }
- s.end(); // EXCEPTION
- }
- if (!exFirst) {
- s.end(); // EXCEPTIONS
- }
- }
-
- s.end().end(); // ApplicationData & Change
- mUploadedIdList.add(eventId);
-
- // Go through the extended properties of this Event and pull out our tokenized
- // attendees list and the user attendee status; we will need them later
- String attendeeString = null;
- long attendeeStringId = -1;
- String userAttendeeStatus = null;
- long userAttendeeStatusId = -1;
- for (NamedContentValues ncv: entity.getSubValues()) {
- if (ncv.uri.equals(ExtendedProperties.CONTENT_URI)) {
- ContentValues ncvValues = ncv.values;
- String propertyName =
- ncvValues.getAsString(ExtendedProperties.NAME);
- if (propertyName.equals(EXTENDED_PROPERTY_ATTENDEES)) {
- attendeeString =
- ncvValues.getAsString(ExtendedProperties.VALUE);
- attendeeStringId =
- ncvValues.getAsLong(ExtendedProperties._ID);
- } else if (propertyName.equals(
- EXTENDED_PROPERTY_USER_ATTENDEE_STATUS)) {
- userAttendeeStatus =
- ncvValues.getAsString(ExtendedProperties.VALUE);
- userAttendeeStatusId =
- ncvValues.getAsLong(ExtendedProperties._ID);
- }
- }
- }
-
- // Send the meeting invite if there are attendees and we're the organizer AND
- // if the Event itself is dirty (we might be syncing only because an exception
- // is dirty, in which case we DON'T send email about the Event)
- if (selfOrganizer &&
- (getInt(entityValues, Events.DIRTY) == 1)) {
- EmailContent.Message msg =
- CalendarUtilities.createMessageForEventId(mContext, eventId,
- EmailContent.Message.FLAG_OUTGOING_MEETING_INVITE, clientId,
- mAccount);
- if (msg != null) {
- userLog("Queueing invitation to ", msg.mTo);
- mOutgoingMailList.add(msg);
- }
- // Make a list out of our tokenized attendees, if we have any
- ArrayList<String> originalAttendeeList = new ArrayList<String>();
- if (attendeeString != null) {
- StringTokenizer st =
- new StringTokenizer(attendeeString, ATTENDEE_TOKENIZER_DELIMITER);
- while (st.hasMoreTokens()) {
- originalAttendeeList.add(st.nextToken());
- }
- }
- StringBuilder newTokenizedAttendees = new StringBuilder();
- // See if any attendees have been dropped and while we're at it, build
- // an updated String with tokenized attendee addresses
- for (NamedContentValues ncv: entity.getSubValues()) {
- if (ncv.uri.equals(Attendees.CONTENT_URI)) {
- String attendeeEmail =
- ncv.values.getAsString(Attendees.ATTENDEE_EMAIL);
- // Remove all found attendees
- originalAttendeeList.remove(attendeeEmail);
- newTokenizedAttendees.append(attendeeEmail);
- newTokenizedAttendees.append(ATTENDEE_TOKENIZER_DELIMITER);
- }
- }
- // Update extended properties with the new attendee list, if we have one
- // Otherwise, create one (this would be the case for Events created on
- // device or "legacy" events (before this code was added)
- ContentValues cv = new ContentValues();
- cv.put(ExtendedProperties.VALUE, newTokenizedAttendees.toString());
- if (attendeeString != null) {
- cr.update(asSyncAdapter(ContentUris.withAppendedId(
- ExtendedProperties.CONTENT_URI, attendeeStringId),
- mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
- cv, null, null);
- } else {
- // If there wasn't an "attendees" property, insert one
- cv.put(ExtendedProperties.NAME, EXTENDED_PROPERTY_ATTENDEES);
- cv.put(ExtendedProperties.EVENT_ID, eventId);
- cr.insert(asSyncAdapter(ExtendedProperties.CONTENT_URI,
- mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv);
- }
- // Whoever is left has been removed from the attendee list; send them
- // a cancellation
- for (String removedAttendee: originalAttendeeList) {
- // Send a cancellation message to each of them
- msg = CalendarUtilities.createMessageForEventId(mContext, eventId,
- Message.FLAG_OUTGOING_MEETING_CANCEL, clientId, mAccount,
- removedAttendee);
- if (msg != null) {
- // Just send it to the removed attendee
- userLog("Queueing cancellation to removed attendee " + msg.mTo);
- mOutgoingMailList.add(msg);
- }
- }
- } else if (!selfOrganizer) {
- // If we're not the organizer, see if we've changed our attendee status
- // Our last synced attendee status is in ExtendedProperties, and we've
- // retrieved it above as userAttendeeStatus
- int currentStatus = entityValues.getAsInteger(Events.SELF_ATTENDEE_STATUS);
- int syncStatus = Attendees.ATTENDEE_STATUS_NONE;
- if (userAttendeeStatus != null) {
- try {
- syncStatus = Integer.parseInt(userAttendeeStatus);
- } catch (NumberFormatException e) {
- // Just in case somebody else mucked with this and it's not Integer
- }
- }
- if ((currentStatus != syncStatus) &&
- (currentStatus != Attendees.ATTENDEE_STATUS_NONE)) {
- // If so, send a meeting reply
- int messageFlag = 0;
- switch (currentStatus) {
- case Attendees.ATTENDEE_STATUS_ACCEPTED:
- messageFlag = Message.FLAG_OUTGOING_MEETING_ACCEPT;
- break;
- case Attendees.ATTENDEE_STATUS_DECLINED:
- messageFlag = Message.FLAG_OUTGOING_MEETING_DECLINE;
- break;
- case Attendees.ATTENDEE_STATUS_TENTATIVE:
- messageFlag = Message.FLAG_OUTGOING_MEETING_TENTATIVE;
- break;
- }
- // Make sure we have a valid status (messageFlag should never be zero)
- if (messageFlag != 0 && userAttendeeStatusId >= 0) {
- // Save away the new status
- cidValues.clear();
- cidValues.put(ExtendedProperties.VALUE,
- Integer.toString(currentStatus));
- cr.update(asSyncAdapter(ContentUris.withAppendedId(
- ExtendedProperties.CONTENT_URI, userAttendeeStatusId),
- mEmailAddress, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE),
- cidValues, null, null);
- // Send mail to the organizer advising of the new status
- EmailContent.Message msg =
- CalendarUtilities.createMessageForEventId(mContext, eventId,
- messageFlag, clientId, mAccount);
- if (msg != null) {
- userLog("Queueing invitation reply to " + msg.mTo);
- mOutgoingMailList.add(msg);
- }
- }
- }
- }
- }
- if (!first) {
- s.end(); // Commands
- }
- } finally {
- eventIterator.close();
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Could not read dirty events.");
- }
-
- return false;
- }
-}
diff --git a/src/com/android/exchange/adapter/ContactsSyncAdapter.java b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
deleted file mode 100644
index ec0299b..0000000
--- a/src/com/android/exchange/adapter/ContactsSyncAdapter.java
+++ /dev/null
@@ -1,1928 +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.adapter;
-
-import com.android.exchange.CommandStatusException;
-import com.android.exchange.Eas;
-import com.android.exchange.EasSyncService;
-
-import android.content.ContentProviderClient;
-import android.content.ContentProviderOperation;
-import android.content.ContentProviderOperation.Builder;
-import android.content.ContentProviderResult;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Entity;
-import android.content.Entity.NamedContentValues;
-import android.content.EntityIterator;
-import android.content.OperationApplicationException;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.RemoteException;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Event;
-import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
-import android.provider.ContactsContract.CommonDataKinds.Im;
-import android.provider.ContactsContract.CommonDataKinds.Nickname;
-import android.provider.ContactsContract.CommonDataKinds.Note;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.CommonDataKinds.Relation;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.provider.ContactsContract.CommonDataKinds.Website;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.Groups;
-import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.RawContactsEntity;
-import android.provider.ContactsContract.Settings;
-import android.provider.ContactsContract.SyncState;
-import android.provider.SyncStateContract;
-import android.text.TextUtils;
-import android.text.util.Rfc822Token;
-import android.text.util.Rfc822Tokenizer;
-import android.util.Base64;
-import android.util.Log;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-
-/**
- * Sync adapter for EAS Contacts
- *
- */
-public class ContactsSyncAdapter extends AbstractSyncAdapter {
-
- private static final String TAG = "EasContactsSyncAdapter";
- private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?";
- private static final String CLIENT_ID_SELECTION = RawContacts.SYNC1 + "=?";
- private static final String[] ID_PROJECTION = new String[] {RawContacts._ID};
- private static final String[] GROUP_TITLE_PROJECTION = new String[] {Groups.TITLE};
- private static final String MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS = Data.MIMETYPE + "='" +
- GroupMembership.CONTENT_ITEM_TYPE + "' AND " + GroupMembership.GROUP_ROW_ID + "=?";
- private static final String[] GROUPS_ID_PROJECTION = new String[] {Groups._ID};
-
- private static final ArrayList<NamedContentValues> EMPTY_ARRAY_NAMEDCONTENTVALUES
- = new ArrayList<NamedContentValues>();
-
- private static final String FOUND_DATA_ROW = "com.android.exchange.FOUND_ROW";
-
- private static final int[] HOME_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
- Tags.CONTACTS_HOME_ADDRESS_COUNTRY,
- Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE,
- Tags.CONTACTS_HOME_ADDRESS_STATE,
- Tags.CONTACTS_HOME_ADDRESS_STREET};
-
- private static final int[] WORK_ADDRESS_TAGS = new int[] {Tags.CONTACTS_BUSINESS_ADDRESS_CITY,
- Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY,
- Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE,
- Tags.CONTACTS_BUSINESS_ADDRESS_STATE,
- Tags.CONTACTS_BUSINESS_ADDRESS_STREET};
-
- private static final int[] OTHER_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
- Tags.CONTACTS_OTHER_ADDRESS_COUNTRY,
- Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE,
- Tags.CONTACTS_OTHER_ADDRESS_STATE,
- Tags.CONTACTS_OTHER_ADDRESS_STREET};
-
- private static final int MAX_IM_ROWS = 3;
- private static final int MAX_EMAIL_ROWS = 3;
- private static final int MAX_PHONE_ROWS = 2;
- private static final String COMMON_DATA_ROW = Im.DATA; // Could have been Email.DATA, etc.
- private static final String COMMON_TYPE_ROW = Phone.TYPE; // Could have been any typed row
-
- private static final int[] IM_TAGS = new int[] {Tags.CONTACTS2_IM_ADDRESS,
- Tags.CONTACTS2_IM_ADDRESS_2, Tags.CONTACTS2_IM_ADDRESS_3};
-
- private static final int[] EMAIL_TAGS = new int[] {Tags.CONTACTS_EMAIL1_ADDRESS,
- Tags.CONTACTS_EMAIL2_ADDRESS, Tags.CONTACTS_EMAIL3_ADDRESS};
-
- private static final int[] WORK_PHONE_TAGS = new int[] {Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER,
- Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER};
-
- private static final int[] HOME_PHONE_TAGS = new int[] {Tags.CONTACTS_HOME_TELEPHONE_NUMBER,
- Tags.CONTACTS_HOME2_TELEPHONE_NUMBER};
-
- private static final Object sSyncKeyLock = new Object();
-
- ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
- ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
-
- private final Uri mAccountUri;
- private final ContentResolver mContentResolver;
- private boolean mGroupsUsed = false;
-
- public ContactsSyncAdapter(EasSyncService service) {
- super(service);
- mAccountUri = uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI);
- mContentResolver = mContext.getContentResolver();
- }
-
- static Uri addCallerIsSyncAdapterParameter(Uri uri) {
- return uri.buildUpon()
- .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
- .build();
- }
-
- @Override
- public void sendSyncOptions(Double protocolVersion, Serializer s) throws IOException {
- setPimSyncOptions(protocolVersion, null, s);
- }
-
- @Override
- public boolean isSyncable() {
- return ContentResolver.getSyncAutomatically(
- mAccountManagerAccount, ContactsContract.AUTHORITY);
- }
-
- @Override
- public boolean parse(InputStream is) throws IOException, CommandStatusException {
- EasContactsSyncParser p = new EasContactsSyncParser(is, this);
- return p.parse();
- }
-
-
- @Override
- public void wipe() {
- mContentResolver.delete(mAccountUri, null, null);
- }
-
- interface UntypedRow {
- public void addValues(RowBuilder builder);
- public boolean isSameAs(int type, String value);
- }
-
- /**
- * We get our SyncKey from ContactsProvider. If there's not one, we set it to "0" (the reset
- * state) and save that away.
- */
- @Override
- public String getSyncKey() throws IOException {
- synchronized (sSyncKeyLock) {
- ContentProviderClient client = mService.mContentResolver
- .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
- try {
- byte[] data = SyncStateContract.Helpers.get(client,
- ContactsContract.SyncState.CONTENT_URI, mAccountManagerAccount);
- if (data == null || data.length == 0) {
- // Initialize the SyncKey
- setSyncKey("0", false);
- // Make sure ungrouped contacts for Exchange are defaultly visible
- ContentValues cv = new ContentValues();
- cv.put(Groups.ACCOUNT_NAME, mAccount.mEmailAddress);
- cv.put(Groups.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
- cv.put(Settings.UNGROUPED_VISIBLE, true);
- client.insert(addCallerIsSyncAdapterParameter(Settings.CONTENT_URI), cv);
- return "0";
- } else {
- return new String(data);
- }
- } catch (RemoteException e) {
- throw new IOException("Can't get SyncKey from ContactsProvider");
- }
- }
- }
-
- /**
- * We only need to set this when we're forced to make the SyncKey "0" (a reset). In all other
- * cases, the SyncKey is set within ContactOperations
- */
- @Override
- public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
- synchronized (sSyncKeyLock) {
- if ("0".equals(syncKey) || !inCommands) {
- ContentProviderClient client = mService.mContentResolver
- .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
- try {
- SyncStateContract.Helpers.set(client, ContactsContract.SyncState.CONTENT_URI,
- mAccountManagerAccount, syncKey.getBytes());
- userLog("SyncKey set to ", syncKey, " in ContactsProvider");
- } catch (RemoteException e) {
- throw new IOException("Can't set SyncKey in ContactsProvider");
- }
- }
- mMailbox.mSyncKey = syncKey;
- }
- }
-
- public static final class EasChildren {
- private EasChildren() {}
-
- /** MIME type used when storing this in data table. */
- public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children";
- public static final int MAX_CHILDREN = 8;
- public static final String[] ROWS =
- new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"};
- }
-
- public static final class EasPersonal {
- String anniversary;
- String fileAs;
-
- /** MIME type used when storing this in data table. */
- public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal";
- public static final String ANNIVERSARY = "data2";
- public static final String FILE_AS = "data4";
-
- boolean hasData() {
- return anniversary != null || fileAs != null;
- }
- }
-
- public static final class EasBusiness {
- String customerId;
- String governmentId;
- String accountName;
-
- /** MIME type used when storing this in data table. */
- public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business";
- public static final String CUSTOMER_ID = "data6";
- public static final String GOVERNMENT_ID = "data7";
- public static final String ACCOUNT_NAME = "data8";
-
- boolean hasData() {
- return customerId != null || governmentId != null || accountName != null;
- }
- }
-
- public static final class Address {
- String city;
- String country;
- String code;
- String street;
- String state;
-
- boolean hasData() {
- return city != null || country != null || code != null || state != null
- || street != null;
- }
- }
-
- class EmailRow implements UntypedRow {
- String email;
- String displayName;
-
- public EmailRow(String _email) {
- Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(_email);
- // Can't happen, but belt & suspenders
- if (tokens.length == 0) {
- email = "";
- displayName = "";
- } else {
- Rfc822Token token = tokens[0];
- email = token.getAddress();
- displayName = token.getName();
- }
- }
-
- public void addValues(RowBuilder builder) {
- builder.withValue(Email.DATA, email);
- builder.withValue(Email.DISPLAY_NAME, displayName);
- }
-
- public boolean isSameAs(int type, String value) {
- return email.equalsIgnoreCase(value);
- }
- }
-
- class ImRow implements UntypedRow {
- String im;
-
- public ImRow(String _im) {
- im = _im;
- }
-
- public void addValues(RowBuilder builder) {
- builder.withValue(Im.DATA, im);
- }
-
- public boolean isSameAs(int type, String value) {
- return im.equalsIgnoreCase(value);
- }
- }
-
- class PhoneRow implements UntypedRow {
- String phone;
- int type;
-
- public PhoneRow(String _phone, int _type) {
- phone = _phone;
- type = _type;
- }
-
- public void addValues(RowBuilder builder) {
- builder.withValue(Im.DATA, phone);
- builder.withValue(Phone.TYPE, type);
- }
-
- public boolean isSameAs(int _type, String value) {
- return type == _type && phone.equalsIgnoreCase(value);
- }
- }
-
- class EasContactsSyncParser extends AbstractSyncParser {
-
- String[] mBindArgument = new String[1];
- String mMailboxIdAsString;
- ContactOperations ops = new ContactOperations();
-
- public EasContactsSyncParser(InputStream in, ContactsSyncAdapter adapter)
- throws IOException {
- super(in, adapter);
- }
-
- public void addData(String serverId, ContactOperations ops, Entity entity)
- throws IOException {
- String fileAs = null;
- String prefix = null;
- String firstName = null;
- String lastName = null;
- String middleName = null;
- String suffix = null;
- String companyName = null;
- String yomiFirstName = null;
- String yomiLastName = null;
- String yomiCompanyName = null;
- String title = null;
- String department = null;
- String officeLocation = null;
- Address home = new Address();
- Address work = new Address();
- Address other = new Address();
- EasBusiness business = new EasBusiness();
- EasPersonal personal = new EasPersonal();
- ArrayList<String> children = new ArrayList<String>();
- ArrayList<UntypedRow> emails = new ArrayList<UntypedRow>();
- ArrayList<UntypedRow> ims = new ArrayList<UntypedRow>();
- ArrayList<UntypedRow> homePhones = new ArrayList<UntypedRow>();
- ArrayList<UntypedRow> workPhones = new ArrayList<UntypedRow>();
- if (entity == null) {
- ops.newContact(serverId);
- }
-
- while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
- switch (tag) {
- case Tags.CONTACTS_FIRST_NAME:
- firstName = getValue();
- break;
- case Tags.CONTACTS_LAST_NAME:
- lastName = getValue();
- break;
- case Tags.CONTACTS_MIDDLE_NAME:
- middleName = getValue();
- break;
- case Tags.CONTACTS_FILE_AS:
- fileAs = getValue();
- break;
- case Tags.CONTACTS_SUFFIX:
- suffix = getValue();
- break;
- case Tags.CONTACTS_COMPANY_NAME:
- companyName = getValue();
- break;
- case Tags.CONTACTS_JOB_TITLE:
- title = getValue();
- break;
- case Tags.CONTACTS_EMAIL1_ADDRESS:
- case Tags.CONTACTS_EMAIL2_ADDRESS:
- case Tags.CONTACTS_EMAIL3_ADDRESS:
- emails.add(new EmailRow(getValue()));
- break;
- case Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER:
- case Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER:
- workPhones.add(new PhoneRow(getValue(), Phone.TYPE_WORK));
- break;
- case Tags.CONTACTS2_MMS:
- ops.addPhone(entity, Phone.TYPE_MMS, getValue());
- break;
- case Tags.CONTACTS_BUSINESS_FAX_NUMBER:
- ops.addPhone(entity, Phone.TYPE_FAX_WORK, getValue());
- break;
- case Tags.CONTACTS2_COMPANY_MAIN_PHONE:
- ops.addPhone(entity, Phone.TYPE_COMPANY_MAIN, getValue());
- break;
- case Tags.CONTACTS_HOME_FAX_NUMBER:
- ops.addPhone(entity, Phone.TYPE_FAX_HOME, getValue());
- break;
- case Tags.CONTACTS_HOME_TELEPHONE_NUMBER:
- case Tags.CONTACTS_HOME2_TELEPHONE_NUMBER:
- homePhones.add(new PhoneRow(getValue(), Phone.TYPE_HOME));
- break;
- case Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER:
- ops.addPhone(entity, Phone.TYPE_MOBILE, getValue());
- break;
- case Tags.CONTACTS_CAR_TELEPHONE_NUMBER:
- ops.addPhone(entity, Phone.TYPE_CAR, getValue());
- break;
- case Tags.CONTACTS_RADIO_TELEPHONE_NUMBER:
- ops.addPhone(entity, Phone.TYPE_RADIO, getValue());
- break;
- case Tags.CONTACTS_PAGER_NUMBER:
- ops.addPhone(entity, Phone.TYPE_PAGER, getValue());
- break;
- case Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER:
- ops.addPhone(entity, Phone.TYPE_ASSISTANT, getValue());
- break;
- case Tags.CONTACTS2_IM_ADDRESS:
- case Tags.CONTACTS2_IM_ADDRESS_2:
- case Tags.CONTACTS2_IM_ADDRESS_3:
- ims.add(new ImRow(getValue()));
- break;
- case Tags.CONTACTS_BUSINESS_ADDRESS_CITY:
- work.city = getValue();
- break;
- case Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY:
- work.country = getValue();
- break;
- case Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE:
- work.code = getValue();
- break;
- case Tags.CONTACTS_BUSINESS_ADDRESS_STATE:
- work.state = getValue();
- break;
- case Tags.CONTACTS_BUSINESS_ADDRESS_STREET:
- work.street = getValue();
- break;
- case Tags.CONTACTS_HOME_ADDRESS_CITY:
- home.city = getValue();
- break;
- case Tags.CONTACTS_HOME_ADDRESS_COUNTRY:
- home.country = getValue();
- break;
- case Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE:
- home.code = getValue();
- break;
- case Tags.CONTACTS_HOME_ADDRESS_STATE:
- home.state = getValue();
- break;
- case Tags.CONTACTS_HOME_ADDRESS_STREET:
- home.street = getValue();
- break;
- case Tags.CONTACTS_OTHER_ADDRESS_CITY:
- other.city = getValue();
- break;
- case Tags.CONTACTS_OTHER_ADDRESS_COUNTRY:
- other.country = getValue();
- break;
- case Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE:
- other.code = getValue();
- break;
- case Tags.CONTACTS_OTHER_ADDRESS_STATE:
- other.state = getValue();
- break;
- case Tags.CONTACTS_OTHER_ADDRESS_STREET:
- other.street = getValue();
- break;
-
- case Tags.CONTACTS_CHILDREN:
- childrenParser(children);
- break;
-
- case Tags.CONTACTS_YOMI_COMPANY_NAME:
- yomiCompanyName = getValue();
- break;
- case Tags.CONTACTS_YOMI_FIRST_NAME:
- yomiFirstName = getValue();
- break;
- case Tags.CONTACTS_YOMI_LAST_NAME:
- yomiLastName = getValue();
- break;
-
- case Tags.CONTACTS2_NICKNAME:
- ops.addNickname(entity, getValue());
- break;
-
- case Tags.CONTACTS_ASSISTANT_NAME:
- ops.addRelation(entity, Relation.TYPE_ASSISTANT, getValue());
- break;
- case Tags.CONTACTS2_MANAGER_NAME:
- ops.addRelation(entity, Relation.TYPE_MANAGER, getValue());
- break;
- case Tags.CONTACTS_SPOUSE:
- ops.addRelation(entity, Relation.TYPE_SPOUSE, getValue());
- break;
- case Tags.CONTACTS_DEPARTMENT:
- department = getValue();
- break;
- case Tags.CONTACTS_TITLE:
- prefix = getValue();
- break;
-
- // EAS Business
- case Tags.CONTACTS_OFFICE_LOCATION:
- officeLocation = getValue();
- break;
- case Tags.CONTACTS2_CUSTOMER_ID:
- business.customerId = getValue();
- break;
- case Tags.CONTACTS2_GOVERNMENT_ID:
- business.governmentId = getValue();
- break;
- case Tags.CONTACTS2_ACCOUNT_NAME:
- business.accountName = getValue();
- break;
-
- // EAS Personal
- case Tags.CONTACTS_ANNIVERSARY:
- personal.anniversary = getValue();
- break;
- case Tags.CONTACTS_BIRTHDAY:
- ops.addBirthday(entity, getValue());
- break;
- case Tags.CONTACTS_WEBPAGE:
- ops.addWebpage(entity, getValue());
- break;
-
- case Tags.CONTACTS_PICTURE:
- ops.addPhoto(entity, getValue());
- break;
-
- case Tags.BASE_BODY:
- ops.addNote(entity, bodyParser());
- break;
- case Tags.CONTACTS_BODY:
- ops.addNote(entity, getValue());
- break;
-
- case Tags.CONTACTS_CATEGORIES:
- mGroupsUsed = true;
- categoriesParser(ops, entity);
- break;
-
- case Tags.CONTACTS_COMPRESSED_RTF:
- // We don't use this, and it isn't necessary to upload, so we'll ignore it
- skipTag();
- break;
-
- default:
- skipTag();
- }
- }
-
- // We must have first name, last name, or company name
- String name = null;
- if (firstName != null || lastName != null) {
- if (firstName == null) {
- name = lastName;
- } else if (lastName == null) {
- name = firstName;
- } else {
- name = firstName + ' ' + lastName;
- }
- } else if (companyName != null) {
- name = companyName;
- }
-
- ops.addName(entity, prefix, firstName, lastName, middleName, suffix, name,
- yomiFirstName, yomiLastName, fileAs);
- ops.addBusiness(entity, business);
- ops.addPersonal(entity, personal);
-
- ops.addUntyped(entity, emails, Email.CONTENT_ITEM_TYPE, -1, MAX_EMAIL_ROWS);
- ops.addUntyped(entity, ims, Im.CONTENT_ITEM_TYPE, -1, MAX_IM_ROWS);
- ops.addUntyped(entity, homePhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_HOME,
- MAX_PHONE_ROWS);
- ops.addUntyped(entity, workPhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_WORK,
- MAX_PHONE_ROWS);
-
- if (!children.isEmpty()) {
- ops.addChildren(entity, children);
- }
-
- if (work.hasData()) {
- ops.addPostal(entity, StructuredPostal.TYPE_WORK, work.street, work.city,
- work.state, work.country, work.code);
- }
- if (home.hasData()) {
- ops.addPostal(entity, StructuredPostal.TYPE_HOME, home.street, home.city,
- home.state, home.country, home.code);
- }
- if (other.hasData()) {
- ops.addPostal(entity, StructuredPostal.TYPE_OTHER, other.street, other.city,
- other.state, other.country, other.code);
- }
-
- if (companyName != null) {
- ops.addOrganization(entity, Organization.TYPE_WORK, companyName, title, department,
- yomiCompanyName, officeLocation);
- }
-
- if (entity != null) {
- // We've been removing rows from the list as they've been found in the xml
- // Any that are left must have been deleted on the server
- ArrayList<NamedContentValues> ncvList = entity.getSubValues();
- for (NamedContentValues ncv: ncvList) {
- // These rows need to be deleted...
- Uri u = dataUriFromNamedContentValues(ncv);
- ops.add(ContentProviderOperation.newDelete(addCallerIsSyncAdapterParameter(u))
- .build());
- }
- }
- }
-
- private void categoriesParser(ContactOperations ops, Entity entity) throws IOException {
- while (nextTag(Tags.CONTACTS_CATEGORIES) != END) {
- switch (tag) {
- case Tags.CONTACTS_CATEGORY:
- ops.addGroup(entity, getValue());
- break;
- default:
- skipTag();
- }
- }
- }
-
- private void childrenParser(ArrayList<String> children) throws IOException {
- while (nextTag(Tags.CONTACTS_CHILDREN) != END) {
- switch (tag) {
- case Tags.CONTACTS_CHILD:
- if (children.size() < EasChildren.MAX_CHILDREN) {
- children.add(getValue());
- }
- break;
- default:
- skipTag();
- }
- }
- }
-
- private String bodyParser() throws IOException {
- String body = null;
- while (nextTag(Tags.BASE_BODY) != END) {
- switch (tag) {
- case Tags.BASE_DATA:
- body = getValue();
- break;
- default:
- skipTag();
- }
- }
- return body;
- }
-
- public void addParser(ContactOperations ops) throws IOException {
- String serverId = null;
- while (nextTag(Tags.SYNC_ADD) != END) {
- switch (tag) {
- case Tags.SYNC_SERVER_ID: // same as
- serverId = getValue();
- break;
- case Tags.SYNC_APPLICATION_DATA:
- addData(serverId, ops, null);
- break;
- default:
- skipTag();
- }
- }
- }
-
- private Cursor getServerIdCursor(String serverId) {
- mBindArgument[0] = serverId;
- return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION,
- mBindArgument, null);
- }
-
- private Cursor getClientIdCursor(String clientId) {
- mBindArgument[0] = clientId;
- return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION,
- mBindArgument, null);
- }
-
- public void deleteParser(ContactOperations ops) throws IOException {
- while (nextTag(Tags.SYNC_DELETE) != END) {
- switch (tag) {
- case Tags.SYNC_SERVER_ID:
- String serverId = getValue();
- // Find the message in this mailbox with the given serverId
- Cursor c = getServerIdCursor(serverId);
- try {
- if (c.moveToFirst()) {
- userLog("Deleting ", serverId);
- ops.delete(c.getLong(0));
- }
- } finally {
- c.close();
- }
- break;
- default:
- skipTag();
- }
- }
- }
-
- class ServerChange {
- long id;
- boolean read;
-
- ServerChange(long _id, boolean _read) {
- id = _id;
- read = _read;
- }
- }
-
- /**
- * Changes are handled row by row, and only changed/new rows are acted upon
- * @param ops the array of pending ContactProviderOperations.
- * @throws IOException
- */
- public void changeParser(ContactOperations ops) throws IOException {
- String serverId = null;
- Entity entity = null;
- while (nextTag(Tags.SYNC_CHANGE) != END) {
- switch (tag) {
- case Tags.SYNC_SERVER_ID:
- serverId = getValue();
- Cursor c = getServerIdCursor(serverId);
- try {
- if (c.moveToFirst()) {
- // TODO Handle deleted individual rows...
- Uri uri = ContentUris.withAppendedId(
- RawContacts.CONTENT_URI, c.getLong(0));
- uri = Uri.withAppendedPath(
- uri, RawContacts.Entity.CONTENT_DIRECTORY);
- EntityIterator entityIterator = RawContacts.newEntityIterator(
- mContentResolver.query(uri, null, null, null, null));
- if (entityIterator.hasNext()) {
- entity = entityIterator.next();
- }
- userLog("Changing contact ", serverId);
- }
- } finally {
- c.close();
- }
- break;
- case Tags.SYNC_APPLICATION_DATA:
- addData(serverId, ops, entity);
- break;
- default:
- skipTag();
- }
- }
- }
-
- @Override
- public void commandsParser() throws IOException {
- while (nextTag(Tags.SYNC_COMMANDS) != END) {
- if (tag == Tags.SYNC_ADD) {
- addParser(ops);
- incrementChangeCount();
- } else if (tag == Tags.SYNC_DELETE) {
- deleteParser(ops);
- incrementChangeCount();
- } else if (tag == Tags.SYNC_CHANGE) {
- changeParser(ops);
- incrementChangeCount();
- } else
- skipTag();
- }
- }
-
- @Override
- public void commit() throws IOException {
- // Save the syncKey here, using the Helper provider by Contacts provider
- userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey);
- ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI,
- mAccountManagerAccount, mMailbox.mSyncKey.getBytes()));
-
- // Execute these all at once...
- ops.execute();
-
- if (ops.mResults != null) {
- ContentValues cv = new ContentValues();
- cv.put(RawContacts.DIRTY, 0);
- for (int i = 0; i < ops.mContactIndexCount; i++) {
- int index = ops.mContactIndexArray[i];
- Uri u = ops.mResults[index].uri;
- if (u != null) {
- String idString = u.getLastPathSegment();
- mContentResolver.update(
- addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), cv,
- RawContacts._ID + "=" + idString, null);
- }
- }
- }
- }
-
- public void addResponsesParser() throws IOException {
- String serverId = null;
- String clientId = null;
- ContentValues cv = new ContentValues();
- while (nextTag(Tags.SYNC_ADD) != END) {
- switch (tag) {
- case Tags.SYNC_SERVER_ID:
- serverId = getValue();
- break;
- case Tags.SYNC_CLIENT_ID:
- clientId = getValue();
- break;
- case Tags.SYNC_STATUS:
- getValue();
- break;
- default:
- skipTag();
- }
- }
-
- // This is theoretically impossible, but...
- if (clientId == null || serverId == null) return;
-
- Cursor c = getClientIdCursor(clientId);
- try {
- if (c.moveToFirst()) {
- cv.put(RawContacts.SOURCE_ID, serverId);
- cv.put(RawContacts.DIRTY, 0);
- ops.add(ContentProviderOperation.newUpdate(
- ContentUris.withAppendedId(
- addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI),
- c.getLong(0)))
- .withValues(cv)
- .build());
- userLog("New contact " + clientId + " was given serverId: " + serverId);
- }
- } finally {
- c.close();
- }
- }
-
- public void changeResponsesParser() throws IOException {
- String serverId = null;
- String status = null;
- while (nextTag(Tags.SYNC_CHANGE) != END) {
- switch (tag) {
- case Tags.SYNC_SERVER_ID:
- serverId = getValue();
- break;
- case Tags.SYNC_STATUS:
- status = getValue();
- break;
- default:
- skipTag();
- }
- }
- if (serverId != null && status != null) {
- userLog("Changed contact " + serverId + " failed with status: " + status);
- }
- }
-
-
- @Override
- public void responsesParser() throws IOException {
- // Handle server responses here (for Add and Change)
- while (nextTag(Tags.SYNC_RESPONSES) != END) {
- if (tag == Tags.SYNC_ADD) {
- addResponsesParser();
- } else if (tag == Tags.SYNC_CHANGE) {
- changeResponsesParser();
- } else
- skipTag();
- }
- }
- }
-
-
- private Uri uriWithAccountAndIsSyncAdapter(Uri uri) {
- return uri.buildUpon()
- .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
- .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE)
- .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
- .build();
- }
-
- /**
- * SmartBuilder is a wrapper for the Builder class that is used to create/update rows for a
- * ContentProvider. It has, in addition to the Builder, ContentValues which, if present,
- * represent the current values of that row, that can be compared against current values to
- * see whether an update is even necessary. The methods on SmartBuilder are delegated to
- * the Builder.
- */
- private class RowBuilder {
- Builder builder;
- ContentValues cv;
-
- public RowBuilder(Builder _builder) {
- builder = _builder;
- }
-
- public RowBuilder(Builder _builder, NamedContentValues _ncv) {
- builder = _builder;
- cv = _ncv.values;
- }
-
- RowBuilder withValues(ContentValues values) {
- builder.withValues(values);
- return this;
- }
-
- RowBuilder withValueBackReference(String key, int previousResult) {
- builder.withValueBackReference(key, previousResult);
- return this;
- }
-
- ContentProviderOperation build() {
- return builder.build();
- }
-
- RowBuilder withValue(String key, Object value) {
- builder.withValue(key, value);
- return this;
- }
- }
-
- private class ContactOperations extends ArrayList<ContentProviderOperation> {
- private static final long serialVersionUID = 1L;
- private int mCount = 0;
- private int mContactBackValue = mCount;
- // Make an array big enough for the PIM window (max items we can get)
- private int[] mContactIndexArray =
- new int[Integer.parseInt(AbstractSyncAdapter.PIM_WINDOW_SIZE)];
- private int mContactIndexCount = 0;
- private ContentProviderResult[] mResults = null;
-
- @Override
- public boolean add(ContentProviderOperation op) {
- super.add(op);
- mCount++;
- return true;
- }
-
- public void newContact(String serverId) {
- Builder builder = ContentProviderOperation
- .newInsert(uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI));
- ContentValues values = new ContentValues();
- values.put(RawContacts.SOURCE_ID, serverId);
- builder.withValues(values);
- mContactBackValue = mCount;
- mContactIndexArray[mContactIndexCount++] = mCount;
- add(builder.build());
- }
-
- public void delete(long id) {
- add(ContentProviderOperation
- .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
- .buildUpon()
- .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
- .build())
- .build());
- }
-
- public void execute() {
- synchronized (mService.getSynchronizer()) {
- if (!mService.isStopped()) {
- try {
- if (!isEmpty()) {
- mService.userLog("Executing ", size(), " CPO's");
- mResults = mContext.getContentResolver().applyBatch(
- ContactsContract.AUTHORITY, this);
- }
- } catch (RemoteException e) {
- // There is nothing sensible to be done here
- Log.e(TAG, "problem inserting contact during server update", e);
- } catch (OperationApplicationException e) {
- // There is nothing sensible to be done here
- Log.e(TAG, "problem inserting contact during server update", e);
- }
- }
- }
- }
-
- /**
- * Given the list of NamedContentValues for an entity, a mime type, and a subtype,
- * tries to find a match, returning it
- * @param list the list of NCV's from the contact entity
- * @param contentItemType the mime type we're looking for
- * @param type the subtype (e.g. HOME, WORK, etc.)
- * @return the matching NCV or null if not found
- */
- private NamedContentValues findTypedData(ArrayList<NamedContentValues> list,
- String contentItemType, int type, String stringType) {
- NamedContentValues result = null;
-
- // Loop through the ncv's, looking for an existing row
- for (NamedContentValues namedContentValues: list) {
- Uri uri = namedContentValues.uri;
- ContentValues cv = namedContentValues.values;
- if (Data.CONTENT_URI.equals(uri)) {
- String mimeType = cv.getAsString(Data.MIMETYPE);
- if (mimeType.equals(contentItemType)) {
- if (stringType != null) {
- if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) {
- result = namedContentValues;
- }
- // Note Email.TYPE could be ANY type column; they are all defined in
- // the private CommonColumns class in ContactsContract
- // We'll accept either type < 0 (don't care), cv doesn't have a type,
- // or the types are equal
- } else if (type < 0 || !cv.containsKey(Email.TYPE) ||
- cv.getAsInteger(Email.TYPE) == type) {
- result = namedContentValues;
- }
- }
- }
- }
-
- // If we've found an existing data row, we'll delete it. Any rows left at the
- // end should be deleted...
- if (result != null) {
- list.remove(result);
- }
-
- // Return the row found (or null)
- return result;
- }
-
- /**
- * Given the list of NamedContentValues for an entity and a mime type
- * gather all of the matching NCV's, returning them
- * @param list the list of NCV's from the contact entity
- * @param contentItemType the mime type we're looking for
- * @param type the subtype (e.g. HOME, WORK, etc.)
- * @return the matching NCVs
- */
- private ArrayList<NamedContentValues> findUntypedData(ArrayList<NamedContentValues> list,
- int type, String contentItemType) {
- ArrayList<NamedContentValues> result = new ArrayList<NamedContentValues>();
-
- // Loop through the ncv's, looking for an existing row
- for (NamedContentValues namedContentValues: list) {
- Uri uri = namedContentValues.uri;
- ContentValues cv = namedContentValues.values;
- if (Data.CONTENT_URI.equals(uri)) {
- String mimeType = cv.getAsString(Data.MIMETYPE);
- if (mimeType.equals(contentItemType)) {
- if (type != -1) {
- int subtype = cv.getAsInteger(Phone.TYPE);
- if (type != subtype) {
- continue;
- }
- }
- result.add(namedContentValues);
- }
- }
- }
-
- // If we've found an existing data row, we'll delete it. Any rows left at the
- // end should be deleted...
- for (NamedContentValues values : result) {
- list.remove(values);
- }
-
- // Return the row found (or null)
- return result;
- }
-
- /**
- * Create a wrapper for a builder (insert or update) that also includes the NCV for
- * an existing row of this type. If the SmartBuilder's cv field is not null, then
- * it represents the current (old) values of this field. The caller can then check
- * whether the field is now different and needs to be updated; if it's not different,
- * the caller will simply return and not generate a new CPO. Otherwise, the builder
- * should have its content values set, and the built CPO should be added to the
- * ContactOperations list.
- *
- * @param entity the contact entity (or null if this is a new contact)
- * @param mimeType the mime type of this row
- * @param type the subtype of this row
- * @param stringType for groups, the name of the group (type will be ignored), or null
- * @return the created SmartBuilder
- */
- public RowBuilder createBuilder(Entity entity, String mimeType, int type,
- String stringType) {
- RowBuilder builder = null;
-
- if (entity != null) {
- NamedContentValues ncv =
- findTypedData(entity.getSubValues(), mimeType, type, stringType);
- if (ncv != null) {
- builder = new RowBuilder(
- ContentProviderOperation
- .newUpdate(addCallerIsSyncAdapterParameter(
- dataUriFromNamedContentValues(ncv))),
- ncv);
- }
- }
-
- if (builder == null) {
- builder = newRowBuilder(entity, mimeType);
- }
-
- // Return the appropriate builder (insert or update)
- // Caller will fill in the appropriate values; 4 MIMETYPE is already set
- return builder;
- }
-
- private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) {
- return createBuilder(entity, mimeType, type, null);
- }
-
- private RowBuilder untypedRowBuilder(Entity entity, String mimeType) {
- return createBuilder(entity, mimeType, -1, null);
- }
-
- private RowBuilder newRowBuilder(Entity entity, String mimeType) {
- // This is a new row; first get the contactId
- // If the Contact is new, use the saved back value; otherwise the value in the entity
- int contactId = mContactBackValue;
- if (entity != null) {
- contactId = entity.getEntityValues().getAsInteger(RawContacts._ID);
- }
-
- // Create an insert operation with the proper contactId reference
- RowBuilder builder =
- new RowBuilder(ContentProviderOperation.newInsert(
- addCallerIsSyncAdapterParameter(Data.CONTENT_URI)));
- if (entity == null) {
- builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId);
- } else {
- builder.withValue(Data.RAW_CONTACT_ID, contactId);
- }
-
- // Set the mime type of the row
- builder.withValue(Data.MIMETYPE, mimeType);
- return builder;
- }
-
- /**
- * Compare a column in a ContentValues with an (old) value, and see if they are the
- * same. For this purpose, null and an empty string are considered the same.
- * @param cv a ContentValues object, from a NamedContentValues
- * @param column a column that might be in the ContentValues
- * @param oldValue an old value (or null) to check against
- * @return whether the column's value in the ContentValues matches oldValue
- */
- private boolean cvCompareString(ContentValues cv, String column, String oldValue) {
- if (cv.containsKey(column)) {
- if (oldValue != null && cv.getAsString(column).equals(oldValue)) {
- return true;
- }
- } else if (oldValue == null || oldValue.length() == 0) {
- return true;
- }
- return false;
- }
-
- public void addChildren(Entity entity, ArrayList<String> children) {
- RowBuilder builder = untypedRowBuilder(entity, EasChildren.CONTENT_ITEM_TYPE);
- int i = 0;
- for (String child: children) {
- builder.withValue(EasChildren.ROWS[i++], child);
- }
- add(builder.build());
- }
-
- public void addGroup(Entity entity, String group) {
- RowBuilder builder =
- createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group);
- builder.withValue(GroupMembership.GROUP_SOURCE_ID, group);
- add(builder.build());
- }
-
- public void addBirthday(Entity entity, String birthday) {
- RowBuilder builder =
- typedRowBuilder(entity, Event.CONTENT_ITEM_TYPE, Event.TYPE_BIRTHDAY);
- ContentValues cv = builder.cv;
- if (cv != null && cvCompareString(cv, Event.START_DATE, birthday)) {
- return;
- }
- builder.withValue(Event.START_DATE, birthday);
- builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
- add(builder.build());
- }
-
- public void addName(Entity entity, String prefix, String givenName, String familyName,
- String middleName, String suffix, String displayName, String yomiFirstName,
- String yomiLastName, String fileAs) {
- RowBuilder builder = untypedRowBuilder(entity, StructuredName.CONTENT_ITEM_TYPE);
- ContentValues cv = builder.cv;
- if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) &&
- cvCompareString(cv, StructuredName.FAMILY_NAME, familyName) &&
- cvCompareString(cv, StructuredName.MIDDLE_NAME, middleName) &&
- cvCompareString(cv, StructuredName.PREFIX, prefix) &&
- cvCompareString(cv, StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName) &&
- cvCompareString(cv, StructuredName.PHONETIC_FAMILY_NAME, yomiLastName) &&
- //cvCompareString(cv, StructuredName.DISPLAY_NAME, fileAs) &&
- cvCompareString(cv, StructuredName.SUFFIX, suffix)) {
- return;
- }
- builder.withValue(StructuredName.GIVEN_NAME, givenName);
- builder.withValue(StructuredName.FAMILY_NAME, familyName);
- builder.withValue(StructuredName.MIDDLE_NAME, middleName);
- builder.withValue(StructuredName.SUFFIX, suffix);
- builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName);
- builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, yomiLastName);
- builder.withValue(StructuredName.PREFIX, prefix);
- //builder.withValue(StructuredName.DISPLAY_NAME, fileAs);
- add(builder.build());
- }
-
- public void addPersonal(Entity entity, EasPersonal personal) {
- RowBuilder builder = untypedRowBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE);
- ContentValues cv = builder.cv;
- if (cv != null && cvCompareString(cv, EasPersonal.ANNIVERSARY, personal.anniversary) &&
- cvCompareString(cv, EasPersonal.FILE_AS , personal.fileAs)) {
- return;
- }
- if (!personal.hasData()) {
- return;
- }
- builder.withValue(EasPersonal.FILE_AS, personal.fileAs);
- builder.withValue(EasPersonal.ANNIVERSARY, personal.anniversary);
- add(builder.build());
- }
-
- public void addBusiness(Entity entity, EasBusiness business) {
- RowBuilder builder = untypedRowBuilder(entity, EasBusiness.CONTENT_ITEM_TYPE);
- ContentValues cv = builder.cv;
- if (cv != null && cvCompareString(cv, EasBusiness.ACCOUNT_NAME, business.accountName) &&
- cvCompareString(cv, EasBusiness.CUSTOMER_ID, business.customerId) &&
- cvCompareString(cv, EasBusiness.GOVERNMENT_ID, business.governmentId)) {
- return;
- }
- if (!business.hasData()) {
- return;
- }
- builder.withValue(EasBusiness.ACCOUNT_NAME, business.accountName);
- builder.withValue(EasBusiness.CUSTOMER_ID, business.customerId);
- builder.withValue(EasBusiness.GOVERNMENT_ID, business.governmentId);
- add(builder.build());
- }
-
- public void addPhoto(Entity entity, String photo) {
- RowBuilder builder = untypedRowBuilder(entity, Photo.CONTENT_ITEM_TYPE);
- // We're always going to add this; it's not worth trying to figure out whether the
- // picture is the same as the one stored.
- byte[] pic = Base64.decode(photo, Base64.DEFAULT);
- builder.withValue(Photo.PHOTO, pic);
- add(builder.build());
- }
-
- public void addPhone(Entity entity, int type, String phone) {
- RowBuilder builder = typedRowBuilder(entity, Phone.CONTENT_ITEM_TYPE, type);
- ContentValues cv = builder.cv;
- if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) {
- return;
- }
- builder.withValue(Phone.TYPE, type);
- builder.withValue(Phone.NUMBER, phone);
- add(builder.build());
- }
-
- public void addWebpage(Entity entity, String url) {
- RowBuilder builder = untypedRowBuilder(entity, Website.CONTENT_ITEM_TYPE);
- ContentValues cv = builder.cv;
- if (cv != null && cvCompareString(cv, Website.URL, url)) {
- return;
- }
- builder.withValue(Website.TYPE, Website.TYPE_WORK);
- builder.withValue(Website.URL, url);
- add(builder.build());
- }
-
- public void addRelation(Entity entity, int type, String value) {
- RowBuilder builder = typedRowBuilder(entity, Relation.CONTENT_ITEM_TYPE, type);
- ContentValues cv = builder.cv;
- if (cv != null && cvCompareString(cv, Relation.DATA, value)) {
- return;
- }
- builder.withValue(Relation.TYPE, type);
- builder.withValue(Relation.DATA, value);
- add(builder.build());
- }
-
- public void addNickname(Entity entity, String name) {
- RowBuilder builder =
- typedRowBuilder(entity, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE_DEFAULT);
- ContentValues cv = builder.cv;
- if (cv != null && cvCompareString(cv, Nickname.NAME, name)) {
- return;
- }
- builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
- builder.withValue(Nickname.NAME, name);
- add(builder.build());
- }
-
- public void addPostal(Entity entity, int type, String street, String city, String state,
- String country, String code) {
- RowBuilder builder = typedRowBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE,
- type);
- ContentValues cv = builder.cv;
- if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) &&
- cvCompareString(cv, StructuredPostal.STREET, street) &&
- cvCompareString(cv, StructuredPostal.COUNTRY, country) &&
- cvCompareString(cv, StructuredPostal.POSTCODE, code) &&
- cvCompareString(cv, StructuredPostal.REGION, state)) {
- return;
- }
- builder.withValue(StructuredPostal.TYPE, type);
- builder.withValue(StructuredPostal.CITY, city);
- builder.withValue(StructuredPostal.STREET, street);
- builder.withValue(StructuredPostal.COUNTRY, country);
- builder.withValue(StructuredPostal.POSTCODE, code);
- builder.withValue(StructuredPostal.REGION, state);
- add(builder.build());
- }
-
- /**
- * We now are dealing with up to maxRows typeless rows of mimeType data. We need to try to
- * match them with existing rows; if there's a match, everything's great. Otherwise, we
- * either need to add a new row for the data, or we have to replace an existing one
- * that no longer matches. This is similar to the way Emails are handled.
- */
- public void addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType,
- int type, int maxRows) {
- // Make a list of all same type rows in the existing entity
- ArrayList<NamedContentValues> oldValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
- ArrayList<NamedContentValues> entityValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
- if (entity != null) {
- oldValues = findUntypedData(entityValues, type, mimeType);
- entityValues = entity.getSubValues();
- }
-
- // These will be rows needing replacement with new values
- ArrayList<UntypedRow> rowsToReplace = new ArrayList<UntypedRow>();
-
- // The count of existing rows
- int numRows = oldValues.size();
- for (UntypedRow row: rows) {
- boolean found = false;
- // If we already have this row, mark it
- for (NamedContentValues ncv: oldValues) {
- ContentValues cv = ncv.values;
- String data = cv.getAsString(COMMON_DATA_ROW);
- int rowType = -1;
- if (cv.containsKey(COMMON_TYPE_ROW)) {
- rowType = cv.getAsInteger(COMMON_TYPE_ROW);
- }
- if (row.isSameAs(rowType, data)) {
- cv.put(FOUND_DATA_ROW, true);
- // Remove this to indicate it's still being used
- entityValues.remove(ncv);
- found = true;
- break;
- }
- }
- if (!found) {
- // If we don't, there are two possibilities
- if (numRows < maxRows) {
- // If there are available rows, add a new one
- RowBuilder builder = newRowBuilder(entity, mimeType);
- row.addValues(builder);
- add(builder.build());
- numRows++;
- } else {
- // Otherwise, say we need to replace a row with this
- rowsToReplace.add(row);
- }
- }
- }
-
- // Go through rows needing replacement
- for (UntypedRow row: rowsToReplace) {
- for (NamedContentValues ncv: oldValues) {
- ContentValues cv = ncv.values;
- // Find a row that hasn't been used (i.e. doesn't match current rows)
- if (!cv.containsKey(FOUND_DATA_ROW)) {
- // And update it
- RowBuilder builder = new RowBuilder(
- ContentProviderOperation
- .newUpdate(addCallerIsSyncAdapterParameter(
- dataUriFromNamedContentValues(ncv))),
- ncv);
- row.addValues(builder);
- add(builder.build());
- }
- }
- }
- }
-
- public void addOrganization(Entity entity, int type, String company, String title,
- String department, String yomiCompanyName, String officeLocation) {
- RowBuilder builder = typedRowBuilder(entity, Organization.CONTENT_ITEM_TYPE, type);
- ContentValues cv = builder.cv;
- if (cv != null && cvCompareString(cv, Organization.COMPANY, company) &&
- cvCompareString(cv, Organization.PHONETIC_NAME, yomiCompanyName) &&
- cvCompareString(cv, Organization.DEPARTMENT, department) &&
- cvCompareString(cv, Organization.TITLE, title) &&
- cvCompareString(cv, Organization.OFFICE_LOCATION, officeLocation)) {
- return;
- }
- builder.withValue(Organization.TYPE, type);
- builder.withValue(Organization.COMPANY, company);
- builder.withValue(Organization.TITLE, title);
- builder.withValue(Organization.DEPARTMENT, department);
- builder.withValue(Organization.PHONETIC_NAME, yomiCompanyName);
- builder.withValue(Organization.OFFICE_LOCATION, officeLocation);
- add(builder.build());
- }
-
- public void addNote(Entity entity, String note) {
- RowBuilder builder = typedRowBuilder(entity, Note.CONTENT_ITEM_TYPE, -1);
- ContentValues cv = builder.cv;
- if (note == null) return;
- note = note.replaceAll("\r\n", "\n");
- if (cv != null && cvCompareString(cv, Note.NOTE, note)) {
- return;
- }
-
- // Reject notes with nothing in them. Often, we get something from Outlook when
- // nothing was ever entered. Sigh.
- int len = note.length();
- int i = 0;
- for (; i < len; i++) {
- char c = note.charAt(i);
- if (!Character.isWhitespace(c)) {
- break;
- }
- }
- if (i == len) return;
-
- builder.withValue(Note.NOTE, note);
- add(builder.build());
- }
- }
-
- /**
- * Generate the uri for the data row associated with this NamedContentValues object
- * @param ncv the NamedContentValues object
- * @return a uri that can be used to refer to this row
- */
- public Uri dataUriFromNamedContentValues(NamedContentValues ncv) {
- long id = ncv.values.getAsLong(RawContacts._ID);
- Uri dataUri = ContentUris.withAppendedId(ncv.uri, id);
- return dataUri;
- }
-
- @Override
- public void cleanup() {
- // Mark the changed contacts dirty = 0
- // Permanently delete the user deletions
- ContactOperations ops = new ContactOperations();
- for (Long id: mUpdatedIdList) {
- ops.add(ContentProviderOperation
- .newUpdate(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
- .buildUpon()
- .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
- .build())
- .withValue(RawContacts.DIRTY, 0).build());
- }
- for (Long id: mDeletedIdList) {
- ops.add(ContentProviderOperation
- .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
- .buildUpon()
- .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
- .build())
- .build());
- }
- ops.execute();
- ContentResolver cr = mContext.getContentResolver();
- if (mGroupsUsed) {
- // Make sure the title column is set for all of our groups
- // And that all of our groups are visible
- // TODO Perhaps the visible part should only happen when the group is created, but
- // this is fine for now.
- Uri groupsUri = uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI);
- Cursor c = cr.query(groupsUri, new String[] {Groups.SOURCE_ID, Groups.TITLE},
- Groups.TITLE + " IS NULL", null, null);
- ContentValues values = new ContentValues();
- values.put(Groups.GROUP_VISIBLE, 1);
- try {
- while (c.moveToNext()) {
- String sourceId = c.getString(0);
- values.put(Groups.TITLE, sourceId);
- cr.update(uriWithAccountAndIsSyncAdapter(groupsUri), values,
- Groups.SOURCE_ID + "=?", new String[] {sourceId});
- }
- } finally {
- c.close();
- }
- }
- }
-
- @Override
- public String getCollectionName() {
- return "Contacts";
- }
-
- private void sendEmail(Serializer s, ContentValues cv, int count, String displayName)
- throws IOException {
- // Get both parts of the email address (a newly created one in the UI won't have a name)
- String addr = cv.getAsString(Email.DATA);
- String name = cv.getAsString(Email.DISPLAY_NAME);
- if (name == null) {
- if (displayName != null) {
- name = displayName;
- } else {
- name = addr;
- }
- }
- // Compose address from name and addr
- if (addr != null) {
- String value = '\"' + name + "\" <" + addr + '>';
- if (count < MAX_EMAIL_ROWS) {
- s.data(EMAIL_TAGS[count], value);
- }
- }
- }
-
- private void sendIm(Serializer s, ContentValues cv, int count) throws IOException {
- String value = cv.getAsString(Im.DATA);
- if (value == null) return;
- if (count < MAX_IM_ROWS) {
- s.data(IM_TAGS[count], value);
- }
- }
-
- private void sendOnePostal(Serializer s, ContentValues cv, int[] fieldNames)
- throws IOException{
- sendStringData(s, cv, StructuredPostal.CITY, fieldNames[0]);
- sendStringData(s, cv, StructuredPostal.COUNTRY, fieldNames[1]);
- sendStringData(s, cv, StructuredPostal.POSTCODE, fieldNames[2]);
- sendStringData(s, cv, StructuredPostal.REGION, fieldNames[3]);
- sendStringData(s, cv, StructuredPostal.STREET, fieldNames[4]);
- }
-
- private void sendStructuredPostal(Serializer s, ContentValues cv) throws IOException {
- switch (cv.getAsInteger(StructuredPostal.TYPE)) {
- case StructuredPostal.TYPE_HOME:
- sendOnePostal(s, cv, HOME_ADDRESS_TAGS);
- break;
- case StructuredPostal.TYPE_WORK:
- sendOnePostal(s, cv, WORK_ADDRESS_TAGS);
- break;
- case StructuredPostal.TYPE_OTHER:
- sendOnePostal(s, cv, OTHER_ADDRESS_TAGS);
- break;
- default:
- break;
- }
- }
-
- private void sendStringData(Serializer s, ContentValues cv, String column, int tag)
- throws IOException {
- if (cv.containsKey(column)) {
- String value = cv.getAsString(column);
- if (!TextUtils.isEmpty(value)) {
- s.data(tag, value);
- }
- }
- }
-
- private String sendStructuredName(Serializer s, ContentValues cv) throws IOException {
- String displayName = null;
- sendStringData(s, cv, StructuredName.FAMILY_NAME, Tags.CONTACTS_LAST_NAME);
- sendStringData(s, cv, StructuredName.GIVEN_NAME, Tags.CONTACTS_FIRST_NAME);
- sendStringData(s, cv, StructuredName.MIDDLE_NAME, Tags.CONTACTS_MIDDLE_NAME);
- sendStringData(s, cv, StructuredName.SUFFIX, Tags.CONTACTS_SUFFIX);
- sendStringData(s, cv, StructuredName.PHONETIC_GIVEN_NAME, Tags.CONTACTS_YOMI_FIRST_NAME);
- sendStringData(s, cv, StructuredName.PHONETIC_FAMILY_NAME, Tags.CONTACTS_YOMI_LAST_NAME);
- sendStringData(s, cv, StructuredName.PREFIX, Tags.CONTACTS_TITLE);
- if (cv.containsKey(StructuredName.DISPLAY_NAME)) {
- displayName = cv.getAsString(StructuredName.DISPLAY_NAME);
- if (!TextUtils.isEmpty(displayName)) {
- s.data(Tags.CONTACTS_FILE_AS, displayName);
- }
- }
- return displayName;
- }
-
- private void sendBusiness(Serializer s, ContentValues cv) throws IOException {
- sendStringData(s, cv, EasBusiness.ACCOUNT_NAME, Tags.CONTACTS2_ACCOUNT_NAME);
- sendStringData(s, cv, EasBusiness.CUSTOMER_ID, Tags.CONTACTS2_CUSTOMER_ID);
- sendStringData(s, cv, EasBusiness.GOVERNMENT_ID, Tags.CONTACTS2_GOVERNMENT_ID);
- }
-
- private void sendPersonal(Serializer s, ContentValues cv) throws IOException {
- sendStringData(s, cv, EasPersonal.ANNIVERSARY, Tags.CONTACTS_ANNIVERSARY);
- sendStringData(s, cv, EasPersonal.FILE_AS, Tags.CONTACTS_FILE_AS);
- }
-
- private void sendBirthday(Serializer s, ContentValues cv) throws IOException {
- sendStringData(s, cv, Event.START_DATE, Tags.CONTACTS_BIRTHDAY);
- }
-
- private void sendPhoto(Serializer s, ContentValues cv) throws IOException {
- if (cv.containsKey(Photo.PHOTO)) {
- byte[] bytes = cv.getAsByteArray(Photo.PHOTO);
- String pic = Base64.encodeToString(bytes, Base64.NO_WRAP);
- s.data(Tags.CONTACTS_PICTURE, pic);
- } else {
- // Send an empty tag, which signals the server to delete any pre-existing photo
- s.tag(Tags.CONTACTS_PICTURE);
- }
- }
-
- private void sendOrganization(Serializer s, ContentValues cv) throws IOException {
- sendStringData(s, cv, Organization.TITLE, Tags.CONTACTS_JOB_TITLE);
- sendStringData(s, cv, Organization.COMPANY, Tags.CONTACTS_COMPANY_NAME);
- sendStringData(s, cv, Organization.DEPARTMENT, Tags.CONTACTS_DEPARTMENT);
- sendStringData(s, cv, Organization.OFFICE_LOCATION, Tags.CONTACTS_OFFICE_LOCATION);
- }
-
- private void sendNickname(Serializer s, ContentValues cv) throws IOException {
- sendStringData(s, cv, Nickname.NAME, Tags.CONTACTS2_NICKNAME);
- }
-
- private void sendWebpage(Serializer s, ContentValues cv) throws IOException {
- sendStringData(s, cv, Website.URL, Tags.CONTACTS_WEBPAGE);
- }
-
- private void sendNote(Serializer s, ContentValues cv) throws IOException {
- // Even when there is no local note, we must explicitly upsync an empty note,
- // which is the only way to force the server to delete any pre-existing note.
- String note = "";
- if (cv.containsKey(Note.NOTE)) {
- // EAS won't accept note data with raw newline characters
- note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n");
- }
- // Format of upsync data depends on protocol version
- if (mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
- s.start(Tags.BASE_BODY);
- s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note);
- s.end();
- } else {
- s.data(Tags.CONTACTS_BODY, note);
- }
- }
-
- private void sendChildren(Serializer s, ContentValues cv) throws IOException {
- boolean first = true;
- for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) {
- String row = EasChildren.ROWS[i];
- if (cv.containsKey(row)) {
- if (first) {
- s.start(Tags.CONTACTS_CHILDREN);
- first = false;
- }
- s.data(Tags.CONTACTS_CHILD, cv.getAsString(row));
- }
- }
- if (!first) {
- s.end();
- }
- }
-
- private void sendPhone(Serializer s, ContentValues cv, int workCount, int homeCount)
- throws IOException {
- String value = cv.getAsString(Phone.NUMBER);
- if (value == null) return;
- switch (cv.getAsInteger(Phone.TYPE)) {
- case Phone.TYPE_WORK:
- if (workCount < MAX_PHONE_ROWS) {
- s.data(WORK_PHONE_TAGS[workCount], value);
- }
- break;
- case Phone.TYPE_MMS:
- s.data(Tags.CONTACTS2_MMS, value);
- break;
- case Phone.TYPE_ASSISTANT:
- s.data(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER, value);
- break;
- case Phone.TYPE_FAX_WORK:
- s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value);
- break;
- case Phone.TYPE_COMPANY_MAIN:
- s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value);
- break;
- case Phone.TYPE_HOME:
- if (homeCount < MAX_PHONE_ROWS) {
- s.data(HOME_PHONE_TAGS[homeCount], value);
- }
- break;
- case Phone.TYPE_MOBILE:
- s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value);
- break;
- case Phone.TYPE_CAR:
- s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value);
- break;
- case Phone.TYPE_PAGER:
- s.data(Tags.CONTACTS_PAGER_NUMBER, value);
- break;
- case Phone.TYPE_RADIO:
- s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value);
- break;
- case Phone.TYPE_FAX_HOME:
- s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value);
- break;
- default:
- break;
- }
- }
-
- private void sendRelation(Serializer s, ContentValues cv) throws IOException {
- String value = cv.getAsString(Relation.DATA);
- if (value == null) return;
- switch (cv.getAsInteger(Relation.TYPE)) {
- case Relation.TYPE_ASSISTANT:
- s.data(Tags.CONTACTS_ASSISTANT_NAME, value);
- break;
- case Relation.TYPE_MANAGER:
- s.data(Tags.CONTACTS2_MANAGER_NAME, value);
- break;
- case Relation.TYPE_SPOUSE:
- s.data(Tags.CONTACTS_SPOUSE, value);
- break;
- default:
- break;
- }
- }
-
- private void dirtyContactsWithinDirtyGroups() {
- ContentResolver cr = mService.mContentResolver;
- Cursor c = cr.query(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI),
- GROUPS_ID_PROJECTION, Groups.DIRTY + "=1", null, null);
- try {
- if (c.getCount() > 0) {
- String[] updateArgs = new String[1];
- ContentValues updateValues = new ContentValues();
- while (c.moveToNext()) {
- // For each, "touch" all data rows with this group id; this will mark contacts
- // in this group as dirty (per ContactsContract). We will then know to upload
- // them to the server with the modified group information
- long id = c.getLong(0);
- updateValues.put(GroupMembership.GROUP_ROW_ID, id);
- updateArgs[0] = Long.toString(id);
- cr.update(Data.CONTENT_URI, updateValues,
- MIMETYPE_GROUP_MEMBERSHIP_AND_ID_EQUALS, updateArgs);
- }
- // Really delete groups that are marked deleted
- cr.delete(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI), Groups.DELETED + "=1",
- null);
- // Clear the dirty flag for all of our groups
- updateValues.clear();
- updateValues.put(Groups.DIRTY, 0);
- cr.update(uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI), updateValues, null,
- null);
- }
- } finally {
- c.close();
- }
- }
-
- @Override
- public boolean sendLocalChanges(Serializer s) throws IOException {
- ContentResolver cr = mService.mContentResolver;
-
- // Find any groups of ours that are dirty and dirty those groups' members
- dirtyContactsWithinDirtyGroups();
-
- // First, let's find Contacts that have changed.
- Uri uri = uriWithAccountAndIsSyncAdapter(RawContactsEntity.CONTENT_URI);
- if (getSyncKey().equals("0")) {
- return false;
- }
-
- // Get them all atomically
- EntityIterator ei = RawContacts.newEntityIterator(
- cr.query(uri, null, RawContacts.DIRTY + "=1", null, null));
- ContentValues cidValues = new ContentValues();
- try {
- boolean first = true;
- final Uri rawContactUri = addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI);
- while (ei.hasNext()) {
- Entity entity = ei.next();
- // For each of these entities, create the change commands
- ContentValues entityValues = entity.getEntityValues();
- String serverId = entityValues.getAsString(RawContacts.SOURCE_ID);
- ArrayList<Integer> groupIds = new ArrayList<Integer>();
- if (first) {
- s.start(Tags.SYNC_COMMANDS);
- userLog("Sending Contacts changes to the server");
- first = false;
- }
- if (serverId == null) {
- // This is a new contact; create a clientId
- String clientId = "new_" + mMailbox.mId + '_' + System.currentTimeMillis();
- userLog("Creating new contact with clientId: ", clientId);
- s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
- // And save it in the raw contact
- cidValues.put(RawContacts.SYNC1, clientId);
- cr.update(ContentUris.
- withAppendedId(rawContactUri,
- entityValues.getAsLong(RawContacts._ID)),
- cidValues, null, null);
- } else {
- if (entityValues.getAsInteger(RawContacts.DELETED) == 1) {
- userLog("Deleting contact with serverId: ", serverId);
- s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
- mDeletedIdList.add(entityValues.getAsLong(RawContacts._ID));
- continue;
- }
- userLog("Upsync change to contact with serverId: " + serverId);
- s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
- }
- s.start(Tags.SYNC_APPLICATION_DATA);
- // Write out the data here
- int imCount = 0;
- int emailCount = 0;
- int homePhoneCount = 0;
- int workPhoneCount = 0;
- String displayName = null;
- ArrayList<ContentValues> emailValues = new ArrayList<ContentValues>();
- for (NamedContentValues ncv: entity.getSubValues()) {
- ContentValues cv = ncv.values;
- String mimeType = cv.getAsString(Data.MIMETYPE);
- if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
- emailValues.add(cv);
- } else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) {
- sendNickname(s, cv);
- } else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) {
- sendChildren(s, cv);
- } else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) {
- sendBusiness(s, cv);
- } else if (mimeType.equals(Website.CONTENT_ITEM_TYPE)) {
- sendWebpage(s, cv);
- } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) {
- sendPersonal(s, cv);
- } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
- sendPhone(s, cv, workPhoneCount, homePhoneCount);
- int type = cv.getAsInteger(Phone.TYPE);
- if (type == Phone.TYPE_HOME) homePhoneCount++;
- if (type == Phone.TYPE_WORK) workPhoneCount++;
- } else if (mimeType.equals(Relation.CONTENT_ITEM_TYPE)) {
- sendRelation(s, cv);
- } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
- displayName = sendStructuredName(s, cv);
- } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
- sendStructuredPostal(s, cv);
- } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) {
- sendOrganization(s, cv);
- } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) {
- sendIm(s, cv, imCount++);
- } else if (mimeType.equals(Event.CONTENT_ITEM_TYPE)) {
- Integer eventType = cv.getAsInteger(Event.TYPE);
- if (eventType != null && eventType.equals(Event.TYPE_BIRTHDAY)) {
- sendBirthday(s, cv);
- }
- } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) {
- // We must gather these, and send them together (below)
- groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID));
- } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) {
- sendNote(s, cv);
- } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
- sendPhoto(s, cv);
- } else {
- userLog("Contacts upsync, unknown data: ", mimeType);
- }
- }
-
- // We do the email rows last, because we need to make sure we've found the
- // displayName (if one exists); this would be in a StructuredName rnow
- for (ContentValues cv: emailValues) {
- sendEmail(s, cv, emailCount++, displayName);
- }
-
- // Now, we'll send up groups, if any
- if (!groupIds.isEmpty()) {
- boolean groupFirst = true;
- for (int id: groupIds) {
- // Since we get id's from the provider, we need to find their names
- Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI, id),
- GROUP_TITLE_PROJECTION, null, null, null);
- try {
- // Presumably, this should always succeed, but ...
- if (c.moveToFirst()) {
- if (groupFirst) {
- s.start(Tags.CONTACTS_CATEGORIES);
- groupFirst = false;
- }
- s.data(Tags.CONTACTS_CATEGORY, c.getString(0));
- }
- } finally {
- c.close();
- }
- }
- if (!groupFirst) {
- s.end();
- }
- }
- s.end().end(); // ApplicationData & Change
- mUpdatedIdList.add(entityValues.getAsLong(RawContacts._ID));
- }
- if (!first) {
- s.end(); // Commands
- }
- } finally {
- ei.close();
- }
-
- return false;
- }
-}
diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java
deleted file mode 100644
index 042ec32..0000000
--- a/src/com/android/exchange/adapter/EmailSyncAdapter.java
+++ /dev/null
@@ -1,1459 +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.adapter;
-
-import android.content.ContentProviderOperation;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.OperationApplicationException;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.RemoteException;
-import android.text.TextUtils;
-import android.util.Base64;
-import android.util.Log;
-import android.webkit.MimeTypeMap;
-
-import com.android.emailcommon.internet.MimeMessage;
-import com.android.emailcommon.internet.MimeUtility;
-import com.android.emailcommon.mail.Address;
-import com.android.emailcommon.mail.MeetingInfo;
-import com.android.emailcommon.mail.MessagingException;
-import com.android.emailcommon.mail.PackedString;
-import com.android.emailcommon.mail.Part;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent;
-import com.android.emailcommon.provider.EmailContent.AccountColumns;
-import com.android.emailcommon.provider.EmailContent.Attachment;
-import com.android.emailcommon.provider.EmailContent.Body;
-import com.android.emailcommon.provider.EmailContent.MailboxColumns;
-import com.android.emailcommon.provider.EmailContent.Message;
-import com.android.emailcommon.provider.EmailContent.MessageColumns;
-import com.android.emailcommon.provider.EmailContent.SyncColumns;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.emailcommon.provider.Policy;
-import com.android.emailcommon.provider.ProviderUnavailableException;
-import com.android.emailcommon.service.SyncWindow;
-import com.android.emailcommon.utility.AttachmentUtilities;
-import com.android.emailcommon.utility.ConversionUtilities;
-import com.android.emailcommon.utility.Utility;
-import com.android.exchange.CommandStatusException;
-import com.android.exchange.Eas;
-import com.android.exchange.EasResponse;
-import com.android.exchange.EasSyncService;
-import com.android.exchange.MessageMoveRequest;
-import com.android.exchange.R;
-import com.android.exchange.utility.CalendarUtilities;
-
-import com.google.common.annotations.VisibleForTesting;
-
-import org.apache.http.HttpStatus;
-import org.apache.http.entity.ByteArrayEntity;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-import java.util.TimeZone;
-
-/**
- * Sync adapter for EAS email
- *
- */
-public class EmailSyncAdapter extends AbstractSyncAdapter {
-
- private static final String TAG = "EmailSyncAdapter";
-
- private static final int UPDATES_READ_COLUMN = 0;
- private static final int UPDATES_MAILBOX_KEY_COLUMN = 1;
- private static final int UPDATES_SERVER_ID_COLUMN = 2;
- private static final int UPDATES_FLAG_COLUMN = 3;
- private static final String[] UPDATES_PROJECTION =
- {MessageColumns.FLAG_READ, MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID,
- MessageColumns.FLAG_FAVORITE};
-
- private static final int MESSAGE_ID_SUBJECT_ID_COLUMN = 0;
- private static final int MESSAGE_ID_SUBJECT_SUBJECT_COLUMN = 1;
- private static final String[] MESSAGE_ID_SUBJECT_PROJECTION =
- new String[] { Message.RECORD_ID, MessageColumns.SUBJECT };
-
- private static final String WHERE_BODY_SOURCE_MESSAGE_KEY = Body.SOURCE_MESSAGE_KEY + "=?";
- private static final String WHERE_MAILBOX_KEY_AND_MOVED =
- MessageColumns.MAILBOX_KEY + "=? AND (" + MessageColumns.FLAGS + "&" +
- EasSyncService.MESSAGE_FLAG_MOVED_MESSAGE + ")!=0";
- private static final String[] FETCH_REQUEST_PROJECTION =
- new String[] {EmailContent.RECORD_ID, SyncColumns.SERVER_ID};
- private static final int FETCH_REQUEST_RECORD_ID = 0;
- private static final int FETCH_REQUEST_SERVER_ID = 1;
-
- private static final String EMAIL_WINDOW_SIZE = "5";
-
- @VisibleForTesting
- static final int LAST_VERB_REPLY = 1;
- @VisibleForTesting
- static final int LAST_VERB_REPLY_ALL = 2;
- @VisibleForTesting
- static final int LAST_VERB_FORWARD = 3;
-
- private final String[] mBindArguments = new String[2];
- private final String[] mBindArgument = new String[1];
-
- @VisibleForTesting
- ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
- @VisibleForTesting
- ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
- private final ArrayList<FetchRequest> mFetchRequestList = new ArrayList<FetchRequest>();
- private boolean mFetchNeeded = false;
-
- // Holds the parser's value for isLooping()
- private boolean mIsLooping = false;
-
- // The policy (if any) for this adapter's Account
- private final Policy mPolicy;
-
- public EmailSyncAdapter(EasSyncService service) {
- super(service);
- // If we've got an account with a policy, cache it now
- if (mAccount.mPolicyKey != 0) {
- mPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
- } else {
- mPolicy = null;
- }
- }
-
- @Override
- public void wipe() {
- mContentResolver.delete(Message.CONTENT_URI,
- Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
- mContentResolver.delete(Message.DELETED_CONTENT_URI,
- Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
- mContentResolver.delete(Message.UPDATED_CONTENT_URI,
- Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
- mService.clearRequests();
- mFetchRequestList.clear();
- // Delete attachments...
- AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext, mAccount.mId, mMailbox.mId);
- }
-
- private String getEmailFilter() {
- int syncLookback = mMailbox.mSyncLookback;
- if (syncLookback == SyncWindow.SYNC_WINDOW_UNKNOWN
- || mMailbox.mType == Mailbox.TYPE_INBOX) {
- syncLookback = mAccount.mSyncLookback;
- }
- switch (syncLookback) {
- case SyncWindow.SYNC_WINDOW_AUTO:
- return Eas.FILTER_AUTO;
- case SyncWindow.SYNC_WINDOW_1_DAY:
- return Eas.FILTER_1_DAY;
- case SyncWindow.SYNC_WINDOW_3_DAYS:
- return Eas.FILTER_3_DAYS;
- case SyncWindow.SYNC_WINDOW_1_WEEK:
- return Eas.FILTER_1_WEEK;
- case SyncWindow.SYNC_WINDOW_2_WEEKS:
- return Eas.FILTER_2_WEEKS;
- case SyncWindow.SYNC_WINDOW_1_MONTH:
- return Eas.FILTER_1_MONTH;
- case SyncWindow.SYNC_WINDOW_ALL:
- return Eas.FILTER_ALL;
- default:
- return Eas.FILTER_1_WEEK;
- }
- }
-
- /**
- * Holder for fetch request information (record id and server id)
- */
- private static class FetchRequest {
- @SuppressWarnings("unused")
- final long messageId;
- final String serverId;
-
- FetchRequest(long _messageId, String _serverId) {
- messageId = _messageId;
- serverId = _serverId;
- }
- }
-
- @Override
- public void sendSyncOptions(Double protocolVersion, Serializer s)
- throws IOException {
- mFetchRequestList.clear();
- // Find partially loaded messages; this should typically be a rare occurrence
- Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI,
- FETCH_REQUEST_PROJECTION,
- MessageColumns.FLAG_LOADED + "=" + Message.FLAG_LOADED_PARTIAL + " AND " +
- MessageColumns.MAILBOX_KEY + "=?",
- new String[] {Long.toString(mMailbox.mId)}, null);
- try {
- // Put all of these messages into a list; we'll need both id and server id
- while (c.moveToNext()) {
- mFetchRequestList.add(new FetchRequest(c.getLong(FETCH_REQUEST_RECORD_ID),
- c.getString(FETCH_REQUEST_SERVER_ID)));
- }
- } finally {
- c.close();
- }
-
- // The "empty" case is typical; we send a request for changes, and also specify a sync
- // window, body preference type (HTML for EAS 12.0 and later; MIME for EAS 2.5), and
- // truncation
- // If there are fetch requests, we only want the fetches (i.e. no changes from the server)
- // so we turn MIME support off. Note that we are always using EAS 2.5 if there are fetch
- // requests
- if (mFetchRequestList.isEmpty()) {
- // Permanently delete if in trash mailbox
- // In Exchange 2003, deletes-as-moves tag = true; no tag = false
- // In Exchange 2007 and up, deletes-as-moves tag is "0" (false) or "1" (true)
- boolean isTrashMailbox = mMailbox.mType == Mailbox.TYPE_TRASH;
- if (protocolVersion < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
- if (!isTrashMailbox) {
- s.tag(Tags.SYNC_DELETES_AS_MOVES);
- }
- } else {
- s.data(Tags.SYNC_DELETES_AS_MOVES, isTrashMailbox ? "0" : "1");
- }
- s.tag(Tags.SYNC_GET_CHANGES);
- s.data(Tags.SYNC_WINDOW_SIZE, EMAIL_WINDOW_SIZE);
- s.start(Tags.SYNC_OPTIONS);
- // Set the lookback appropriately (EAS calls this a "filter")
- String filter = getEmailFilter();
- // We shouldn't get FILTER_AUTO here, but if we do, make it something legal...
- if (filter.equals(Eas.FILTER_AUTO)) {
- filter = Eas.FILTER_3_DAYS;
- }
- s.data(Tags.SYNC_FILTER_TYPE, filter);
- // Set the truncation amount for all classes
- if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
- s.start(Tags.BASE_BODY_PREFERENCE);
- // HTML for email
- s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_HTML);
- s.data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE);
- s.end();
- } else {
- // Use MIME data for EAS 2.5
- s.data(Tags.SYNC_MIME_SUPPORT, Eas.MIME_BODY_PREFERENCE_MIME);
- s.data(Tags.SYNC_MIME_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
- }
- s.end();
- } else {
- s.start(Tags.SYNC_OPTIONS);
- // Ask for plain text, rather than MIME data. This guarantees that we'll get a usable
- // text body
- s.data(Tags.SYNC_MIME_SUPPORT, Eas.MIME_BODY_PREFERENCE_TEXT);
- s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
- s.end();
- }
- }
-
- @Override
- public boolean parse(InputStream is) throws IOException, CommandStatusException {
- EasEmailSyncParser p = new EasEmailSyncParser(is, this);
- mFetchNeeded = false;
- boolean res = p.parse();
- // Hold on to the parser's value for isLooping() to pass back to the service
- mIsLooping = p.isLooping();
- // If we've need a body fetch, or we've just finished one, return true in order to continue
- if (mFetchNeeded || !mFetchRequestList.isEmpty()) {
- return true;
- }
-
- // Don't check for "auto" on the initial sync
- if (!("0".equals(mMailbox.mSyncKey))) {
- // We've completed the first successful sync
- if (getEmailFilter().equals(Eas.FILTER_AUTO)) {
- getAutomaticLookback();
- }
- }
-
- return res;
- }
-
- private void getAutomaticLookback() throws IOException {
- // If we're using an auto lookback, check how many items in the past week
- // TODO Make the literal ints below constants once we twiddle them a bit
- int items = getEstimate(Eas.FILTER_1_WEEK);
- int lookback;
- if (items > 1050) {
- // Over 150/day, just use one day (smallest)
- lookback = SyncWindow.SYNC_WINDOW_1_DAY;
- } else if (items > 350 || (items == -1)) {
- // 50-150/day, use 3 days (150 to 450 messages synced)
- lookback = SyncWindow.SYNC_WINDOW_3_DAYS;
- } else if (items > 150) {
- // 20-50/day, use 1 week (140 to 350 messages synced)
- lookback = SyncWindow.SYNC_WINDOW_1_WEEK;
- } else if (items > 75) {
- // 10-25/day, use 1 week (140 to 350 messages synced)
- lookback = SyncWindow.SYNC_WINDOW_2_WEEKS;
- } else if (items < 5) {
- // If there are only a couple, see if it makes sense to get everything
- items = getEstimate(Eas.FILTER_ALL);
- if (items >= 0 && items < 100) {
- lookback = SyncWindow.SYNC_WINDOW_ALL;
- } else {
- lookback = SyncWindow.SYNC_WINDOW_1_MONTH;
- }
- } else {
- lookback = SyncWindow.SYNC_WINDOW_1_MONTH;
- }
-
- // Limit lookback to policy limit
- if (mAccount.mPolicyKey > 0) {
- Policy policy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
- if (policy != null) {
- int maxLookback = policy.mMaxEmailLookback;
- if (maxLookback != 0 && (lookback > policy.mMaxEmailLookback)) {
- lookback = policy.mMaxEmailLookback;
- }
- }
- }
-
- // Store the new lookback and persist it
- // TODO Code similar to this is used elsewhere (e.g. MailboxSettings); try to clean this up
- ContentValues cv = new ContentValues();
- Uri uri;
- if (mMailbox.mType == Mailbox.TYPE_INBOX) {
- mAccount.mSyncLookback = lookback;
- cv.put(AccountColumns.SYNC_LOOKBACK, lookback);
- uri = ContentUris.withAppendedId(Account.CONTENT_URI, mAccount.mId);
- } else {
- mMailbox.mSyncLookback = lookback;
- cv.put(MailboxColumns.SYNC_LOOKBACK, lookback);
- uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId);
- }
- mContentResolver.update(uri, cv, null, null);
-
- CharSequence[] windowEntries = mContext.getResources().getTextArray(
- R.array.account_settings_mail_window_entries);
- Log.d(TAG, "Auto lookback: " + windowEntries[lookback]);
- }
-
- private static class GetItemEstimateParser extends Parser {
- @SuppressWarnings("hiding")
- private static final String TAG = "GetItemEstimateParser";
- private int mEstimate = -1;
-
- public GetItemEstimateParser(InputStream in) throws IOException {
- super(in);
- }
-
- @Override
- public boolean parse() throws IOException {
- // Loop here through the remaining xml
- while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
- if (tag == Tags.GIE_GET_ITEM_ESTIMATE) {
- parseGetItemEstimate();
- } else {
- skipTag();
- }
- }
- return true;
- }
-
- public void parseGetItemEstimate() throws IOException {
- while (nextTag(Tags.GIE_GET_ITEM_ESTIMATE) != END) {
- if (tag == Tags.GIE_RESPONSE) {
- parseResponse();
- } else {
- skipTag();
- }
- }
- }
-
- public void parseResponse() throws IOException {
- while (nextTag(Tags.GIE_RESPONSE) != END) {
- if (tag == Tags.GIE_STATUS) {
- Log.d(TAG, "GIE status: " + getValue());
- } else if (tag == Tags.GIE_COLLECTION) {
- parseCollection();
- } else {
- skipTag();
- }
- }
- }
-
- public void parseCollection() throws IOException {
- while (nextTag(Tags.GIE_COLLECTION) != END) {
- if (tag == Tags.GIE_CLASS) {
- Log.d(TAG, "GIE class: " + getValue());
- } else if (tag == Tags.GIE_COLLECTION_ID) {
- Log.d(TAG, "GIE collectionId: " + getValue());
- } else if (tag == Tags.GIE_ESTIMATE) {
- mEstimate = getValueInt();
- Log.d(TAG, "GIE estimate: " + mEstimate);
- } else {
- skipTag();
- }
- }
- }
- }
-
- /**
- * Return the estimated number of items to be synced in the current mailbox, based on the
- * passed in filter argument
- * @param filter an EAS "window" filter
- * @return the estimated number of items to be synced, or -1 if unknown
- * @throws IOException
- */
- private int getEstimate(String filter) throws IOException {
- Serializer s = new Serializer();
- boolean ex10 = mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE;
- boolean ex03 = mService.mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE;
- boolean ex07 = !ex10 && !ex03;
-
- String className = getCollectionName();
- String syncKey = getSyncKey();
- userLog("gie, sending ", className, " syncKey: ", syncKey);
-
- s.start(Tags.GIE_GET_ITEM_ESTIMATE).start(Tags.GIE_COLLECTIONS);
- s.start(Tags.GIE_COLLECTION);
- if (ex07) {
- // Exchange 2007 likes collection id first
- s.data(Tags.GIE_COLLECTION_ID, mMailbox.mServerId);
- s.data(Tags.SYNC_FILTER_TYPE, filter);
- s.data(Tags.SYNC_SYNC_KEY, syncKey);
- } else if (ex03) {
- // Exchange 2003 needs the "class" element
- s.data(Tags.GIE_CLASS, className);
- s.data(Tags.SYNC_SYNC_KEY, syncKey);
- s.data(Tags.GIE_COLLECTION_ID, mMailbox.mServerId);
- s.data(Tags.SYNC_FILTER_TYPE, filter);
- } else {
- // Exchange 2010 requires the filter inside an OPTIONS container and sync key first
- s.data(Tags.SYNC_SYNC_KEY, syncKey);
- s.data(Tags.GIE_COLLECTION_ID, mMailbox.mServerId);
- s.start(Tags.SYNC_OPTIONS).data(Tags.SYNC_FILTER_TYPE, filter).end();
- }
- s.end().end().end().done(); // GIE_COLLECTION, GIE_COLLECTIONS, GIE_GET_ITEM_ESTIMATE
-
- EasResponse resp = mService.sendHttpClientPost("GetItemEstimate",
- new ByteArrayEntity(s.toByteArray()), EasSyncService.COMMAND_TIMEOUT);
- try {
- int code = resp.getStatus();
- if (code == HttpStatus.SC_OK) {
- if (!resp.isEmpty()) {
- InputStream is = resp.getInputStream();
- GetItemEstimateParser gieParser = new GetItemEstimateParser(is);
- gieParser.parse();
- // Return the estimated number of items
- return gieParser.mEstimate;
- }
- }
- } finally {
- resp.close();
- }
- // If we can't get an estimate, indicate this...
- return -1;
- }
-
- /**
- * Return the value of isLooping() as returned from the parser
- */
- @Override
- public boolean isLooping() {
- return mIsLooping;
- }
-
- @Override
- public boolean isSyncable() {
- return true;
- }
-
- public class EasEmailSyncParser extends AbstractSyncParser {
-
- private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY =
- SyncColumns.SERVER_ID + "=? and " + MessageColumns.MAILBOX_KEY + "=?";
-
- private final String mMailboxIdAsString;
-
- private final ArrayList<Message> newEmails = new ArrayList<Message>();
- private final ArrayList<Message> fetchedEmails = new ArrayList<Message>();
- private final ArrayList<Long> deletedEmails = new ArrayList<Long>();
- private final ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>();
-
- public EasEmailSyncParser(InputStream in, EmailSyncAdapter adapter) throws IOException {
- super(in, adapter);
- mMailboxIdAsString = Long.toString(mMailbox.mId);
- }
-
- public EasEmailSyncParser(Parser parser, EmailSyncAdapter adapter) throws IOException {
- super(parser, adapter);
- mMailboxIdAsString = Long.toString(mMailbox.mId);
- }
-
- public void addData (Message msg, int endingTag) throws IOException {
- ArrayList<Attachment> atts = new ArrayList<Attachment>();
- boolean truncated = false;
-
- while (nextTag(endingTag) != END) {
- switch (tag) {
- case Tags.EMAIL_ATTACHMENTS:
- case Tags.BASE_ATTACHMENTS: // BASE_ATTACHMENTS is used in EAS 12.0 and up
- attachmentsParser(atts, msg);
- break;
- case Tags.EMAIL_TO:
- msg.mTo = Address.pack(Address.parse(getValue()));
- break;
- case Tags.EMAIL_FROM:
- Address[] froms = Address.parse(getValue());
- if (froms != null && froms.length > 0) {
- msg.mDisplayName = froms[0].toFriendly();
- }
- msg.mFrom = Address.pack(froms);
- break;
- case Tags.EMAIL_CC:
- msg.mCc = Address.pack(Address.parse(getValue()));
- break;
- case Tags.EMAIL_REPLY_TO:
- msg.mReplyTo = Address.pack(Address.parse(getValue()));
- break;
- case Tags.EMAIL_DATE_RECEIVED:
- msg.mTimeStamp = Utility.parseEmailDateTimeToMillis(getValue());
- break;
- case Tags.EMAIL_SUBJECT:
- msg.mSubject = getValue();
- break;
- case Tags.EMAIL_READ:
- msg.mFlagRead = getValueInt() == 1;
- break;
- case Tags.BASE_BODY:
- bodyParser(msg);
- break;
- case Tags.EMAIL_FLAG:
- msg.mFlagFavorite = flagParser();
- break;
- case Tags.EMAIL_MIME_TRUNCATED:
- truncated = getValueInt() == 1;
- break;
- case Tags.EMAIL_MIME_DATA:
- // We get MIME data for EAS 2.5. First we parse it, then we take the
- // html and/or plain text data and store it in the message
- if (truncated) {
- // If the MIME data is truncated, don't bother parsing it, because
- // it will take time and throw an exception anyway when EOF is reached
- // In this case, we will load the body separately by tagging the message
- // "partially loaded".
- // Get the data (and ignore it)
- getValue();
- userLog("Partially loaded: ", msg.mServerId);
- msg.mFlagLoaded = Message.FLAG_LOADED_PARTIAL;
- mFetchNeeded = true;
- } else {
- mimeBodyParser(msg, getValue());
- }
- break;
- case Tags.EMAIL_BODY:
- 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_INCOMING_MEETING_INVITE;
- } else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) {
- msg.mFlags |= Message.FLAG_INCOMING_MEETING_CANCEL;
- }
- break;
- case Tags.EMAIL_MEETING_REQUEST:
- meetingRequestParser(msg);
- break;
- case Tags.RIGHTS_LICENSE:
- skipParser(tag);
- break;
- case Tags.EMAIL2_CONVERSATION_ID:
- msg.mServerConversationId =
- Base64.encodeToString(getValueBytes(), Base64.URL_SAFE);
- break;
- case Tags.EMAIL2_CONVERSATION_INDEX:
- // Ignore this byte array since we're not constructing a tree.
- getValueBytes();
- break;
- case Tags.EMAIL2_LAST_VERB_EXECUTED:
- int val = getValueInt();
- if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) {
- // We aren't required to distinguish between reply and reply all here
- msg.mFlags |= Message.FLAG_REPLIED_TO;
- } else if (val == LAST_VERB_FORWARD) {
- msg.mFlags |= Message.FLAG_FORWARDED;
- }
- break;
- default:
- skipTag();
- }
- }
-
- if (atts.size() > 0) {
- msg.mAttachments = atts;
- }
- }
-
- /**
- * Set up the meetingInfo field in the message with various pieces of information gleaned
- * from MeetingRequest tags. This information will be used later to generate an appropriate
- * reply email if the user chooses to respond
- * @param msg the Message being built
- * @throws IOException
- */
- private void meetingRequestParser(Message msg) throws IOException {
- PackedString.Builder packedString = new PackedString.Builder();
- while (nextTag(Tags.EMAIL_MEETING_REQUEST) != END) {
- switch (tag) {
- case Tags.EMAIL_DTSTAMP:
- packedString.put(MeetingInfo.MEETING_DTSTAMP, getValue());
- break;
- case Tags.EMAIL_START_TIME:
- packedString.put(MeetingInfo.MEETING_DTSTART, getValue());
- break;
- case Tags.EMAIL_END_TIME:
- packedString.put(MeetingInfo.MEETING_DTEND, getValue());
- break;
- case Tags.EMAIL_ORGANIZER:
- packedString.put(MeetingInfo.MEETING_ORGANIZER_EMAIL, getValue());
- break;
- case Tags.EMAIL_LOCATION:
- packedString.put(MeetingInfo.MEETING_LOCATION, getValue());
- break;
- case Tags.EMAIL_GLOBAL_OBJID:
- packedString.put(MeetingInfo.MEETING_UID,
- CalendarUtilities.getUidFromGlobalObjId(getValue()));
- break;
- case Tags.EMAIL_CATEGORIES:
- skipParser(tag);
- break;
- case Tags.EMAIL_RECURRENCES:
- recurrencesParser();
- break;
- case Tags.EMAIL_RESPONSE_REQUESTED:
- packedString.put(MeetingInfo.MEETING_RESPONSE_REQUESTED, getValue());
- break;
- default:
- skipTag();
- }
- }
- if (msg.mSubject != null) {
- packedString.put(MeetingInfo.MEETING_TITLE, msg.mSubject);
- }
- msg.mMeetingInfo = packedString.toString();
- }
-
- private void recurrencesParser() throws IOException {
- while (nextTag(Tags.EMAIL_RECURRENCES) != END) {
- switch (tag) {
- case Tags.EMAIL_RECURRENCE:
- skipParser(tag);
- break;
- default:
- skipTag();
- }
- }
- }
-
- /**
- * Parse a message from the server stream.
- * @return the parsed Message
- * @throws IOException
- */
- private Message addParser() throws IOException, CommandStatusException {
- Message msg = new Message();
- msg.mAccountKey = mAccount.mId;
- msg.mMailboxKey = mMailbox.mId;
- msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
- // Default to 1 (success) in case we don't get this tag
- int status = 1;
-
- while (nextTag(Tags.SYNC_ADD) != END) {
- switch (tag) {
- case Tags.SYNC_SERVER_ID:
- msg.mServerId = getValue();
- break;
- case Tags.SYNC_STATUS:
- status = getValueInt();
- break;
- case Tags.SYNC_APPLICATION_DATA:
- addData(msg, tag);
- break;
- default:
- skipTag();
- }
- }
- // For sync, status 1 = success
- if (status != 1) {
- throw new CommandStatusException(status, msg.mServerId);
- }
- return msg;
- }
-
- // For now, we only care about the "active" state
- private Boolean flagParser() throws IOException {
- Boolean state = false;
- while (nextTag(Tags.EMAIL_FLAG) != END) {
- switch (tag) {
- case Tags.EMAIL_FLAG_STATUS:
- state = getValueInt() == 2;
- break;
- default:
- skipTag();
- }
- }
- return state;
- }
-
- private void bodyParser(Message msg) throws IOException {
- String bodyType = Eas.BODY_PREFERENCE_TEXT;
- String body = "";
- while (nextTag(Tags.EMAIL_BODY) != END) {
- switch (tag) {
- case Tags.BASE_TYPE:
- bodyType = getValue();
- break;
- case Tags.BASE_DATA:
- body = getValue();
- break;
- default:
- skipTag();
- }
- }
- // We always ask for TEXT or HTML; there's no third option
- if (bodyType.equals(Eas.BODY_PREFERENCE_HTML)) {
- msg.mHtml = body;
- } else {
- msg.mText = body;
- }
- }
-
- /**
- * Parses untruncated MIME data, saving away the text parts
- * @param msg the message we're building
- * @param mimeData the MIME data we've received from the server
- * @throws IOException
- */
- private void mimeBodyParser(Message msg, String mimeData) throws IOException {
- try {
- ByteArrayInputStream in = new ByteArrayInputStream(mimeData.getBytes());
- // The constructor parses the message
- MimeMessage mimeMessage = new MimeMessage(in);
- // Now process body parts & attachments
- ArrayList<Part> viewables = new ArrayList<Part>();
- // We'll ignore the attachments, as we'll get them directly from EAS
- ArrayList<Part> attachments = new ArrayList<Part>();
- MimeUtility.collectParts(mimeMessage, viewables, attachments);
- Body tempBody = new Body();
- // updateBodyFields fills in the content fields of the Body
- ConversionUtilities.updateBodyFields(tempBody, msg, viewables);
- // But we need them in the message itself for handling during commit()
- msg.mHtml = tempBody.mHtmlContent;
- msg.mText = tempBody.mTextContent;
- } catch (MessagingException e) {
- // This would most likely indicate a broken stream
- throw new IOException(e);
- }
- }
-
- private void attachmentsParser(ArrayList<Attachment> atts, Message msg) throws IOException {
- while (nextTag(Tags.EMAIL_ATTACHMENTS) != END) {
- switch (tag) {
- case Tags.EMAIL_ATTACHMENT:
- case Tags.BASE_ATTACHMENT: // BASE_ATTACHMENT is used in EAS 12.0 and up
- attachmentParser(atts, msg);
- break;
- default:
- skipTag();
- }
- }
- }
-
- private void attachmentParser(ArrayList<Attachment> atts, Message msg) throws IOException {
- String fileName = null;
- String length = null;
- String location = null;
- boolean isInline = false;
- String contentId = null;
-
- while (nextTag(Tags.EMAIL_ATTACHMENT) != END) {
- switch (tag) {
- // We handle both EAS 2.5 and 12.0+ attachments here
- case Tags.EMAIL_DISPLAY_NAME:
- case Tags.BASE_DISPLAY_NAME:
- fileName = getValue();
- break;
- case Tags.EMAIL_ATT_NAME:
- case Tags.BASE_FILE_REFERENCE:
- location = getValue();
- break;
- case Tags.EMAIL_ATT_SIZE:
- case Tags.BASE_ESTIMATED_DATA_SIZE:
- length = getValue();
- break;
- case Tags.BASE_IS_INLINE:
- isInline = getValueInt() == 1;
- break;
- case Tags.BASE_CONTENT_ID:
- contentId = getValue();
- break;
- default:
- skipTag();
- }
- }
-
- if ((fileName != null) && (length != null) && (location != null)) {
- Attachment att = new Attachment();
- att.mEncoding = "base64";
- att.mSize = Long.parseLong(length);
- att.mFileName = fileName;
- att.mLocation = location;
- att.mMimeType = getMimeTypeFromFileName(fileName);
- att.mAccountKey = mService.mAccount.mId;
- // Save away the contentId, if we've got one (for inline images); note that the
- // EAS docs appear to be wrong about the tags used; inline images come with
- // contentId rather than contentLocation, when sent from Ex03, Ex07, and Ex10
- if (isInline && !TextUtils.isEmpty(contentId)) {
- att.mContentId = contentId;
- }
- // Check if this attachment can't be downloaded due to an account policy
- if (mPolicy != null) {
- if (mPolicy.mDontAllowAttachments ||
- (mPolicy.mMaxAttachmentSize > 0 &&
- (att.mSize > mPolicy.mMaxAttachmentSize))) {
- att.mFlags = Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD;
- }
- }
- atts.add(att);
- msg.mFlagAttachment = true;
- }
- }
-
- /**
- * Returns an appropriate mimetype for the given file name's extension. If a mimetype
- * cannot be determined, {@code application/<<x>>} [where @{code <<x>> is the extension,
- * if it exists or {@code application/octet-stream}].
- * At the moment, this is somewhat lame, since many file types aren't recognized
- * @param fileName the file name to ponder
- */
- // Note: The MimeTypeMap method currently uses a very limited set of mime types
- // A bug has been filed against this issue.
- public String getMimeTypeFromFileName(String fileName) {
- String mimeType;
- int lastDot = fileName.lastIndexOf('.');
- String extension = null;
- if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
- extension = fileName.substring(lastDot + 1).toLowerCase();
- }
- if (extension == null) {
- // A reasonable default for now.
- mimeType = "application/octet-stream";
- } else {
- mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
- if (mimeType == null) {
- mimeType = "application/" + extension;
- }
- }
- return mimeType;
- }
-
- private Cursor getServerIdCursor(String serverId, String[] projection) {
- mBindArguments[0] = serverId;
- mBindArguments[1] = mMailboxIdAsString;
- Cursor c = mContentResolver.query(Message.CONTENT_URI, projection,
- WHERE_SERVER_ID_AND_MAILBOX_KEY, mBindArguments, null);
- if (c == null) throw new ProviderUnavailableException();
- if (c.getCount() > 1) {
- userLog("Multiple messages with the same serverId/mailbox: " + serverId);
- }
- return c;
- }
-
- @VisibleForTesting
- void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException {
- while (nextTag(entryTag) != END) {
- switch (tag) {
- case Tags.SYNC_SERVER_ID:
- String serverId = getValue();
- // Find the message in this mailbox with the given serverId
- Cursor c = getServerIdCursor(serverId, MESSAGE_ID_SUBJECT_PROJECTION);
- try {
- if (c.moveToFirst()) {
- deletes.add(c.getLong(MESSAGE_ID_SUBJECT_ID_COLUMN));
- if (Eas.USER_LOG) {
- userLog("Deleting ", serverId + ", "
- + c.getString(MESSAGE_ID_SUBJECT_SUBJECT_COLUMN));
- }
- }
- } finally {
- c.close();
- }
- break;
- default:
- skipTag();
- }
- }
- }
-
- @VisibleForTesting
- class ServerChange {
- final long id;
- final Boolean read;
- final Boolean flag;
- final Integer flags;
-
- ServerChange(long _id, Boolean _read, Boolean _flag, Integer _flags) {
- id = _id;
- read = _read;
- flag = _flag;
- flags = _flags;
- }
- }
-
- @VisibleForTesting
- void changeParser(ArrayList<ServerChange> changes) throws IOException {
- String serverId = null;
- Boolean oldRead = false;
- Boolean oldFlag = false;
- int flags = 0;
- long id = 0;
- while (nextTag(Tags.SYNC_CHANGE) != END) {
- switch (tag) {
- case Tags.SYNC_SERVER_ID:
- serverId = getValue();
- Cursor c = getServerIdCursor(serverId, Message.LIST_PROJECTION);
- try {
- if (c.moveToFirst()) {
- userLog("Changing ", serverId);
- oldRead = c.getInt(Message.LIST_READ_COLUMN) == Message.READ;
- oldFlag = c.getInt(Message.LIST_FAVORITE_COLUMN) == 1;
- flags = c.getInt(Message.LIST_FLAGS_COLUMN);
- id = c.getLong(Message.LIST_ID_COLUMN);
- }
- } finally {
- c.close();
- }
- break;
- case Tags.SYNC_APPLICATION_DATA:
- changeApplicationDataParser(changes, oldRead, oldFlag, flags, id);
- break;
- default:
- skipTag();
- }
- }
- }
-
- private void changeApplicationDataParser(ArrayList<ServerChange> changes, Boolean oldRead,
- Boolean oldFlag, int oldFlags, long id) throws IOException {
- Boolean read = null;
- Boolean flag = null;
- Integer flags = null;
- while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
- switch (tag) {
- case Tags.EMAIL_READ:
- read = getValueInt() == 1;
- break;
- case Tags.EMAIL_FLAG:
- flag = flagParser();
- break;
- case Tags.EMAIL2_LAST_VERB_EXECUTED:
- int val = getValueInt();
- // Clear out the old replied/forward flags and add in the new flag
- flags = oldFlags & ~(Message.FLAG_REPLIED_TO | Message.FLAG_FORWARDED);
- if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) {
- // We aren't required to distinguish between reply and reply all here
- flags |= Message.FLAG_REPLIED_TO;
- } else if (val == LAST_VERB_FORWARD) {
- flags |= Message.FLAG_FORWARDED;
- }
- break;
- default:
- skipTag();
- }
- }
- // See if there are flag changes re: read, flag (favorite) or replied/forwarded
- if (((read != null) && !oldRead.equals(read)) ||
- ((flag != null) && !oldFlag.equals(flag)) || (flags != null)) {
- changes.add(new ServerChange(id, read, flag, flags));
- }
- }
-
- /* (non-Javadoc)
- * @see com.android.exchange.adapter.EasContentParser#commandsParser()
- */
- @Override
- public void commandsParser() throws IOException, CommandStatusException {
- while (nextTag(Tags.SYNC_COMMANDS) != END) {
- if (tag == Tags.SYNC_ADD) {
- newEmails.add(addParser());
- incrementChangeCount();
- } else if (tag == Tags.SYNC_DELETE || tag == Tags.SYNC_SOFT_DELETE) {
- deleteParser(deletedEmails, tag);
- incrementChangeCount();
- } else if (tag == Tags.SYNC_CHANGE) {
- changeParser(changedEmails);
- incrementChangeCount();
- } else
- skipTag();
- }
- }
-
- /**
- * Removed any messages with status 7 (mismatch) from the updatedIdList
- * @param endTag the tag we end with
- * @throws IOException
- */
- public void failedUpdateParser(int endTag) throws IOException {
- // We get serverId and status in the responses
- String serverId = null;
- while (nextTag(endTag) != END) {
- if (tag == Tags.SYNC_STATUS) {
- int status = getValueInt();
- if (status == 7 && serverId != null) {
- Cursor c = getServerIdCursor(serverId, Message.ID_COLUMN_PROJECTION);
- try {
- if (c.moveToFirst()) {
- Long id = c.getLong(Message.ID_PROJECTION_COLUMN);
- mService.userLog("Update of " + serverId + " failed; will retry");
- mUpdatedIdList.remove(id);
- mService.mUpsyncFailed = true;
- }
- } finally {
- c.close();
- }
- }
- } else if (tag == Tags.SYNC_SERVER_ID) {
- serverId = getValue();
- } else {
- skipTag();
- }
- }
- }
-
- @Override
- public void responsesParser() throws IOException {
- while (nextTag(Tags.SYNC_RESPONSES) != END) {
- if (tag == Tags.SYNC_ADD || tag == Tags.SYNC_CHANGE || tag == Tags.SYNC_DELETE) {
- failedUpdateParser(tag);
- } else if (tag == Tags.SYNC_FETCH) {
- try {
- fetchedEmails.add(addParser());
- } catch (CommandStatusException sse) {
- if (sse.mStatus == 8) {
- // 8 = object not found; delete the message from EmailProvider
- // No other status should be seen in a fetch response, except, perhaps,
- // for some temporary server failure
- mBindArguments[0] = sse.mItemId;
- mBindArguments[1] = mMailboxIdAsString;
- mContentResolver.delete(Message.CONTENT_URI,
- WHERE_SERVER_ID_AND_MAILBOX_KEY, mBindArguments);
- }
- }
- }
- }
- }
-
- @Override
- public void commit() {
- // Use a batch operation to handle the changes
- ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
-
- for (Message msg: fetchedEmails) {
- // Find the original message's id (by serverId and mailbox)
- Cursor c = getServerIdCursor(msg.mServerId, EmailContent.ID_PROJECTION);
- String id = null;
- try {
- if (c.moveToFirst()) {
- id = c.getString(EmailContent.ID_PROJECTION_COLUMN);
- while (c.moveToNext()) {
- // This shouldn't happen, but clean up if it does
- Long dupId =
- Long.parseLong(c.getString(EmailContent.ID_PROJECTION_COLUMN));
- userLog("Delete duplicate with id: " + dupId);
- deletedEmails.add(dupId);
- }
- }
- } finally {
- c.close();
- }
-
- // If we find one, we do two things atomically: 1) set the body text for the
- // message, and 2) mark the message loaded (i.e. completely loaded)
- if (id != null) {
- userLog("Fetched body successfully for ", id);
- mBindArgument[0] = id;
- ops.add(ContentProviderOperation.newUpdate(Body.CONTENT_URI)
- .withSelection(Body.MESSAGE_KEY + "=?", mBindArgument)
- .withValue(Body.TEXT_CONTENT, msg.mText)
- .build());
- ops.add(ContentProviderOperation.newUpdate(Message.CONTENT_URI)
- .withSelection(EmailContent.RECORD_ID + "=?", mBindArgument)
- .withValue(Message.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE)
- .build());
- }
- }
-
- for (Message msg: newEmails) {
- msg.addSaveOps(ops);
- }
-
- for (Long id : deletedEmails) {
- ops.add(ContentProviderOperation.newDelete(
- ContentUris.withAppendedId(Message.CONTENT_URI, id)).build());
- AttachmentUtilities.deleteAllAttachmentFiles(mContext, mAccount.mId, id);
- }
-
- if (!changedEmails.isEmpty()) {
- // Server wins in a conflict...
- for (ServerChange change : changedEmails) {
- ContentValues cv = new ContentValues();
- if (change.read != null) {
- cv.put(MessageColumns.FLAG_READ, change.read);
- }
- if (change.flag != null) {
- cv.put(MessageColumns.FLAG_FAVORITE, change.flag);
- }
- if (change.flags != null) {
- cv.put(MessageColumns.FLAGS, change.flags);
- }
- ops.add(ContentProviderOperation.newUpdate(
- ContentUris.withAppendedId(Message.CONTENT_URI, change.id))
- .withValues(cv)
- .build());
- }
- }
-
- // We only want to update the sync key here
- ContentValues mailboxValues = new ContentValues();
- mailboxValues.put(Mailbox.SYNC_KEY, mMailbox.mSyncKey);
- ops.add(ContentProviderOperation.newUpdate(
- ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId))
- .withValues(mailboxValues).build());
-
- // No commits if we're stopped
- synchronized (mService.getSynchronizer()) {
- if (mService.isStopped()) return;
- try {
- mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
- userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey);
- } catch (RemoteException e) {
- // There is nothing to be done here; fail by returning null
- } catch (OperationApplicationException e) {
- // There is nothing to be done here; fail by returning null
- }
- }
- }
- }
-
- @Override
- public String getCollectionName() {
- return "Email";
- }
-
- private void addCleanupOps(ArrayList<ContentProviderOperation> ops) {
- // If we've sent local deletions, clear out the deleted table
- for (Long id: mDeletedIdList) {
- ops.add(ContentProviderOperation.newDelete(
- ContentUris.withAppendedId(Message.DELETED_CONTENT_URI, id)).build());
- }
- // And same with the updates
- for (Long id: mUpdatedIdList) {
- ops.add(ContentProviderOperation.newDelete(
- ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, id)).build());
- }
- }
-
- @Override
- public void cleanup() {
- ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
- // Delete any moved messages (since we've just synced the mailbox, and no longer need the
- // placeholder message); this prevents duplicates from appearing in the mailbox.
- mBindArgument[0] = Long.toString(mMailbox.mId);
- ops.add(ContentProviderOperation.newDelete(Message.CONTENT_URI)
- .withSelection(WHERE_MAILBOX_KEY_AND_MOVED, mBindArgument).build());
- // If we've done deletions/updates, clean up the deleted/updated tables
- if (!mDeletedIdList.isEmpty() || !mUpdatedIdList.isEmpty()) {
- addCleanupOps(ops);
- }
- try {
- mContext.getContentResolver()
- .applyBatch(EmailContent.AUTHORITY, ops);
- } catch (RemoteException e) {
- // There is nothing to be done here; fail by returning null
- } catch (OperationApplicationException e) {
- // There is nothing to be done here; fail by returning null
- }
- }
-
- private String formatTwo(int num) {
- if (num < 10) {
- return "0" + (char)('0' + num);
- } else
- return Integer.toString(num);
- }
-
- /**
- * Create date/time in RFC8601 format. Oddly enough, for calendar date/time, Microsoft uses
- * a different format that excludes the punctuation (this is why I'm not putting this in a
- * parent class)
- */
- public String formatDateTime(Calendar calendar) {
- StringBuilder sb = new StringBuilder();
- //YYYY-MM-DDTHH:MM:SS.MSSZ
- sb.append(calendar.get(Calendar.YEAR));
- sb.append('-');
- sb.append(formatTwo(calendar.get(Calendar.MONTH) + 1));
- sb.append('-');
- sb.append(formatTwo(calendar.get(Calendar.DAY_OF_MONTH)));
- sb.append('T');
- sb.append(formatTwo(calendar.get(Calendar.HOUR_OF_DAY)));
- sb.append(':');
- sb.append(formatTwo(calendar.get(Calendar.MINUTE)));
- sb.append(':');
- sb.append(formatTwo(calendar.get(Calendar.SECOND)));
- sb.append(".000Z");
- return sb.toString();
- }
-
- /**
- * Note that messages in the deleted database preserve the message's unique id; therefore, we
- * can utilize this id to find references to the message. The only reference situation at this
- * point is in the Body table; it is when sending messages via SmartForward and SmartReply
- */
- private boolean messageReferenced(ContentResolver cr, long id) {
- mBindArgument[0] = Long.toString(id);
- // See if this id is referenced in a body
- Cursor c = cr.query(Body.CONTENT_URI, Body.ID_PROJECTION, WHERE_BODY_SOURCE_MESSAGE_KEY,
- mBindArgument, null);
- try {
- return c.moveToFirst();
- } finally {
- c.close();
- }
- }
-
- /*private*/ /**
- * Serialize commands to delete items from the server; as we find items to delete, add their
- * id's to the deletedId's array
- *
- * @param s the Serializer we're using to create post data
- * @param deletedIds ids whose deletions are being sent to the server
- * @param first whether or not this is the first command being sent
- * @return true if SYNC_COMMANDS hasn't been sent (false otherwise)
- * @throws IOException
- */
- @VisibleForTesting
- boolean sendDeletedItems(Serializer s, ArrayList<Long> deletedIds, boolean first)
- throws IOException {
- ContentResolver cr = mContext.getContentResolver();
-
- // Find any of our deleted items
- Cursor c = cr.query(Message.DELETED_CONTENT_URI, Message.LIST_PROJECTION,
- MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
- // We keep track of the list of deleted item id's so that we can remove them from the
- // deleted table after the server receives our command
- deletedIds.clear();
- try {
- while (c.moveToNext()) {
- String serverId = c.getString(Message.LIST_SERVER_ID_COLUMN);
- // Keep going if there's no serverId
- if (serverId == null) {
- continue;
- // Also check if this message is referenced elsewhere
- } else if (messageReferenced(cr, c.getLong(Message.CONTENT_ID_COLUMN))) {
- userLog("Postponing deletion of referenced message: ", serverId);
- continue;
- } else if (first) {
- s.start(Tags.SYNC_COMMANDS);
- first = false;
- }
- // Send the command to delete this message
- s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
- deletedIds.add(c.getLong(Message.LIST_ID_COLUMN));
- }
- } finally {
- c.close();
- }
-
- return first;
- }
-
- @Override
- public boolean sendLocalChanges(Serializer s) throws IOException {
- ContentResolver cr = mContext.getContentResolver();
-
- if (getSyncKey().equals("0")) {
- return false;
- }
-
- // Never upsync from these folders
- if (mMailbox.mType == Mailbox.TYPE_DRAFTS || mMailbox.mType == Mailbox.TYPE_OUTBOX) {
- return false;
- }
-
- // This code is split out for unit testing purposes
- boolean firstCommand = sendDeletedItems(s, mDeletedIdList, true);
-
- if (!mFetchRequestList.isEmpty()) {
- // Add FETCH commands for messages that need a body (i.e. we didn't find it during
- // our earlier sync; this happens only in EAS 2.5 where the body couldn't be found
- // after parsing the message's MIME data)
- if (firstCommand) {
- s.start(Tags.SYNC_COMMANDS);
- firstCommand = false;
- }
- for (FetchRequest req: mFetchRequestList) {
- s.start(Tags.SYNC_FETCH).data(Tags.SYNC_SERVER_ID, req.serverId).end();
- }
- }
-
- // Find our trash mailbox, since deletions will have been moved there...
- long trashMailboxId =
- Mailbox.findMailboxOfType(mContext, mMailbox.mAccountKey, Mailbox.TYPE_TRASH);
-
- // Do the same now for updated items
- Cursor c = cr.query(Message.UPDATED_CONTENT_URI, Message.LIST_PROJECTION,
- MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
-
- // We keep track of the list of updated item id's as we did above with deleted items
- mUpdatedIdList.clear();
- try {
- ContentValues cv = new ContentValues();
- while (c.moveToNext()) {
- long id = c.getLong(Message.LIST_ID_COLUMN);
- // Say we've handled this update
- mUpdatedIdList.add(id);
- // We have the id of the changed item. But first, we have to find out its current
- // state, since the updated table saves the opriginal state
- Cursor currentCursor = cr.query(ContentUris.withAppendedId(Message.CONTENT_URI, id),
- UPDATES_PROJECTION, null, null, null);
- try {
- // If this item no longer exists (shouldn't be possible), just move along
- if (!currentCursor.moveToFirst()) {
- continue;
- }
- // Keep going if there's no serverId
- String serverId = currentCursor.getString(UPDATES_SERVER_ID_COLUMN);
- if (serverId == null) {
- continue;
- }
-
- boolean flagChange = false;
- boolean readChange = false;
-
- long mailbox = currentCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN);
- // If the message is now in the trash folder, it has been deleted by the user
- if (mailbox == trashMailboxId) {
- if (firstCommand) {
- s.start(Tags.SYNC_COMMANDS);
- firstCommand = false;
- }
- // Send the command to delete this message
- s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
- // Mark the message as moved (so the copy will be deleted if/when the server
- // version is synced)
- int flags = c.getInt(Message.LIST_FLAGS_COLUMN);
- cv.put(MessageColumns.FLAGS,
- flags | EasSyncService.MESSAGE_FLAG_MOVED_MESSAGE);
- cr.update(ContentUris.withAppendedId(Message.CONTENT_URI, id), cv,
- null, null);
- continue;
- } else if (mailbox != c.getLong(Message.LIST_MAILBOX_KEY_COLUMN)) {
- // The message has moved to another mailbox; add a request for this
- // Note: The Sync command doesn't handle moving messages, so we need
- // to handle this as a "request" (similar to meeting response and
- // attachment load)
- mService.addRequest(new MessageMoveRequest(id, mailbox));
- // Regardless of other changes that might be made, we don't want to indicate
- // that this message has been updated until the move request has been
- // handled (without this, a crash between the flag upsync and the move
- // would cause the move to be lost)
- mUpdatedIdList.remove(id);
- }
-
- // We can only send flag changes to the server in 12.0 or later
- int flag = 0;
- if (mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
- flag = currentCursor.getInt(UPDATES_FLAG_COLUMN);
- if (flag != c.getInt(Message.LIST_FAVORITE_COLUMN)) {
- flagChange = true;
- }
- }
-
- int read = currentCursor.getInt(UPDATES_READ_COLUMN);
- if (read != c.getInt(Message.LIST_READ_COLUMN)) {
- readChange = true;
- }
-
- if (!flagChange && !readChange) {
- // In this case, we've got nothing to send to the server
- continue;
- }
-
- if (firstCommand) {
- s.start(Tags.SYNC_COMMANDS);
- firstCommand = false;
- }
- // Send the change to "read" and "favorite" (flagged)
- s.start(Tags.SYNC_CHANGE)
- .data(Tags.SYNC_SERVER_ID, c.getString(Message.LIST_SERVER_ID_COLUMN))
- .start(Tags.SYNC_APPLICATION_DATA);
- if (readChange) {
- s.data(Tags.EMAIL_READ, Integer.toString(read));
- }
- // "Flag" is a relatively complex concept in EAS 12.0 and above. It is not only
- // the boolean "favorite" that we think of in Gmail, but it also represents a
- // follow up action, which can include a subject, start and due dates, and even
- // recurrences. We don't support any of this as yet, but EAS 12.0 and higher
- // require that a flag contain a status, a type, and four date fields, two each
- // for start date and end (due) date.
- if (flagChange) {
- if (flag != 0) {
- // Status 2 = set flag
- s.start(Tags.EMAIL_FLAG).data(Tags.EMAIL_FLAG_STATUS, "2");
- // "FollowUp" is the standard type
- s.data(Tags.EMAIL_FLAG_TYPE, "FollowUp");
- long now = System.currentTimeMillis();
- Calendar calendar =
- GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT"));
- calendar.setTimeInMillis(now);
- // Flags are required to have a start date and end date (duplicated)
- // First, we'll set the current date/time in GMT as the start time
- String utc = formatDateTime(calendar);
- s.data(Tags.TASK_START_DATE, utc).data(Tags.TASK_UTC_START_DATE, utc);
- // And then we'll use one week from today for completion date
- calendar.setTimeInMillis(now + 1*WEEKS);
- utc = formatDateTime(calendar);
- s.data(Tags.TASK_DUE_DATE, utc).data(Tags.TASK_UTC_DUE_DATE, utc);
- s.end();
- } else {
- s.tag(Tags.EMAIL_FLAG);
- }
- }
- s.end().end(); // SYNC_APPLICATION_DATA, SYNC_CHANGE
- } finally {
- currentCursor.close();
- }
- }
- } finally {
- c.close();
- }
-
- if (!firstCommand) {
- s.end(); // SYNC_COMMANDS
- }
- return false;
- }
-}
diff --git a/src/com/android/exchange/adapter/FolderSyncParser.java b/src/com/android/exchange/adapter/FolderSyncParser.java
deleted file mode 100644
index 3b0bc7c..0000000
--- a/src/com/android/exchange/adapter/FolderSyncParser.java
+++ /dev/null
@@ -1,698 +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.adapter;
-
-import android.content.ContentProviderOperation;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.OperationApplicationException;
-import android.database.Cursor;
-import android.os.RemoteException;
-import android.text.TextUtils;
-
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent;
-import com.android.emailcommon.provider.EmailContent.AccountColumns;
-import com.android.emailcommon.provider.EmailContent.MailboxColumns;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.emailcommon.service.SyncWindow;
-import com.android.emailcommon.utility.AttachmentUtilities;
-import com.android.emailcommon.utility.EmailAsyncTask;
-import com.android.emailcommon.utility.Utility;
-import com.android.exchange.CommandStatusException;
-import com.android.exchange.CommandStatusException.CommandStatus;
-import com.android.exchange.Eas;
-import com.android.exchange.ExchangeService;
-import com.android.exchange.provider.MailboxUtilities;
-import com.google.common.annotations.VisibleForTesting;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-
-/**
- * Parse the result of a FolderSync command
- *
- * Handles the addition, deletion, and changes to folders in the user's Exchange account.
- **/
-
-public class FolderSyncParser extends AbstractSyncParser {
-
- public static final String TAG = "FolderSyncParser";
-
- // These are defined by the EAS protocol
- public static final int USER_GENERIC_TYPE = 1;
- public static final int INBOX_TYPE = 2;
- public static final int DRAFTS_TYPE = 3;
- public static final int DELETED_TYPE = 4;
- public static final int SENT_TYPE = 5;
- public static final int OUTBOX_TYPE = 6;
- public static final int TASKS_TYPE = 7;
- public static final int CALENDAR_TYPE = 8;
- public static final int CONTACTS_TYPE = 9;
- public static final int NOTES_TYPE = 10;
- public static final int JOURNAL_TYPE = 11;
- public static final int USER_MAILBOX_TYPE = 12;
-
- // Chunk size for our mailbox commits
- public final static int MAILBOX_COMMIT_SIZE = 20;
-
- // EAS types that we are willing to consider valid folders for EAS sync
- public static final List<Integer> VALID_EAS_FOLDER_TYPES = Arrays.asList(INBOX_TYPE,
- DRAFTS_TYPE, DELETED_TYPE, SENT_TYPE, OUTBOX_TYPE, USER_MAILBOX_TYPE, CALENDAR_TYPE,
- CONTACTS_TYPE, USER_GENERIC_TYPE);
-
- public static final String ALL_BUT_ACCOUNT_MAILBOX = MailboxColumns.ACCOUNT_KEY + "=? and " +
- MailboxColumns.TYPE + "!=" + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
-
- private static final String WHERE_SERVER_ID_AND_ACCOUNT = MailboxColumns.SERVER_ID + "=? and " +
- MailboxColumns.ACCOUNT_KEY + "=?";
-
- private static final String WHERE_DISPLAY_NAME_AND_ACCOUNT = MailboxColumns.DISPLAY_NAME +
- "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
-
- private static final String WHERE_PARENT_SERVER_ID_AND_ACCOUNT =
- MailboxColumns.PARENT_SERVER_ID +"=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
-
- private static final String[] MAILBOX_ID_COLUMNS_PROJECTION =
- new String[] {MailboxColumns.ID, MailboxColumns.SERVER_ID, MailboxColumns.PARENT_SERVER_ID};
- private static final int MAILBOX_ID_COLUMNS_ID = 0;
- private static final int MAILBOX_ID_COLUMNS_SERVER_ID = 1;
- private static final int MAILBOX_ID_COLUMNS_PARENT_SERVER_ID = 2;
-
- @VisibleForTesting
- long mAccountId;
- @VisibleForTesting
- String mAccountIdAsString;
- @VisibleForTesting
- boolean mInUnitTest = false;
-
- private String[] mBindArguments = new String[2];
- private ArrayList<ContentProviderOperation> mOperations =
- new ArrayList<ContentProviderOperation>();
- private boolean mInitialSync;
- private ArrayList<String> mParentFixupsNeeded = new ArrayList<String>();
- private boolean mFixupUninitializedNeeded = false;
- // If true, we only care about status (this is true when validating an account) and ignore
- // other data
- private final boolean mStatusOnly;
-
- private static final ContentValues UNINITIALIZED_PARENT_KEY = new ContentValues();
-
- {
- UNINITIALIZED_PARENT_KEY.put(MailboxColumns.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED);
- }
-
- public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
- this(in, adapter, false);
- }
-
- public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter, boolean statusOnly)
- throws IOException {
- super(in, adapter);
- mAccountId = mAccount.mId;
- mAccountIdAsString = Long.toString(mAccountId);
- mStatusOnly = statusOnly;
- }
-
- @Override
- public boolean parse() throws IOException, CommandStatusException {
- int status;
- boolean res = false;
- boolean resetFolders = false;
- // Since we're now (potentially) committing mailboxes in chunks, ensure that we start with
- // only the account mailbox
- String key = mAccount.mSyncKey;
- mInitialSync = (key == null) || "0".equals(key);
- if (mInitialSync) {
- mContentResolver.delete(Mailbox.CONTENT_URI, ALL_BUT_ACCOUNT_MAILBOX,
- new String[] {Long.toString(mAccountId)});
- }
- if (nextTag(START_DOCUMENT) != Tags.FOLDER_FOLDER_SYNC)
- throw new EasParserException();
- while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
- if (tag == Tags.FOLDER_STATUS) {
- status = getValueInt();
- if (status != Eas.FOLDER_STATUS_OK) {
- mService.errorLog("FolderSync failed: " + CommandStatus.toString(status));
- // If the account hasn't been saved, this is a validation attempt, so we don't
- // try reloading the folder list...
- if (CommandStatus.isDeniedAccess(status) ||
- CommandStatus.isNeedsProvisioning(status) ||
- (mAccount.mId == Account.NOT_SAVED)) {
- throw new CommandStatusException(status);
- // Note that we need to catch both old-style (Eas.FOLDER_STATUS_INVALID_KEY)
- // and EAS 14 style command status
- } else if (status == Eas.FOLDER_STATUS_INVALID_KEY ||
- CommandStatus.isBadSyncKey(status)) {
- mService.errorLog("Bad sync key; RESET and delete all folders");
- // Reset the sync key and save
- mAccount.mSyncKey = "0";
- ContentValues cv = new ContentValues();
- cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
- mContentResolver.update(ContentUris.withAppendedId(Account.CONTENT_URI,
- mAccount.mId), cv, null, null);
- // Delete PIM data
- ExchangeService.deleteAccountPIMData(mAccountId);
- // Save away any mailbox sync information that is NOT default
- saveMailboxSyncOptions();
- // And only then, delete mailboxes
- mContentResolver.delete(Mailbox.CONTENT_URI, ALL_BUT_ACCOUNT_MAILBOX,
- new String[] {Long.toString(mAccountId)});
- // Stop existing syncs and reconstruct _main
- ExchangeService.stopNonAccountMailboxSyncsForAccount(mAccountId);
- res = true;
- resetFolders = true;
- } else {
- // Other errors are at the server, so let's throw an error that will
- // cause this sync to be retried at a later time
- mService.errorLog("Throwing IOException; will retry later");
- throw new EasParserException("Folder status error");
- }
- }
- } else if (tag == Tags.FOLDER_SYNC_KEY) {
- mAccount.mSyncKey = getValue();
- userLog("New Account SyncKey: ", mAccount.mSyncKey);
- } else if (tag == Tags.FOLDER_CHANGES) {
- if (mStatusOnly) return res;
- changesParser(mOperations, mInitialSync);
- } else
- skipTag();
- }
- if (mStatusOnly) return res;
- synchronized (mService.getSynchronizer()) {
- if (!mService.isStopped() || resetFolders) {
- commit();
- userLog("Leaving FolderSyncParser with Account syncKey=", mAccount.mSyncKey);
- }
- }
- return res;
- }
-
- private Cursor getServerIdCursor(String serverId) {
- mBindArguments[0] = serverId;
- mBindArguments[1] = mAccountIdAsString;
- return mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_ID_COLUMNS_PROJECTION,
- WHERE_SERVER_ID_AND_ACCOUNT, mBindArguments, null);
- }
-
- public void deleteParser(ArrayList<ContentProviderOperation> ops) throws IOException {
- while (nextTag(Tags.FOLDER_DELETE) != END) {
- switch (tag) {
- case Tags.FOLDER_SERVER_ID:
- String serverId = getValue();
- // Find the mailbox in this account with the given serverId
- Cursor c = getServerIdCursor(serverId);
- try {
- if (c.moveToFirst()) {
- userLog("Deleting ", serverId);
- ops.add(ContentProviderOperation.newDelete(
- ContentUris.withAppendedId(Mailbox.CONTENT_URI,
- c.getLong(MAILBOX_ID_COLUMNS_ID))).build());
- AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext,
- mAccountId, mMailbox.mId);
- if (!mInitialSync) {
- String parentId = c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
- if (!TextUtils.isEmpty(parentId)) {
- mParentFixupsNeeded.add(parentId);
- }
- }
- }
- } finally {
- c.close();
- }
- break;
- default:
- skipTag();
- }
- }
- }
-
- private static class SyncOptions {
- private final int mInterval;
- private final int mLookback;
-
- private SyncOptions(int interval, int lookback) {
- mInterval = interval;
- mLookback = lookback;
- }
- }
-
- private static final String MAILBOX_STATE_SELECTION =
- MailboxColumns.ACCOUNT_KEY + "=? AND (" + MailboxColumns.SYNC_INTERVAL + "!=" +
- Account.CHECK_INTERVAL_NEVER + " OR " + Mailbox.SYNC_LOOKBACK + "!=" +
- SyncWindow.SYNC_WINDOW_UNKNOWN + ")";
-
- private static final String[] MAILBOX_STATE_PROJECTION = new String[] {
- MailboxColumns.SERVER_ID, MailboxColumns.SYNC_INTERVAL, MailboxColumns.SYNC_LOOKBACK};
- private static final int MAILBOX_STATE_SERVER_ID = 0;
- private static final int MAILBOX_STATE_INTERVAL = 1;
- private static final int MAILBOX_STATE_LOOKBACK = 2;
- @VisibleForTesting
- final HashMap<String, SyncOptions> mSyncOptionsMap = new HashMap<String, SyncOptions>();
-
- /**
- * For every mailbox in this account that has a non-default interval or lookback, save those
- * values.
- */
- @VisibleForTesting
- void saveMailboxSyncOptions() {
- // Shouldn't be necessary, but...
- mSyncOptionsMap.clear();
- Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, MAILBOX_STATE_PROJECTION,
- MAILBOX_STATE_SELECTION, new String[] {mAccountIdAsString}, null);
- if (c != null) {
- try {
- while (c.moveToNext()) {
- mSyncOptionsMap.put(c.getString(MAILBOX_STATE_SERVER_ID),
- new SyncOptions(c.getInt(MAILBOX_STATE_INTERVAL),
- c.getInt(MAILBOX_STATE_LOOKBACK)));
- }
- } finally {
- c.close();
- }
- }
- }
-
- /**
- * For every set of saved mailbox sync options, try to find and restore those values
- */
- @VisibleForTesting
- void restoreMailboxSyncOptions() {
- try {
- ContentValues cv = new ContentValues();
- mBindArguments[1] = mAccountIdAsString;
- for (String serverId: mSyncOptionsMap.keySet()) {
- SyncOptions options = mSyncOptionsMap.get(serverId);
- cv.put(MailboxColumns.SYNC_INTERVAL, options.mInterval);
- cv.put(MailboxColumns.SYNC_LOOKBACK, options.mLookback);
- mBindArguments[0] = serverId;
- // If we match account and server id, set the sync options
- mContentResolver.update(Mailbox.CONTENT_URI, cv, WHERE_SERVER_ID_AND_ACCOUNT,
- mBindArguments);
- }
- } finally {
- mSyncOptionsMap.clear();
- }
- }
-
- public Mailbox addParser() throws IOException {
- String name = null;
- String serverId = null;
- String parentId = null;
- int type = 0;
-
- while (nextTag(Tags.FOLDER_ADD) != END) {
- switch (tag) {
- case Tags.FOLDER_DISPLAY_NAME: {
- name = getValue();
- break;
- }
- case Tags.FOLDER_TYPE: {
- type = getValueInt();
- break;
- }
- case Tags.FOLDER_PARENT_ID: {
- parentId = getValue();
- break;
- }
- case Tags.FOLDER_SERVER_ID: {
- serverId = getValue();
- break;
- }
- default:
- skipTag();
- }
- }
-
- if (VALID_EAS_FOLDER_TYPES.contains(type)) {
- Mailbox mailbox = new Mailbox();
- mailbox.mDisplayName = name;
- mailbox.mServerId = serverId;
- mailbox.mAccountKey = mAccountId;
- mailbox.mType = Mailbox.TYPE_MAIL;
- // Note that all mailboxes default to checking "never" (i.e. manual sync only)
- // We set specific intervals for inbox, contacts, and (eventually) calendar
- mailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
- switch (type) {
- case INBOX_TYPE:
- mailbox.mType = Mailbox.TYPE_INBOX;
- mailbox.mSyncInterval = mAccount.mSyncInterval;
- break;
- case CONTACTS_TYPE:
- mailbox.mType = Mailbox.TYPE_CONTACTS;
- mailbox.mSyncInterval = mAccount.mSyncInterval;
- break;
- case OUTBOX_TYPE:
- // TYPE_OUTBOX mailboxes are known by ExchangeService to sync whenever they
- // aren't empty. The value of mSyncFrequency is ignored for this kind of
- // mailbox.
- mailbox.mType = Mailbox.TYPE_OUTBOX;
- break;
- case SENT_TYPE:
- mailbox.mType = Mailbox.TYPE_SENT;
- break;
- case DRAFTS_TYPE:
- mailbox.mType = Mailbox.TYPE_DRAFTS;
- break;
- case DELETED_TYPE:
- mailbox.mType = Mailbox.TYPE_TRASH;
- break;
- case CALENDAR_TYPE:
- mailbox.mType = Mailbox.TYPE_CALENDAR;
- mailbox.mSyncInterval = mAccount.mSyncInterval;
- break;
- case USER_GENERIC_TYPE:
- mailbox.mType = Mailbox.TYPE_UNKNOWN;
- break;
- }
-
- // Make boxes like Contacts and Calendar invisible in the folder list
- mailbox.mFlagVisible = (mailbox.mType < Mailbox.TYPE_NOT_EMAIL);
-
- if (!parentId.equals("0")) {
- mailbox.mParentServerId = parentId;
- if (!mInitialSync) {
- mParentFixupsNeeded.add(parentId);
- }
- }
- // At the least, we'll need to set flags
- mFixupUninitializedNeeded = true;
-
- return mailbox;
- }
- return null;
- }
-
- /**
- * Determine whether a given mailbox holds mail, rather than other data. We do this by first
- * checking the type of the mailbox (if it's a known good type, great; if it's a known bad
- * type, return false). If it's unknown, we check the parent, first by trying to find it in
- * the current set of newly synced items, and then by looking it up in EmailProvider. If
- * we can find the parent, we use the same rules to determine if it holds mail; if it does,
- * then its children do as well, so that's a go.
- *
- * @param mailbox the mailbox we're checking
- * @param mailboxMap a HashMap relating server id's of mailboxes in the current sync set to
- * the corresponding mailbox structures
- * @return whether or not the mailbox contains email (rather than PIM or unknown data)
- */
- /*package*/ boolean isValidMailFolder(Mailbox mailbox, HashMap<String, Mailbox> mailboxMap) {
- int folderType = mailbox.mType;
- // Automatically accept our email types
- if (folderType < Mailbox.TYPE_NOT_EMAIL) return true;
- // Automatically reject everything else but "unknown"
- if (folderType != Mailbox.TYPE_UNKNOWN) return false;
- // If this is TYPE_UNKNOWN, check the parent
- Mailbox parent = mailboxMap.get(mailbox.mParentServerId);
- // If the parent is in the map, then check it out; if not, it could be an existing saved
- // Mailbox, so we'll have to query the database
- if (parent == null) {
- mBindArguments[0] = Long.toString(mAccount.mId);
- mBindArguments[1] = mailbox.mParentServerId;
- long parentId = Utility.getFirstRowInt(mContext, Mailbox.CONTENT_URI,
- EmailContent.ID_PROJECTION,
- MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.SERVER_ID + "=?",
- mBindArguments, null, EmailContent.ID_PROJECTION_COLUMN, -1);
- if (parentId != -1) {
- // Get the parent from the database
- parent = Mailbox.restoreMailboxWithId(mContext, parentId);
- if (parent == null) return false;
- } else {
- return false;
- }
- }
- return isValidMailFolder(parent, mailboxMap);
- }
-
- public void updateParser(ArrayList<ContentProviderOperation> ops) throws IOException {
- String serverId = null;
- String displayName = null;
- String parentId = null;
- while (nextTag(Tags.FOLDER_UPDATE) != END) {
- switch (tag) {
- case Tags.FOLDER_SERVER_ID:
- serverId = getValue();
- break;
- case Tags.FOLDER_DISPLAY_NAME:
- displayName = getValue();
- break;
- case Tags.FOLDER_PARENT_ID:
- parentId = getValue();
- break;
- default:
- skipTag();
- break;
- }
- }
- // We'll make a change if one of parentId or displayName are specified
- // serverId is required, but let's be careful just the same
- if (serverId != null && (displayName != null || parentId != null)) {
- Cursor c = getServerIdCursor(serverId);
- try {
- // If we find the mailbox (using serverId), make the change
- if (c.moveToFirst()) {
- userLog("Updating ", serverId);
- // Fix up old and new parents, as needed
- if (!TextUtils.isEmpty(parentId)) {
- mParentFixupsNeeded.add(parentId);
- }
- String oldParentId = c.getString(MAILBOX_ID_COLUMNS_PARENT_SERVER_ID);
- if (!TextUtils.isEmpty(oldParentId)) {
- mParentFixupsNeeded.add(oldParentId);
- }
- // Set display name if we've got one
- ContentValues cv = new ContentValues();
- if (displayName != null) {
- cv.put(Mailbox.DISPLAY_NAME, displayName);
- }
- // Save away the server id and uninitialize the parent key
- cv.put(Mailbox.PARENT_SERVER_ID, parentId);
- // Clear the parent key; it will be fixed up after the commit
- cv.put(Mailbox.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED);
- ops.add(ContentProviderOperation.newUpdate(
- ContentUris.withAppendedId(Mailbox.CONTENT_URI,
- c.getLong(MAILBOX_ID_COLUMNS_ID))).withValues(cv).build());
- // Say we need to fixup uninitialized mailboxes
- mFixupUninitializedNeeded = true;
- }
- } finally {
- c.close();
- }
- }
- }
-
- private boolean commitMailboxes(ArrayList<Mailbox> validMailboxes,
- ArrayList<Mailbox> userMailboxes, HashMap<String, Mailbox> mailboxMap,
- ArrayList<ContentProviderOperation> ops) {
-
- // Go through the generic user mailboxes; we'll call them valid if any parent is valid
- for (Mailbox m: userMailboxes) {
- if (isValidMailFolder(m, mailboxMap)) {
- m.mType = Mailbox.TYPE_MAIL;
- validMailboxes.add(m);
- } else {
- userLog("Rejecting unknown type mailbox: " + m.mDisplayName);
- }
- }
-
- // Add operations for all valid mailboxes
- for (Mailbox m: validMailboxes) {
- userLog("Adding mailbox: ", m.mDisplayName);
- ops.add(ContentProviderOperation
- .newInsert(Mailbox.CONTENT_URI).withValues(m.toContentValues()).build());
- }
-
- // Commit the mailboxes
- userLog("Applying ", mOperations.size(), " mailbox operations.");
- // Execute the batch; throw IOExceptions if this fails, hoping the issue isn't repeatable
- // If it IS repeatable, there's no good result, since the folder list will be invalid
- try {
- mContentResolver.applyBatch(EmailContent.AUTHORITY, mOperations);
- return true;
- } catch (RemoteException e) {
- userLog("RemoteException in commitMailboxes");
- return false;
- } catch (OperationApplicationException e) {
- userLog("OperationApplicationException in commitMailboxes");
- return false;
- }
- }
-
- public void changesParser(final ArrayList<ContentProviderOperation> ops,
- final boolean initialSync) throws IOException {
- // Array of added mailboxes
- final ArrayList<Mailbox> addMailboxes = new ArrayList<Mailbox>();
-
- // Indicate start of (potential) mailbox changes
- MailboxUtilities.startMailboxChanges(mContext, mAccount.mId);
-
- while (nextTag(Tags.FOLDER_CHANGES) != END) {
- if (tag == Tags.FOLDER_ADD) {
- Mailbox mailbox = addParser();
- if (mailbox != null) {
- addMailboxes.add(mailbox);
- }
- } else if (tag == Tags.FOLDER_DELETE) {
- deleteParser(ops);
- } else if (tag == Tags.FOLDER_UPDATE) {
- updateParser(ops);
- } else if (tag == Tags.FOLDER_COUNT) {
- getValueInt();
- } else
- skipTag();
- }
-
- EmailAsyncTask<?, ?, ?> task =
- EmailAsyncTask.runAsyncParallel(new Runnable() {
- @Override
- public void run() {
- // Synchronize on the parser to prevent this being run concurrently
- // (an extremely unlikely event, but nonetheless possible)
- synchronized (FolderSyncParser.this) {
- // Mailboxes that we known contain email
- ArrayList<Mailbox> validMailboxes = new ArrayList<Mailbox>();
- // Mailboxes that we're unsure about
- ArrayList<Mailbox> userMailboxes = new ArrayList<Mailbox>();
-
- // Maps folder serverId to mailbox (used to validate user mailboxes)
- HashMap<String, Mailbox> mailboxMap = new HashMap<String, Mailbox>();
- for (Mailbox mailbox : addMailboxes) {
- mailboxMap.put(mailbox.mServerId, mailbox);
- }
-
- int mailboxCommitCount = 0;
- for (Mailbox mailbox : addMailboxes) {
- // And add the mailbox to the proper list
- if (type == USER_MAILBOX_TYPE) {
- userMailboxes.add(mailbox);
- } else {
- validMailboxes.add(mailbox);
- }
- // On initial sync, we commit what we have every 20 mailboxes
- if (initialSync && (++mailboxCommitCount == MAILBOX_COMMIT_SIZE)) {
- if (!commitMailboxes(validMailboxes, userMailboxes, mailboxMap,
- ops)) {
- mService.stop();
- return;
- }
- // Clear our arrays to prepare for more
- userMailboxes.clear();
- validMailboxes.clear();
- ops.clear();
- mailboxCommitCount = 0;
- }
- }
- // Commit the sync key and mailboxes
- ContentValues cv = new ContentValues();
- cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
- ops.add(ContentProviderOperation
- .newUpdate(
- ContentUris.withAppendedId(Account.CONTENT_URI,
- mAccount.mId))
- .withValues(cv).build());
- if (!commitMailboxes(validMailboxes, userMailboxes, mailboxMap, ops)) {
- mService.stop();
- return;
- }
- String accountSelector = Mailbox.ACCOUNT_KEY + "=" + mAccount.mId;
- // For new boxes, setup the parent key and flags
- if (mFixupUninitializedNeeded) {
- MailboxUtilities.fixupUninitializedParentKeys(mContext,
- accountSelector);
- }
- // For modified parents, reset the flags (and children's parent key)
- for (String parentServerId: mParentFixupsNeeded) {
- Cursor c = mContentResolver.query(Mailbox.CONTENT_URI,
- Mailbox.CONTENT_PROJECTION, Mailbox.PARENT_SERVER_ID + "=?",
- new String[] {parentServerId}, null);
- try {
- if (c.moveToFirst()) {
- MailboxUtilities.setFlagsAndChildrensParentKey(mContext, c,
- accountSelector);
- }
- } finally {
- c.close();
- }
- }
-
- // Signal completion of mailbox changes
- MailboxUtilities.endMailboxChanges(mContext, mAccount.mId);
- }
- }});
- // Make this synchronous if in a unit test
- if (mInUnitTest) {
- try {
- task.get();
- } catch (Exception e) {
- }
- }
- }
-
- /**
- * Not needed for FolderSync parsing; everything is done within changesParser
- */
- @Override
- public void commandsParser() throws IOException {
- }
-
- /**
- * Clean up after sync
- */
- @Override
- public void commit() throws IOException {
- // Look for sync issues and its children and delete them
- // I'm not aware of any other way to deal with this properly
- mBindArguments[0] = "Sync Issues";
- mBindArguments[1] = mAccountIdAsString;
- Cursor c = mContentResolver.query(Mailbox.CONTENT_URI,
- MAILBOX_ID_COLUMNS_PROJECTION, WHERE_DISPLAY_NAME_AND_ACCOUNT,
- mBindArguments, null);
- String parentServerId = null;
- long id = 0;
- try {
- if (c.moveToFirst()) {
- id = c.getLong(MAILBOX_ID_COLUMNS_ID);
- parentServerId = c.getString(MAILBOX_ID_COLUMNS_SERVER_ID);
- }
- } finally {
- c.close();
- }
- if (parentServerId != null) {
- mContentResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id),
- null, null);
- mBindArguments[0] = parentServerId;
- mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_PARENT_SERVER_ID_AND_ACCOUNT,
- mBindArguments);
- }
-
- // If we have saved options, restore them now
- if (mInitialSync) {
- restoreMailboxSyncOptions();
- }
- }
-
- @Override
- public void responsesParser() throws IOException {
- }
-
-}
diff --git a/src/com/android/exchange/adapter/GalParser.java b/src/com/android/exchange/adapter/GalParser.java
deleted file mode 100644
index 94c8cdf..0000000
--- a/src/com/android/exchange/adapter/GalParser.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/* Copyright (C) 2010 The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.adapter;
-
-import com.android.exchange.EasSyncService;
-import com.android.exchange.provider.GalResult;
-import com.android.exchange.provider.GalResult.GalData;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Parse the result of a GAL command.
- */
-public class GalParser extends Parser {
- private EasSyncService mService;
- GalResult mGalResult = new GalResult();
-
- public GalParser(InputStream in, EasSyncService service) throws IOException {
- super(in);
- mService = service;
- }
-
- public GalResult getGalResult() {
- return mGalResult;
- }
-
- @Override
- public boolean parse() throws IOException {
- if (nextTag(START_DOCUMENT) != Tags.SEARCH_SEARCH) {
- throw new IOException();
- }
- while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
- if (tag == Tags.SEARCH_RESPONSE) {
- parseResponse(mGalResult);
- } else {
- skipTag();
- }
- }
- return mGalResult.total > 0;
- }
-
- public void parseProperties(GalResult galResult) throws IOException {
- GalData galData = new GalData();
- while (nextTag(Tags.SEARCH_STORE) != END) {
- switch(tag) {
- // Display name and email address use both legacy and new code for galData
- case Tags.GAL_DISPLAY_NAME:
- String displayName = getValue();
- galData.put(GalData.DISPLAY_NAME, displayName);
- galData.displayName = displayName;
- break;
- case Tags.GAL_EMAIL_ADDRESS:
- String emailAddress = getValue();
- galData.put(GalData.EMAIL_ADDRESS, emailAddress);
- galData.emailAddress = emailAddress;
- break;
- case Tags.GAL_PHONE:
- galData.put(GalData.WORK_PHONE, getValue());
- break;
- case Tags.GAL_OFFICE:
- galData.put(GalData.OFFICE, getValue());
- break;
- case Tags.GAL_TITLE:
- galData.put(GalData.TITLE, getValue());
- break;
- case Tags.GAL_COMPANY:
- galData.put(GalData.COMPANY, getValue());
- break;
- case Tags.GAL_ALIAS:
- galData.put(GalData.ALIAS, getValue());
- break;
- case Tags.GAL_FIRST_NAME:
- galData.put(GalData.FIRST_NAME, getValue());
- break;
- case Tags.GAL_LAST_NAME:
- galData.put(GalData.LAST_NAME, getValue());
- break;
- case Tags.GAL_HOME_PHONE:
- galData.put(GalData.HOME_PHONE, getValue());
- break;
- case Tags.GAL_MOBILE_PHONE:
- galData.put(GalData.MOBILE_PHONE, getValue());
- break;
- default:
- skipTag();
- }
- }
- galResult.addGalData(galData);
- }
-
- public void parseResult(GalResult galResult) throws IOException {
- while (nextTag(Tags.SEARCH_STORE) != END) {
- if (tag == Tags.SEARCH_PROPERTIES) {
- parseProperties(galResult);
- } else {
- skipTag();
- }
- }
- }
-
- public void parseResponse(GalResult galResult) throws IOException {
- while (nextTag(Tags.SEARCH_RESPONSE) != END) {
- if (tag == Tags.SEARCH_STORE) {
- parseStore(galResult);
- } else {
- skipTag();
- }
- }
- }
-
- public void parseStore(GalResult galResult) throws IOException {
- while (nextTag(Tags.SEARCH_STORE) != END) {
- if (tag == Tags.SEARCH_RESULT) {
- parseResult(galResult);
- } else if (tag == Tags.SEARCH_RANGE) {
- // Retrieve value, even if we're not using it for debug logging
- String range = getValue();
- if (EasSyncService.DEBUG_GAL_SERVICE) {
- mService.userLog("GAL result range: " + range);
- }
- } else if (tag == Tags.SEARCH_TOTAL) {
- galResult.total = getValueInt();
- } else {
- skipTag();
- }
- }
- }
-}
-
diff --git a/src/com/android/exchange/adapter/ItemOperationsParser.java b/src/com/android/exchange/adapter/ItemOperationsParser.java
deleted file mode 100644
index 5f412ad..0000000
--- a/src/com/android/exchange/adapter/ItemOperationsParser.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/* Copyright (C) 2011 The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.adapter;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * Parse the result of an ItemOperations command; we use this to load attachments in EAS 14.0
- */
-public class ItemOperationsParser extends Parser {
- private final AttachmentLoader mAttachmentLoader;
- private int mStatusCode = 0;
- private final OutputStream mAttachmentOutputStream;
- private final int mAttachmentSize;
-
- public ItemOperationsParser(AttachmentLoader loader, InputStream in, OutputStream out, int size)
- throws IOException {
- super(in);
- mAttachmentLoader = loader;
- mAttachmentOutputStream = out;
- mAttachmentSize = size;
- }
-
- public int getStatusCode() {
- return mStatusCode;
- }
-
- private void parseProperties() throws IOException {
- while (nextTag(Tags.ITEMS_PROPERTIES) != END) {
- if (tag == Tags.ITEMS_DATA) {
- // Wrap the input stream in our custom base64 input stream
- Base64InputStream bis = new Base64InputStream(getInput());
- // Read the attachment
- mAttachmentLoader.readChunked(bis, mAttachmentOutputStream, mAttachmentSize);
- } else {
- skipTag();
- }
- }
- }
-
- private void parseFetch() throws IOException {
- while (nextTag(Tags.ITEMS_FETCH) != END) {
- if (tag == Tags.ITEMS_PROPERTIES) {
- parseProperties();
- } else {
- skipTag();
- }
- }
- }
-
- private void parseResponse() throws IOException {
- while (nextTag(Tags.ITEMS_RESPONSE) != END) {
- if (tag == Tags.ITEMS_FETCH) {
- parseFetch();
- } else {
- skipTag();
- }
- }
- }
-
- @Override
- public boolean parse() throws IOException {
- boolean res = false;
- if (nextTag(START_DOCUMENT) != Tags.ITEMS_ITEMS) {
- throw new IOException();
- }
- while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
- if (tag == Tags.ITEMS_STATUS) {
- // Save the status code
- mStatusCode = getValueInt();
- } else if (tag == Tags.ITEMS_RESPONSE) {
- parseResponse();
- } else {
- skipTag();
- }
- }
- return res;
- }
-}
diff --git a/src/com/android/exchange/adapter/MeetingResponseParser.java b/src/com/android/exchange/adapter/MeetingResponseParser.java
deleted file mode 100644
index 142c419..0000000
--- a/src/com/android/exchange/adapter/MeetingResponseParser.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/* Copyright (C) 2010 The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.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/MoveItemsParser.java b/src/com/android/exchange/adapter/MoveItemsParser.java
deleted file mode 100644
index 542331d..0000000
--- a/src/com/android/exchange/adapter/MoveItemsParser.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/* Copyright (C) 2010 The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.adapter;
-
-import com.android.exchange.EasSyncService;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Parse the result of a MoveItems command.
- */
-public class MoveItemsParser extends Parser {
- private final EasSyncService mService;
- private int mStatusCode = 0;
- private String mNewServerId;
-
- // These are the EAS status codes for MoveItems
- private static final int STATUS_NO_SOURCE_FOLDER = 1;
- private static final int STATUS_NO_DESTINATION_FOLDER = 2;
- private static final int STATUS_SUCCESS = 3;
- private static final int STATUS_SOURCE_DESTINATION_SAME = 4;
- private static final int STATUS_INTERNAL_ERROR = 5;
- private static final int STATUS_ALREADY_EXISTS = 6;
- private static final int STATUS_LOCKED = 7;
-
- // These are the status values we return to callers
- public static final int STATUS_CODE_SUCCESS = 1;
- public static final int STATUS_CODE_REVERT = 2;
- public static final int STATUS_CODE_RETRY = 3;
-
- public MoveItemsParser(InputStream in, EasSyncService service) throws IOException {
- super(in);
- mService = service;
- }
-
- public int getStatusCode() {
- return mStatusCode;
- }
-
- public String getNewServerId() {
- return mNewServerId;
- }
-
- public void parseResponse() throws IOException {
- while (nextTag(Tags.MOVE_RESPONSE) != END) {
- if (tag == Tags.MOVE_STATUS) {
- int status = getValueInt();
- // Convert the EAS status code with our external codes
- switch(status) {
- case STATUS_SUCCESS:
- case STATUS_SOURCE_DESTINATION_SAME:
- case STATUS_ALREADY_EXISTS:
- // Same destination and already exists are ok with us; we'll continue as
- // if the move succeeded
- mStatusCode = STATUS_CODE_SUCCESS;
- break;
- case STATUS_LOCKED:
- // This sounds like a transient error, so we can safely retry
- mStatusCode = STATUS_CODE_RETRY;
- break;
- case STATUS_NO_SOURCE_FOLDER:
- case STATUS_NO_DESTINATION_FOLDER:
- case STATUS_INTERNAL_ERROR:
- default:
- // These are non-recoverable, so we'll revert the message to its original
- // mailbox. If there's an unknown response, revert
- mStatusCode = STATUS_CODE_REVERT;
- break;
- }
- if (status != STATUS_SUCCESS) {
- // There's not much to be done if this fails
- mService.userLog("Error in MoveItems: " + status);
- }
- } else if (tag == Tags.MOVE_DSTMSGID) {
- mNewServerId = getValue();
- mService.userLog("Moved message id is now: " + mNewServerId);
- } else {
- skipTag();
- }
- }
- }
-
- @Override
- public boolean parse() throws IOException {
- boolean res = false;
- if (nextTag(START_DOCUMENT) != Tags.MOVE_MOVE_ITEMS) {
- throw new IOException();
- }
- while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
- if (tag == Tags.MOVE_RESPONSE) {
- parseResponse();
- } else {
- skipTag();
- }
- }
- return res;
- }
-}
-
diff --git a/src/com/android/exchange/adapter/Parser.java b/src/com/android/exchange/adapter/Parser.java
deleted file mode 100644
index d24e6c1..0000000
--- a/src/com/android/exchange/adapter/Parser.java
+++ /dev/null
@@ -1,606 +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.adapter;
-
-import android.content.Context;
-import android.util.Log;
-
-import com.android.exchange.Eas;
-import com.android.exchange.EasException;
-import com.android.exchange.utility.FileLogger;
-import com.google.common.annotations.VisibleForTesting;
-
-import java.io.ByteArrayOutputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-
-/**
- * Extremely fast and lightweight WBXML parser, implementing only the subset of WBXML that
- * EAS uses (as defined in the EAS specification)
- *
- */
-public abstract class Parser {
- private static final boolean LOG_VERBOSE = false;
-
- // The following constants are Wbxml standard
- public static final int START_DOCUMENT = 0;
- public static final int DONE = 1;
- public static final int START = 2;
- public static final int END = 3;
- public static final int TEXT = 4;
- public static final int END_DOCUMENT = 3;
- private static final int NOT_FETCHED = Integer.MIN_VALUE;
- private static final int NOT_ENDED = Integer.MIN_VALUE;
- private static final int EOF_BYTE = -1;
- private boolean logging = false;
- private boolean capture = false;
- private String logTag = "EAS Parser";
-
- // Where tags start in a page
- private static final int TAG_BASE = 5;
-
- private ArrayList<Integer> captureArray;
-
- // The input stream for this parser
- private InputStream in;
-
- // The current tag depth
- private int depth;
-
- // The upcoming (saved) id from the stream
- private int nextId = NOT_FETCHED;
-
- // The current tag table (i.e. the tag table for the current page)
- private String[] tagTable;
-
- // An array of tag tables, as defined in EasTags
- static private String[][] tagTables = new String[Tags.pages.length + 1][];
-
- // The stack of names of tags being processed; used when debug = true
- private String[] nameArray = new String[32];
-
- // The stack of tags being processed
- private int[] startTagArray = new int[32];
-
- // The following vars are available to all to avoid method calls that represent the state of
- // the parser at any given time
- public int endTag = NOT_ENDED;
-
- public int startTag;
-
- // The type of the last token read
- public int type;
-
- // The current page
- public int page;
-
- // The current tag
- public int tag;
-
- // The name of the current tag
- public String name;
-
- // Whether the current tag is associated with content (a value)
- public boolean noContent;
-
- // The value read, as a String. Only one of text or num will be valid, depending on whether the
- // value was requested as a String or an int (to avoid wasted effort in parsing)
- public String text;
-
- // The value read, as an int
- public int num;
-
- // The value read, as bytes
- public byte[] bytes;
-
- /**
- * Generated when the parser comes to EOF prematurely during parsing (i.e. in error)
- */
- public class EofException extends IOException {
- private static final long serialVersionUID = 1L;
- }
-
- /**
- * An EmptyStreamException is an EofException that occurs reading the first byte in the parser's
- * input stream; in other words, the stream had no content.
- */
- public class EmptyStreamException extends EofException {
- private static final long serialVersionUID = 1L;
- }
-
- public class EodException extends IOException {
- private static final long serialVersionUID = 1L;
- }
-
- public class EasParserException extends IOException {
- private static final long serialVersionUID = 1L;
-
- EasParserException() {
- super("WBXML format error");
- }
-
- EasParserException(String reason) {
- super(reason);
- }
- }
-
- public boolean parse() throws IOException, EasException {
- return false;
- }
-
- /**
- * Initialize the tag tables; they are constant
- *
- */
- {
- String[][] pages = Tags.pages;
- for (int i = 0; i < pages.length; i++) {
- String[] page = pages[i];
- if (page.length > 0) {
- tagTables[i] = page;
- }
- }
- }
-
- public Parser(InputStream in) throws IOException {
- setInput(in, true);
- logging = Eas.PARSER_LOG;
- }
-
- /**
- * Constructor for use when switching parsers within a input stream
- * @param parser an existing, initialized parser
- * @throws IOException
- */
- public Parser(Parser parser) throws IOException {
- setInput(parser.in, false);
- logging = Eas.PARSER_LOG;
- }
-
- /**
- * Set the debug state of the parser. When debugging is on, every token is logged (Log.v) to
- * the console.
- *
- * @param val the desired state for debug output
- */
- public void setDebug(boolean val) {
- logging = val;
- }
-
- protected InputStream getInput() {
- return in;
- }
-
- /**
- * Set the tag used for logging. When debugging is on, every token is logged (Log.v) to
- * the console.
- *
- * @param val the logging tag
- */
- public void setLoggingTag(String val) {
- logTag = val;
- }
-
- /**
- * Turns on data capture; this is used to create test streams that represent "live" data and
- * can be used against the various parsers.
- */
- public void captureOn() {
- capture = true;
- captureArray = new ArrayList<Integer>();
- }
-
- /**
- * Turns off data capture; writes the captured data to a specified file.
- */
- public void captureOff(Context context, String file) {
- try {
- FileOutputStream out = context.openFileOutput(file, Context.MODE_WORLD_WRITEABLE);
- out.write(captureArray.toString().getBytes());
- out.close();
- } catch (FileNotFoundException e) {
- // This is debug code; exceptions aren't interesting.
- } catch (IOException e) {
- // This is debug code; exceptions aren't interesting.
- }
- }
-
- /**
- * Return the value of the current tag, as a byte array. Note that the result of this call
- * is indeterminate, and possibly null, if the value of the tag is not a byte array
- *
- * @return the byte array value of the current tag
- * @throws IOException
- */
- public byte[] getValueBytes() throws IOException {
- getValue();
- return bytes;
- }
-
- /**
- * Return the value of the current tag, as a String. Note that the result of this call is
- * indeterminate, and possibly null, if the value of the tag is not an immediate string
- *
- * @return the String value of the current tag
- * @throws IOException
- */
- public String getValue() throws IOException {
- // The false argument tells getNext to return the value as a String
- getNext(false);
- // This means there was no value given, just <Foo/>; we'll return empty string for now
- if (type == END) {
- if (logging) {
- log("No value for tag: " + tagTable[startTag - TAG_BASE]);
- }
- return "";
- }
- // Save the value
- String val = text;
- // Read the next token; it had better be the end of the current tag
- getNext(false);
- // If not, throw an exception
- if (type != END) {
- throw new IOException("No END found!");
- }
- return val;
- }
-
- /**
- * Return the value of the current tag, as an integer. Note that the value of this call is
- * indeterminate if the value of this tag is not an immediate string parsed as an integer
- *
- * @return the integer value of the current tag
- * @throws IOException
- */
- public int getValueInt() throws IOException {
- // The true argument to getNext indicates the desire for an integer return value
- getNext(true);
- if (type == END) {
- return 0;
- }
- // Save the value
- int val = num;
- // Read the next token; it had better be the end of the current tag
- getNext(false);
- // If not, throw an exception
- if (type != END) {
- throw new IOException("No END found!");
- }
- return val;
- }
-
- /**
- * Return the next tag found in the stream; special tags END and END_DOCUMENT are used to
- * mark the end of the current tag and end of document. If we hit end of document without
- * looking for it, generate an EodException. The tag returned consists of the page number
- * shifted PAGE_SHIFT bits OR'd with the tag retrieved from the stream. Thus, all tags returned
- * are unique.
- *
- * @param endingTag the tag that would represent the end of the tag we're processing
- * @return the next tag found
- * @throws IOException
- */
- public int nextTag(int endingTag) throws IOException {
- // Lose the page information
- endTag = endingTag &= Tags.PAGE_MASK;
- while (getNext(false) != DONE) {
- // If we're a start, set tag to include the page and return it
- if (type == START) {
- tag = page | startTag;
- return tag;
- // If we're at the ending tag we're looking for, return the END signal
- } else if (type == END && startTag == endTag) {
- return END;
- }
- }
- // We're at end of document here. If we're looking for it, return END_DOCUMENT
- if (endTag == START_DOCUMENT) {
- return END_DOCUMENT;
- }
- // Otherwise, we've prematurely hit end of document, so exception out
- // EodException is a subclass of IOException; this will be treated as an IO error by
- // ExchangeService
- throw new EodException();
- }
-
- /**
- * Skip anything found in the stream until the end of the current tag is reached. This can be
- * used to ignore stretches of xml that aren't needed by the parser.
- *
- * @throws IOException
- */
- public void skipTag() throws IOException {
- int thisTag = startTag;
- // Just loop until we hit the end of the current tag
- while (getNext(false) != DONE) {
- if (type == END && startTag == thisTag) {
- return;
- }
- }
-
- // If we're at end of document, that's bad
- throw new EofException();
- }
-
- /**
- * Retrieve the next token from the input stream
- *
- * @return the token found
- * @throws IOException
- */
- public int nextToken() throws IOException {
- getNext(false);
- return type;
- }
-
- /**
- * Initializes the parser with an input stream; reads the first 4 bytes (which are always the
- * same in EAS, and then sets the tag table to point to page 0 (by definition, the starting
- * page).
- *
- * @param in the InputStream associated with this parser
- * @throws IOException
- */
- public void setInput(InputStream in, boolean initialize) throws IOException {
- this.in = in;
- if ((in != null) && initialize) {
- // If we fail on the very first byte, report an empty stream
- try {
- readByte(); // version
- } catch (EofException e) {
- throw new EmptyStreamException();
- }
- readInt(); // ?
- readInt(); // 106 (UTF-8)
- readInt(); // string table length
- }
- tagTable = tagTables[0];
- }
-
- @VisibleForTesting
- void resetInput(InputStream in) {
- this.in = in;
- try {
- // Read leading zero
- read();
- } catch (IOException e) {
- }
- }
-
- void log(String str) {
- int cr = str.indexOf('\n');
- if (cr > 0) {
- str = str.substring(0, cr);
- }
- Log.v(logTag, str);
- if (Eas.FILE_LOG) {
- FileLogger.log(logTag, str);
- }
- }
-
- protected void pushTag(int id) {
- page = id >> Tags.PAGE_SHIFT;
- tagTable = tagTables[page];
- push(id);
- }
-
- private void pop() {
- if (logging) {
- name = nameArray[depth];
- log("</" + name + '>');
- }
- // Retrieve the now-current startTag from our stack
- startTag = endTag = startTagArray[depth];
- depth--;
- }
-
- private void push(int id) {
- // The tag is in the low 6 bits
- startTag = id & 0x3F;
- // If the high bit is set, there is content (a value) to be read
- noContent = (id & 0x40) == 0;
- depth++;
- if (logging) {
- name = tagTable[startTag - TAG_BASE];
- nameArray[depth] = name;
- log("<" + name + (noContent ? '/' : "") + '>');
- }
- // Save the startTag to our stack
- startTagArray[depth] = startTag;
- }
-
- /**
- * Return the next piece of data from the stream. The return value indicates the type of data
- * that has been retrieved - START (start of tag), END (end of tag), DONE (end of stream), or
- * TEXT (the value of a tag)
- *
- * @param asInt whether a TEXT value should be parsed as a String or an int.
- * @return the type of data retrieved
- * @throws IOException
- */
- private final int getNext(boolean asInt) throws IOException {
- if (noContent) {
- nameArray[depth--] = null;
- type = END;
- noContent = false;
- return type;
- }
-
- text = null;
- name = null;
-
- int id = nextId ();
- while (id == Wbxml.SWITCH_PAGE) {
- nextId = NOT_FETCHED;
- // Get the new page number
- int pg = readByte();
- // Save the shifted page to add into the startTag in nextTag
- page = pg << Tags.PAGE_SHIFT;
- if (LOG_VERBOSE) {
- log("Page: " + page);
- }
- // Retrieve the current tag table
- tagTable = tagTables[pg];
- id = nextId();
- }
- nextId = NOT_FETCHED;
-
- switch (id) {
- case EOF_BYTE:
- // End of document
- type = DONE;
- break;
-
- case Wbxml.END:
- type = END;
- pop();
- break;
-
- case Wbxml.STR_I:
- // Inline string
- type = TEXT;
- if (asInt) {
- num = readInlineInt();
- } else {
- text = readInlineString();
- }
- if (logging) {
- name = tagTable[startTag - TAG_BASE];
- log(name + ": " + (asInt ? Integer.toString(num) : text));
- }
- break;
-
- case Wbxml.OPAQUE:
- // Integer length + opaque data
- int length = readInt();
- bytes = new byte[length];
- for (int i = 0; i < length; i++) {
- bytes[i] = (byte)readByte();
- }
- if (logging) {
- name = tagTable[startTag - TAG_BASE];
- log(name + ": (opaque:" + length + ") ");
- }
- break;
-
- default:
- type = START;
- push(id);
- }
-
- // Return the type of data we're dealing with
- return type;
- }
-
- /**
- * Read an int from the input stream, and capture it if necessary for debugging. Seems a small
- * price to pay...
- *
- * @return the int read
- * @throws IOException
- */
- private int read() throws IOException {
- int i;
- i = in.read();
- if (capture) {
- captureArray.add(i);
- }
- if (LOG_VERBOSE) {
- log("Byte: " + i);
- }
- return i;
- }
-
- private int nextId() throws IOException {
- if (nextId == NOT_FETCHED) {
- nextId = read();
- }
- return nextId;
- }
-
- private int readByte() throws IOException {
- int i = read();
- if (i == EOF_BYTE) {
- throw new EofException();
- }
- return i;
- }
-
- /**
- * Read an integer from the stream; this is called when the parser knows that what follows is
- * an inline string representing an integer (e.g. the Read tag in Email has a value known to
- * be either "0" or "1")
- *
- * @return the integer as parsed from the stream
- * @throws IOException
- */
- private int readInlineInt() throws IOException {
- int result = 0;
-
- while (true) {
- int i = readByte();
- // Inline strings are always terminated with a zero byte
- if (i == 0) {
- return result;
- }
- if (i >= '0' && i <= '9') {
- result = (result * 10) + (i - '0');
- } else {
- throw new IOException("Non integer");
- }
- }
- }
-
- private int readInt() throws IOException {
- int result = 0;
- int i;
-
- do {
- i = readByte();
- result = (result << 7) | (i & 0x7f);
- } while ((i & 0x80) != 0);
-
- return result;
- }
-
- /**
- * Read an inline string from the stream
- *
- * @return the String as parsed from the stream
- * @throws IOException
- */
- private String readInlineString() throws IOException {
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream(256);
- while (true) {
- int i = read();
- if (i == 0) {
- break;
- } else if (i == EOF_BYTE) {
- throw new EofException();
- }
- outputStream.write(i);
- }
- outputStream.flush();
- String res = outputStream.toString("UTF-8");
- outputStream.close();
- return res;
- }
-}
diff --git a/src/com/android/exchange/adapter/PingParser.java b/src/com/android/exchange/adapter/PingParser.java
deleted file mode 100644
index f061472..0000000
--- a/src/com/android/exchange/adapter/PingParser.java
+++ /dev/null
@@ -1,97 +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.adapter;
-
-import com.android.exchange.EasSyncService;
-import com.android.exchange.IllegalHeartbeatException;
-import com.android.exchange.StaleFolderListException;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-
-/**
- * Parse the result of a Ping command.
- *
- * If there are folders with changes, add the serverId of those folders to the syncList array.
- * If the folder list needs to be reloaded, throw a StaleFolderListException, which will be caught
- * by the sync server, which will sync the updated folder list.
- */
-public class PingParser extends Parser {
- private ArrayList<String> syncList = new ArrayList<String>();
- private EasSyncService mService;
- private int mSyncStatus = 0;
-
- public ArrayList<String> getSyncList() {
- return syncList;
- }
-
- public int getSyncStatus() {
- return mSyncStatus;
- }
-
- public PingParser(InputStream in, EasSyncService service) throws IOException {
- super(in);
- mService = service;
- }
-
- public void parsePingFolders(ArrayList<String> syncList) throws IOException {
- while (nextTag(Tags.PING_FOLDERS) != END) {
- if (tag == Tags.PING_FOLDER) {
- // Here we'll keep track of which mailboxes need syncing
- String serverId = getValue();
- syncList.add(serverId);
- mService.userLog("Changes found in: ", serverId);
- } else {
- skipTag();
- }
- }
- }
-
- @Override
- public boolean parse() throws IOException, StaleFolderListException, IllegalHeartbeatException {
- boolean res = false;
- if (nextTag(START_DOCUMENT) != Tags.PING_PING) {
- throw new IOException();
- }
- while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
- if (tag == Tags.PING_STATUS) {
- int status = getValueInt();
- mSyncStatus = status;
- mService.userLog("Ping completed, status = ", status);
- if (status == 2) {
- res = true;
- } else if (status == 7 || status == 4) {
- // Status of 7 or 4 indicate a stale folder list
- throw new StaleFolderListException();
- } else if (status == 5) {
- // Status 5 means our heartbeat is beyond allowable limits
- // In this case, there will be a heartbeat interval set
- }
- } else if (tag == Tags.PING_FOLDERS) {
- parsePingFolders(syncList);
- } else if (tag == Tags.PING_HEARTBEAT_INTERVAL) {
- // Throw an exception, saving away the legal heartbeat interval specified
- throw new IllegalHeartbeatException(getValueInt());
- } else {
- skipTag();
- }
- }
- return res;
- }
-}
-
diff --git a/src/com/android/exchange/adapter/ProvisionParser.java b/src/com/android/exchange/adapter/ProvisionParser.java
deleted file mode 100644
index ca71cf9..0000000
--- a/src/com/android/exchange/adapter/ProvisionParser.java
+++ /dev/null
@@ -1,616 +0,0 @@
-/* Copyright (C) 2010 The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.adapter;
-
-import android.app.admin.DevicePolicyManager;
-import android.content.Context;
-import android.content.res.Resources;
-import android.os.storage.StorageManager;
-import android.os.storage.StorageVolume;
-
-import com.android.emailcommon.provider.Policy;
-import com.android.exchange.EasSyncService;
-import com.android.exchange.ExchangeService;
-import com.android.exchange.R;
-import com.android.exchange.SecurityPolicyDelegate;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlPullParserFactory;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-
-/**
- * Parse the result of the Provision command
- *
- * Assuming a successful parse, we store the PolicySet and the policy key
- */
-public class ProvisionParser extends Parser {
- private final EasSyncService mService;
- Policy mPolicy = null;
- String mSecuritySyncKey = null;
- boolean mRemoteWipe = false;
- boolean mIsSupportable = true;
- // An array of string resource id's describing policies that are unsupported by the device/app
- String[] mUnsupportedPolicies;
- boolean smimeRequired = false;
-
- public ProvisionParser(InputStream in, EasSyncService service) throws IOException {
- super(in);
- mService = service;
- }
-
- public Policy getPolicy() {
- return mPolicy;
- }
-
- public String getSecuritySyncKey() {
- return mSecuritySyncKey;
- }
-
- public void setSecuritySyncKey(String securitySyncKey) {
- mSecuritySyncKey = securitySyncKey;
- }
-
- public boolean getRemoteWipe() {
- return mRemoteWipe;
- }
-
- public boolean hasSupportablePolicySet() {
- return (mPolicy != null) && mIsSupportable;
- }
-
- public void clearUnsupportedPolicies() {
- mPolicy = SecurityPolicyDelegate.clearUnsupportedPolicies(mService.mContext, mPolicy);
- mIsSupportable = true;
- mUnsupportedPolicies = null;
- }
-
- public String[] getUnsupportedPolicies() {
- return mUnsupportedPolicies;
- }
-
- private void setPolicy(Policy policy) {
- policy.normalize();
- mPolicy = policy;
- }
-
- private boolean deviceSupportsEncryption() {
- DevicePolicyManager dpm = (DevicePolicyManager)
- mService.mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
- int status = dpm.getStorageEncryptionStatus();
- return status != DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED;
- }
-
- private void parseProvisionDocWbxml() throws IOException {
- Policy policy = new Policy();
- ArrayList<Integer> unsupportedList = new ArrayList<Integer>();
- boolean passwordEnabled = false;
-
- while (nextTag(Tags.PROVISION_EAS_PROVISION_DOC) != END) {
- boolean tagIsSupported = true;
- int res = 0;
- switch (tag) {
- case Tags.PROVISION_DEVICE_PASSWORD_ENABLED:
- if (getValueInt() == 1) {
- passwordEnabled = true;
- if (policy.mPasswordMode == Policy.PASSWORD_MODE_NONE) {
- policy.mPasswordMode = Policy.PASSWORD_MODE_SIMPLE;
- }
- }
- break;
- case Tags.PROVISION_MIN_DEVICE_PASSWORD_LENGTH:
- policy.mPasswordMinLength = getValueInt();
- break;
- case Tags.PROVISION_ALPHA_DEVICE_PASSWORD_ENABLED:
- if (getValueInt() == 1) {
- policy.mPasswordMode = Policy.PASSWORD_MODE_STRONG;
- }
- break;
- case Tags.PROVISION_MAX_INACTIVITY_TIME_DEVICE_LOCK:
- // EAS gives us seconds, which is, happily, what the PolicySet requires
- policy.mMaxScreenLockTime = getValueInt();
- break;
- case Tags.PROVISION_MAX_DEVICE_PASSWORD_FAILED_ATTEMPTS:
- policy.mPasswordMaxFails = getValueInt();
- break;
- case Tags.PROVISION_DEVICE_PASSWORD_EXPIRATION:
- policy.mPasswordExpirationDays = getValueInt();
- break;
- case Tags.PROVISION_DEVICE_PASSWORD_HISTORY:
- policy.mPasswordHistory = getValueInt();
- break;
- case Tags.PROVISION_ALLOW_CAMERA:
- policy.mDontAllowCamera = (getValueInt() == 0);
- break;
- case Tags.PROVISION_ALLOW_SIMPLE_DEVICE_PASSWORD:
- // Ignore this unless there's any MSFT documentation for what this means
- // Hint: I haven't seen any that's more specific than "simple"
- getValue();
- break;
- // The following policies, if false, can't be supported at the moment
- case Tags.PROVISION_ALLOW_STORAGE_CARD:
- case Tags.PROVISION_ALLOW_UNSIGNED_APPLICATIONS:
- case Tags.PROVISION_ALLOW_UNSIGNED_INSTALLATION_PACKAGES:
- case Tags.PROVISION_ALLOW_WIFI:
- case Tags.PROVISION_ALLOW_TEXT_MESSAGING:
- case Tags.PROVISION_ALLOW_POP_IMAP_EMAIL:
- case Tags.PROVISION_ALLOW_IRDA:
- case Tags.PROVISION_ALLOW_HTML_EMAIL:
- case Tags.PROVISION_ALLOW_BROWSER:
- case Tags.PROVISION_ALLOW_CONSUMER_EMAIL:
- case Tags.PROVISION_ALLOW_INTERNET_SHARING:
- if (getValueInt() == 0) {
- tagIsSupported = false;
- switch(tag) {
- case Tags.PROVISION_ALLOW_STORAGE_CARD:
- res = R.string.policy_dont_allow_storage_cards;
- break;
- case Tags.PROVISION_ALLOW_UNSIGNED_APPLICATIONS:
- res = R.string.policy_dont_allow_unsigned_apps;
- break;
- case Tags.PROVISION_ALLOW_UNSIGNED_INSTALLATION_PACKAGES:
- res = R.string.policy_dont_allow_unsigned_installers;
- break;
- case Tags.PROVISION_ALLOW_WIFI:
- res = R.string.policy_dont_allow_wifi;
- break;
- case Tags.PROVISION_ALLOW_TEXT_MESSAGING:
- res = R.string.policy_dont_allow_text_messaging;
- break;
- case Tags.PROVISION_ALLOW_POP_IMAP_EMAIL:
- res = R.string.policy_dont_allow_pop_imap;
- break;
- case Tags.PROVISION_ALLOW_IRDA:
- res = R.string.policy_dont_allow_irda;
- break;
- case Tags.PROVISION_ALLOW_HTML_EMAIL:
- res = R.string.policy_dont_allow_html;
- policy.mDontAllowHtml = true;
- break;
- case Tags.PROVISION_ALLOW_BROWSER:
- res = R.string.policy_dont_allow_browser;
- break;
- case Tags.PROVISION_ALLOW_CONSUMER_EMAIL:
- res = R.string.policy_dont_allow_consumer_email;
- break;
- case Tags.PROVISION_ALLOW_INTERNET_SHARING:
- res = R.string.policy_dont_allow_internet_sharing;
- break;
- }
- if (res > 0) {
- unsupportedList.add(res);
- }
- }
- break;
- case Tags.PROVISION_ATTACHMENTS_ENABLED:
- policy.mDontAllowAttachments = getValueInt() != 1;
- break;
- // Bluetooth: 0 = no bluetooth; 1 = only hands-free; 2 = allowed
- case Tags.PROVISION_ALLOW_BLUETOOTH:
- if (getValueInt() != 2) {
- tagIsSupported = false;
- unsupportedList.add(R.string.policy_bluetooth_restricted);
- }
- break;
- // We may now support device (internal) encryption; we'll check this capability
- // below with the call to SecurityPolicy.isSupported()
- case Tags.PROVISION_REQUIRE_DEVICE_ENCRYPTION:
- if (getValueInt() == 1) {
- if (!deviceSupportsEncryption()) {
- tagIsSupported = false;
- unsupportedList.add(R.string.policy_require_encryption);
- } else {
- policy.mRequireEncryption = true;
- }
- }
- break;
- // Note that DEVICE_ENCRYPTION_ENABLED refers to SD card encryption, which the OS
- // does not yet support.
- case Tags.PROVISION_DEVICE_ENCRYPTION_ENABLED:
- if (getValueInt() == 1) {
- log("Policy requires SD card encryption");
- // Let's see if this can be supported on our device...
- if (deviceSupportsEncryption()) {
- StorageManager sm = (StorageManager)mService.mContext.getSystemService(
- Context.STORAGE_SERVICE);
- // NOTE: Private API!
- // Go through volumes; if ANY are removable, we can't support this
- // policy.
- StorageVolume[] volumeList = sm.getVolumeList();
- for (StorageVolume volume: volumeList) {
- if (volume.isRemovable()) {
- tagIsSupported = false;
- log("Removable: " + volume.getDescription());
- break; // Break only from the storage volume loop
- } else {
- log("Not Removable: " + volume.getDescription());
- }
- }
- if (tagIsSupported) {
- // If this policy is requested, we MUST also require encryption
- log("Device supports SD card encryption");
- policy.mRequireEncryption = true;
- break;
- }
- } else {
- log("Device doesn't support encryption; failing");
- tagIsSupported = false;
- }
- // If we fall through, we can't support the policy
- unsupportedList.add(R.string.policy_require_sd_encryption);
- }
- break;
- // Note this policy; we enforce it in ExchangeService
- case Tags.PROVISION_REQUIRE_MANUAL_SYNC_WHEN_ROAMING:
- policy.mRequireManualSyncWhenRoaming = getValueInt() == 1;
- break;
- // We are allowed to accept policies, regardless of value of this tag
- // TODO: When we DO support a recovery password, we need to store the value in
- // the account (so we know to utilize it)
- case Tags.PROVISION_PASSWORD_RECOVERY_ENABLED:
- // Read, but ignore, value
- policy.mPasswordRecoveryEnabled = getValueInt() == 1;
- break;
- // The following policies, if true, can't be supported at the moment
- case Tags.PROVISION_REQUIRE_SIGNED_SMIME_MESSAGES:
- case Tags.PROVISION_REQUIRE_ENCRYPTED_SMIME_MESSAGES:
- case Tags.PROVISION_REQUIRE_SIGNED_SMIME_ALGORITHM:
- case Tags.PROVISION_REQUIRE_ENCRYPTION_SMIME_ALGORITHM:
- if (getValueInt() == 1) {
- tagIsSupported = false;
- if (!smimeRequired) {
- unsupportedList.add(R.string.policy_require_smime);
- smimeRequired = true;
- }
- }
- break;
- case Tags.PROVISION_MAX_ATTACHMENT_SIZE:
- int max = getValueInt();
- if (max > 0) {
- policy.mMaxAttachmentSize = max;
- }
- break;
- // Complex characters are supported
- case Tags.PROVISION_MIN_DEVICE_PASSWORD_COMPLEX_CHARS:
- policy.mPasswordComplexChars = getValueInt();
- break;
- // The following policies are moot; they allow functionality that we don't support
- case Tags.PROVISION_ALLOW_DESKTOP_SYNC:
- case Tags.PROVISION_ALLOW_SMIME_ENCRYPTION_NEGOTIATION:
- case Tags.PROVISION_ALLOW_SMIME_SOFT_CERTS:
- case Tags.PROVISION_ALLOW_REMOTE_DESKTOP:
- skipTag();
- break;
- // We don't handle approved/unapproved application lists
- case Tags.PROVISION_UNAPPROVED_IN_ROM_APPLICATION_LIST:
- case Tags.PROVISION_APPROVED_APPLICATION_LIST:
- // Parse and throw away the content
- if (specifiesApplications(tag)) {
- tagIsSupported = false;
- if (tag == Tags.PROVISION_UNAPPROVED_IN_ROM_APPLICATION_LIST) {
- unsupportedList.add(R.string.policy_app_blacklist);
- } else {
- unsupportedList.add(R.string.policy_app_whitelist);
- }
- }
- break;
- // We accept calendar age, since we never ask for more than two weeks, and that's
- // the most restrictive policy
- case Tags.PROVISION_MAX_CALENDAR_AGE_FILTER:
- policy.mMaxCalendarLookback = getValueInt();
- break;
- // We handle max email lookback
- case Tags.PROVISION_MAX_EMAIL_AGE_FILTER:
- policy.mMaxEmailLookback = getValueInt();
- break;
- // We currently reject these next two policies
- case Tags.PROVISION_MAX_EMAIL_BODY_TRUNCATION_SIZE:
- case Tags.PROVISION_MAX_EMAIL_HTML_BODY_TRUNCATION_SIZE:
- String value = getValue();
- // -1 indicates no required truncation
- if (!value.equals("-1")) {
- max = Integer.parseInt(value);
- if (tag == Tags.PROVISION_MAX_EMAIL_BODY_TRUNCATION_SIZE) {
- policy.mMaxTextTruncationSize = max;
- unsupportedList.add(R.string.policy_text_truncation);
- } else {
- policy.mMaxHtmlTruncationSize = max;
- unsupportedList.add(R.string.policy_html_truncation);
- }
- tagIsSupported = false;
- }
- break;
- default:
- skipTag();
- }
-
- if (!tagIsSupported) {
- log("Policy not supported: " + tag);
- mIsSupportable = false;
- }
- }
-
- // Make sure policy settings are valid; password not enabled trumps other password settings
- if (!passwordEnabled) {
- policy.mPasswordMode = Policy.PASSWORD_MODE_NONE;
- }
- setPolicy(policy);
-
- // We can only determine whether encryption is supported on device by using isSupported here
- if (!SecurityPolicyDelegate.isSupported(mService.mContext, policy)) {
- log("SecurityPolicy reports PolicySet not supported.");
- mIsSupportable = false;
- unsupportedList.add(R.string.policy_require_encryption);
- }
-
- if (!unsupportedList.isEmpty()) {
- mUnsupportedPolicies = new String[unsupportedList.size()];
- int i = 0;
- Context context = ExchangeService.getContext();
- if (context != null) {
- Resources resources = context.getResources();
- for (int res: unsupportedList) {
- mUnsupportedPolicies[i++] = resources.getString(res);
- }
- }
- }
- }
-
- /**
- * Return whether or not either of the application list tags specifies any applications
- * @param endTag the tag whose children we're walking through
- * @return whether any applications were specified (by name or by hash)
- * @throws IOException
- */
- private boolean specifiesApplications(int endTag) throws IOException {
- boolean specifiesApplications = false;
- while (nextTag(endTag) != END) {
- switch (tag) {
- case Tags.PROVISION_APPLICATION_NAME:
- case Tags.PROVISION_HASH:
- specifiesApplications = true;
- break;
- default:
- skipTag();
- }
- }
- return specifiesApplications;
- }
-
- /*package*/ void parseProvisionDocXml(String doc) throws IOException {
- Policy policy = new Policy();
-
- try {
- XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
- XmlPullParser parser = factory.newPullParser();
- parser.setInput(new ByteArrayInputStream(doc.getBytes()), "UTF-8");
- int type = parser.getEventType();
- if (type == XmlPullParser.START_DOCUMENT) {
- type = parser.next();
- if (type == XmlPullParser.START_TAG) {
- String tagName = parser.getName();
- if (tagName.equals("wap-provisioningdoc")) {
- parseWapProvisioningDoc(parser, policy);
- }
- }
- }
- } catch (XmlPullParserException e) {
- throw new IOException();
- }
-
- setPolicy(policy);
- }
-
- /**
- * Return true if password is required; otherwise false.
- */
- private boolean parseSecurityPolicy(XmlPullParser parser, Policy policy)
- throws XmlPullParserException, IOException {
- boolean passwordRequired = true;
- while (true) {
- int type = parser.nextTag();
- if (type == XmlPullParser.END_TAG && parser.getName().equals("characteristic")) {
- break;
- } else if (type == XmlPullParser.START_TAG) {
- String tagName = parser.getName();
- if (tagName.equals("parm")) {
- String name = parser.getAttributeValue(null, "name");
- if (name.equals("4131")) {
- String value = parser.getAttributeValue(null, "value");
- if (value.equals("1")) {
- passwordRequired = false;
- }
- }
- }
- }
- }
- return passwordRequired;
- }
-
- private void parseCharacteristic(XmlPullParser parser, Policy policy)
- throws XmlPullParserException, IOException {
- boolean enforceInactivityTimer = true;
- while (true) {
- int type = parser.nextTag();
- if (type == XmlPullParser.END_TAG && parser.getName().equals("characteristic")) {
- break;
- } else if (type == XmlPullParser.START_TAG) {
- if (parser.getName().equals("parm")) {
- String name = parser.getAttributeValue(null, "name");
- String value = parser.getAttributeValue(null, "value");
- if (name.equals("AEFrequencyValue")) {
- if (enforceInactivityTimer) {
- if (value.equals("0")) {
- policy.mMaxScreenLockTime = 1;
- } else {
- policy.mMaxScreenLockTime = 60*Integer.parseInt(value);
- }
- }
- } else if (name.equals("AEFrequencyType")) {
- // "0" here means we don't enforce an inactivity timeout
- if (value.equals("0")) {
- enforceInactivityTimer = false;
- }
- } else if (name.equals("DeviceWipeThreshold")) {
- policy.mPasswordMaxFails = Integer.parseInt(value);
- } else if (name.equals("CodewordFrequency")) {
- // Ignore; has no meaning for us
- } else if (name.equals("MinimumPasswordLength")) {
- policy.mPasswordMinLength = Integer.parseInt(value);
- } else if (name.equals("PasswordComplexity")) {
- if (value.equals("0")) {
- policy.mPasswordMode = Policy.PASSWORD_MODE_STRONG;
- } else {
- policy.mPasswordMode = Policy.PASSWORD_MODE_SIMPLE;
- }
- }
- }
- }
- }
- }
-
- private void parseRegistry(XmlPullParser parser, Policy policy)
- throws XmlPullParserException, IOException {
- while (true) {
- int type = parser.nextTag();
- if (type == XmlPullParser.END_TAG && parser.getName().equals("characteristic")) {
- break;
- } else if (type == XmlPullParser.START_TAG) {
- String name = parser.getName();
- if (name.equals("characteristic")) {
- parseCharacteristic(parser, policy);
- }
- }
- }
- }
-
- private void parseWapProvisioningDoc(XmlPullParser parser, Policy policy)
- throws XmlPullParserException, IOException {
- while (true) {
- int type = parser.nextTag();
- if (type == XmlPullParser.END_TAG && parser.getName().equals("wap-provisioningdoc")) {
- break;
- } else if (type == XmlPullParser.START_TAG) {
- String name = parser.getName();
- if (name.equals("characteristic")) {
- String atype = parser.getAttributeValue(null, "type");
- if (atype.equals("SecurityPolicy")) {
- // If a password isn't required, stop here
- if (!parseSecurityPolicy(parser, policy)) {
- return;
- }
- } else if (atype.equals("Registry")) {
- parseRegistry(parser, policy);
- return;
- }
- }
- }
- }
- }
-
- private void parseProvisionData() throws IOException {
- while (nextTag(Tags.PROVISION_DATA) != END) {
- if (tag == Tags.PROVISION_EAS_PROVISION_DOC) {
- parseProvisionDocWbxml();
- } else {
- skipTag();
- }
- }
- }
-
- private void parsePolicy() throws IOException {
- String policyType = null;
- while (nextTag(Tags.PROVISION_POLICY) != END) {
- switch (tag) {
- case Tags.PROVISION_POLICY_TYPE:
- policyType = getValue();
- mService.userLog("Policy type: ", policyType);
- break;
- case Tags.PROVISION_POLICY_KEY:
- mSecuritySyncKey = getValue();
- break;
- case Tags.PROVISION_STATUS:
- mService.userLog("Policy status: ", getValue());
- break;
- case Tags.PROVISION_DATA:
- if (policyType.equalsIgnoreCase(EasSyncService.EAS_2_POLICY_TYPE)) {
- // Parse the old style XML document
- parseProvisionDocXml(getValue());
- } else {
- // Parse the newer WBXML data
- parseProvisionData();
- }
- break;
- default:
- skipTag();
- }
- }
- }
-
- private void parsePolicies() throws IOException {
- while (nextTag(Tags.PROVISION_POLICIES) != END) {
- if (tag == Tags.PROVISION_POLICY) {
- parsePolicy();
- } else {
- skipTag();
- }
- }
- }
-
- private void parseDeviceInformation() throws IOException {
- while (nextTag(Tags.SETTINGS_DEVICE_INFORMATION) != END) {
- if (tag == Tags.SETTINGS_STATUS) {
- mService.userLog("DeviceInformation status: " + getValue());
- } else {
- skipTag();
- }
- }
- }
-
- @Override
- public boolean parse() throws IOException {
- boolean res = false;
- if (nextTag(START_DOCUMENT) != Tags.PROVISION_PROVISION) {
- throw new IOException();
- }
- while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
- switch (tag) {
- case Tags.PROVISION_STATUS:
- int status = getValueInt();
- mService.userLog("Provision status: ", status);
- res = (status == 1);
- break;
- case Tags.SETTINGS_DEVICE_INFORMATION:
- parseDeviceInformation();
- break;
- case Tags.PROVISION_POLICIES:
- parsePolicies();
- break;
- case Tags.PROVISION_REMOTE_WIPE:
- // Indicate remote wipe command received
- mRemoteWipe = true;
- break;
- default:
- skipTag();
- }
- }
- return res;
- }
-}
diff --git a/src/com/android/exchange/adapter/Search.java b/src/com/android/exchange/adapter/Search.java
deleted file mode 100644
index 309ff79..0000000
--- a/src/com/android/exchange/adapter/Search.java
+++ /dev/null
@@ -1,248 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.adapter;
-
-import android.content.ContentProviderOperation;
-import android.content.Context;
-import android.content.OperationApplicationException;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent;
-import com.android.emailcommon.provider.EmailContent.Message;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.emailcommon.service.EmailServiceStatus;
-import com.android.emailcommon.service.SearchParams;
-import com.android.emailcommon.utility.TextUtilities;
-import com.android.exchange.Eas;
-import com.android.exchange.EasResponse;
-import com.android.exchange.EasSyncService;
-import com.android.exchange.ExchangeService;
-import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
-
-import org.apache.http.HttpStatus;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-
-/**
- * Implementation of server-side search for EAS using the EmailService API
- */
-public class Search {
- // The shortest search query we'll accept
- // TODO Check with UX whether this is correct
- private static final int MIN_QUERY_LENGTH = 3;
- // The largest number of results we'll ask for per server request
- private static final int MAX_SEARCH_RESULTS = 100;
-
- public static int searchMessages(Context context, long accountId, SearchParams searchParams,
- long destMailboxId) {
- // Sanity check for arguments
- int offset = searchParams.mOffset;
- int limit = searchParams.mLimit;
- String filter = searchParams.mFilter;
- if (limit < 0 || limit > MAX_SEARCH_RESULTS || offset < 0) return 0;
- // TODO Should this be checked in UI? Are there guidelines for minimums?
- if (filter == null || filter.length() < MIN_QUERY_LENGTH) return 0;
-
- int res = 0;
- Account account = Account.restoreAccountWithId(context, accountId);
- if (account == null) return res;
- EasSyncService svc = EasSyncService.setupServiceForAccount(context, account);
- if (svc == null) return res;
- try {
- Mailbox searchMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId);
- // Sanity check; account might have been deleted?
- if (searchMailbox == null) return res;
- svc.mMailbox = searchMailbox;
- svc.mAccount = account;
- Serializer s = new Serializer();
- s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE);
- s.data(Tags.SEARCH_NAME, "Mailbox");
- s.start(Tags.SEARCH_QUERY).start(Tags.SEARCH_AND);
- s.data(Tags.SYNC_CLASS, "Email");
-
- // If this isn't an inbox search, then include the collection id
- Mailbox inbox = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_INBOX);
- if (inbox == null) return 0;
- if (searchParams.mMailboxId != inbox.mId) {
- s.data(Tags.SYNC_COLLECTION_ID, inbox.mServerId);
- }
-
- s.data(Tags.SEARCH_FREE_TEXT, filter);
- s.end().end(); // SEARCH_AND, SEARCH_QUERY
- s.start(Tags.SEARCH_OPTIONS);
- if (offset == 0) {
- s.tag(Tags.SEARCH_REBUILD_RESULTS);
- }
- if (searchParams.mIncludeChildren) {
- s.tag(Tags.SEARCH_DEEP_TRAVERSAL);
- }
- // Range is sent in the form first-last (e.g. 0-9)
- s.data(Tags.SEARCH_RANGE, offset + "-" + (offset + limit - 1));
- s.start(Tags.BASE_BODY_PREFERENCE);
- s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_HTML);
- s.data(Tags.BASE_TRUNCATION_SIZE, "20000");
- s.end(); // BASE_BODY_PREFERENCE
- s.end().end().end().done(); // SEARCH_OPTIONS, SEARCH_STORE, SEARCH_SEARCH
- EasResponse resp = svc.sendHttpClientPost("Search", s.toByteArray());
- try {
- int code = resp.getStatus();
- if (code == HttpStatus.SC_OK) {
- InputStream is = resp.getInputStream();
- try {
- SearchParser sp = new SearchParser(is, svc, filter);
- sp.parse();
- res = sp.getTotalResults();
- } finally {
- is.close();
- }
- } else {
- svc.userLog("Search returned " + code);
- }
- } finally {
- resp.close();
- }
- } catch (IOException e) {
- svc.userLog("Search exception " + e);
- } finally {
- try {
- ExchangeService.callback().syncMailboxStatus(destMailboxId,
- EmailServiceStatus.SUCCESS, 100);
- } catch (RemoteException e) {
- }
- }
- // Return the total count
- return res;
- }
-
- /**
- * Parse the result of a Search command
- */
- static class SearchParser extends Parser {
- private final EasSyncService mService;
- private final String mQuery;
- private int mTotalResults;
-
- private SearchParser(InputStream in, EasSyncService service, String query)
- throws IOException {
- super(in);
- mService = service;
- mQuery = query;
- }
-
- protected int getTotalResults() {
- return mTotalResults;
- }
-
- @Override
- public boolean parse() throws IOException {
- boolean res = false;
- if (nextTag(START_DOCUMENT) != Tags.SEARCH_SEARCH) {
- throw new IOException();
- }
- while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
- if (tag == Tags.SEARCH_STATUS) {
- String status = getValue();
- if (Eas.USER_LOG) {
- Log.d(Logging.LOG_TAG, "Search status: " + status);
- }
- } else if (tag == Tags.SEARCH_RESPONSE) {
- parseResponse();
- } else {
- skipTag();
- }
- }
- return res;
- }
-
- private boolean parseResponse() throws IOException {
- boolean res = false;
- while (nextTag(Tags.SEARCH_RESPONSE) != END) {
- if (tag == Tags.SEARCH_STORE) {
- parseStore();
- } else {
- skipTag();
- }
- }
- return res;
- }
-
- private boolean parseStore() throws IOException {
- EmailSyncAdapter adapter = new EmailSyncAdapter(mService);
- EasEmailSyncParser parser = adapter.new EasEmailSyncParser(this, adapter);
- ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
- boolean res = false;
-
- while (nextTag(Tags.SEARCH_STORE) != END) {
- if (tag == Tags.SEARCH_STATUS) {
- String status = getValue();
- } else if (tag == Tags.SEARCH_TOTAL) {
- mTotalResults = getValueInt();
- } else if (tag == Tags.SEARCH_RESULT) {
- parseResult(parser, ops);
- } else {
- skipTag();
- }
- }
-
- try {
- adapter.mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
- if (Eas.USER_LOG) {
- mService.userLog("Saved " + ops.size() + " search results");
- }
- } catch (RemoteException e) {
- Log.d(Logging.LOG_TAG, "RemoteException while saving search results.");
- } catch (OperationApplicationException e) {
- }
-
- return res;
- }
-
- private boolean parseResult(EasEmailSyncParser parser,
- ArrayList<ContentProviderOperation> ops) throws IOException {
- // Get an email sync parser for our incoming message data
- boolean res = false;
- Message msg = new Message();
- while (nextTag(Tags.SEARCH_RESULT) != END) {
- if (tag == Tags.SYNC_CLASS) {
- getValue();
- } else if (tag == Tags.SYNC_COLLECTION_ID) {
- getValue();
- } else if (tag == Tags.SEARCH_LONG_ID) {
- msg.mProtocolSearchInfo = getValue();
- } else if (tag == Tags.SEARCH_PROPERTIES) {
- msg.mAccountKey = mService.mAccount.mId;
- msg.mMailboxKey = mService.mMailbox.mId;
- msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
- parser.pushTag(tag);
- parser.addData(msg, tag);
- if (msg.mHtml != null) {
- msg.mHtml = TextUtilities.highlightTermsInHtml(msg.mHtml, mQuery);
- }
- msg.addSaveOps(ops);
- } else {
- skipTag();
- }
- }
- return res;
- }
- }
-}
diff --git a/src/com/android/exchange/adapter/Serializer.java b/src/com/android/exchange/adapter/Serializer.java
deleted file mode 100644
index bc79603..0000000
--- a/src/com/android/exchange/adapter/Serializer.java
+++ /dev/null
@@ -1,250 +0,0 @@
-/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or
- * sell copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
- * IN THE SOFTWARE. */
-
-//Contributors: Jonathan Cox, Bogdan Onoiu, Jerry Tian
-// Greatly simplified for Google, Inc. by Marc Blank
-
-package com.android.exchange.adapter;
-
-import android.content.ContentValues;
-import android.util.Log;
-
-import com.android.exchange.Eas;
-import com.android.exchange.utility.FileLogger;
-import com.google.common.annotations.VisibleForTesting;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-public class Serializer {
- private static final String TAG = "Serializer";
- private static final int BUFFER_SIZE = 16*1024;
- private static final int NOT_PENDING = -1;
-
- private final OutputStream mOutput;
- private int mPendingTag = NOT_PENDING;
- private int mDepth;
- private String[] mNameStack = new String[20];
- private int mTagPage = 0;
- private boolean mLogging = Log.isLoggable(TAG, Log.VERBOSE);
-
- public Serializer() throws IOException {
- this(new ByteArrayOutputStream(), true);
- }
-
- public Serializer(OutputStream os) throws IOException {
- this(os, true);
- }
-
- @VisibleForTesting
- public Serializer(boolean startDocument) throws IOException {
- this(new ByteArrayOutputStream(), startDocument);
- }
-
- /**
- * Base constructor
- * @param outputStream the stream we're serializing to
- * @param startDocument whether or not to start a document
- * @param _logging whether or not to log our output
- * @throws IOException
- */
- public Serializer(OutputStream outputStream, boolean startDocument) throws IOException {
- super();
- mOutput = outputStream;
- if (startDocument) {
- startDocument();
- } else {
- mOutput.write(0);
- }
- }
-
- void log(String str) {
- int cr = str.indexOf('\n');
- if (cr > 0) {
- str = str.substring(0, cr);
- }
- Log.v(TAG, str);
- if (Eas.FILE_LOG) {
- FileLogger.log(TAG, str);
- }
- }
-
- public void done() throws IOException {
- if (mDepth != 0) {
- throw new IOException("Done received with unclosed tags");
- }
- mOutput.flush();
- }
-
- public void startDocument() throws IOException{
- mOutput.write(0x03); // version 1.3
- mOutput.write(0x01); // unknown or missing public identifier
- mOutput.write(106); // UTF-8
- mOutput.write(0); // 0 length string array
- }
-
- public void checkPendingTag(boolean degenerated) throws IOException {
- if (mPendingTag == NOT_PENDING)
- return;
-
- int page = mPendingTag >> Tags.PAGE_SHIFT;
- int tag = mPendingTag & Tags.PAGE_MASK;
- if (page != mTagPage) {
- mTagPage = page;
- mOutput.write(Wbxml.SWITCH_PAGE);
- mOutput.write(page);
- }
-
- mOutput.write(degenerated ? tag : tag | Wbxml.WITH_CONTENT);
- if (mLogging) {
- String name = Tags.pages[page][tag - 5];
- mNameStack[mDepth] = name;
- log("<" + name + '>');
- }
- mPendingTag = NOT_PENDING;
- }
-
- public Serializer start(int tag) throws IOException {
- checkPendingTag(false);
- mPendingTag = tag;
- mDepth++;
- return this;
- }
-
- public Serializer end() throws IOException {
- if (mPendingTag >= 0) {
- checkPendingTag(true);
- } else {
- mOutput.write(Wbxml.END);
- if (mLogging) {
- log("</" + mNameStack[mDepth] + '>');
- }
- }
- mDepth--;
- return this;
- }
-
- public Serializer tag(int t) throws IOException {
- start(t);
- end();
- return this;
- }
-
- public Serializer data(int tag, String value) throws IOException {
- if (value == null) {
- Log.e(TAG, "Writing null data for tag: " + tag);
- }
- start(tag);
- text(value);
- end();
- return this;
- }
-
- public Serializer text(String text) throws IOException {
- if (text == null) {
- Log.e(TAG, "Writing null text for pending tag: " + mPendingTag);
- }
- checkPendingTag(false);
- mOutput.write(Wbxml.STR_I);
- writeLiteralString(mOutput, text);
- if (mLogging) {
- log(text);
- }
- return this;
- }
-
- public Serializer opaque(InputStream is, int length) throws IOException {
- checkPendingTag(false);
- mOutput.write(Wbxml.OPAQUE);
- writeInteger(mOutput, length);
- if (mLogging) {
- log("Opaque, length: " + length);
- }
- // Now write out the opaque data in batches
- byte[] buffer = new byte[BUFFER_SIZE];
- while (length > 0) {
- int bytesRead = is.read(buffer, 0, (int)Math.min(BUFFER_SIZE, length));
- if (bytesRead == -1) {
- break;
- }
- mOutput.write(buffer, 0, bytesRead);
- length -= bytesRead;
- }
- return this;
- }
-
- public Serializer opaqueWithoutData(int length) throws IOException {
- checkPendingTag(false);
- mOutput.write(Wbxml.OPAQUE);
- writeInteger(mOutput, length);
- return this;
- }
-
- void writeInteger(OutputStream out, int i) throws IOException {
- byte[] buf = new byte[5];
- int idx = 0;
-
- do {
- buf[idx++] = (byte) (i & 0x7f);
- i = i >> 7;
- } while (i != 0);
-
- while (idx > 1) {
- out.write(buf[--idx] | 0x80);
- }
- out.write(buf[0]);
- if (mLogging) {
- log(Integer.toString(i));
- }
- }
-
- void writeLiteralString(OutputStream out, String s) throws IOException {
- byte[] data = s.getBytes("UTF-8");
- out.write(data);
- out.write(0);
- }
-
- void writeStringValue (ContentValues cv, String key, int tag) throws IOException {
- String value = cv.getAsString(key);
- if (value != null && value.length() > 0) {
- data(tag, value);
- } else {
- tag(tag);
- }
- }
-
- @Override
- public String toString() {
- if (mOutput instanceof ByteArrayOutputStream) {
- return ((ByteArrayOutputStream)mOutput).toString();
- }
- throw new IllegalStateException();
- }
-
- public byte[] toByteArray() {
- if (mOutput instanceof ByteArrayOutputStream) {
- return ((ByteArrayOutputStream)mOutput).toByteArray();
- }
- throw new IllegalStateException();
- }
-
-}
diff --git a/src/com/android/exchange/adapter/SettingsParser.java b/src/com/android/exchange/adapter/SettingsParser.java
deleted file mode 100644
index 2cbc753..0000000
--- a/src/com/android/exchange/adapter/SettingsParser.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/* Copyright (C) 2011 The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.adapter;
-
-import com.android.exchange.EasSyncService;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Parse the result of a Settings command.
- *
- * We only send the Settings command in EAS 14.0 after sending a Provision command for the first
- * time. parse() returns true in the normal case; false if access to the account is denied due
- * to the actual settings (e.g. if a particular device type isn't allowed by the server)
- */
-public class SettingsParser extends Parser {
- private final EasSyncService mService;
-
- public SettingsParser(InputStream in, EasSyncService service) throws IOException {
- super(in);
- mService = service;
- }
-
- @Override
- public boolean parse() throws IOException {
- boolean res = false;
- if (nextTag(START_DOCUMENT) != Tags.SETTINGS_SETTINGS) {
- throw new IOException();
- }
- while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
- if (tag == Tags.SETTINGS_STATUS) {
- int status = getValueInt();
- mService.userLog("Settings status = ", status);
- if (status == 1) {
- res = true;
- } else {
- // Access denied = 3; others should never be seen
- res = false;
- }
- } else if (tag == Tags.SETTINGS_DEVICE_INFORMATION) {
- parseDeviceInformation();
- } else {
- skipTag();
- }
- }
- return res;
- }
-
- public void parseDeviceInformation() throws IOException {
- while (nextTag(Tags.SETTINGS_DEVICE_INFORMATION) != END) {
- if (tag == Tags.SETTINGS_SET) {
- parseSet();
- } else {
- skipTag();
- }
- }
- }
-
- public void parseSet() throws IOException {
- while (nextTag(Tags.SETTINGS_SET) != END) {
- if (tag == Tags.SETTINGS_STATUS) {
- mService.userLog("Set status = ", getValueInt());
- } else {
- skipTag();
- }
- }
- }
-}
diff --git a/src/com/android/exchange/adapter/Tags.java b/src/com/android/exchange/adapter/Tags.java
deleted file mode 100644
index 183a103..0000000
--- a/src/com/android/exchange/adapter/Tags.java
+++ /dev/null
@@ -1,813 +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.adapter;
-
-/**
- * The wbxml tags for EAS are all defined here.
- *
- * The static final int's, of the form <page>_<tag> = <constant> are used in parsing incoming
- * responses from the server (i.e. EasParser and its subclasses) and sending requests to the
- * server (via Serializer)
- *
- * The array of string arrays is used only for generating logging output
- */
-public class Tags {
-
- // Wbxml page definitions for EAS
- public static final int AIRSYNC = 0x00;
- public static final int CONTACTS = 0x01;
- public static final int EMAIL = 0x02;
- public static final int CALENDAR = 0x04;
- public static final int MOVE = 0x05;
- public static final int GIE = 0x06;
- public static final int FOLDER = 0x07;
- public static final int MREQ = 0x08;
- public static final int TASK = 0x09;
- public static final int CONTACTS2 = 0x0C;
- public static final int PING = 0x0D;
- public static final int PROVISION = 0x0E;
- public static final int SEARCH = 0x0F;
- public static final int GAL = 0x10;
- public static final int BASE = 0x11;
- public static final int SETTINGS = 0x12;
- public static final int DOCS = 0x13;
- public static final int ITEMS = 0x14;
- // 14.0
- public static final int COMPOSE = 0x15;
- public static final int EMAIL2 = 0x16;
- // 14.1
- public static final int NOTES = 0x17;
- public static final int RIGHTS = 0x18;
-
- // Shift applied to page numbers to generate tag
- public static final int PAGE_SHIFT = 6;
- public static final int PAGE_MASK = 0x3F; // 6 bits
-
- public static final int SYNC_PAGE = 0 << PAGE_SHIFT;
- public static final int SYNC_SYNC = SYNC_PAGE + 5;
- public static final int SYNC_RESPONSES = SYNC_PAGE + 6;
- public static final int SYNC_ADD = SYNC_PAGE + 7;
- public static final int SYNC_CHANGE = SYNC_PAGE + 8;
- public static final int SYNC_DELETE = SYNC_PAGE + 9;
- public static final int SYNC_FETCH = SYNC_PAGE + 0xA;
- public static final int SYNC_SYNC_KEY = SYNC_PAGE + 0xB;
- public static final int SYNC_CLIENT_ID = SYNC_PAGE + 0xC;
- public static final int SYNC_SERVER_ID = SYNC_PAGE + 0xD;
- public static final int SYNC_STATUS = SYNC_PAGE + 0xE;
- public static final int SYNC_COLLECTION = SYNC_PAGE + 0xF;
- public static final int SYNC_CLASS = SYNC_PAGE + 0x10;
- public static final int SYNC_VERSION = SYNC_PAGE + 0x11;
- public static final int SYNC_COLLECTION_ID = SYNC_PAGE + 0x12;
- public static final int SYNC_GET_CHANGES = SYNC_PAGE + 0x13;
- public static final int SYNC_MORE_AVAILABLE = SYNC_PAGE + 0x14;
- public static final int SYNC_WINDOW_SIZE = SYNC_PAGE + 0x15;
- public static final int SYNC_COMMANDS = SYNC_PAGE + 0x16;
- public static final int SYNC_OPTIONS = SYNC_PAGE + 0x17;
- public static final int SYNC_FILTER_TYPE = SYNC_PAGE + 0x18;
- public static final int SYNC_TRUNCATION = SYNC_PAGE + 0x19;
- public static final int SYNC_RTF_TRUNCATION = SYNC_PAGE + 0x1A;
- public static final int SYNC_CONFLICT = SYNC_PAGE + 0x1B;
- public static final int SYNC_COLLECTIONS = SYNC_PAGE + 0x1C;
- public static final int SYNC_APPLICATION_DATA = SYNC_PAGE + 0x1D;
- public static final int SYNC_DELETES_AS_MOVES = SYNC_PAGE + 0x1E;
- public static final int SYNC_NOTIFY_GUID = SYNC_PAGE + 0x1F;
- public static final int SYNC_SUPPORTED = SYNC_PAGE + 0x20;
- public static final int SYNC_SOFT_DELETE = SYNC_PAGE + 0x21;
- public static final int SYNC_MIME_SUPPORT = SYNC_PAGE + 0x22;
- public static final int SYNC_MIME_TRUNCATION = SYNC_PAGE + 0x23;
- public static final int SYNC_WAIT = SYNC_PAGE + 0x24;
- public static final int SYNC_LIMIT = SYNC_PAGE + 0x25;
- public static final int SYNC_PARTIAL = SYNC_PAGE + 0x26;
-
- public static final int GIE_PAGE = GIE << PAGE_SHIFT;
- public static final int GIE_GET_ITEM_ESTIMATE = GIE_PAGE + 5;
- public static final int GIE_VERSION = GIE_PAGE + 6;
- public static final int GIE_COLLECTIONS = GIE_PAGE + 7;
- public static final int GIE_COLLECTION = GIE_PAGE + 8;
- public static final int GIE_CLASS = GIE_PAGE + 9;
- public static final int GIE_COLLECTION_ID = GIE_PAGE + 0xA;
- public static final int GIE_DATE_TIME = GIE_PAGE + 0xB;
- public static final int GIE_ESTIMATE = GIE_PAGE + 0xC;
- public static final int GIE_RESPONSE = GIE_PAGE + 0xD;
- public static final int GIE_STATUS = GIE_PAGE + 0xE;
-
- public static final int CONTACTS_PAGE = CONTACTS << PAGE_SHIFT;
- public static final int CONTACTS_ANNIVERSARY = CONTACTS_PAGE + 5;
- public static final int CONTACTS_ASSISTANT_NAME = CONTACTS_PAGE + 6;
- public static final int CONTACTS_ASSISTANT_TELEPHONE_NUMBER = CONTACTS_PAGE + 7;
- public static final int CONTACTS_BIRTHDAY = CONTACTS_PAGE + 8;
- public static final int CONTACTS_BODY = CONTACTS_PAGE + 9;
- public static final int CONTACTS_BODY_SIZE = CONTACTS_PAGE + 0xA;
- public static final int CONTACTS_BODY_TRUNCATED = CONTACTS_PAGE + 0xB;
- public static final int CONTACTS_BUSINESS2_TELEPHONE_NUMBER = CONTACTS_PAGE + 0xC;
- public static final int CONTACTS_BUSINESS_ADDRESS_CITY = CONTACTS_PAGE + 0xD;
- public static final int CONTACTS_BUSINESS_ADDRESS_COUNTRY = CONTACTS_PAGE + 0xE;
- public static final int CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE = CONTACTS_PAGE + 0xF;
- public static final int CONTACTS_BUSINESS_ADDRESS_STATE = CONTACTS_PAGE + 0x10;
- public static final int CONTACTS_BUSINESS_ADDRESS_STREET = CONTACTS_PAGE + 0x11;
- public static final int CONTACTS_BUSINESS_FAX_NUMBER = CONTACTS_PAGE + 0x12;
- public static final int CONTACTS_BUSINESS_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x13;
- public static final int CONTACTS_CAR_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x14;
- public static final int CONTACTS_CATEGORIES = CONTACTS_PAGE + 0x15;
- public static final int CONTACTS_CATEGORY = CONTACTS_PAGE + 0x16;
- public static final int CONTACTS_CHILDREN = CONTACTS_PAGE + 0x17;
- public static final int CONTACTS_CHILD = CONTACTS_PAGE + 0x18;
- public static final int CONTACTS_COMPANY_NAME = CONTACTS_PAGE + 0x19;
- public static final int CONTACTS_DEPARTMENT = CONTACTS_PAGE + 0x1A;
- public static final int CONTACTS_EMAIL1_ADDRESS = CONTACTS_PAGE + 0x1B;
- public static final int CONTACTS_EMAIL2_ADDRESS = CONTACTS_PAGE + 0x1C;
- public static final int CONTACTS_EMAIL3_ADDRESS = CONTACTS_PAGE + 0x1D;
- public static final int CONTACTS_FILE_AS = CONTACTS_PAGE + 0x1E;
- public static final int CONTACTS_FIRST_NAME = CONTACTS_PAGE + 0x1F;
- public static final int CONTACTS_HOME2_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x20;
- public static final int CONTACTS_HOME_ADDRESS_CITY = CONTACTS_PAGE + 0x21;
- public static final int CONTACTS_HOME_ADDRESS_COUNTRY = CONTACTS_PAGE + 0x22;
- public static final int CONTACTS_HOME_ADDRESS_POSTAL_CODE = CONTACTS_PAGE + 0x23;
- public static final int CONTACTS_HOME_ADDRESS_STATE = CONTACTS_PAGE + 0x24;
- public static final int CONTACTS_HOME_ADDRESS_STREET = CONTACTS_PAGE + 0x25;
- public static final int CONTACTS_HOME_FAX_NUMBER = CONTACTS_PAGE + 0x26;
- public static final int CONTACTS_HOME_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x27;
- public static final int CONTACTS_JOB_TITLE = CONTACTS_PAGE + 0x28;
- public static final int CONTACTS_LAST_NAME = CONTACTS_PAGE + 0x29;
- public static final int CONTACTS_MIDDLE_NAME = CONTACTS_PAGE + 0x2A;
- public static final int CONTACTS_MOBILE_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x2B;
- public static final int CONTACTS_OFFICE_LOCATION = CONTACTS_PAGE + 0x2C;
- public static final int CONTACTS_OTHER_ADDRESS_CITY = CONTACTS_PAGE + 0x2D;
- public static final int CONTACTS_OTHER_ADDRESS_COUNTRY = CONTACTS_PAGE + 0x2E;
- public static final int CONTACTS_OTHER_ADDRESS_POSTAL_CODE = CONTACTS_PAGE + 0x2F;
- public static final int CONTACTS_OTHER_ADDRESS_STATE = CONTACTS_PAGE + 0x30;
- public static final int CONTACTS_OTHER_ADDRESS_STREET = CONTACTS_PAGE + 0x31;
- public static final int CONTACTS_PAGER_NUMBER = CONTACTS_PAGE + 0x32;
- public static final int CONTACTS_RADIO_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x33;
- public static final int CONTACTS_SPOUSE = CONTACTS_PAGE + 0x34;
- public static final int CONTACTS_SUFFIX = CONTACTS_PAGE + 0x35;
- public static final int CONTACTS_TITLE = CONTACTS_PAGE + 0x36;
- public static final int CONTACTS_WEBPAGE = CONTACTS_PAGE + 0x37;
- public static final int CONTACTS_YOMI_COMPANY_NAME = CONTACTS_PAGE + 0x38;
- public static final int CONTACTS_YOMI_FIRST_NAME = CONTACTS_PAGE + 0x39;
- public static final int CONTACTS_YOMI_LAST_NAME = CONTACTS_PAGE + 0x3A;
- public static final int CONTACTS_COMPRESSED_RTF = CONTACTS_PAGE + 0x3B;
- public static final int CONTACTS_PICTURE = CONTACTS_PAGE + 0x3C;
-
- public static final int CALENDAR_PAGE = CALENDAR << PAGE_SHIFT;
- public static final int CALENDAR_TIME_ZONE = CALENDAR_PAGE + 5;
- public static final int CALENDAR_ALL_DAY_EVENT = CALENDAR_PAGE + 6;
- public static final int CALENDAR_ATTENDEES = CALENDAR_PAGE + 7;
- public static final int CALENDAR_ATTENDEE = CALENDAR_PAGE + 8;
- public static final int CALENDAR_ATTENDEE_EMAIL = CALENDAR_PAGE + 9;
- public static final int CALENDAR_ATTENDEE_NAME = CALENDAR_PAGE + 0xA;
- public static final int CALENDAR_BODY = CALENDAR_PAGE + 0xB;
- public static final int CALENDAR_BODY_TRUNCATED = CALENDAR_PAGE + 0xC;
- public static final int CALENDAR_BUSY_STATUS = CALENDAR_PAGE + 0xD;
- public static final int CALENDAR_CATEGORIES = CALENDAR_PAGE + 0xE;
- public static final int CALENDAR_CATEGORY = CALENDAR_PAGE + 0xF;
- public static final int CALENDAR_COMPRESSED_RTF = CALENDAR_PAGE + 0x10;
- public static final int CALENDAR_DTSTAMP = CALENDAR_PAGE + 0x11;
- public static final int CALENDAR_END_TIME = CALENDAR_PAGE + 0x12;
- public static final int CALENDAR_EXCEPTION = CALENDAR_PAGE + 0x13;
- public static final int CALENDAR_EXCEPTIONS = CALENDAR_PAGE + 0x14;
- public static final int CALENDAR_EXCEPTION_IS_DELETED = CALENDAR_PAGE + 0x15;
- public static final int CALENDAR_EXCEPTION_START_TIME = CALENDAR_PAGE + 0x16;
- public static final int CALENDAR_LOCATION = CALENDAR_PAGE + 0x17;
- public static final int CALENDAR_MEETING_STATUS = CALENDAR_PAGE + 0x18;
- public static final int CALENDAR_ORGANIZER_EMAIL = CALENDAR_PAGE + 0x19;
- public static final int CALENDAR_ORGANIZER_NAME = CALENDAR_PAGE + 0x1A;
- public static final int CALENDAR_RECURRENCE = CALENDAR_PAGE + 0x1B;
- public static final int CALENDAR_RECURRENCE_TYPE = CALENDAR_PAGE + 0x1C;
- public static final int CALENDAR_RECURRENCE_UNTIL = CALENDAR_PAGE + 0x1D;
- public static final int CALENDAR_RECURRENCE_OCCURRENCES = CALENDAR_PAGE + 0x1E;
- public static final int CALENDAR_RECURRENCE_INTERVAL = CALENDAR_PAGE + 0x1F;
- public static final int CALENDAR_RECURRENCE_DAYOFWEEK = CALENDAR_PAGE + 0x20;
- public static final int CALENDAR_RECURRENCE_DAYOFMONTH = CALENDAR_PAGE + 0x21;
- public static final int CALENDAR_RECURRENCE_WEEKOFMONTH = CALENDAR_PAGE + 0x22;
- public static final int CALENDAR_RECURRENCE_MONTHOFYEAR = CALENDAR_PAGE + 0x23;
- public static final int CALENDAR_REMINDER_MINS_BEFORE = CALENDAR_PAGE + 0x24;
- public static final int CALENDAR_SENSITIVITY = CALENDAR_PAGE + 0x25;
- public static final int CALENDAR_SUBJECT = CALENDAR_PAGE + 0x26;
- public static final int CALENDAR_START_TIME = CALENDAR_PAGE + 0x27;
- public static final int CALENDAR_UID = CALENDAR_PAGE + 0x28;
- public static final int CALENDAR_ATTENDEE_STATUS = CALENDAR_PAGE + 0x29;
- public static final int CALENDAR_ATTENDEE_TYPE = CALENDAR_PAGE + 0x2A;
- public static final int CALENDAR_ATTACHMENT = CALENDAR_PAGE + 0x2B;
- public static final int CALENDAR_ATTACHMENTS = CALENDAR_PAGE + 0x2C;
- public static final int CALENDAR_ATT_NAME = CALENDAR_PAGE + 0x2D;
- public static final int CALENDAR_ATT_SIZE = CALENDAR_PAGE + 0x2E;
- public static final int CALENDAR_ATT_OID = CALENDAR_PAGE + 0x2F;
- public static final int CALENDAR_ATT_METHOD = CALENDAR_PAGE + 0x30;
- public static final int CALENDAR_ATT_REMOVED = CALENDAR_PAGE + 0x31;
- public static final int CALENDAR_DISPLAY_NAME = CALENDAR_PAGE + 0x32;
- public static final int CALENDAR_DISALLOW_NEW_TIME_PROPOSAL = CALENDAR_PAGE + 0x33;
- public static final int CALENDAR_RESPONSE_REQUESTED = CALENDAR_PAGE + 0x34;
- public static final int CALENDAR_APPOINTMENT_REPLY_TIME = CALENDAR_PAGE + 0x35;
- public static final int CALENDAR_RESPONSE_TYPE = CALENDAR_PAGE + 0x36;
- public static final int CALENDAR_CALENDAR_TYPE = CALENDAR_PAGE + 0x37;
- public static final int CALENDAR_IS_LEAP_MONTH = CALENDAR_PAGE + 0x38;
- public static final int CALENDAR_FIRST_DAY_OF_WEEK = CALENDAR_PAGE + 0x39;
- public static final int CALENDAR_ONLINE_MEETING_CONFLINK = CALENDAR_PAGE + 0x3A;
- public static final int CALENDAR_ONLINE_MEETING_EXTERNAL_LINK = CALENDAR_PAGE + 0x3B;
-
- public static final int FOLDER_PAGE = FOLDER << PAGE_SHIFT;
- public static final int FOLDER_FOLDERS = FOLDER_PAGE + 5;
- public static final int FOLDER_FOLDER = FOLDER_PAGE + 6;
- public static final int FOLDER_DISPLAY_NAME = FOLDER_PAGE + 7;
- public static final int FOLDER_SERVER_ID = FOLDER_PAGE + 8;
- public static final int FOLDER_PARENT_ID = FOLDER_PAGE + 9;
- public static final int FOLDER_TYPE = FOLDER_PAGE + 0xA;
- public static final int FOLDER_RESPONSE = FOLDER_PAGE + 0xB;
- public static final int FOLDER_STATUS = FOLDER_PAGE + 0xC;
- public static final int FOLDER_CONTENT_CLASS = FOLDER_PAGE + 0xD;
- public static final int FOLDER_CHANGES = FOLDER_PAGE + 0xE;
- public static final int FOLDER_ADD = FOLDER_PAGE + 0xF;
- public static final int FOLDER_DELETE = FOLDER_PAGE + 0x10;
- public static final int FOLDER_UPDATE = FOLDER_PAGE + 0x11;
- public static final int FOLDER_SYNC_KEY = FOLDER_PAGE + 0x12;
- public static final int FOLDER_FOLDER_CREATE = FOLDER_PAGE + 0x13;
- public static final int FOLDER_FOLDER_DELETE= FOLDER_PAGE + 0x14;
- public static final int FOLDER_FOLDER_UPDATE = FOLDER_PAGE + 0x15;
- public static final int FOLDER_FOLDER_SYNC = FOLDER_PAGE + 0x16;
- public static final int FOLDER_COUNT = FOLDER_PAGE + 0x17;
- public static final int FOLDER_VERSION = FOLDER_PAGE + 0x18;
-
- public static final int MREQ_PAGE = MREQ << PAGE_SHIFT;
- public static final int MREQ_CAL_ID = MREQ_PAGE + 5;
- public static final int MREQ_COLLECTION_ID = MREQ_PAGE + 6;
- public static final int MREQ_MEETING_RESPONSE = MREQ_PAGE + 7;
- public static final int MREQ_REQ_ID = MREQ_PAGE + 8;
- public static final int MREQ_REQUEST = MREQ_PAGE + 9;
- public static final int MREQ_RESULT = MREQ_PAGE + 0xA;
- public static final int MREQ_STATUS = MREQ_PAGE + 0xB;
- public static final int MREQ_USER_RESPONSE = MREQ_PAGE + 0xC;
- public static final int MREQ_VERSION = MREQ_PAGE + 0xD;
-
- public static final int EMAIL_PAGE = EMAIL << PAGE_SHIFT;
- public static final int EMAIL_ATTACHMENT = EMAIL_PAGE + 5;
- public static final int EMAIL_ATTACHMENTS = EMAIL_PAGE + 6;
- public static final int EMAIL_ATT_NAME = EMAIL_PAGE + 7;
- public static final int EMAIL_ATT_SIZE = EMAIL_PAGE + 8;
- public static final int EMAIL_ATT0ID = EMAIL_PAGE + 9;
- public static final int EMAIL_ATT_METHOD = EMAIL_PAGE + 0xA;
- public static final int EMAIL_ATT_REMOVED = EMAIL_PAGE + 0xB;
- public static final int EMAIL_BODY = EMAIL_PAGE + 0xC;
- public static final int EMAIL_BODY_SIZE = EMAIL_PAGE + 0xD;
- public static final int EMAIL_BODY_TRUNCATED = EMAIL_PAGE + 0xE;
- public static final int EMAIL_DATE_RECEIVED = EMAIL_PAGE + 0xF;
- public static final int EMAIL_DISPLAY_NAME = EMAIL_PAGE + 0x10;
- public static final int EMAIL_DISPLAY_TO = EMAIL_PAGE + 0x11;
- public static final int EMAIL_IMPORTANCE = EMAIL_PAGE + 0x12;
- public static final int EMAIL_MESSAGE_CLASS = EMAIL_PAGE + 0x13;
- public static final int EMAIL_SUBJECT = EMAIL_PAGE + 0x14;
- public static final int EMAIL_READ = EMAIL_PAGE + 0x15;
- public static final int EMAIL_TO = EMAIL_PAGE + 0x16;
- public static final int EMAIL_CC = EMAIL_PAGE + 0x17;
- public static final int EMAIL_FROM = EMAIL_PAGE + 0x18;
- public static final int EMAIL_REPLY_TO = EMAIL_PAGE + 0x19;
- public static final int EMAIL_ALL_DAY_EVENT = EMAIL_PAGE + 0x1A;
- public static final int EMAIL_CATEGORIES = EMAIL_PAGE + 0x1B;
- public static final int EMAIL_CATEGORY = EMAIL_PAGE + 0x1C;
- public static final int EMAIL_DTSTAMP = EMAIL_PAGE + 0x1D;
- public static final int EMAIL_END_TIME = EMAIL_PAGE + 0x1E;
- public static final int EMAIL_INSTANCE_TYPE = EMAIL_PAGE + 0x1F;
- public static final int EMAIL_INTD_BUSY_STATUS = EMAIL_PAGE + 0x20;
- public static final int EMAIL_LOCATION = EMAIL_PAGE + 0x21;
- public static final int EMAIL_MEETING_REQUEST = EMAIL_PAGE + 0x22;
- public static final int EMAIL_ORGANIZER = EMAIL_PAGE + 0x23;
- public static final int EMAIL_RECURRENCE_ID = EMAIL_PAGE + 0x24;
- public static final int EMAIL_REMINDER = EMAIL_PAGE + 0x25;
- public static final int EMAIL_RESPONSE_REQUESTED = EMAIL_PAGE + 0x26;
- public static final int EMAIL_RECURRENCES = EMAIL_PAGE + 0x27;
- public static final int EMAIL_RECURRENCE = EMAIL_PAGE + 0x28;
- public static final int EMAIL_RECURRENCE_TYPE = EMAIL_PAGE + 0x29;
- public static final int EMAIL_RECURRENCE_UNTIL = EMAIL_PAGE + 0x2A;
- public static final int EMAIL_RECURRENCE_OCCURRENCES = EMAIL_PAGE + 0x2B;
- public static final int EMAIL_RECURRENCE_INTERVAL = EMAIL_PAGE + 0x2C;
- public static final int EMAIL_RECURRENCE_DAYOFWEEK = EMAIL_PAGE + 0x2D;
- public static final int EMAIL_RECURRENCE_DAYOFMONTH = EMAIL_PAGE + 0x2E;
- public static final int EMAIL_RECURRENCE_WEEKOFMONTH = EMAIL_PAGE + 0x2F;
- public static final int EMAIL_RECURRENCE_MONTHOFYEAR = EMAIL_PAGE + 0x30;
- public static final int EMAIL_START_TIME = EMAIL_PAGE + 0x31;
- public static final int EMAIL_SENSITIVITY = EMAIL_PAGE + 0x32;
- public static final int EMAIL_TIME_ZONE = EMAIL_PAGE + 0x33;
- public static final int EMAIL_GLOBAL_OBJID = EMAIL_PAGE + 0x34;
- public static final int EMAIL_THREAD_TOPIC = EMAIL_PAGE + 0x35;
- public static final int EMAIL_MIME_DATA = EMAIL_PAGE + 0x36;
- public static final int EMAIL_MIME_TRUNCATED = EMAIL_PAGE + 0x37;
- public static final int EMAIL_MIME_SIZE = EMAIL_PAGE + 0x38;
- public static final int EMAIL_INTERNET_CPID = EMAIL_PAGE + 0x39;
- public static final int EMAIL_FLAG = EMAIL_PAGE + 0x3A;
- public static final int EMAIL_FLAG_STATUS = EMAIL_PAGE + 0x3B;
- public static final int EMAIL_CONTENT_CLASS = EMAIL_PAGE + 0x3C;
- public static final int EMAIL_FLAG_TYPE = EMAIL_PAGE + 0x3D;
- public static final int EMAIL_COMPLETE_TIME = EMAIL_PAGE + 0x3E;
- public static final int EMAIL_DISALLOW_NEW_TIME_PROPOSAL = EMAIL_PAGE + 0x3F;
-
- public static final int TASK_PAGE = TASK << PAGE_SHIFT;
- public static final int TASK_BODY = TASK_PAGE + 5;
- public static final int TASK_BODY_SIZE = TASK_PAGE + 6;
- public static final int TASK_BODY_TRUNCATED = TASK_PAGE + 7;
- public static final int TASK_CATEGORIES = TASK_PAGE + 8;
- public static final int TASK_CATEGORY = TASK_PAGE + 9;
- public static final int TASK_COMPLETE = TASK_PAGE + 0xA;
- public static final int TASK_DATE_COMPLETED = TASK_PAGE + 0xB;
- public static final int TASK_DUE_DATE = TASK_PAGE + 0xC;
- public static final int TASK_UTC_DUE_DATE = TASK_PAGE + 0xD;
- public static final int TASK_IMPORTANCE = TASK_PAGE + 0xE;
- public static final int TASK_RECURRENCE = TASK_PAGE + 0xF;
- public static final int TASK_RECURRENCE_TYPE = TASK_PAGE + 0x10;
- public static final int TASK_RECURRENCE_START = TASK_PAGE + 0x11;
- public static final int TASK_RECURRENCE_UNTIL = TASK_PAGE + 0x12;
- public static final int TASK_RECURRENCE_OCCURRENCES = TASK_PAGE + 0x13;
- public static final int TASK_RECURRENCE_INTERVAL = TASK_PAGE + 0x14;
- public static final int TASK_RECURRENCE_DAY_OF_MONTH = TASK_PAGE + 0x15;
- public static final int TASK_RECURRENCE_DAY_OF_WEEK = TASK_PAGE + 0x16;
- public static final int TASK_RECURRENCE_WEEK_OF_MONTH = TASK_PAGE + 0x17;
- public static final int TASK_RECURRENCE_MONTH_OF_YEAR = TASK_PAGE + 0x18;
- public static final int TASK_RECURRENCE_REGENERATE = TASK_PAGE + 0x19;
- public static final int TASK_RECURRENCE_DEAD_OCCUR = TASK_PAGE + 0x1A;
- public static final int TASK_REMINDER_SET = TASK_PAGE + 0x1B;
- public static final int TASK_REMINDER_TIME = TASK_PAGE + 0x1C;
- public static final int TASK_SENSITIVITY = TASK_PAGE + 0x1D;
- public static final int TASK_START_DATE = TASK_PAGE + 0x1E;
- public static final int TASK_UTC_START_DATE = TASK_PAGE + 0x1F;
- public static final int TASK_SUBJECT = TASK_PAGE + 0x20;
- public static final int COMPRESSED_RTF = TASK_PAGE + 0x21;
- public static final int ORDINAL_DATE = TASK_PAGE + 0x22;
- public static final int SUBORDINAL_DATE = TASK_PAGE + 0x23;
-
- public static final int MOVE_PAGE = MOVE << PAGE_SHIFT;
- public static final int MOVE_MOVE_ITEMS = MOVE_PAGE + 5;
- public static final int MOVE_MOVE = MOVE_PAGE + 6;
- public static final int MOVE_SRCMSGID = MOVE_PAGE + 7;
- public static final int MOVE_SRCFLDID = MOVE_PAGE + 8;
- public static final int MOVE_DSTFLDID = MOVE_PAGE + 9;
- public static final int MOVE_RESPONSE = MOVE_PAGE + 0xA;
- public static final int MOVE_STATUS = MOVE_PAGE + 0xB;
- public static final int MOVE_DSTMSGID = MOVE_PAGE + 0xC;
-
- public static final int CONTACTS2_PAGE = CONTACTS2 << PAGE_SHIFT;
- public static final int CONTACTS2_CUSTOMER_ID = CONTACTS2_PAGE + 5;
- public static final int CONTACTS2_GOVERNMENT_ID = CONTACTS2_PAGE + 6;
- public static final int CONTACTS2_IM_ADDRESS = CONTACTS2_PAGE + 7;
- public static final int CONTACTS2_IM_ADDRESS_2 = CONTACTS2_PAGE + 8;
- public static final int CONTACTS2_IM_ADDRESS_3 = CONTACTS2_PAGE + 9;
- public static final int CONTACTS2_MANAGER_NAME = CONTACTS2_PAGE + 0xA;
- public static final int CONTACTS2_COMPANY_MAIN_PHONE = CONTACTS2_PAGE + 0xB;
- public static final int CONTACTS2_ACCOUNT_NAME = CONTACTS2_PAGE + 0xC;
- public static final int CONTACTS2_NICKNAME = CONTACTS2_PAGE + 0xD;
- public static final int CONTACTS2_MMS = CONTACTS2_PAGE + 0xE;
-
- public static final int PING_PAGE = PING << PAGE_SHIFT;
- public static final int PING_PING = PING_PAGE + 5;
- public static final int PING_AUTD_STATE = PING_PAGE + 6;
- public static final int PING_STATUS = PING_PAGE + 7;
- public static final int PING_HEARTBEAT_INTERVAL = PING_PAGE + 8;
- public static final int PING_FOLDERS = PING_PAGE + 9;
- public static final int PING_FOLDER = PING_PAGE + 0xA;
- public static final int PING_ID = PING_PAGE + 0xB;
- public static final int PING_CLASS = PING_PAGE + 0xC;
- public static final int PING_MAX_FOLDERS = PING_PAGE + 0xD;
-
- public static final int SEARCH_PAGE = SEARCH << PAGE_SHIFT;
- public static final int SEARCH_SEARCH = SEARCH_PAGE + 5;
- public static final int SEARCH_STORES = SEARCH_PAGE + 6;
- public static final int SEARCH_STORE = SEARCH_PAGE + 7;
- public static final int SEARCH_NAME = SEARCH_PAGE + 8;
- public static final int SEARCH_QUERY = SEARCH_PAGE + 9;
- public static final int SEARCH_OPTIONS = SEARCH_PAGE + 0xA;
- public static final int SEARCH_RANGE = SEARCH_PAGE + 0xB;
- public static final int SEARCH_STATUS = SEARCH_PAGE + 0xC;
- public static final int SEARCH_RESPONSE = SEARCH_PAGE + 0xD;
- public static final int SEARCH_RESULT = SEARCH_PAGE + 0xE;
- public static final int SEARCH_PROPERTIES = SEARCH_PAGE + 0xF;
- public static final int SEARCH_TOTAL = SEARCH_PAGE + 0x10;
- public static final int SEARCH_EQUAL_TO = SEARCH_PAGE + 0x11;
- public static final int SEARCH_VALUE = SEARCH_PAGE + 0x12;
- public static final int SEARCH_AND = SEARCH_PAGE + 0x13;
- public static final int SEARCH_OR = SEARCH_PAGE + 0x14;
- public static final int SEARCH_FREE_TEXT = SEARCH_PAGE + 0x15;
- public static final int SEARCH_SUBSTRING_OP = SEARCH_PAGE + 0x16;
- public static final int SEARCH_DEEP_TRAVERSAL = SEARCH_PAGE + 0x17;
- public static final int SEARCH_LONG_ID = SEARCH_PAGE + 0x18;
- public static final int SEARCH_REBUILD_RESULTS = SEARCH_PAGE + 0x19;
- public static final int SEARCH_LESS_THAN = SEARCH_PAGE + 0x1A;
- public static final int SEARCH_GREATER_THAN = SEARCH_PAGE + 0x1B;
- public static final int SEARCH_SCHEMA = SEARCH_PAGE + 0x1C;
- public static final int SEARCH_SUPPORTED = SEARCH_PAGE + 0x1D;
-
- public static final int GAL_PAGE = GAL << PAGE_SHIFT;
- public static final int GAL_DISPLAY_NAME = GAL_PAGE + 5;
- public static final int GAL_PHONE = GAL_PAGE + 6;
- public static final int GAL_OFFICE = GAL_PAGE + 7;
- public static final int GAL_TITLE = GAL_PAGE + 8;
- public static final int GAL_COMPANY = GAL_PAGE + 9;
- public static final int GAL_ALIAS = GAL_PAGE + 0xA;
- public static final int GAL_FIRST_NAME = GAL_PAGE + 0xB;
- public static final int GAL_LAST_NAME = GAL_PAGE + 0xC;
- public static final int GAL_HOME_PHONE = GAL_PAGE + 0xD;
- public static final int GAL_MOBILE_PHONE = GAL_PAGE + 0xE;
- public static final int GAL_EMAIL_ADDRESS = GAL_PAGE + 0xF;
-
- public static final int PROVISION_PAGE = PROVISION << PAGE_SHIFT;
- // EAS 2.5
- public static final int PROVISION_PROVISION = PROVISION_PAGE + 5;
- public static final int PROVISION_POLICIES = PROVISION_PAGE + 6;
- public static final int PROVISION_POLICY = PROVISION_PAGE + 7;
- public static final int PROVISION_POLICY_TYPE = PROVISION_PAGE + 8;
- public static final int PROVISION_POLICY_KEY = PROVISION_PAGE + 9;
- public static final int PROVISION_DATA = PROVISION_PAGE + 0xA;
- public static final int PROVISION_STATUS = PROVISION_PAGE + 0xB;
- public static final int PROVISION_REMOTE_WIPE = PROVISION_PAGE + 0xC;
- // EAS 12.0
- public static final int PROVISION_EAS_PROVISION_DOC = PROVISION_PAGE + 0xD;
- public static final int PROVISION_DEVICE_PASSWORD_ENABLED = PROVISION_PAGE + 0xE;
- public static final int PROVISION_ALPHA_DEVICE_PASSWORD_ENABLED = PROVISION_PAGE + 0xF;
- public static final int PROVISION_DEVICE_ENCRYPTION_ENABLED = PROVISION_PAGE + 0x10;
- public static final int PROVISION_PASSWORD_RECOVERY_ENABLED = PROVISION_PAGE + 0x11;
- public static final int PROVISION_ATTACHMENTS_ENABLED = PROVISION_PAGE + 0x13;
- public static final int PROVISION_MIN_DEVICE_PASSWORD_LENGTH = PROVISION_PAGE + 0x14;
- public static final int PROVISION_MAX_INACTIVITY_TIME_DEVICE_LOCK = PROVISION_PAGE + 0x15;
- public static final int PROVISION_MAX_DEVICE_PASSWORD_FAILED_ATTEMPTS = PROVISION_PAGE + 0x16;
- public static final int PROVISION_MAX_ATTACHMENT_SIZE = PROVISION_PAGE + 0x17;
- public static final int PROVISION_ALLOW_SIMPLE_DEVICE_PASSWORD = PROVISION_PAGE + 0x18;
- public static final int PROVISION_DEVICE_PASSWORD_EXPIRATION = PROVISION_PAGE + 0x19;
- public static final int PROVISION_DEVICE_PASSWORD_HISTORY = PROVISION_PAGE + 0x1A;
- public static final int PROVISION_MAX_SUPPORTED_TAG = PROVISION_DEVICE_PASSWORD_HISTORY;
-
- // EAS 12.1
- public static final int PROVISION_ALLOW_STORAGE_CARD = PROVISION_PAGE + 0x1B;
- public static final int PROVISION_ALLOW_CAMERA = PROVISION_PAGE + 0x1C;
- public static final int PROVISION_REQUIRE_DEVICE_ENCRYPTION = PROVISION_PAGE + 0x1D;
- public static final int PROVISION_ALLOW_UNSIGNED_APPLICATIONS = PROVISION_PAGE + 0x1E;
- public static final int PROVISION_ALLOW_UNSIGNED_INSTALLATION_PACKAGES = PROVISION_PAGE + 0x1F;
- public static final int PROVISION_MIN_DEVICE_PASSWORD_COMPLEX_CHARS = PROVISION_PAGE + 0x20;
- public static final int PROVISION_ALLOW_WIFI = PROVISION_PAGE + 0x21;
- public static final int PROVISION_ALLOW_TEXT_MESSAGING = PROVISION_PAGE + 0x22;
- public static final int PROVISION_ALLOW_POP_IMAP_EMAIL = PROVISION_PAGE + 0x23;
- public static final int PROVISION_ALLOW_BLUETOOTH = PROVISION_PAGE + 0x24;
- public static final int PROVISION_ALLOW_IRDA = PROVISION_PAGE + 0x25;
- public static final int PROVISION_REQUIRE_MANUAL_SYNC_WHEN_ROAMING = PROVISION_PAGE + 0x26;
- public static final int PROVISION_ALLOW_DESKTOP_SYNC = PROVISION_PAGE + 0x27;
- public static final int PROVISION_MAX_CALENDAR_AGE_FILTER = PROVISION_PAGE + 0x28;
- public static final int PROVISION_ALLOW_HTML_EMAIL = PROVISION_PAGE + 0x29;
- public static final int PROVISION_MAX_EMAIL_AGE_FILTER = PROVISION_PAGE + 0x2A;
- public static final int PROVISION_MAX_EMAIL_BODY_TRUNCATION_SIZE = PROVISION_PAGE + 0x2B;
- public static final int PROVISION_MAX_EMAIL_HTML_BODY_TRUNCATION_SIZE = PROVISION_PAGE + 0x2C;
- public static final int PROVISION_REQUIRE_SIGNED_SMIME_MESSAGES = PROVISION_PAGE + 0x2D;
- public static final int PROVISION_REQUIRE_ENCRYPTED_SMIME_MESSAGES = PROVISION_PAGE + 0x2E;
- public static final int PROVISION_REQUIRE_SIGNED_SMIME_ALGORITHM = PROVISION_PAGE + 0x2F;
- public static final int PROVISION_REQUIRE_ENCRYPTION_SMIME_ALGORITHM = PROVISION_PAGE + 0x30;
- public static final int PROVISION_ALLOW_SMIME_ENCRYPTION_NEGOTIATION = PROVISION_PAGE + 0x31;
- public static final int PROVISION_ALLOW_SMIME_SOFT_CERTS = PROVISION_PAGE + 0x32;
- public static final int PROVISION_ALLOW_BROWSER = PROVISION_PAGE + 0x33;
- public static final int PROVISION_ALLOW_CONSUMER_EMAIL = PROVISION_PAGE + 0x34;
- public static final int PROVISION_ALLOW_REMOTE_DESKTOP = PROVISION_PAGE + 0x35;
- public static final int PROVISION_ALLOW_INTERNET_SHARING = PROVISION_PAGE + 0x36;
- public static final int PROVISION_UNAPPROVED_IN_ROM_APPLICATION_LIST = PROVISION_PAGE + 0x37;
- public static final int PROVISION_APPLICATION_NAME = PROVISION_PAGE + 0x38;
- public static final int PROVISION_APPROVED_APPLICATION_LIST = PROVISION_PAGE + 0x39;
- public static final int PROVISION_HASH = PROVISION_PAGE + 0x3A;
-
- public static final int BASE_PAGE = BASE << PAGE_SHIFT;
- public static final int BASE_BODY_PREFERENCE = BASE_PAGE + 5;
- public static final int BASE_TYPE = BASE_PAGE + 6;
- public static final int BASE_TRUNCATION_SIZE = BASE_PAGE + 7;
- public static final int BASE_ALL_OR_NONE = BASE_PAGE + 8;
- public static final int BASE_RESERVED = BASE_PAGE + 9;
- public static final int BASE_BODY = BASE_PAGE + 0xA;
- public static final int BASE_DATA = BASE_PAGE + 0xB;
- public static final int BASE_ESTIMATED_DATA_SIZE = BASE_PAGE + 0xC;
- public static final int BASE_TRUNCATED = BASE_PAGE + 0xD;
- public static final int BASE_ATTACHMENTS = BASE_PAGE + 0xE;
- public static final int BASE_ATTACHMENT = BASE_PAGE + 0xF;
- public static final int BASE_DISPLAY_NAME = BASE_PAGE + 0x10;
- public static final int BASE_FILE_REFERENCE = BASE_PAGE + 0x11;
- public static final int BASE_METHOD = BASE_PAGE + 0x12;
- public static final int BASE_CONTENT_ID = BASE_PAGE + 0x13;
- public static final int BASE_CONTENT_LOCATION = BASE_PAGE + 0x14;
- public static final int BASE_IS_INLINE = BASE_PAGE + 0x15;
- public static final int BASE_NATIVE_BODY_TYPE = BASE_PAGE + 0x16;
- public static final int BASE_CONTENT_TYPE = BASE_PAGE + 0x17;
-
- public static final int SETTINGS_PAGE = SETTINGS << PAGE_SHIFT;
- public static final int SETTINGS_SETTINGS = SETTINGS_PAGE + 5;
- public static final int SETTINGS_STATUS = SETTINGS_PAGE + 6;
- public static final int SETTINGS_GET = SETTINGS_PAGE + 7;
- public static final int SETTINGS_SET = SETTINGS_PAGE + 8;
- public static final int SETTINGS_OOF = SETTINGS_PAGE + 9;
- public static final int SETTINGS_OOF_STATE = SETTINGS_PAGE + 0xA;
- public static final int SETTINGS_START_TIME = SETTINGS_PAGE + 0xB;
- public static final int SETTINGS_END_TIME = SETTINGS_PAGE + 0xC;
- public static final int SETTINGS_OOF_MESSAGE = SETTINGS_PAGE + 0xD;
- public static final int SETTINGS_APPLIES_TO_INTERNAL = SETTINGS_PAGE + 0xE;
- public static final int SETTINGS_APPLIES_TO_EXTERNAL_KNOWN = SETTINGS_PAGE + 0xF;
- public static final int SETTINGS_APPLIES_TO_EXTERNAL_UNKNOWN = SETTINGS_PAGE + 0x10;
- public static final int SETTINGS_ENABLED = SETTINGS_PAGE + 0x11;
- public static final int SETTINGS_REPLY_MESSAGE = SETTINGS_PAGE + 0x12;
- public static final int SETTINGS_BODY_TYPE = SETTINGS_PAGE + 0x13;
- public static final int SETTINGS_DEVICE_PASSWORD = SETTINGS_PAGE + 0x14;
- public static final int SETTINGS_PASSWORD = SETTINGS_PAGE + 0x15;
- public static final int SETTINGS_DEVICE_INFORMATION = SETTINGS_PAGE + 0x16;
- public static final int SETTINGS_MODEL = SETTINGS_PAGE + 0x17;
- public static final int SETTINGS_IMEI = SETTINGS_PAGE + 0x18;
- public static final int SETTINGS_FRIENDLY_NAME = SETTINGS_PAGE + 0x19;
- public static final int SETTINGS_OS = SETTINGS_PAGE + 0x1A;
- public static final int SETTINGS_OS_LANGUAGE = SETTINGS_PAGE + 0x1B;
- public static final int SETTINGS_PHONE_NUMBER = SETTINGS_PAGE + 0x1C;
- public static final int SETTINGS_USER_INFORMATION = SETTINGS_PAGE + 0x1D;
- public static final int SETTINGS_EMAIL_ADDRESS = SETTINGS_PAGE + 0x1E;
- public static final int SETTINGS_SMTP_ADDRESS = SETTINGS_PAGE + 0x1F;
- public static final int SETTINGS_USER_AGENT = SETTINGS_PAGE + 0x20;
- public static final int SETTINGS_ENABLE_OUTGOING_SMS = SETTINGS_PAGE + 0x21;
- public static final int SETTINGS_MOBILE_OPERATOR = SETTINGS_PAGE + 0x22;
-
- public static final int ITEMS_PAGE = ITEMS << PAGE_SHIFT;
- public static final int ITEMS_ITEMS = ITEMS_PAGE + 5;
- public static final int ITEMS_FETCH = ITEMS_PAGE + 6;
- public static final int ITEMS_STORE = ITEMS_PAGE + 7;
- public static final int ITEMS_OPTIONS = ITEMS_PAGE + 8;
- public static final int ITEMS_RANGE = ITEMS_PAGE + 9;
- public static final int ITEMS_TOTAL = ITEMS_PAGE + 0xA;
- public static final int ITEMS_PROPERTIES = ITEMS_PAGE + 0xB;
- public static final int ITEMS_DATA = ITEMS_PAGE + 0xC;
- public static final int ITEMS_STATUS = ITEMS_PAGE + 0xD;
- public static final int ITEMS_RESPONSE = ITEMS_PAGE + 0xE;
- public static final int ITEMS_VERSION = ITEMS_PAGE + 0xF;
- public static final int ITEMS_SCHEMA = ITEMS_PAGE + 0x10;
- public static final int ITEMS_PART = ITEMS_PAGE + 0x11;
- public static final int ITEMS_EMPTY_FOLDER = ITEMS_PAGE + 0x12;
- public static final int ITEMS_DELETE_SUB_FOLDERS = ITEMS_PAGE + 0x13;
- public static final int ITEMS_USERNAME = ITEMS_PAGE + 0x14;
- public static final int ITEMS_PASSWORD = ITEMS_PAGE + 0x15;
- public static final int ITEMS_MOVE = ITEMS_PAGE + 0x16;
- public static final int ITEMS_DSTFLDID = ITEMS_PAGE + 0x17;
- public static final int ITEMS_CONVERSATION_ID = ITEMS_PAGE + 0x18;
- public static final int ITEMS_MOVE_ALWAYS = ITEMS_PAGE + 0x19;
-
- public static final int COMPOSE_PAGE = COMPOSE << PAGE_SHIFT;
- public static final int COMPOSE_SEND_MAIL = COMPOSE_PAGE + 5;
- public static final int COMPOSE_SMART_FORWARD = COMPOSE_PAGE + 6;
- public static final int COMPOSE_SMART_REPLY = COMPOSE_PAGE + 7;
- public static final int COMPOSE_SAVE_IN_SENT_ITEMS = COMPOSE_PAGE + 8;
- public static final int COMPOSE_REPLACE_MIME = COMPOSE_PAGE + 9;
- // There no tag for COMPOSE_PAGE + 0xA
- public static final int COMPOSE_SOURCE = COMPOSE_PAGE + 0xB;
- public static final int COMPOSE_FOLDER_ID = COMPOSE_PAGE + 0xC;
- public static final int COMPOSE_ITEM_ID = COMPOSE_PAGE + 0xD;
- public static final int COMPOSE_LONG_ID = COMPOSE_PAGE + 0xE;
- public static final int COMPOSE_INSTANCE_ID = COMPOSE_PAGE + 0xF;
- public static final int COMPOSE_MIME = COMPOSE_PAGE + 0x10;
- public static final int COMPOSE_CLIENT_ID = COMPOSE_PAGE + 0x11;
- public static final int COMPOSE_STATUS = COMPOSE_PAGE + 0x12;
- public static final int COMPOSE_ACCOUNT_ID = COMPOSE_PAGE + 0x13;
-
- public static final int EMAIL2_PAGE = EMAIL2 << PAGE_SHIFT;
- public static final int EMAIL2_UM_CALLER_ID = EMAIL2_PAGE + 5;
- public static final int EMAIL2_UM_USER_NOTES = EMAIL2_PAGE + 6;
- public static final int EMAIL2_UM_ATT_DURATION = EMAIL2_PAGE + 7;
- public static final int EMAIL2_UM_ATT_ORDER = EMAIL2_PAGE + 8;
- public static final int EMAIL2_CONVERSATION_ID = EMAIL2_PAGE + 9;
- public static final int EMAIL2_CONVERSATION_INDEX = EMAIL2_PAGE + 0xA;
- public static final int EMAIL2_LAST_VERB_EXECUTED = EMAIL2_PAGE + 0xB;
- public static final int EMAIL2_LAST_VERB_EXECUTION_TIME = EMAIL2_PAGE + 0xC;
- public static final int EMAIL2_RECEIVED_AS_BCC = EMAIL2_PAGE + 0xD;
- public static final int EMAIL2_SENDER = EMAIL2_PAGE + 0xE;
- public static final int EMAIL2_CALENDAR_TYPE = EMAIL2_PAGE + 0xF;
- public static final int EMAIL2_IS_LEAP_MONTH = EMAIL2_PAGE + 0x10;
- public static final int EMAIL2_ACCOUNT_ID = EMAIL2_PAGE + 0x11;
- public static final int EMAIL2_FIRST_DAY_OF_WEEK = EMAIL2_PAGE + 0x12;
- public static final int EMAIL2_MEETING_MESSAGE_TYPE = EMAIL2_PAGE + 0x13;
-
- public static final int RIGHTS_PAGE = RIGHTS << PAGE_SHIFT;
- public static final int RIGHTS_SUPPORT = RIGHTS_PAGE + 5;
- public static final int RIGHTS_TEMPLATES = RIGHTS_PAGE + 6;
- public static final int RIGHTS_TEMPLATE = RIGHTS_PAGE + 7;
- public static final int RIGHTS_LICENSE = RIGHTS_PAGE + 8;
- public static final int RIGHTS_EDIT_ALLOWED = RIGHTS_PAGE + 9;
- public static final int RIGHTS_REPLY_ALLOWED = RIGHTS_PAGE + 0xA;
- public static final int RIGHTS_REPLY_ALL_ALLOWED = RIGHTS_PAGE + 0xB;
- public static final int RIGHTS_FORWARD_ALLOWED = RIGHTS_PAGE + 0xC;
- public static final int RIGHTS_MODIFY_RECIPIENTS_ALLOWED = RIGHTS_PAGE + 0xD;
- public static final int RIGHTS_EXTRACT_ALLOWED = RIGHTS_PAGE + 0xE;
- public static final int RIGHTS_PRINT_ALLOWED = RIGHTS_PAGE + 0xF;
- public static final int RIGHTS_EXPORT_ALLOWED = RIGHTS_PAGE + 0x10;
- public static final int RIGHTS_PROGRAMMATIC_ACCESS_ALLOWED = RIGHTS_PAGE + 0x11;
- public static final int RIGHTS_OWNER = RIGHTS_PAGE + 0x12;
- public static final int RIGHTS_CONTENT_EXPIRY_DATE = RIGHTS_PAGE + 0x13;
- public static final int RIGHTS_TEMPLATE_ID = RIGHTS_PAGE + 0x14;
- public static final int RIGHTS_TEMPLATE_NAME = RIGHTS_PAGE + 0x15;
- public static final int RIGHTS_TEMPLATE_DESCRIPTION = RIGHTS_PAGE + 0x16;
- public static final int RIGHTS_CONTENT_OWNER = RIGHTS_PAGE + 0x17;
- public static final int RIGHTS_REMOVE_RM_DISTRIBUTION = RIGHTS_PAGE + 0x18;
-
- static public String[][] pages = {
- { // 0x00 AirSync
- "Sync", "Responses", "Add", "Change", "Delete", "Fetch", "SyncKey", "ClientId",
- "ServerId", "Status", "Collection", "Class", "Version", "CollectionId", "GetChanges",
- "MoreAvailable", "WindowSize", "Commands", "Options", "FilterType", "Truncation",
- "RTFTruncation", "Conflict", "Collections", "ApplicationData", "DeletesAsMoves",
- "NotifyGUID", "Supported", "SoftDelete", "MIMESupport", "MIMETruncation", "Wait",
- "Limit", "Partial"
- },
- {
- // 0x01 Contacts
- "Anniversary", "AssistantName", "AssistantTelephoneNumber", "Birthday", "ContactsBody",
- "ContactsBodySize", "ContactsBodyTruncated", "Business2TelephoneNumber",
- "BusinessAddressCity",
- "BusinessAddressCountry", "BusinessAddressPostalCode", "BusinessAddressState",
- "BusinessAddressStreet", "BusinessFaxNumber", "BusinessTelephoneNumber",
- "CarTelephoneNumber", "ContactsCategories", "ContactsCategory", "Children", "Child",
- "CompanyName", "Department", "Email1Address", "Email2Address", "Email3Address",
- "FileAs", "FirstName", "Home2TelephoneNumber", "HomeAddressCity", "HomeAddressCountry",
- "HomeAddressPostalCode", "HomeAddressState", "HomeAddressStreet", "HomeFaxNumber",
- "HomeTelephoneNumber", "JobTitle", "LastName", "MiddleName", "MobileTelephoneNumber",
- "OfficeLocation", "OtherAddressCity", "OtherAddressCountry",
- "OtherAddressPostalCode", "OtherAddressState", "OtherAddressStreet", "PagerNumber",
- "RadioTelephoneNumber", "Spouse", "Suffix", "Title", "Webpage", "YomiCompanyName",
- "YomiFirstName", "YomiLastName", "CompressedRTF", "Picture"
- },
- {
- // 0x02 Email
- "Attachment", "Attachments", "AttName", "AttSize", "Add0Id", "AttMethod", "AttRemoved",
- "Body", "BodySize", "BodyTruncated", "DateReceived", "DisplayName", "DisplayTo",
- "Importance", "MessageClass", "Subject", "Read", "To", "CC", "From", "ReplyTo",
- "AllDayEvent", "Categories", "Category", "DTStamp", "EndTime", "InstanceType",
- "IntDBusyStatus", "Location", "MeetingRequest", "Organizer", "RecurrenceId", "Reminder",
- "ResponseRequested", "Recurrences", "Recurence", "Recurrence_Type", "Recurrence_Until",
- "Recurrence_Occurrences", "Recurrence_Interval", "Recurrence_DayOfWeek",
- "Recurrence_DayOfMonth", "Recurrence_WeekOfMonth", "Recurrence_MonthOfYear",
- "StartTime", "Sensitivity", "TimeZone", "GlobalObjId", "ThreadTopic", "MIMEData",
- "MIMETruncated", "MIMESize", "InternetCPID", "Flag", "FlagStatus", "EmailContentClass",
- "FlagType", "CompleteTime", "DisallowNewTimeProposal"
- },
- {
- // 0x03 AirNotify
- },
- {
- // 0x04 Calendar
- "CalTimeZone", "CalAllDayEvent", "CalAttendees", "CalAttendee", "CalAttendee_Email",
- "CalAttendee_Name", "CalBody", "CalBodyTruncated", "CalBusyStatus", "CalCategories",
- "CalCategory", "CalCompressed_RTF", "CalDTStamp", "CalEndTime", "CalExeption",
- "CalExceptions", "CalException_IsDeleted", "CalException_StartTime", "CalLocation",
- "CalMeetingStatus", "CalOrganizer_Email", "CalOrganizer_Name", "CalRecurrence",
- "CalRecurrence_Type", "CalRecurrence_Until", "CalRecurrence_Occurrences",
- "CalRecurrence_Interval", "CalRecurrence_DayOfWeek", "CalRecurrence_DayOfMonth",
- "CalRecurrence_WeekOfMonth", "CalRecurrence_MonthOfYear", "CalReminder_MinsBefore",
- "CalSensitivity", "CalSubject", "CalStartTime", "CalUID", "CalAttendee_Status",
- "CalAttendee_Type", "CalAttachment", "CalAttachments", "CalAttName", "CalAttSize",
- "CalAttOid", "CalAttMethod", "CalAttRemoved", "CalDisplayName",
- "CalDisallowNewTimeProposal", "CalResponseRequested", "CalAppointmentReplyTime",
- "CalResponseType", "CalCalendarType", "CalIsLeapMonth", "CalFirstDayOfWeek",
- "CalOnlineMeetingConfLink", "CalOnlineMeetingExternalLink"
- },
- {
- // 0x05 Move
- "MoveItems", "Move", "SrcMsgId", "SrcFldId", "DstFldId", "MoveResponse", "MoveStatus",
- "DstMsgId"
- },
- {
- // 0x06 ItemEstimate
- "GetItemEstimate", "Version", "IECollections", "IECollection", "IEClass",
- "IECollectionId", "DateTime", "Estimate", "IEResponse", "ItemEstimateStatus"
- },
- {
- // 0x07 FolderHierarchy
- "Folders", "Folder", "FolderDisplayName", "FolderServerId", "FolderParentId", "Type",
- "FolderResponse", "FolderStatus", "FolderContentClass", "Changes", "FolderAdd",
- "FolderDelete", "FolderUpdate", "FolderSyncKey", "FolderFolderCreate",
- "FolderFolderDelete", "FolderFolderUpdate", "FolderSync", "Count", "FolderVersion"
- },
- {
- // 0x08 MeetingResponse
- "CalId", "CollectionId", "MeetingResponse", "ReqId", "Request",
- "MeetingResponseResult", "MeetingResponseStatus", "UserResponse", "Version"
- },
- {
- // 0x09 Tasks
- "TasksBody", "TasksBodySize", "TasksBodyTruncated", "TasksCategories", "TasksCategory",
- "Complete", "DateCompleted", "DueDate", "UTCDueDate", "TasksImportance", "Recurrence",
- "RecurrenceType", "RecurrenceStart", "RecurrenceUntil", "RecurrenceOccurrences",
- "RecurrenceInterval", "RecurrenceDOM", "RecurrenceDOW", "RecurrenceWOM",
- "RecurrenceMOY", "RecurrenceRegenerate", "RecurrenceDeadOccur", "ReminderSet",
- "ReminderTime", "TasksSensitivity", "StartDate", "UTCStartDate", "TasksSubject",
- "TasksCompressedRTF", "OrdinalDate", "SubordinalDate"
- },
- {
- // 0x0A ResolveRecipients
- },
- {
- // 0x0B ValidateCert
- },
- {
- // 0x0C Contacts2
- "CustomerId", "GovernmentId", "IMAddress", "IMAddress2", "IMAddress3", "ManagerName",
- "CompanyMainPhone", "AccountName", "NickName", "MMS"
- },
- {
- // 0x0D Ping
- "Ping", "AutdState", "PingStatus", "HeartbeatInterval", "PingFolders", "PingFolder",
- "PingId", "PingClass", "MaxFolders"
- },
- {
- // 0x0E Provision
- "Provision", "Policies", "Policy", "PolicyType", "PolicyKey", "Data", "ProvisionStatus",
- "RemoteWipe", "EASProvidionDoc", "DevicePasswordEnabled",
- "AlphanumericDevicePasswordRequired",
- "DeviceEncryptionEnabled", "PasswordRecoveryEnabled", "-unused-", "AttachmentsEnabled",
- "MinDevicePasswordLength",
- "MaxInactivityTimeDeviceLock", "MaxDevicePasswordFailedAttempts", "MaxAttachmentSize",
- "AllowSimpleDevicePassword", "DevicePasswordExpiration", "DevicePasswordHistory",
- "AllowStorageCard", "AllowCamera", "RequireDeviceEncryption",
- "AllowUnsignedApplications", "AllowUnsignedInstallationPackages",
- "MinDevicePasswordComplexCharacters", "AllowWiFi", "AllowTextMessaging",
- "AllowPOPIMAPEmail", "AllowBluetooth", "AllowIrDA", "RequireManualSyncWhenRoaming",
- "AllowDesktopSync",
- "MaxCalendarAgeFilder", "AllowHTMLEmail", "MaxEmailAgeFilter",
- "MaxEmailBodyTruncationSize", "MaxEmailHTMLBodyTruncationSize",
- "RequireSignedSMIMEMessages", "RequireEncryptedSMIMEMessages",
- "RequireSignedSMIMEAlgorithm", "RequireEncryptionSMIMEAlgorithm",
- "AllowSMIMEEncryptionAlgorithmNegotiation", "AllowSMIMESoftCerts", "AllowBrowser",
- "AllowConsumerEmail", "AllowRemoteDesktop", "AllowInternetSharing",
- "UnapprovedInROMApplicationList", "ApplicationName", "ApprovedApplicationList", "Hash"
- },
- {
- // 0x0F Search
- "Search", "Stores", "Store", "Name", "Query",
- "SearchOptions", "Range", "SearchStatus", "Response", "Result",
- "Properties", "Total", "EqualTo", "Value", "And",
- "Or", "FreeText", "SubstringOp", "DeepTraversal", "LongId",
- "RebuildResults", "LessThan", "GreateerThan", "Schema", "SearchSupported"
- },
- {
- // 0x10 Gal
- "GalDisplayName", "GalPhone", "GalOffice", "GalTitle", "GalCompany", "GalAlias",
- "GalFirstName", "GalLastName", "GalHomePhone", "GalMobilePhone", "GalEmailAddress"
- },
- {
- // 0x11 AirSyncBase
- "BodyPreference", "BodyPreferenceType", "BodyPreferenceTruncationSize", "AllOrNone",
- "--unused1--", "BaseBody", "BaseData", "BaseEstimatedDataSize", "BaseTruncated",
- "BaseAttachments", "BaseAttachment", "BaseDisplayName", "FileReference", "BaseMethod",
- "BaseContentId", "BaseContentLocation", "BaseIsInline", "BaseNativeBodyType",
- "BaseContentType"
- },
- {
- // 0x12 Settings
- "Settings", "SettingsStatus", "Get", "Set", "Oof", "OofState", "SettingsStartTime",
- "SettingsEndTime", "OofMessage", "AppliesToInternal", "AppliesToExternalKnown",
- "AppliesToExternalUnknown", "Enabled", "ReplyMessage", "BodyType", "DevicePassword",
- "Password", "DeviceInformation", "Model", "IMEI", "FriendlyName", "OS", "OSLanguage",
- "PhoneNumber", "UserInformation", "EmailAddress", "StmpAddress", "UserAgent",
- "EnableOutboundSMS", "MobileOperator"
- },
- {
- // 0x13 DocumentLibrary
- },
- {
- // 0x14 ItemOperations
- "Items", "ItemsFetch", "ItemsStore", "ItemsOptions", "ItemsRange",
- "ItemsTotal", "ItemsProperties", "ItemsData", "ItemsStatus", "ItemsResponse",
- "ItemsVersion", "ItemsSchema", "ItemsPart", "ItemsEmptyFolder", "ItemsDeleteSubFolders",
- "ItemsUserName", "ItemsPassword", "ItemsMove", "ItemsDstFldId", "ItemsConversationId",
- "ItemsMoveAlways"
- },
- {
- // 0x15 ComposeMail
- "SendMail", "SmartForward", "SmartReply", "SaveInSentItems", "ReplaceMime",
- "--unused2--", "ComposeSource", "ComposeFolderId", "ComposeItemId", "ComposeLongId",
- "ComposeInstanceId", "ComposeMime", "ComposeClientId", "ComposeStatus",
- "ComposeAccountId"
- },
- {
- // 0x16 Email2
- "UmCallerId", "UmUserNotes", "UmAttDuration", "UmAttOrder", "ConversationId",
- "ConversationIndex", "LastVerbExecuted", "LastVerbExecutionTime", "ReceivedAsBcc",
- "Sender", "CalendarType", "IsLeapMonth", "AccountId", "FirstDayOfWeek",
- "MeetingMessageType"
- },
- {
- // 0x17 Notes
- },
- {
- // 0x18 Rights Management
- "RMSupport", "RMTemplates", "RMTemplate", "RMLicense", "EditAllowed", "ReplyAllowed",
- "ReplyAllAllowed", "ForwardAllowed", "ModifyRecipientsAllowed", "ExtractAllowed",
- "PrintAllowed", "ExportAllowed", "ProgrammaticAccessAllowed", "RMOwner",
- "ContentExpiryDate", "TemplateID", "TemplateName", "TemplateDescription",
- "ContentOwner", "RemoveRMDistribution"
- }
- };
-}
diff --git a/src/com/android/exchange/adapter/Wbxml.java b/src/com/android/exchange/adapter/Wbxml.java
deleted file mode 100644
index b8ae789..0000000
--- a/src/com/android/exchange/adapter/Wbxml.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or
- * sell copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
- * IN THE SOFTWARE. */
-
-package com.android.exchange.adapter;
-
-
-/** contains the WBXML constants */
-
-
-public interface Wbxml {
-
- static public final int SWITCH_PAGE = 0;
- static public final int END = 1;
- static public final int ENTITY = 2;
- static public final int STR_I = 3;
- static public final int LITERAL = 4;
- static public final int EXT_I_0 = 0x40;
- static public final int EXT_I_1 = 0x41;
- static public final int EXT_I_2 = 0x42;
- static public final int PI = 0x43;
- static public final int LITERAL_C = 0x44;
- static public final int EXT_T_0 = 0x80;
- static public final int EXT_T_1 = 0x81;
- static public final int EXT_T_2 = 0x82;
- static public final int STR_T = 0x83;
- static public final int LITERAL_A = 0x084;
- static public final int EXT_0 = 0x0c0;
- static public final int EXT_1 = 0x0c1;
- static public final int EXT_2 = 0x0c2;
- static public final int OPAQUE = 0x0c3;
- static public final int LITERAL_AC = 0x0c4;
-
- static public final int WITH_CONTENT = 0x40;
-}
diff --git a/src/com/android/exchange/adapter/patent_disclaimer.txt b/src/com/android/exchange/adapter/patent_disclaimer.txt
deleted file mode 100644
index 8a715a3..0000000
--- a/src/com/android/exchange/adapter/patent_disclaimer.txt
+++ /dev/null
@@ -1,9 +0,0 @@
------------------------------
-THIS IS NOT A GRANT OF PATENT RIGHTS.
-
-Google makes no representation or warranty that the source code made available hereunder is
-unencumbered by third-party patents. Those intending to use this source code in hardware or
-software products are advised that implementations of this code, including in open source software
-or shareware, may require patent licenses from the relevant patent holders.
-
------------------------------
diff --git a/src/com/android/exchange/patent_disclaimer.txt b/src/com/android/exchange/patent_disclaimer.txt
deleted file mode 100644
index 8a715a3..0000000
--- a/src/com/android/exchange/patent_disclaimer.txt
+++ /dev/null
@@ -1,9 +0,0 @@
------------------------------
-THIS IS NOT A GRANT OF PATENT RIGHTS.
-
-Google makes no representation or warranty that the source code made available hereunder is
-unencumbered by third-party patents. Those intending to use this source code in hardware or
-software products are advised that implementations of this code, including in open source software
-or shareware, may require patent licenses from the relevant patent holders.
-
------------------------------
diff --git a/src/com/android/exchange/provider/ExchangeDirectoryProvider.java b/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
deleted file mode 100644
index 6d725ff..0000000
--- a/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
+++ /dev/null
@@ -1,441 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.provider;
-
-import com.android.emailcommon.Configuration;
-import com.android.emailcommon.mail.PackedString;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent;
-import com.android.emailcommon.provider.EmailContent.AccountColumns;
-import com.android.emailcommon.service.AccountServiceProxy;
-import com.android.emailcommon.utility.Utility;
-import com.android.exchange.Eas;
-import com.android.exchange.EasSyncService;
-import com.android.exchange.R;
-import com.android.exchange.provider.GalResult.GalData;
-
-import android.accounts.AccountManager;
-import android.content.ContentProvider;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.net.Uri;
-import android.os.Binder;
-import android.os.Bundle;
-import android.os.RemoteException;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.CommonDataKinds;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Contacts.Data;
-import android.provider.ContactsContract.Directory;
-import android.provider.ContactsContract.RawContacts;
-import android.text.TextUtils;
-
-import java.util.HashMap;
-import java.util.List;
-
-/**
- * ExchangeDirectoryProvider provides real-time data from the Exchange server; at the moment, it is
- * used solely to provide GAL (Global Address Lookup) service to email address adapters
- */
-public class ExchangeDirectoryProvider extends ContentProvider {
- public static final String EXCHANGE_GAL_AUTHORITY = "com.android.exchange.directory.provider";
-
- private static final int DEFAULT_CONTACT_ID = 1;
- private static final int DEFAULT_LOOKUP_LIMIT = 20;
-
- private static final int GAL_BASE = 0;
- private static final int GAL_DIRECTORIES = GAL_BASE;
- private static final int GAL_FILTER = GAL_BASE + 1;
- private static final int GAL_CONTACT = GAL_BASE + 2;
- private static final int GAL_CONTACT_WITH_ID = GAL_BASE + 3;
- private static final int GAL_EMAIL_FILTER = GAL_BASE + 4;
-
- private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
- /*package*/ final HashMap<String, Long> mAccountIdMap = new HashMap<String, Long>();
-
- static {
- sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "directories", GAL_DIRECTORIES);
- sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/filter/*", GAL_FILTER);
- sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/entities", GAL_CONTACT);
- sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/#/entities",
- GAL_CONTACT_WITH_ID);
- sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/emails/filter/*", GAL_EMAIL_FILTER);
- }
-
- @Override
- public boolean onCreate() {
- return true;
- }
-
- static class GalProjection {
- final int size;
- final HashMap<String, Integer> columnMap = new HashMap<String, Integer>();
-
- GalProjection(String[] projection) {
- size = projection.length;
- for (int i = 0; i < projection.length; i++) {
- columnMap.put(projection[i], i);
- }
- }
- }
-
- static class GalContactRow {
- private final GalProjection mProjection;
- private Object[] row;
- static long dataId = 1;
-
- GalContactRow(GalProjection projection, long contactId, String lookupKey,
- String accountName, String displayName) {
- this.mProjection = projection;
- row = new Object[projection.size];
-
- put(Contacts.Entity.CONTACT_ID, contactId);
-
- // We only have one raw contact per aggregate, so they can have the same ID
- put(Contacts.Entity.RAW_CONTACT_ID, contactId);
- put(Contacts.Entity.DATA_ID, dataId++);
-
- put(Contacts.DISPLAY_NAME, displayName);
-
- // TODO alternative display name
- put(Contacts.DISPLAY_NAME_ALTERNATIVE, displayName);
-
- put(RawContacts.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
- put(RawContacts.ACCOUNT_NAME, accountName);
- put(RawContacts.RAW_CONTACT_IS_READ_ONLY, 1);
- put(Data.IS_READ_ONLY, 1);
- }
-
- Object[] getRow () {
- return row;
- }
-
- void put(String columnName, Object value) {
- Integer integer = mProjection.columnMap.get(columnName);
- if (integer != null) {
- row[integer] = value;
- } else {
- System.out.println("Unsupported column: " + columnName);
- }
- }
-
- static void addEmailAddress(MatrixCursor cursor, GalProjection galProjection,
- long contactId, String lookupKey, String accountName, String displayName,
- String address) {
- if (!TextUtils.isEmpty(address)) {
- GalContactRow r = new GalContactRow(
- galProjection, contactId, lookupKey, accountName, displayName);
- r.put(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
- r.put(Email.TYPE, Email.TYPE_WORK);
- r.put(Email.ADDRESS, address);
- cursor.addRow(r.getRow());
- }
- }
-
- static void addPhoneRow(MatrixCursor cursor, GalProjection projection, long contactId,
- String lookupKey, String accountName, String displayName, int type, String number) {
- if (!TextUtils.isEmpty(number)) {
- GalContactRow r = new GalContactRow(
- projection, contactId, lookupKey, accountName, displayName);
- r.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
- r.put(Phone.TYPE, type);
- r.put(Phone.NUMBER, number);
- cursor.addRow(r.getRow());
- }
- }
-
- public static void addNameRow(MatrixCursor cursor, GalProjection galProjection,
- long contactId, String lookupKey, String accountName, String displayName,
- String firstName, String lastName) {
- GalContactRow r = new GalContactRow(
- galProjection, contactId, lookupKey, accountName, displayName);
- r.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
- r.put(StructuredName.GIVEN_NAME, firstName);
- r.put(StructuredName.FAMILY_NAME, lastName);
- r.put(StructuredName.DISPLAY_NAME, displayName);
- cursor.addRow(r.getRow());
- }
- }
-
- /**
- * Find the record id of an Account, given its name (email address)
- * @param accountName the name of the account
- * @return the record id of the Account, or -1 if not found
- */
- /*package*/ long getAccountIdByName(Context context, String accountName) {
- Long accountId = mAccountIdMap.get(accountName);
- if (accountId == null) {
- accountId = Utility.getFirstRowLong(context, Account.CONTENT_URI,
- EmailContent.ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?",
- new String[] {accountName}, null, EmailContent.ID_PROJECTION_COLUMN , -1L);
- if (accountId != -1) {
- mAccountIdMap.put(accountName, accountId);
- }
- }
- return accountId;
- }
-
- @Override
- public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
- String sortOrder) {
- int match = sURIMatcher.match(uri);
- MatrixCursor cursor;
- Object[] row;
- PackedString ps;
- String lookupKey;
-
- switch (match) {
- case GAL_DIRECTORIES: {
- // Assuming that GAL can be used with all exchange accounts
- android.accounts.Account[] accounts = AccountManager.get(getContext())
- .getAccountsByType(Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
- cursor = new MatrixCursor(projection);
- if (accounts != null) {
- for (android.accounts.Account account : accounts) {
- row = new Object[projection.length];
-
- for (int i = 0; i < projection.length; i++) {
- String column = projection[i];
- if (column.equals(Directory.ACCOUNT_NAME)) {
- row[i] = account.name;
- } else if (column.equals(Directory.ACCOUNT_TYPE)) {
- row[i] = account.type;
- } else if (column.equals(Directory.TYPE_RESOURCE_ID)) {
- Bundle bundle = null;
- String accountType = Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE;
- bundle = new AccountServiceProxy(getContext())
- .getConfigurationData(accountType);
- // Default to the alternative name, erring on the conservative side
- int exchangeName = R.string.exchange_name_alternate;
- if (bundle != null && !bundle.getBoolean(
- Configuration.EXCHANGE_CONFIGURATION_USE_ALTERNATE_STRINGS,
- true)) {
- exchangeName = R.string.exchange_name;
- }
- row[i] = exchangeName;
- } else if (column.equals(Directory.DISPLAY_NAME)) {
- // If the account name is an email address, extract
- // the domain name and use it as the directory display name
- final String accountName = account.name;
- int atIndex = accountName.indexOf('@');
- if (atIndex != -1 && atIndex < accountName.length() - 2) {
- final char firstLetter = Character.toUpperCase(
- accountName.charAt(atIndex + 1));
- row[i] = firstLetter + accountName.substring(atIndex + 2);
- } else {
- row[i] = account.name;
- }
- } else if (column.equals(Directory.EXPORT_SUPPORT)) {
- row[i] = Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY;
- } else if (column.equals(Directory.SHORTCUT_SUPPORT)) {
- row[i] = Directory.SHORTCUT_SUPPORT_NONE;
- }
- }
- cursor.addRow(row);
- }
- }
- return cursor;
- }
-
- case GAL_FILTER:
- case GAL_EMAIL_FILTER: {
- String filter = uri.getLastPathSegment();
- // We should have at least two characters before doing a GAL search
- if (filter == null || filter.length() < 2) {
- return null;
- }
-
- String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
- if (accountName == null) {
- return null;
- }
-
- // Enforce a limit on the number of lookup responses
- String limitString = uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY);
- int limit = DEFAULT_LOOKUP_LIMIT;
- if (limitString != null) {
- try {
- limit = Integer.parseInt(limitString);
- } catch (NumberFormatException e) {
- limit = 0;
- }
- if (limit <= 0) {
- throw new IllegalArgumentException("Limit not valid: " + limitString);
- }
- }
-
- long callingId = Binder.clearCallingIdentity();
- try {
- // Find the account id to pass along to EasSyncService
- long accountId = getAccountIdByName(getContext(), accountName);
- if (accountId == -1) {
- // The account was deleted?
- return null;
- }
-
- // Get results from the Exchange account
- GalResult galResult = EasSyncService.searchGal(getContext(), accountId,
- filter, limit);
- if (galResult != null) {
- return buildGalResultCursor(projection, galResult);
- }
- } finally {
- Binder.restoreCallingIdentity(callingId);
- }
- break;
- }
-
- case GAL_CONTACT:
- case GAL_CONTACT_WITH_ID: {
- String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
- if (accountName == null) {
- return null;
- }
-
- GalProjection galProjection = new GalProjection(projection);
- cursor = new MatrixCursor(projection);
- // Handle the decomposition of the key into rows suitable for CP2
- List<String> pathSegments = uri.getPathSegments();
- lookupKey = pathSegments.get(2);
- long contactId = (match == GAL_CONTACT_WITH_ID)
- ? Long.parseLong(pathSegments.get(3))
- : DEFAULT_CONTACT_ID;
- ps = new PackedString(lookupKey);
- String displayName = ps.get(GalData.DISPLAY_NAME);
- GalContactRow.addEmailAddress(cursor, galProjection, contactId, lookupKey,
- accountName, displayName, ps.get(GalData.EMAIL_ADDRESS));
- GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName,
- displayName, displayName, Phone.TYPE_HOME, ps.get(GalData.HOME_PHONE));
- GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName,
- displayName, displayName, Phone.TYPE_WORK, ps.get(GalData.WORK_PHONE));
- GalContactRow.addPhoneRow(cursor, galProjection, contactId, accountName,
- displayName, displayName, Phone.TYPE_MOBILE, ps.get(GalData.MOBILE_PHONE));
- GalContactRow.addNameRow(cursor, galProjection, contactId, accountName, displayName,
- ps.get(GalData.FIRST_NAME), ps.get(GalData.LAST_NAME), displayName);
- return cursor;
- }
- }
-
- return null;
- }
-
- /*package*/ Cursor buildGalResultCursor(String[] projection, GalResult galResult) {
- int displayNameIndex = -1;
- int alternateDisplayNameIndex = -1;;
- int emailIndex = -1;
- int idIndex = -1;
- int lookupIndex = -1;
-
- for (int i = 0; i < projection.length; i++) {
- String column = projection[i];
- if (Contacts.DISPLAY_NAME.equals(column) ||
- Contacts.DISPLAY_NAME_PRIMARY.equals(column)) {
- displayNameIndex = i;
- } else if (Contacts.DISPLAY_NAME_ALTERNATIVE.equals(column)) {
- alternateDisplayNameIndex = i;
- } else if (CommonDataKinds.Email.ADDRESS.equals(column)) {
- emailIndex = i;
- } else if (Contacts._ID.equals(column)) {
- idIndex = i;
- } else if (Contacts.LOOKUP_KEY.equals(column)) {
- lookupIndex = i;
- }
- }
-
- Object[] row = new Object[projection.length];
-
- /*
- * ContactsProvider will ensure that every request has a non-null projection.
- */
- MatrixCursor cursor = new MatrixCursor(projection);
- int count = galResult.galData.size();
- for (int i = 0; i < count; i++) {
- GalData galDataRow = galResult.galData.get(i);
- String firstName = galDataRow.get(GalData.FIRST_NAME);
- String lastName = galDataRow.get(GalData.LAST_NAME);
- String displayName = galDataRow.get(GalData.DISPLAY_NAME);
- // If we don't have a display name, try to create one using first and last name
- if (displayName == null) {
- if (firstName != null && lastName != null) {
- displayName = firstName + " " + lastName;
- } else if (firstName != null) {
- displayName = firstName;
- } else if (lastName != null) {
- displayName = lastName;
- }
- }
- galDataRow.put(GalData.DISPLAY_NAME, displayName);
-
- if (displayNameIndex != -1) {
- row[displayNameIndex] = displayName;
- }
- if (alternateDisplayNameIndex != -1) {
- // Try to create an alternate display name, using first and last name
- // TODO: Check with Contacts team to make sure we're using this properly
- if (firstName != null && lastName != null) {
- row[alternateDisplayNameIndex] = lastName + " " + firstName;
- } else {
- row[alternateDisplayNameIndex] = displayName;
- }
- }
- if (emailIndex != -1) {
- row[emailIndex] = galDataRow.get(GalData.EMAIL_ADDRESS);
- }
- if (idIndex != -1) {
- row[idIndex] = i + 1; // Let's be 1 based
- }
- if (lookupIndex != -1) {
- // We use the packed string as our lookup key; it contains ALL of the gal data
- // We do this because we are not able to provide a stable id to ContactsProvider
- row[lookupIndex] = Uri.encode(galDataRow.toPackedString());
- }
- cursor.addRow(row);
- }
- return cursor;
- }
-
- @Override
- public String getType(Uri uri) {
- int match = sURIMatcher.match(uri);
- switch (match) {
- case GAL_FILTER:
- return Contacts.CONTENT_ITEM_TYPE;
- }
- return null;
- }
-
- @Override
- public int delete(Uri uri, String selection, String[] selectionArgs) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public Uri insert(Uri uri, ContentValues values) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- throw new UnsupportedOperationException();
- }
-}
diff --git a/src/com/android/exchange/provider/GalResult.java b/src/com/android/exchange/provider/GalResult.java
deleted file mode 100644
index 4d0d908..0000000
--- a/src/com/android/exchange/provider/GalResult.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/* Copyright (C) 2010 The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.provider;
-
-import com.android.emailcommon.mail.PackedString;
-
-import java.util.ArrayList;
-
-/**
- * A container for GAL results from EAS
- * Each element of the galData array becomes an element of the list used by autocomplete
- */
-public class GalResult {
- // Total number of matches in this result
- public int total;
- public ArrayList<GalData> galData = new ArrayList<GalData>();
-
- public GalResult() {
- }
-
- /**
- * Legacy method for email address autocomplete
- */
- public void addGalData(long id, String displayName, String emailAddress) {
- galData.add(new GalData(id, displayName, emailAddress));
- }
-
- public void addGalData(GalData data) {
- galData.add(data);
- }
-
- public static class GalData {
- // PackedString constants for GalData
- public static final String ID = "_id";
- public static final String DISPLAY_NAME = "displayName";
- public static final String EMAIL_ADDRESS = "emailAddress";
- public static final String WORK_PHONE = "workPhone";
- public static final String HOME_PHONE = "homePhone";
- public static final String MOBILE_PHONE = "mobilePhone";
- public static final String FIRST_NAME = "firstName";
- public static final String LAST_NAME = "lastName";
- public static final String COMPANY = "company";
- public static final String TITLE = "title";
- public static final String OFFICE = "office";
- public static final String ALIAS = "alias";
- // The Builder we use to construct the PackedString
- PackedString.Builder builder = new PackedString.Builder();
-
- // The following three fields are for legacy email autocomplete
- public long _id = 0;
- public String displayName;
- public String emailAddress;
-
- /**
- * Legacy constructor for email address autocomplete
- */
- private GalData(long id, String _displayName, String _emailAddress) {
- put(ID, Long.toString(id));
- _id = id;
- put(DISPLAY_NAME, _displayName);
- displayName = _displayName;
- put(EMAIL_ADDRESS, _emailAddress);
- emailAddress = _emailAddress;
- }
-
- public GalData() {
- }
-
- public String get(String field) {
- return builder.get(field);
- }
-
- public void put(String field, String value) {
- builder.put(field, value);
- }
-
- public String toPackedString() {
- return builder.toString();
- }
- }
-}
diff --git a/src/com/android/exchange/provider/MailboxUtilities.java b/src/com/android/exchange/provider/MailboxUtilities.java
deleted file mode 100644
index 6089257..0000000
--- a/src/com/android/exchange/provider/MailboxUtilities.java
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.provider;
-
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.util.Log;
-
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent.MailboxColumns;
-import com.android.emailcommon.provider.Mailbox;
-
-public class MailboxUtilities {
- public static final String WHERE_PARENT_KEY_UNINITIALIZED =
- "(" + MailboxColumns.PARENT_KEY + " isnull OR " + MailboxColumns.PARENT_KEY + "=" +
- Mailbox.PARENT_KEY_UNINITIALIZED + ")";
- // The flag we use in Account to indicate a mailbox change in progress
- private static final int ACCOUNT_MAILBOX_CHANGE_FLAG = Account.FLAGS_SYNC_ADAPTER;
-
- /**
- * Recalculate a mailbox's flags and the parent key of any children
- * @param context the caller's context
- * @param parentCursor a cursor to a mailbox that requires fixup
- */
- public static void setFlagsAndChildrensParentKey(Context context, Cursor parentCursor,
- String accountSelector) {
- ContentResolver resolver = context.getContentResolver();
- String[] selectionArgs = new String[1];
- ContentValues parentValues = new ContentValues();
- // Get the data we need first
- long parentId = parentCursor.getLong(Mailbox.CONTENT_ID_COLUMN);
- int parentFlags = 0;
- int parentType = parentCursor.getInt(Mailbox.CONTENT_TYPE_COLUMN);
- String parentServerId = parentCursor.getString(Mailbox.CONTENT_SERVER_ID_COLUMN);
- // All email-type boxes hold mail
- if (parentType <= Mailbox.TYPE_NOT_EMAIL) {
- parentFlags |= Mailbox.FLAG_HOLDS_MAIL;
- }
- // Outbox, Drafts, and Sent don't allow mail to be moved to them
- if (parentType == Mailbox.TYPE_MAIL || parentType == Mailbox.TYPE_TRASH ||
- parentType == Mailbox.TYPE_JUNK || parentType == Mailbox.TYPE_INBOX) {
- parentFlags |= Mailbox.FLAG_ACCEPTS_MOVED_MAIL;
- }
- // There's no concept of "append" in EAS so FLAG_ACCEPTS_APPENDED_MAIL is never used
- // Mark parent mailboxes as parents & add parent key to children
- // An example of a mailbox with a null serverId would be an Outbox that we create locally
- // for hotmail accounts (which don't have a server-based Outbox)
- if (parentServerId != null) {
- selectionArgs[0] = parentServerId;
- Cursor childCursor = resolver.query(Mailbox.CONTENT_URI,
- Mailbox.ID_PROJECTION, MailboxColumns.PARENT_SERVER_ID + "=? AND " +
- accountSelector, selectionArgs, null);
- if (childCursor == null) return;
- try {
- while (childCursor.moveToNext()) {
- parentFlags |= Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE;
- ContentValues childValues = new ContentValues();
- childValues.put(Mailbox.PARENT_KEY, parentId);
- long childId = childCursor.getLong(Mailbox.ID_PROJECTION_COLUMN);
- resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, childId),
- childValues, null, null);
- }
- } finally {
- childCursor.close();
- }
- } else {
- // Mark this is having no parent, so that we don't examine this mailbox again
- parentValues.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX);
- Log.w(Logging.LOG_TAG, "Mailbox with null serverId: " +
- parentCursor.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) + ", type: " +
- parentType);
- }
- // Save away updated flags and parent key (if any)
- parentValues.put(Mailbox.FLAGS, parentFlags);
- resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, parentId),
- parentValues, null, null);
- }
-
- /**
- * Recalculate a mailbox's flags and the parent key of any children
- * @param context the caller's context
- * @param accountSelector (see description below in fixupUninitializedParentKeys)
- * @param serverId the server id of an individual mailbox
- */
- public static void setFlagsAndChildrensParentKey(Context context, String accountSelector,
- String serverId) {
- Cursor cursor = context.getContentResolver().query(Mailbox.CONTENT_URI,
- Mailbox.CONTENT_PROJECTION, MailboxColumns.SERVER_ID + "=? AND " + accountSelector,
- new String[] {serverId}, null);
- if (cursor == null) return;
- try {
- if (cursor.moveToFirst()) {
- setFlagsAndChildrensParentKey(context, cursor, accountSelector);
- }
- } finally {
- cursor.close();
- }
- }
-
- /**
- * Given an account selector, specifying the account(s) on which to work, create the parentKey
- * and flags for each mailbox in the account(s) that is uninitialized (parentKey = 0 or null)
- *
- * @param accountSelector a sqlite WHERE clause expression to be used in determining the
- * mailboxes to be acted upon, e.g. accountKey IN (1, 2), accountKey = 12, etc.
- */
- public static void fixupUninitializedParentKeys(Context context, String accountSelector) {
- // Sanity check first on our arguments
- if (accountSelector == null) throw new IllegalArgumentException();
- // The selection we'll use to find uninitialized parent key mailboxes
- String noParentKeySelection = WHERE_PARENT_KEY_UNINITIALIZED + " AND " + accountSelector;
-
- // We'll loop through mailboxes with an uninitialized parent key
- ContentResolver resolver = context.getContentResolver();
- Cursor parentCursor = resolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
- noParentKeySelection, null, null);
- if (parentCursor == null) return;
- try {
- while (parentCursor.moveToNext()) {
- setFlagsAndChildrensParentKey(context, parentCursor, accountSelector);
- // Fix up the parent as well
- String parentServerId =
- parentCursor.getString(Mailbox.CONTENT_PARENT_SERVER_ID_COLUMN);
- if (parentServerId != null) {
- setFlagsAndChildrensParentKey(context, accountSelector, parentServerId);
- }
- }
- } finally {
- parentCursor.close();
- }
-
- // Any mailboxes without a parent key should have parentKey set to -1 (no parent)
- ContentValues values = new ContentValues();
- values.clear();
- values.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX);
- resolver.update(Mailbox.CONTENT_URI, values, noParentKeySelection, null);
- }
-
- private static void setAccountSyncAdapterFlag(Context context, long accountId, boolean start) {
- Account account = Account.restoreAccountWithId(context, accountId);
- if (account == null) return;
- // Set temporary flag indicating state of update of mailbox list
- ContentValues cv = new ContentValues();
- cv.put(Account.FLAGS, start ? (account.mFlags | ACCOUNT_MAILBOX_CHANGE_FLAG) :
- account.mFlags & ~ACCOUNT_MAILBOX_CHANGE_FLAG);
- context.getContentResolver().update(
- ContentUris.withAppendedId(Account.CONTENT_URI, account.mId), cv, null, null);
- }
-
- /**
- * Indicate that the specified account is starting the process of changing its mailbox list
- * @param context the caller's context
- * @param accountId the account that is starting to change its mailbox list
- */
- public static void startMailboxChanges(Context context, long accountId) {
- setAccountSyncAdapterFlag(context, accountId, true);
- }
-
- /**
- * Indicate that the specified account is ending the process of changing its mailbox list
- * @param context the caller's context
- * @param accountId the account that is finished with changes to its mailbox list
- */
- public static void endMailboxChanges(Context context, long accountId) {
- setAccountSyncAdapterFlag(context, accountId, false);
- }
-
- /**
- * Check that we didn't leave the account's mailboxes in a (possibly) inconsistent state
- * If we did, make them consistent again
- * @param context the caller's context
- * @param accountId the account whose mailboxes are to be checked
- */
- public static void checkMailboxConsistency(Context context, long accountId) {
- // If our temporary flag is set, we were interrupted during an update
- // First, make sure we're current (really fast w/ caching)
- Account account = Account.restoreAccountWithId(context, accountId);
- if (account == null) return;
- if ((account.mFlags & ACCOUNT_MAILBOX_CHANGE_FLAG) != 0) {
- Log.w(Logging.LOG_TAG, "Account " + account.mDisplayName +
- " has inconsistent mailbox data; fixing up...");
- // Set all account mailboxes to uninitialized parent key
- ContentValues values = new ContentValues();
- values.put(Mailbox.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED);
- String accountSelector = Mailbox.ACCOUNT_KEY + "=" + account.mId;
- ContentResolver resolver = context.getContentResolver();
- resolver.update(Mailbox.CONTENT_URI, values, accountSelector, null);
- // Fix up keys and flags
- MailboxUtilities.fixupUninitializedParentKeys(context, accountSelector);
- // Clear the temporary flag
- endMailboxChanges(context, accountId);
- }
- }
-}
diff --git a/src/com/android/exchange/service/ExchangeBroadcastProcessorService.java b/src/com/android/exchange/service/ExchangeBroadcastProcessorService.java
deleted file mode 100644
index 8f88910..0000000
--- a/src/com/android/exchange/service/ExchangeBroadcastProcessorService.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.service;
-
-import android.accounts.AccountManager;
-import android.app.IntentService;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-import com.android.emailcommon.Logging;
-import com.android.exchange.Eas;
-import com.android.exchange.ExchangeService;
-
-/**
- * The service that really handles broadcast intents on a worker thread.
- *
- * We make it a service, because:
- * <ul>
- * <li>So that it's less likely for the process to get killed.
- * <li>Even if it does, the Intent that have started it will be re-delivered by the system,
- * and we can start the process again. (Using {@link #setIntentRedelivery}).
- * </ul>
- */
-public class ExchangeBroadcastProcessorService extends IntentService {
- // Action used for BroadcastReceiver entry point
- private static final String ACTION_BROADCAST = "broadcast_receiver";
-
- public ExchangeBroadcastProcessorService() {
- // Class name will be the thread name.
- super(ExchangeBroadcastProcessorService.class.getName());
- // Intent should be redelivered if the process gets killed before completing the job.
- setIntentRedelivery(true);
- }
-
- /**
- * Entry point for {@link ExchangeBroadcastReceiver}.
- */
- public static void processBroadcastIntent(Context context, Intent broadcastIntent) {
- Intent i = new Intent(context, ExchangeBroadcastProcessorService.class);
- i.setAction(ACTION_BROADCAST);
- i.putExtra(Intent.EXTRA_INTENT, broadcastIntent);
- context.startService(i);
- }
-
- @Override
- protected void onHandleIntent(Intent intent) {
- // Dispatch from entry point
- final String action = intent.getAction();
- if (ACTION_BROADCAST.equals(action)) {
- final Intent broadcastIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT);
- final String broadcastAction = broadcastIntent.getAction();
-
- if (Intent.ACTION_BOOT_COMPLETED.equals(broadcastAction)) {
- onBootCompleted();
- } else if (AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(broadcastAction)) {
- if (Eas.USER_LOG) {
- Log.d(Logging.LOG_TAG, "Login accounts changed; reconciling...");
- }
- ExchangeService.runAccountReconcilerSync(this);
- }
- }
- }
-
- /**
- * Handles {@link Intent#ACTION_BOOT_COMPLETED}. Called on a worker thread.
- */
- private void onBootCompleted() {
- startService(new Intent(this, ExchangeService.class));
- }
-}
diff --git a/src/com/android/exchange/service/ExchangeBroadcastReceiver.java b/src/com/android/exchange/service/ExchangeBroadcastReceiver.java
deleted file mode 100644
index e79eec0..0000000
--- a/src/com/android/exchange/service/ExchangeBroadcastReceiver.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.service;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-/**
- * The broadcast receiver. The actual job is done in EmailBroadcastProcessor on a worker thread.
- */
-public class ExchangeBroadcastReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- ExchangeBroadcastProcessorService.processBroadcastIntent(context, intent);
- }
-}
diff --git a/src/com/android/exchange/utility/CalendarUtilities.java b/src/com/android/exchange/utility/CalendarUtilities.java
deleted file mode 100644
index 2f466b8..0000000
--- a/src/com/android/exchange/utility/CalendarUtilities.java
+++ /dev/null
@@ -1,1947 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.utility;
-
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Entity;
-import android.content.Entity.NamedContentValues;
-import android.content.EntityIterator;
-import android.content.res.Resources;
-import android.net.Uri;
-import android.os.RemoteException;
-import android.provider.CalendarContract.Attendees;
-import android.provider.CalendarContract.Calendars;
-import android.provider.CalendarContract.Events;
-import android.provider.CalendarContract.EventsEntity;
-import android.text.TextUtils;
-import android.text.format.Time;
-import android.util.Base64;
-import android.util.Log;
-
-import com.android.emailcommon.Logging;
-import com.android.emailcommon.mail.Address;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent;
-import com.android.emailcommon.provider.EmailContent.Attachment;
-import com.android.emailcommon.provider.EmailContent.Message;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.emailcommon.service.AccountServiceProxy;
-import com.android.emailcommon.utility.Utility;
-import com.android.exchange.Eas;
-import com.android.exchange.EasSyncService;
-import com.android.exchange.ExchangeService;
-import com.android.exchange.R;
-import com.android.exchange.adapter.Serializer;
-import com.android.exchange.adapter.Tags;
-import com.android.internal.util.ArrayUtils;
-import com.google.common.annotations.VisibleForTesting;
-
-import java.io.IOException;
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.GregorianCalendar;
-import java.util.HashMap;
-import java.util.TimeZone;
-
-public class CalendarUtilities {
-
- // NOTE: Most definitions in this class are have package visibility for testing purposes
- private static final String TAG = "CalendarUtility";
-
- // Time related convenience constants, in milliseconds
- static final int SECONDS = 1000;
- static final int MINUTES = SECONDS*60;
- static final int HOURS = MINUTES*60;
- static final long DAYS = HOURS*24;
-
- // We want to find a time zone whose DST info is accurate to one minute
- static final int STANDARD_DST_PRECISION = MINUTES;
- // If we can't find one, we'll try a more lenient standard (this is better than guessing a
- // time zone, which is what we otherwise do). Note that this specifically addresses an issue
- // seen in some time zones sent by MS Exchange in which the start and end hour differ
- // for no apparent reason
- static final int LENIENT_DST_PRECISION = 4*HOURS;
-
- private static final String SYNC_VERSION = Events.SYNC_DATA4;
- // NOTE All Microsoft data structures are little endian
-
- // The following constants relate to standard Microsoft data sizes
- // For documentation, see http://msdn.microsoft.com/en-us/library/aa505945.aspx
- static final int MSFT_LONG_SIZE = 4;
- static final int MSFT_WCHAR_SIZE = 2;
- static final int MSFT_WORD_SIZE = 2;
-
- // The following constants relate to Microsoft's SYSTEMTIME structure
- // For documentation, see: http://msdn.microsoft.com/en-us/library/ms724950(VS.85).aspx?ppud=4
-
- static final int MSFT_SYSTEMTIME_YEAR = 0 * MSFT_WORD_SIZE;
- static final int MSFT_SYSTEMTIME_MONTH = 1 * MSFT_WORD_SIZE;
- static final int MSFT_SYSTEMTIME_DAY_OF_WEEK = 2 * MSFT_WORD_SIZE;
- static final int MSFT_SYSTEMTIME_DAY = 3 * MSFT_WORD_SIZE;
- static final int MSFT_SYSTEMTIME_HOUR = 4 * MSFT_WORD_SIZE;
- static final int MSFT_SYSTEMTIME_MINUTE = 5 * MSFT_WORD_SIZE;
- //static final int MSFT_SYSTEMTIME_SECONDS = 6 * MSFT_WORD_SIZE;
- //static final int MSFT_SYSTEMTIME_MILLIS = 7 * MSFT_WORD_SIZE;
- static final int MSFT_SYSTEMTIME_SIZE = 8*MSFT_WORD_SIZE;
-
- // The following constants relate to Microsoft's TIME_ZONE_INFORMATION structure
- // For documentation, see http://msdn.microsoft.com/en-us/library/ms725481(VS.85).aspx
- static final int MSFT_TIME_ZONE_BIAS_OFFSET = 0;
- static final int MSFT_TIME_ZONE_STANDARD_NAME_OFFSET =
- MSFT_TIME_ZONE_BIAS_OFFSET + MSFT_LONG_SIZE;
- static final int MSFT_TIME_ZONE_STANDARD_DATE_OFFSET =
- MSFT_TIME_ZONE_STANDARD_NAME_OFFSET + (MSFT_WCHAR_SIZE*32);
- static final int MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET =
- MSFT_TIME_ZONE_STANDARD_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
- static final int MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET =
- MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET + MSFT_LONG_SIZE;
- static final int MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET =
- MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET + (MSFT_WCHAR_SIZE*32);
- static final int MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET =
- MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
- static final int MSFT_TIME_ZONE_SIZE =
- MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET + MSFT_LONG_SIZE;
-
- // TimeZone cache; we parse/decode as little as possible, because the process is quite slow
- private static HashMap<String, TimeZone> sTimeZoneCache = new HashMap<String, TimeZone>();
- // TZI string cache; we keep around our encoded TimeZoneInformation strings
- private static HashMap<TimeZone, String> sTziStringCache = new HashMap<TimeZone, String>();
-
- private static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
- // Default, Popup
- private static final String ALLOWED_REMINDER_TYPES = "0,1";
- // None, required, optional
- private static final String ALLOWED_ATTENDEE_TYPES = "0,1,2";
- // Busy, free, tentative
- private static final String ALLOWED_AVAILABILITIES = "0,1,2";
-
- // There is no type 4 (thus, the "")
- static final String[] sTypeToFreq =
- new String[] {"DAILY", "WEEKLY", "MONTHLY", "MONTHLY", "", "YEARLY", "YEARLY"};
-
- static final String[] sDayTokens =
- new String[] {"SU", "MO", "TU", "WE", "TH", "FR", "SA"};
-
- static final String[] sTwoCharacterNumbers =
- new String[] {"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"};
-
- // Bits used in EAS recurrences for days of the week
- protected static final int EAS_SUNDAY = 1<<0;
- protected static final int EAS_MONDAY = 1<<1;
- protected static final int EAS_TUESDAY = 1<<2;
- protected static final int EAS_WEDNESDAY = 1<<3;
- protected static final int EAS_THURSDAY = 1<<4;
- protected static final int EAS_FRIDAY = 1<<5;
- protected static final int EAS_SATURDAY = 1<<6;
- protected static final int EAS_WEEKDAYS =
- EAS_MONDAY | EAS_TUESDAY | EAS_WEDNESDAY | EAS_THURSDAY | EAS_FRIDAY;
- protected static final int EAS_WEEKENDS = EAS_SATURDAY | EAS_SUNDAY;
-
- static final int sCurrentYear = new GregorianCalendar().get(Calendar.YEAR);
- static final TimeZone sGmtTimeZone = TimeZone.getTimeZone("GMT");
-
- private static final String ICALENDAR_ATTENDEE = "ATTENDEE;ROLE=REQ-PARTICIPANT";
- static final String ICALENDAR_ATTENDEE_CANCEL = ICALENDAR_ATTENDEE;
- static final String ICALENDAR_ATTENDEE_INVITE =
- ICALENDAR_ATTENDEE + ";PARTSTAT=NEEDS-ACTION;RSVP=TRUE";
- static final String ICALENDAR_ATTENDEE_ACCEPT =
- ICALENDAR_ATTENDEE + ";PARTSTAT=ACCEPTED";
- static final String ICALENDAR_ATTENDEE_DECLINE =
- ICALENDAR_ATTENDEE + ";PARTSTAT=DECLINED";
- static final String ICALENDAR_ATTENDEE_TENTATIVE =
- ICALENDAR_ATTENDEE + ";PARTSTAT=TENTATIVE";
-
- // Note that these constants apply to Calendar items
- // For future reference: MeetingRequest data can also include free/busy information, but the
- // constants for these four options in MeetingRequest data have different values!
- // See [MS-ASCAL] 2.2.2.8 for Calendar BusyStatus
- // See [MS-EMAIL] 2.2.2.34 for MeetingRequest BusyStatus
- public static final int BUSY_STATUS_FREE = 0;
- public static final int BUSY_STATUS_TENTATIVE = 1;
- public static final int BUSY_STATUS_BUSY = 2;
- public static final int BUSY_STATUS_OUT_OF_OFFICE = 3;
-
- // Note that these constants apply to Calendar items, and are used in EAS 14+
- // See [MS-ASCAL] 2.2.2.22 for Calendar ResponseType
- public static final int RESPONSE_TYPE_NONE = 0;
- public static final int RESPONSE_TYPE_ORGANIZER = 1;
- public static final int RESPONSE_TYPE_TENTATIVE = 2;
- public static final int RESPONSE_TYPE_ACCEPTED = 3;
- public static final int RESPONSE_TYPE_DECLINED = 4;
- public static final int RESPONSE_TYPE_NOT_RESPONDED = 5;
-
- // Return a 4-byte long from a byte array (little endian)
- static int getLong(byte[] bytes, int offset) {
- return (bytes[offset++] & 0xFF) | ((bytes[offset++] & 0xFF) << 8) |
- ((bytes[offset++] & 0xFF) << 16) | ((bytes[offset] & 0xFF) << 24);
- }
-
- // Put a 4-byte long into a byte array (little endian)
- static void setLong(byte[] bytes, int offset, int value) {
- bytes[offset++] = (byte) (value & 0xFF);
- bytes[offset++] = (byte) ((value >> 8) & 0xFF);
- bytes[offset++] = (byte) ((value >> 16) & 0xFF);
- bytes[offset] = (byte) ((value >> 24) & 0xFF);
- }
-
- // Return a 2-byte word from a byte array (little endian)
- static int getWord(byte[] bytes, int offset) {
- return (bytes[offset++] & 0xFF) | ((bytes[offset] & 0xFF) << 8);
- }
-
- // Put a 2-byte word into a byte array (little endian)
- static void setWord(byte[] bytes, int offset, int value) {
- bytes[offset++] = (byte) (value & 0xFF);
- bytes[offset] = (byte) ((value >> 8) & 0xFF);
- }
-
- // Internal structure for storing a time zone date from a SYSTEMTIME structure
- // This date represents either the start or the end time for DST
- static class TimeZoneDate {
- String year;
- int month;
- int dayOfWeek;
- int day;
- int time;
- int hour;
- int minute;
- }
-
- @VisibleForTesting
- static void clearTimeZoneCache() {
- sTimeZoneCache.clear();
- }
-
- static void putRuleIntoTimeZoneInformation(byte[] bytes, int offset, RRule rrule, int hour,
- int minute) {
- // MSFT months are 1 based, same as RRule
- setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, rrule.month);
- // MSFT day of week starts w/ Sunday = 0; RRule starts w/ Sunday = 1
- setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, rrule.dayOfWeek - 1);
- // 5 means "last" in MSFT land; for RRule, it's -1
- setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, rrule.week < 0 ? 5 : rrule.week);
- // Turn hours/minutes into ms from midnight (per TimeZone)
- setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, hour);
- setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, minute);
- }
-
- // Write a transition time into SYSTEMTIME data (via an offset into a byte array)
- static void putTransitionMillisIntoSystemTime(byte[] bytes, int offset, long millis) {
- GregorianCalendar cal = new GregorianCalendar(TimeZone.getDefault());
- // Round to the next highest minute; we always write seconds as zero
- cal.setTimeInMillis(millis + 30*SECONDS);
-
- // MSFT months are 1 based; TimeZone is 0 based
- setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, cal.get(Calendar.MONTH) + 1);
- // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1
- setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, cal.get(Calendar.DAY_OF_WEEK) - 1);
-
- // Get the "day" in TimeZone format
- int wom = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
- // 5 means "last" in MSFT land; for TimeZone, it's -1
- setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, wom < 0 ? 5 : wom);
-
- // Turn hours/minutes into ms from midnight (per TimeZone)
- setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, getTrueTransitionHour(cal));
- setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, getTrueTransitionMinute(cal));
- }
-
- // Build a TimeZoneDate structure from a SYSTEMTIME within a byte array at a given offset
- static TimeZoneDate getTimeZoneDateFromSystemTime(byte[] bytes, int offset) {
- TimeZoneDate tzd = new TimeZoneDate();
-
- // MSFT year is an int; TimeZone is a String
- int num = getWord(bytes, offset + MSFT_SYSTEMTIME_YEAR);
- tzd.year = Integer.toString(num);
-
- // MSFT month = 0 means no daylight time
- // MSFT months are 1 based; TimeZone is 0 based
- num = getWord(bytes, offset + MSFT_SYSTEMTIME_MONTH);
- if (num == 0) {
- return null;
- } else {
- tzd.month = num -1;
- }
-
- // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1
- tzd.dayOfWeek = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK) + 1;
-
- // Get the "day" in TimeZone format
- num = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY);
- // 5 means "last" in MSFT land; for TimeZone, it's -1
- if (num == 5) {
- tzd.day = -1;
- } else {
- tzd.day = num;
- }
-
- // Turn hours/minutes into ms from midnight (per TimeZone)
- int hour = getWord(bytes, offset + MSFT_SYSTEMTIME_HOUR);
- tzd.hour = hour;
- int minute = getWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE);
- tzd.minute = minute;
- tzd.time = (hour*HOURS) + (minute*MINUTES);
-
- return tzd;
- }
-
- /**
- * Build a GregorianCalendar, based on a time zone and TimeZoneDate.
- * @param timeZone the time zone we're checking
- * @param tzd the TimeZoneDate we're interested in
- * @return a GregorianCalendar with the given time zone and date
- */
- static long getMillisAtTimeZoneDateTransition(TimeZone timeZone, TimeZoneDate tzd) {
- GregorianCalendar testCalendar = new GregorianCalendar(timeZone);
- testCalendar.set(GregorianCalendar.YEAR, sCurrentYear);
- testCalendar.set(GregorianCalendar.MONTH, tzd.month);
- testCalendar.set(GregorianCalendar.DAY_OF_WEEK, tzd.dayOfWeek);
- testCalendar.set(GregorianCalendar.DAY_OF_WEEK_IN_MONTH, tzd.day);
- testCalendar.set(GregorianCalendar.HOUR_OF_DAY, tzd.hour);
- testCalendar.set(GregorianCalendar.MINUTE, tzd.minute);
- testCalendar.set(GregorianCalendar.SECOND, 0);
- return testCalendar.getTimeInMillis();
- }
-
- /**
- * Return a GregorianCalendar representing the first standard/daylight transition between a
- * start time and an end time in the given time zone
- * @param tz a TimeZone the time zone in which we're looking for transitions
- * @param startTime the start time for the test
- * @param endTime the end time for the test
- * @param startInDaylightTime whether daylight time is in effect at the startTime
- * @return a GregorianCalendar representing the transition or null if none
- */
- static GregorianCalendar findTransitionDate(TimeZone tz, long startTime,
- long endTime, boolean startInDaylightTime) {
- long startingEndTime = endTime;
- Date date = null;
-
- // We'll keep splitting the difference until we're within a minute
- while ((endTime - startTime) > MINUTES) {
- long checkTime = ((startTime + endTime) / 2) + 1;
- date = new Date(checkTime);
- boolean inDaylightTime = tz.inDaylightTime(date);
- if (inDaylightTime != startInDaylightTime) {
- endTime = checkTime;
- } else {
- startTime = checkTime;
- }
- }
-
- // If these are the same, we're really messed up; return null
- if (endTime == startingEndTime) {
- return null;
- }
-
- // Set up our calendar and return it
- GregorianCalendar calendar = new GregorianCalendar(tz);
- calendar.setTimeInMillis(startTime);
- return calendar;
- }
-
- /**
- * Return a Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone
- * that might be found in an Event; use cached result, if possible
- * @param tz the TimeZone
- * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
- */
- static public String timeZoneToTziString(TimeZone tz) {
- String tziString = sTziStringCache.get(tz);
- if (tziString != null) {
- if (Eas.USER_LOG) {
- ExchangeService.log(TAG, "TZI string for " + tz.getDisplayName() +
- " found in cache.");
- }
- return tziString;
- }
- tziString = timeZoneToTziStringImpl(tz);
- sTziStringCache.put(tz, tziString);
- return tziString;
- }
-
- /**
- * A class for storing RRULE information. The RRULE members can be accessed individually or
- * an RRULE string can be created with toString()
- */
- static class RRule {
- static final int RRULE_NONE = 0;
- static final int RRULE_DAY_WEEK = 1;
- static final int RRULE_DATE = 2;
-
- int type;
- int dayOfWeek;
- int week;
- int month;
- int date;
-
- /**
- * Create an RRULE based on month and date
- * @param _month the month (1 = JAN, 12 = DEC)
- * @param _date the date in the month (1-31)
- */
- RRule(int _month, int _date) {
- type = RRULE_DATE;
- month = _month;
- date = _date;
- }
-
- /**
- * Create an RRULE based on month, day of week, and week #
- * @param _month the month (1 = JAN, 12 = DEC)
- * @param _dayOfWeek the day of the week (1 = SU, 7 = SA)
- * @param _week the week in the month (1-5 or -1 for last)
- */
- RRule(int _month, int _dayOfWeek, int _week) {
- type = RRULE_DAY_WEEK;
- month = _month;
- dayOfWeek = _dayOfWeek;
- week = _week;
- }
-
- @Override
- public String toString() {
- if (type == RRULE_DAY_WEEK) {
- return "FREQ=YEARLY;BYMONTH=" + month + ";BYDAY=" + week +
- sDayTokens[dayOfWeek - 1];
- } else {
- return "FREQ=YEARLY;BYMONTH=" + month + ";BYMONTHDAY=" + date;
- }
- }
- }
-
- /**
- * Generate an RRULE string for an array of GregorianCalendars, if possible. For now, we are
- * only looking for rules based on the same date in a month or a specific instance of a day of
- * the week in a month (e.g. 2nd Tuesday or last Friday). Indeed, these are the only kinds of
- * rules used in the current tzinfo database.
- * @param calendars an array of GregorianCalendar, set to a series of transition times in
- * consecutive years starting with the current year
- * @return an RRULE or null if none could be inferred from the calendars
- */
- static RRule inferRRuleFromCalendars(GregorianCalendar[] calendars) {
- // Let's see if we can make a rule about these
- GregorianCalendar calendar = calendars[0];
- if (calendar == null) return null;
- int month = calendar.get(Calendar.MONTH);
- int date = calendar.get(Calendar.DAY_OF_MONTH);
- int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
- int week = calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH);
- int maxWeek = calendar.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH);
- boolean dateRule = false;
- boolean dayOfWeekRule = false;
- for (int i = 1; i < calendars.length; i++) {
- GregorianCalendar cal = calendars[i];
- if (cal == null) return null;
- // If it's not the same month, there's no rule
- if (cal.get(Calendar.MONTH) != month) {
- return null;
- } else if (dayOfWeek == cal.get(Calendar.DAY_OF_WEEK)) {
- // Ok, it seems to be the same day of the week
- if (dateRule) {
- return null;
- }
- dayOfWeekRule = true;
- int thisWeek = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
- if (week != thisWeek) {
- if (week < 0 || week == maxWeek) {
- int thisMaxWeek = cal.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH);
- if (thisWeek == thisMaxWeek) {
- // We'll use -1 (i.e. last) week
- week = -1;
- continue;
- }
- }
- return null;
- }
- } else if (date == cal.get(Calendar.DAY_OF_MONTH)) {
- // Maybe the same day of the month?
- if (dayOfWeekRule) {
- return null;
- }
- dateRule = true;
- } else {
- return null;
- }
- }
-
- if (dateRule) {
- return new RRule(month + 1, date);
- }
- // sDayTokens is 0 based (SU = 0); Calendar days of week are 1 based (SU = 1)
- // iCalendar months are 1 based; Calendar months are 0 based
- // So we adjust these when building the string
- return new RRule(month + 1, dayOfWeek, week);
- }
-
- /**
- * Generate an rfc2445 utcOffset from minutes offset from GMT
- * These look like +0800 or -0100
- * @param offsetMinutes minutes offset from GMT (east is positive, west is negative
- * @return a utcOffset
- */
- static String utcOffsetString(int offsetMinutes) {
- StringBuilder sb = new StringBuilder();
- int hours = offsetMinutes / 60;
- if (hours < 0) {
- sb.append('-');
- hours = 0 - hours;
- } else {
- sb.append('+');
- }
- int minutes = offsetMinutes % 60;
- if (hours < 10) {
- sb.append('0');
- }
- sb.append(hours);
- if (minutes < 10) {
- sb.append('0');
- }
- sb.append(minutes);
- return sb.toString();
- }
-
- /**
- * Fill the passed in GregorianCalendars arrays with DST transition information for this and
- * the following years (based on the length of the arrays)
- * @param tz the time zone
- * @param toDaylightCalendars an array of GregorianCalendars, one for each year, representing
- * the transition to daylight time
- * @param toStandardCalendars an array of GregorianCalendars, one for each year, representing
- * the transition to standard time
- * @return true if transitions could be found for all years, false otherwise
- */
- static boolean getDSTCalendars(TimeZone tz, GregorianCalendar[] toDaylightCalendars,
- GregorianCalendar[] toStandardCalendars) {
- // We'll use the length of the arrays to determine how many years to check
- int maxYears = toDaylightCalendars.length;
- if (toStandardCalendars.length != maxYears) {
- return false;
- }
- // Get the transitions for this year and the next few years
- for (int i = 0; i < maxYears; i++) {
- GregorianCalendar cal = new GregorianCalendar(tz);
- cal.set(sCurrentYear + i, Calendar.JANUARY, 1, 0, 0, 0);
- long startTime = cal.getTimeInMillis();
- // Calculate end of year; no need to be insanely precise
- long endOfYearTime = startTime + (365*DAYS) + (DAYS>>2);
- Date date = new Date(startTime);
- boolean startInDaylightTime = tz.inDaylightTime(date);
- // Find the first transition, and store
- cal = findTransitionDate(tz, startTime, endOfYearTime, startInDaylightTime);
- if (cal == null) {
- return false;
- } else if (startInDaylightTime) {
- toStandardCalendars[i] = cal;
- } else {
- toDaylightCalendars[i] = cal;
- }
- // Find the second transition, and store
- cal = findTransitionDate(tz, startTime, endOfYearTime, !startInDaylightTime);
- if (cal == null) {
- return false;
- } else if (startInDaylightTime) {
- toDaylightCalendars[i] = cal;
- } else {
- toStandardCalendars[i] = cal;
- }
- }
- return true;
- }
-
- /**
- * Write out the STANDARD block of VTIMEZONE and end the VTIMEZONE
- * @param writer the SimpleIcsWriter we're using
- * @param tz the time zone
- * @param offsetString the offset string in VTIMEZONE format (e.g. +0800)
- * @throws IOException
- */
- static private void writeNoDST(SimpleIcsWriter writer, TimeZone tz, String offsetString)
- throws IOException {
- writer.writeTag("BEGIN", "STANDARD");
- writer.writeTag("TZOFFSETFROM", offsetString);
- writer.writeTag("TZOFFSETTO", offsetString);
- // Might as well use start of epoch for start date
- writer.writeTag("DTSTART", millisToEasDateTime(0L));
- writer.writeTag("END", "STANDARD");
- writer.writeTag("END", "VTIMEZONE");
- }
-
- /** Write a VTIMEZONE block for a given TimeZone into a SimpleIcsWriter
- * @param tz the TimeZone to be used in the conversion
- * @param writer the SimpleIcsWriter to be used
- * @throws IOException
- */
- static void timeZoneToVTimezone(TimeZone tz, SimpleIcsWriter writer)
- throws IOException {
- // We'll use these regardless of whether there's DST in this time zone or not
- int rawOffsetMinutes = tz.getRawOffset() / MINUTES;
- String standardOffsetString = utcOffsetString(rawOffsetMinutes);
-
- // Preamble for all of our VTIMEZONEs
- writer.writeTag("BEGIN", "VTIMEZONE");
- writer.writeTag("TZID", tz.getID());
- writer.writeTag("X-LIC-LOCATION", tz.getDisplayName());
-
- // Simplest case is no daylight time
- if (!tz.useDaylightTime()) {
- writeNoDST(writer, tz, standardOffsetString);
- return;
- }
-
- int maxYears = 3;
- GregorianCalendar[] toDaylightCalendars = new GregorianCalendar[maxYears];
- GregorianCalendar[] toStandardCalendars = new GregorianCalendar[maxYears];
- if (!getDSTCalendars(tz, toDaylightCalendars, toStandardCalendars)) {
- writeNoDST(writer, tz, standardOffsetString);
- return;
- }
- // Try to find a rule to cover these yeras
- RRule daylightRule = inferRRuleFromCalendars(toDaylightCalendars);
- RRule standardRule = inferRRuleFromCalendars(toStandardCalendars);
- String daylightOffsetString =
- utcOffsetString(rawOffsetMinutes + (tz.getDSTSavings() / MINUTES));
- // We'll use RRULE's if we found both
- // Otherwise we write the first as DTSTART and the others as RDATE
- boolean hasRule = daylightRule != null && standardRule != null;
-
- // Write the DAYLIGHT block
- writer.writeTag("BEGIN", "DAYLIGHT");
- writer.writeTag("TZOFFSETFROM", standardOffsetString);
- writer.writeTag("TZOFFSETTO", daylightOffsetString);
- writer.writeTag("DTSTART",
- transitionMillisToVCalendarTime(
- toDaylightCalendars[0].getTimeInMillis(), tz, true));
- if (hasRule) {
- writer.writeTag("RRULE", daylightRule.toString());
- } else {
- for (int i = 1; i < maxYears; i++) {
- writer.writeTag("RDATE", transitionMillisToVCalendarTime(
- toDaylightCalendars[i].getTimeInMillis(), tz, true));
- }
- }
- writer.writeTag("END", "DAYLIGHT");
- // Write the STANDARD block
- writer.writeTag("BEGIN", "STANDARD");
- writer.writeTag("TZOFFSETFROM", daylightOffsetString);
- writer.writeTag("TZOFFSETTO", standardOffsetString);
- writer.writeTag("DTSTART",
- transitionMillisToVCalendarTime(
- toStandardCalendars[0].getTimeInMillis(), tz, false));
- if (hasRule) {
- writer.writeTag("RRULE", standardRule.toString());
- } else {
- for (int i = 1; i < maxYears; i++) {
- writer.writeTag("RDATE", transitionMillisToVCalendarTime(
- toStandardCalendars[i].getTimeInMillis(), tz, true));
- }
- }
- writer.writeTag("END", "STANDARD");
- // And we're done
- writer.writeTag("END", "VTIMEZONE");
- }
-
- /**
- * Find the next transition to occur (i.e. after the current date/time)
- * @param transitions calendars representing transitions to/from DST
- * @return millis for the first transition after the current date/time
- */
- static long findNextTransition(long startingMillis, GregorianCalendar[] transitions) {
- for (GregorianCalendar transition: transitions) {
- long transitionMillis = transition.getTimeInMillis();
- if (transitionMillis > startingMillis) {
- return transitionMillis;
- }
- }
- return 0;
- }
-
- /**
- * Calculate the Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone
- * that might be found in an Event. Since the internal representation of the TimeZone is hidden
- * from us we'll find the DST transitions and build the structure from that information
- * @param tz the TimeZone
- * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
- */
- static String timeZoneToTziStringImpl(TimeZone tz) {
- String tziString;
- byte[] tziBytes = new byte[MSFT_TIME_ZONE_SIZE];
- int standardBias = - tz.getRawOffset();
- standardBias /= 60*SECONDS;
- setLong(tziBytes, MSFT_TIME_ZONE_BIAS_OFFSET, standardBias);
- // If this time zone has daylight savings time, we need to do more work
- if (tz.useDaylightTime()) {
- GregorianCalendar[] toDaylightCalendars = new GregorianCalendar[3];
- GregorianCalendar[] toStandardCalendars = new GregorianCalendar[3];
- // See if we can get transitions for a few years; if not, we can't generate DST info
- // for this time zone
- if (getDSTCalendars(tz, toDaylightCalendars, toStandardCalendars)) {
- // Try to find a rule to cover these years
- RRule daylightRule = inferRRuleFromCalendars(toDaylightCalendars);
- RRule standardRule = inferRRuleFromCalendars(toStandardCalendars);
- if ((daylightRule != null) && (daylightRule.type == RRule.RRULE_DAY_WEEK) &&
- (standardRule != null) && (standardRule.type == RRule.RRULE_DAY_WEEK)) {
- // We need both rules and they have to be DAY/WEEK type
- // Write month, day of week, week, hour, minute
- putRuleIntoTimeZoneInformation(tziBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET,
- standardRule,
- getTrueTransitionHour(toStandardCalendars[0]),
- getTrueTransitionMinute(toStandardCalendars[0]));
- putRuleIntoTimeZoneInformation(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET,
- daylightRule,
- getTrueTransitionHour(toDaylightCalendars[0]),
- getTrueTransitionMinute(toDaylightCalendars[0]));
- } else {
- // If there's no rule, we'll use the first transition to standard/to daylight
- // And indicate that it's just for this year...
- long now = System.currentTimeMillis();
- long standardTransition = findNextTransition(now, toStandardCalendars);
- long daylightTransition = findNextTransition(now, toDaylightCalendars);
- // If we can't find transitions, we can't do DST
- if (standardTransition != 0 && daylightTransition != 0) {
- putTransitionMillisIntoSystemTime(tziBytes,
- MSFT_TIME_ZONE_STANDARD_DATE_OFFSET, standardTransition);
- putTransitionMillisIntoSystemTime(tziBytes,
- MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET, daylightTransition);
- }
- }
- }
- int dstOffset = tz.getDSTSavings();
- setLong(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET, - dstOffset / MINUTES);
- }
- byte[] tziEncodedBytes = Base64.encode(tziBytes, Base64.NO_WRAP);
- tziString = new String(tziEncodedBytes);
- return tziString;
- }
-
- /**
- * Given a String as directly read from EAS, returns a TimeZone corresponding to that String
- * @param timeZoneString the String read from the server
- * @param precision the number of milliseconds of precision in TimeZone determination
- * @return the TimeZone, or TimeZone.getDefault() if not found
- */
- @VisibleForTesting
- static TimeZone tziStringToTimeZone(String timeZoneString, int precision) {
- // If we have this time zone cached, use that value and return
- TimeZone timeZone = sTimeZoneCache.get(timeZoneString);
- if (timeZone != null) {
- if (Eas.USER_LOG) {
- ExchangeService.log(TAG, " Using cached TimeZone " + timeZone.getID());
- }
- } else {
- timeZone = tziStringToTimeZoneImpl(timeZoneString, precision);
- if (timeZone == null) {
- // If we don't find a match, we just return the current TimeZone. In theory, this
- // shouldn't be happening...
- ExchangeService.alwaysLog("TimeZone not found using default: " + timeZoneString);
- timeZone = TimeZone.getDefault();
- }
- sTimeZoneCache.put(timeZoneString, timeZone);
- }
- return timeZone;
- }
-
- /**
- * The standard entry to EAS time zone conversion, using one minute as the precision
- */
- static public TimeZone tziStringToTimeZone(String timeZoneString) {
- return tziStringToTimeZone(timeZoneString, MINUTES);
- }
-
- /**
- * Given a String as directly read from EAS, tries to find a TimeZone in the database of all
- * time zones that corresponds to that String. If the test time zone string includes DST and
- * we don't find a match, and we're using standard precision, we try again with lenient
- * precision, which is a bit better than guessing
- * @param timeZoneString the String read from the server
- * @return the TimeZone, or null if not found
- */
- static TimeZone tziStringToTimeZoneImpl(String timeZoneString, int precision) {
- TimeZone timeZone = null;
- // First, we need to decode the base64 string
- byte[] timeZoneBytes = Base64.decode(timeZoneString, Base64.DEFAULT);
-
- // Then, we get the bias (similar to a rawOffset); for TimeZone, we need ms
- // but EAS gives us minutes, so do the conversion. Note that EAS is the bias that's added
- // to the time zone to reach UTC; our library uses the time from UTC to our time zone, so
- // we need to change the sign
- int bias = -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_BIAS_OFFSET) * MINUTES;
-
- // Get all of the time zones with the bias as a rawOffset; if there aren't any, we return
- // the default time zone
- String[] zoneIds = TimeZone.getAvailableIDs(bias);
- if (zoneIds.length > 0) {
- // Try to find an existing TimeZone from the data provided by EAS
- // We start by pulling out the date that standard time begins
- TimeZoneDate dstEnd =
- getTimeZoneDateFromSystemTime(timeZoneBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET);
- if (dstEnd == null) {
- // If the default time zone is a match
- TimeZone defaultTimeZone = TimeZone.getDefault();
- if (!defaultTimeZone.useDaylightTime() &&
- ArrayUtils.contains(zoneIds, defaultTimeZone.getID())) {
- if (Eas.USER_LOG) {
- ExchangeService.log(TAG, "TimeZone without DST found to be default: " +
- defaultTimeZone.getID());
- }
- return defaultTimeZone;
- }
- // In this case, there is no daylight savings time, so the only interesting data
- // for possible matches is the offset and DST availability; we'll take the first
- // match for those
- for (String zoneId: zoneIds) {
- timeZone = TimeZone.getTimeZone(zoneId);
- if (!timeZone.useDaylightTime()) {
- if (Eas.USER_LOG) {
- ExchangeService.log(TAG, "TimeZone without DST found by offset: " +
- timeZone.getID());
- }
- return timeZone;
- }
- }
- // None found, return null
- return null;
- } else {
- TimeZoneDate dstStart = getTimeZoneDateFromSystemTime(timeZoneBytes,
- MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET);
- // See comment above for bias...
- long dstSavings =
- -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET) * MINUTES;
-
- // We'll go through each time zone to find one with the same DST transitions and
- // savings length
- for (String zoneId: zoneIds) {
- // Get the TimeZone using the zoneId
- timeZone = TimeZone.getTimeZone(zoneId);
-
- // Our strategy here is to check just before and just after the transitions
- // and see whether the check for daylight time matches the expectation
- // If both transitions match, then we have a match for the offset and start/end
- // of dst. That's the best we can do for now, since there's no other info
- // provided by EAS (i.e. we can't get dynamic transitions, etc.)
-
- // Check one minute before and after DST start transition
- long millisAtTransition = getMillisAtTimeZoneDateTransition(timeZone, dstStart);
- Date before = new Date(millisAtTransition - precision);
- Date after = new Date(millisAtTransition + precision);
- if (timeZone.inDaylightTime(before)) continue;
- if (!timeZone.inDaylightTime(after)) continue;
-
- // Check one minute before and after DST end transition
- millisAtTransition = getMillisAtTimeZoneDateTransition(timeZone, dstEnd);
- // Note that we need to subtract an extra hour here, because we end up with
- // gaining an hour in the transition BACK to standard time
- before = new Date(millisAtTransition - (dstSavings + precision));
- after = new Date(millisAtTransition + precision);
- if (!timeZone.inDaylightTime(before)) continue;
- if (timeZone.inDaylightTime(after)) continue;
-
- // Check that the savings are the same
- if (dstSavings != timeZone.getDSTSavings()) continue;
- return timeZone;
- }
- // In this case, there is no daylight savings time, so the only interesting data
- // is the offset, and we know that all of the zoneId's match; we'll take the first
- boolean lenient = false;
- if ((dstStart.hour != dstEnd.hour) && (precision == STANDARD_DST_PRECISION)) {
- timeZone = tziStringToTimeZoneImpl(timeZoneString, LENIENT_DST_PRECISION);
- lenient = true;
- } else {
- timeZone = TimeZone.getTimeZone(zoneIds[0]);
- }
- if (Eas.USER_LOG) {
- ExchangeService.log(TAG,
- "No TimeZone with correct DST settings; using " +
- (lenient ? "lenient" : "first") + ": " + timeZone.getID());
- }
- return timeZone;
- }
- }
- return null;
- }
-
- static public String convertEmailDateTimeToCalendarDateTime(String date) {
- // Format for email date strings is 2010-02-23T16:00:00.000Z
- // Format for calendar date strings is 20100223T160000Z
- return date.substring(0, 4) + date.substring(5, 7) + date.substring(8, 13) +
- date.substring(14, 16) + date.substring(17, 19) + 'Z';
- }
-
- static String formatTwo(int num) {
- if (num <= 12) {
- return sTwoCharacterNumbers[num];
- } else
- return Integer.toString(num);
- }
-
- /**
- * Generate an EAS formatted date/time string based on GMT. See below for details.
- */
- static public String millisToEasDateTime(long millis) {
- return millisToEasDateTime(millis, sGmtTimeZone, true);
- }
-
- /**
- * Generate an EAS formatted local date/time string from a time and a time zone. If the final
- * argument is false, only a date will be returned (e.g. 20100331)
- * @param millis a time in milliseconds
- * @param tz a time zone
- * @param withTime if the time is to be included in the string
- * @return an EAS formatted string indicating the date (and time) in the given time zone
- */
- static public String millisToEasDateTime(long millis, TimeZone tz, boolean withTime) {
- StringBuilder sb = new StringBuilder();
- GregorianCalendar cal = new GregorianCalendar(tz);
- cal.setTimeInMillis(millis);
- sb.append(cal.get(Calendar.YEAR));
- sb.append(formatTwo(cal.get(Calendar.MONTH) + 1));
- sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH)));
- if (withTime) {
- sb.append('T');
- sb.append(formatTwo(cal.get(Calendar.HOUR_OF_DAY)));
- sb.append(formatTwo(cal.get(Calendar.MINUTE)));
- sb.append(formatTwo(cal.get(Calendar.SECOND)));
- if (tz == sGmtTimeZone) {
- sb.append('Z');
- }
- }
- return sb.toString();
- }
-
- /**
- * Return the true minute at which a transition occurs
- * Our transition time should be the in the minute BEFORE the transition
- * If this minute is 59, set minute to 0 and increment the hour
- * NOTE: We don't want to add a minute and retrieve minute/hour from the Calendar, because
- * Calendar time will itself be influenced by the transition! So adding 1 minute to
- * 01:59 (assume PST->PDT) will become 03:00, which isn't what we want (we want 02:00)
- *
- * @param calendar the calendar holding the transition date/time
- * @return the true minute of the transition
- */
- static int getTrueTransitionMinute(GregorianCalendar calendar) {
- int minute = calendar.get(Calendar.MINUTE);
- if (minute == 59) {
- minute = 0;
- }
- return minute;
- }
-
- /**
- * Return the true hour at which a transition occurs
- * See description for getTrueTransitionMinute, above
- * @param calendar the calendar holding the transition date/time
- * @return the true hour of the transition
- */
- static int getTrueTransitionHour(GregorianCalendar calendar) {
- int hour = calendar.get(Calendar.HOUR_OF_DAY);
- hour++;
- if (hour == 24) {
- hour = 0;
- }
- return hour;
- }
-
- /**
- * Generate a date/time string suitable for VTIMEZONE from a transition time in millis
- * The format is YYYYMMDDTHHMMSS
- * @param millis a transition time in milliseconds
- * @param tz a time zone
- * @param dst whether we're entering daylight time
- */
- static String transitionMillisToVCalendarTime(long millis, TimeZone tz, boolean dst) {
- StringBuilder sb = new StringBuilder();
- GregorianCalendar cal = new GregorianCalendar(tz);
- cal.setTimeInMillis(millis);
- sb.append(cal.get(Calendar.YEAR));
- sb.append(formatTwo(cal.get(Calendar.MONTH) + 1));
- sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH)));
- sb.append('T');
- sb.append(formatTwo(getTrueTransitionHour(cal)));
- sb.append(formatTwo(getTrueTransitionMinute(cal)));
- sb.append(formatTwo(0));
- return sb.toString();
- }
-
- /**
- * Returns a UTC calendar with year/month/day from local calendar and h/m/s/ms = 0
- * @param time the time in seconds of an all-day event in local time
- * @return the time in seconds in UTC
- */
- static public long getUtcAllDayCalendarTime(long time, TimeZone localTimeZone) {
- return transposeAllDayTime(time, localTimeZone, UTC_TIMEZONE);
- }
-
- /**
- * Returns a local calendar with year/month/day from UTC calendar and h/m/s/ms = 0
- * @param time the time in seconds of an all-day event in UTC
- * @return the time in seconds in local time
- */
- static public long getLocalAllDayCalendarTime(long time, TimeZone localTimeZone) {
- return transposeAllDayTime(time, UTC_TIMEZONE, localTimeZone);
- }
-
- static private long transposeAllDayTime(long time, TimeZone fromTimeZone,
- TimeZone toTimeZone) {
- GregorianCalendar fromCalendar = new GregorianCalendar(fromTimeZone);
- fromCalendar.setTimeInMillis(time);
- GregorianCalendar toCalendar = new GregorianCalendar(toTimeZone);
- // Set this calendar with correct year, month, and day, but zero hour, minute, and seconds
- toCalendar.set(fromCalendar.get(GregorianCalendar.YEAR),
- fromCalendar.get(GregorianCalendar.MONTH),
- fromCalendar.get(GregorianCalendar.DATE), 0, 0, 0);
- toCalendar.set(GregorianCalendar.MILLISECOND, 0);
- return toCalendar.getTimeInMillis();
- }
-
- static void addByDay(StringBuilder rrule, int dow, int wom) {
- rrule.append(";BYDAY=");
- boolean addComma = false;
- for (int i = 0; i < 7; i++) {
- if ((dow & 1) == 1) {
- if (addComma) {
- rrule.append(',');
- }
- if (wom > 0) {
- // 5 = last week -> -1
- // So -1SU = last sunday
- rrule.append(wom == 5 ? -1 : wom);
- }
- rrule.append(sDayTokens[i]);
- addComma = true;
- }
- dow >>= 1;
- }
- }
-
- static void addBySetpos(StringBuilder rrule, int dow, int wom) {
- // Indicate the days, but don't use wom in this case (it's used in the BYSETPOS);
- addByDay(rrule, dow, 0);
- rrule.append(";BYSETPOS=");
- rrule.append(wom == 5 ? "-1" : wom);
- }
-
- static void addByMonthDay(StringBuilder rrule, int dom) {
- // 127 means last day of the month
- if (dom == 127) {
- dom = -1;
- }
- rrule.append(";BYMONTHDAY=" + dom);
- }
-
- /**
- * Generate the String version of the EAS integer for a given BYDAY value in an rrule
- * @param dow the BYDAY value of the rrule
- * @return the String version of the EAS value of these days
- */
- static String generateEasDayOfWeek(String dow) {
- int bits = 0;
- int bit = 1;
- for (String token: sDayTokens) {
- // If we can find the day in the dow String, add the bit to our bits value
- if (dow.indexOf(token) >= 0) {
- bits |= bit;
- }
- bit <<= 1;
- }
- return Integer.toString(bits);
- }
-
- /**
- * Extract the value of a token in an RRULE string
- * @param rrule an RRULE string
- * @param token a token to look for in the RRULE
- * @return the value of that token
- */
- static String tokenFromRrule(String rrule, String token) {
- int start = rrule.indexOf(token);
- if (start < 0) return null;
- int len = rrule.length();
- start += token.length();
- int end = start;
- char c;
- do {
- c = rrule.charAt(end++);
- if ((c == ';') || (end == len)) {
- if (end == len) end++;
- return rrule.substring(start, end -1);
- }
- } while (true);
- }
-
- /**
- * Reformat an RRULE style UNTIL to an EAS style until
- */
- @VisibleForTesting
- static String recurrenceUntilToEasUntil(String until) {
- // Get a calendar in our local time zone
- GregorianCalendar localCalendar = new GregorianCalendar(TimeZone.getDefault());
- // Set the time per GMT time in the 'until'
- localCalendar.setTimeInMillis(Utility.parseDateTimeToMillis(until));
- StringBuilder sb = new StringBuilder();
- // Build a string with local year/month/date
- sb.append(localCalendar.get(Calendar.YEAR));
- sb.append(formatTwo(localCalendar.get(Calendar.MONTH) + 1));
- sb.append(formatTwo(localCalendar.get(Calendar.DAY_OF_MONTH)));
- // EAS ignores the time in 'until'; go figure
- sb.append("T000000Z");
- return sb.toString();
- }
-
- /**
- * Convenience method to add "count", "interval", and "until" to an EAS calendar stream
- * According to EAS docs, OCCURRENCES must always come before INTERVAL
- */
- static private void addCountIntervalAndUntil(String rrule, Serializer s) throws IOException {
- String count = tokenFromRrule(rrule, "COUNT=");
- if (count != null) {
- s.data(Tags.CALENDAR_RECURRENCE_OCCURRENCES, count);
- }
- String interval = tokenFromRrule(rrule, "INTERVAL=");
- if (interval != null) {
- s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, interval);
- }
- String until = tokenFromRrule(rrule, "UNTIL=");
- if (until != null) {
- s.data(Tags.CALENDAR_RECURRENCE_UNTIL, recurrenceUntilToEasUntil(until));
- }
- }
-
- static private void addByDay(String byDay, Serializer s) throws IOException {
- // This can be 1WE (1st Wednesday) or -1FR (last Friday)
- int weekOfMonth = byDay.charAt(0);
- String bareByDay;
- if (weekOfMonth == '-') {
- // -1 is the only legal case (last week) Use "5" for EAS
- weekOfMonth = 5;
- bareByDay = byDay.substring(2);
- } else {
- weekOfMonth = weekOfMonth - '0';
- bareByDay = byDay.substring(1);
- }
- s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(weekOfMonth));
- s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(bareByDay));
- }
-
- static private void addByDaySetpos(String byDay, String bySetpos, Serializer s)
- throws IOException {
- int weekOfMonth = bySetpos.charAt(0);
- if (weekOfMonth == '-') {
- // -1 is the only legal case (last week) Use "5" for EAS
- weekOfMonth = 5;
- } else {
- weekOfMonth = weekOfMonth - '0';
- }
- s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(weekOfMonth));
- s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay));
- }
-
- /**
- * Write recurrence information to EAS based on the RRULE in CalendarProvider
- * @param rrule the RRULE, from CalendarProvider
- * @param startTime, the DTSTART of this Event
- * @param s the Serializer we're using to write WBXML data
- * @throws IOException
- */
- // NOTE: For the moment, we're only parsing recurrence types that are supported by the
- // Calendar app UI, which is a subset of possible recurrence types
- // This code must be updated when the Calendar adds new functionality
- static public void recurrenceFromRrule(String rrule, long startTime, Serializer s)
- throws IOException {
- if (Eas.USER_LOG) {
- ExchangeService.log(TAG, "RRULE: " + rrule);
- }
- String freq = tokenFromRrule(rrule, "FREQ=");
- // If there's no FREQ=X, then we don't write a recurrence
- // Note that we duplicate s.start(Tags.CALENDAR_RECURRENCE); s.end(); to prevent the
- // possibility of writing out a partial recurrence stanza
- if (freq != null) {
- if (freq.equals("DAILY")) {
- s.start(Tags.CALENDAR_RECURRENCE);
- s.data(Tags.CALENDAR_RECURRENCE_TYPE, "0");
- addCountIntervalAndUntil(rrule, s);
- s.end();
- } else if (freq.equals("WEEKLY")) {
- s.start(Tags.CALENDAR_RECURRENCE);
- s.data(Tags.CALENDAR_RECURRENCE_TYPE, "1");
- // Requires a day of week (whereas RRULE does not)
- addCountIntervalAndUntil(rrule, s);
- String byDay = tokenFromRrule(rrule, "BYDAY=");
- if (byDay != null) {
- s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay));
- // Find week number (1-4 and 5 for last)
- if (byDay.startsWith("-1")) {
- s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, "5");
- } else {
- char c = byDay.charAt(0);
- if (c >= '1' && c <= '4') {
- s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, byDay.substring(0, 1));
- }
- }
- }
- s.end();
- } else if (freq.equals("MONTHLY")) {
- String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
- if (byMonthDay != null) {
- s.start(Tags.CALENDAR_RECURRENCE);
- // Special case for last day of month
- if (byMonthDay == "-1") {
- s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3");
- addCountIntervalAndUntil(rrule, s);
- s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, "127");
- } else {
- // The nth day of the month
- s.data(Tags.CALENDAR_RECURRENCE_TYPE, "2");
- addCountIntervalAndUntil(rrule, s);
- s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
- }
- s.end();
- } else {
- String byDay = tokenFromRrule(rrule, "BYDAY=");
- String bySetpos = tokenFromRrule(rrule, "BYSETPOS=");
- if (byDay != null) {
- s.start(Tags.CALENDAR_RECURRENCE);
- s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3");
- addCountIntervalAndUntil(rrule, s);
- if (bySetpos != null) {
- addByDaySetpos(byDay, bySetpos, s);
- } else {
- addByDay(byDay, s);
- }
- s.end();
- }
- }
- } else if (freq.equals("YEARLY")) {
- String byMonth = tokenFromRrule(rrule, "BYMONTH=");
- String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
- String byDay = tokenFromRrule(rrule, "BYDAY=");
- if (byMonth == null && byMonthDay == null) {
- // Calculate the month and day from the startDate
- GregorianCalendar cal = new GregorianCalendar();
- cal.setTimeInMillis(startTime);
- cal.setTimeZone(TimeZone.getDefault());
- byMonth = Integer.toString(cal.get(Calendar.MONTH) + 1);
- byMonthDay = Integer.toString(cal.get(Calendar.DAY_OF_MONTH));
- }
- if (byMonth != null && (byMonthDay != null || byDay != null)) {
- s.start(Tags.CALENDAR_RECURRENCE);
- s.data(Tags.CALENDAR_RECURRENCE_TYPE, byDay == null ? "5" : "6");
- addCountIntervalAndUntil(rrule, s);
- s.data(Tags.CALENDAR_RECURRENCE_MONTHOFYEAR, byMonth);
- // Note that both byMonthDay and byDay can't be true in a valid RRULE
- if (byMonthDay != null) {
- s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
- } else {
- addByDay(byDay, s);
- }
- s.end();
- }
- }
- }
- }
-
- /**
- * Build an RRULE String from EAS recurrence information
- * @param type the type of recurrence
- * @param occurrences how many recurrences (instances)
- * @param interval the interval between recurrences
- * @param dow day of the week
- * @param dom day of the month
- * @param wom week of the month
- * @param moy month of the year
- * @param until the last recurrence time
- * @return a valid RRULE String
- */
- static public String rruleFromRecurrence(int type, int occurrences, int interval, int dow,
- int dom, int wom, int moy, String until) {
- StringBuilder rrule = new StringBuilder("FREQ=" + sTypeToFreq[type]);
- // INTERVAL and COUNT
- if (occurrences > 0) {
- rrule.append(";COUNT=" + occurrences);
- }
- if (interval > 0) {
- rrule.append(";INTERVAL=" + interval);
- }
-
- // Days, weeks, months, etc.
- switch(type) {
- case 0: // DAILY
- case 1: // WEEKLY
- if (dow > 0) addByDay(rrule, dow, wom);
- break;
- case 2: // MONTHLY
- if (dom > 0) addByMonthDay(rrule, dom);
- break;
- case 3: // MONTHLY (on the nth day)
- // 127 is a special case meaning "last day of the month"
- if (dow == 127) {
- rrule.append(";BYMONTHDAY=-1");
- // week 5 and dow = weekdays -> last weekday (need BYSETPOS)
- } else if (wom == 5 && (dow == EAS_WEEKDAYS || dow == EAS_WEEKENDS)) {
- addBySetpos(rrule, dow, wom);
- } else if (dow > 0) addByDay(rrule, dow, wom);
- break;
- case 5: // YEARLY (specific day)
- if (dom > 0) addByMonthDay(rrule, dom);
- if (moy > 0) {
- rrule.append(";BYMONTH=" + moy);
- }
- break;
- case 6: // YEARLY
- if (dow > 0) addByDay(rrule, dow, wom);
- if (dom > 0) addByMonthDay(rrule, dom);
- if (moy > 0) {
- rrule.append(";BYMONTH=" + moy);
- }
- break;
- default:
- break;
- }
-
- // UNTIL comes last
- if (until != null) {
- rrule.append(";UNTIL=" + until);
- }
-
- if (Eas.USER_LOG) {
- Log.d(Logging.LOG_TAG, "Created rrule: " + rrule);
- }
- return rrule.toString();
- }
-
- /**
- * Create a Calendar in CalendarProvider to which synced Events will be linked
- * @param service the sync service requesting Calendar creation
- * @param account the account being synced
- * @param mailbox the Exchange mailbox for the calendar
- * @return the unique id of the Calendar
- */
- static public long createCalendar(EasSyncService service, Account account, Mailbox mailbox) {
- // Create a Calendar object
- ContentValues cv = new ContentValues();
- // TODO How will this change if the user changes his account display name?
- cv.put(Calendars.CALENDAR_DISPLAY_NAME, account.mDisplayName);
- cv.put(Calendars.ACCOUNT_NAME, account.mEmailAddress);
- cv.put(Calendars.ACCOUNT_TYPE, Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
- cv.put(Calendars.SYNC_EVENTS, 1);
- cv.put(Calendars.VISIBLE, 1);
- // Don't show attendee status if we're the organizer
- cv.put(Calendars.CAN_ORGANIZER_RESPOND, 0);
- cv.put(Calendars.CAN_MODIFY_TIME_ZONE, 0);
- cv.put(Calendars.MAX_REMINDERS, 1);
- cv.put(Calendars.ALLOWED_REMINDERS, ALLOWED_REMINDER_TYPES);
- cv.put(Calendars.ALLOWED_ATTENDEE_TYPES, ALLOWED_ATTENDEE_TYPES);
- cv.put(Calendars.ALLOWED_AVAILABILITY, ALLOWED_AVAILABILITIES);
-
- // TODO Coordinate account colors w/ Calendar, if possible
- int color = new AccountServiceProxy(service.mContext).getAccountColor(account.mId);
- cv.put(Calendars.CALENDAR_COLOR, color);
- cv.put(Calendars.CALENDAR_TIME_ZONE, Time.getCurrentTimezone());
- cv.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
- cv.put(Calendars.OWNER_ACCOUNT, account.mEmailAddress);
-
- Uri uri = service.mContentResolver.insert(
- asSyncAdapter(Calendars.CONTENT_URI, account.mEmailAddress,
- Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), cv);
- // We save the id of the calendar into mSyncStatus
- if (uri != null) {
- String stringId = uri.getPathSegments().get(1);
- mailbox.mSyncStatus = stringId;
- return Long.parseLong(stringId);
- }
- return -1;
- }
-
- static Uri asSyncAdapter(Uri uri, String account, String accountType) {
- return uri.buildUpon()
- .appendQueryParameter(android.provider.CalendarContract.CALLER_IS_SYNCADAPTER,
- "true")
- .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
- .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
- }
-
- /**
- * Return the uid for an event based on its globalObjId
- * @param globalObjId the base64 encoded String provided by EAS
- * @return the uid for the calendar event
- */
- static public String getUidFromGlobalObjId(String globalObjId) {
- StringBuilder sb = new StringBuilder();
- // First get the decoded base64
- try {
- byte[] idBytes = Base64.decode(globalObjId, Base64.DEFAULT);
- String idString = new String(idBytes);
- // If the base64 decoded string contains the magic substring: "vCal-Uid", then
- // the actual uid is hidden within; the magic substring is never at the start of the
- // decoded base64
- int index = idString.indexOf("vCal-Uid");
- if (index > 0) {
- // The uid starts after "vCal-Uidxxxx", where xxxx are padding
- // characters. And it ends before the last character, which is ascii 0
- return idString.substring(index + 12, idString.length() - 1);
- } else {
- // This is an EAS uid. Go through the bytes and write out the hex
- // values as characters; this is what we'll need to pass back to EAS
- // when responding to the invitation
- for (byte b: idBytes) {
- Utility.byteToHex(sb, b);
- }
- return sb.toString();
- }
- } catch (RuntimeException e) {
- // In the worst of cases (bad format, etc.), we can always return the input
- return globalObjId;
- }
- }
-
- /**
- * Get a selfAttendeeStatus from a busy status
- * The default here is NONE (i.e. we don't know the status)
- * Note that a busy status of FREE must mean NONE as well, since it can't mean declined
- * (there would be no event)
- * @param busyStatus the busy status, from EAS
- * @return the corresponding value for selfAttendeeStatus
- */
- static public int attendeeStatusFromBusyStatus(int busyStatus) {
- int attendeeStatus;
- switch (busyStatus) {
- case BUSY_STATUS_BUSY:
- attendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED;
- break;
- case BUSY_STATUS_TENTATIVE:
- attendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE;
- break;
- case BUSY_STATUS_FREE:
- case BUSY_STATUS_OUT_OF_OFFICE:
- default:
- attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
- }
- return attendeeStatus;
- }
-
- /**
- * Get a selfAttendeeStatus from a response type (EAS 14+)
- * The default here is NONE (i.e. we don't know the status), though in theory this can't happen
- * @param busyStatus the response status, from EAS
- * @return the corresponding value for selfAttendeeStatus
- */
- static public int attendeeStatusFromResponseType(int responseType) {
- int attendeeStatus;
- switch (responseType) {
- case RESPONSE_TYPE_NOT_RESPONDED:
- attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
- break;
- case RESPONSE_TYPE_ACCEPTED:
- attendeeStatus = Attendees.ATTENDEE_STATUS_ACCEPTED;
- break;
- case RESPONSE_TYPE_TENTATIVE:
- attendeeStatus = Attendees.ATTENDEE_STATUS_TENTATIVE;
- break;
- case RESPONSE_TYPE_DECLINED:
- attendeeStatus = Attendees.ATTENDEE_STATUS_DECLINED;
- break;
- default:
- attendeeStatus = Attendees.ATTENDEE_STATUS_NONE;
- }
- return attendeeStatus;
- }
-
- /** Get a busy status from a selfAttendeeStatus
- * The default here is BUSY
- * @param selfAttendeeStatus from CalendarProvider2
- * @return the corresponding value of busy status
- */
- static public int busyStatusFromAttendeeStatus(int selfAttendeeStatus) {
- int busyStatus;
- switch (selfAttendeeStatus) {
- case Attendees.ATTENDEE_STATUS_DECLINED:
- case Attendees.ATTENDEE_STATUS_NONE:
- case Attendees.ATTENDEE_STATUS_INVITED:
- busyStatus = BUSY_STATUS_FREE;
- break;
- case Attendees.ATTENDEE_STATUS_TENTATIVE:
- busyStatus = BUSY_STATUS_TENTATIVE;
- break;
- case Attendees.ATTENDEE_STATUS_ACCEPTED:
- default:
- busyStatus = BUSY_STATUS_BUSY;
- break;
- }
- return busyStatus;
- }
-
- static public String buildMessageTextFromEntityValues(Context context,
- ContentValues entityValues, StringBuilder sb) {
- if (sb == null) {
- sb = new StringBuilder();
- }
- Resources resources = context.getResources();
- Date date = new Date(entityValues.getAsLong(Events.DTSTART));
- // TODO: Add more detail to message text
- // Right now, we're using.. When: Tuesday, March 5th at 2:00pm
- // What we're missing is the duration and any recurrence information. So this should be
- // more like... When: Tuesdays, starting March 5th from 2:00pm - 3:00pm
- // This would require code to build complex strings, and it will have to wait
- // For now, we'll just use the meeting_recurring string
-
- boolean allDayEvent = false;
- if (entityValues.containsKey(Events.ALL_DAY)) {
- Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
- allDayEvent = (ade != null) && (ade == 1);
- }
- boolean recurringEvent = !entityValues.containsKey(Events.ORIGINAL_SYNC_ID) &&
- entityValues.containsKey(Events.RRULE);
-
- String dateTimeString;
- int res;
- if (allDayEvent) {
- dateTimeString = DateFormat.getDateInstance().format(date);
- res = recurringEvent ? R.string.meeting_allday_recurring : R.string.meeting_allday;
- } else {
- dateTimeString = DateFormat.getDateTimeInstance().format(date);
- res = recurringEvent ? R.string.meeting_recurring : R.string.meeting_when;
- }
- sb.append(resources.getString(res, dateTimeString));
-
- String location = null;
- if (entityValues.containsKey(Events.EVENT_LOCATION)) {
- location = entityValues.getAsString(Events.EVENT_LOCATION);
- if (!TextUtils.isEmpty(location)) {
- sb.append("\n");
- sb.append(resources.getString(R.string.meeting_where, location));
- }
- }
- // If there's a description for this event, append it
- String desc = entityValues.getAsString(Events.DESCRIPTION);
- if (desc != null) {
- sb.append("\n--\n");
- sb.append(desc);
- }
- return sb.toString();
- }
-
- /**
- * Add an attendee to the ics attachment and the to list of the Message being composed
- * @param ics the ics attachment writer
- * @param toList the list of addressees for this email
- * @param attendeeName the name of the attendee
- * @param attendeeEmail the email address of the attendee
- * @param messageFlag the flag indicating the action to be indicated by the message
- * @param account the sending account of the email
- */
- static private void addAttendeeToMessage(SimpleIcsWriter ics, ArrayList<Address> toList,
- String attendeeName, String attendeeEmail, int messageFlag, Account account) {
- if ((messageFlag & Message.FLAG_OUTGOING_MEETING_REQUEST_MASK) != 0) {
- String icalTag = ICALENDAR_ATTENDEE_INVITE;
- if ((messageFlag & Message.FLAG_OUTGOING_MEETING_CANCEL) != 0) {
- icalTag = ICALENDAR_ATTENDEE_CANCEL;
- }
- if (attendeeName != null) {
- icalTag += ";CN=" + SimpleIcsWriter.quoteParamValue(attendeeName);
- }
- ics.writeTag(icalTag, "MAILTO:" + attendeeEmail);
- toList.add(attendeeName == null ? new Address(attendeeEmail) :
- new Address(attendeeEmail, attendeeName));
- } else if (attendeeEmail.equalsIgnoreCase(account.mEmailAddress)) {
- String icalTag = null;
- switch (messageFlag) {
- case Message.FLAG_OUTGOING_MEETING_ACCEPT:
- icalTag = ICALENDAR_ATTENDEE_ACCEPT;
- break;
- case Message.FLAG_OUTGOING_MEETING_DECLINE:
- icalTag = ICALENDAR_ATTENDEE_DECLINE;
- break;
- case Message.FLAG_OUTGOING_MEETING_TENTATIVE:
- icalTag = ICALENDAR_ATTENDEE_TENTATIVE;
- break;
- }
- if (icalTag != null) {
- if (attendeeName != null) {
- icalTag += ";CN="
- + SimpleIcsWriter.quoteParamValue(attendeeName);
- }
- ics.writeTag(icalTag, "MAILTO:" + attendeeEmail);
- }
- }
- }
-
- /**
- * Create a Message for an (Event) Entity
- * @param entity the Entity for the Event (as might be retrieved by CalendarProvider)
- * @param messageFlag the Message.FLAG_XXX constant indicating the type of email to be sent
- * @param the unique id of this Event, or null if it can be retrieved from the Event
- * @param the user's account
- * @return a Message with many fields pre-filled (more later)
- */
- static public Message createMessageForEntity(Context context, Entity entity,
- int messageFlag, String uid, Account account) {
- return createMessageForEntity(context, entity, messageFlag, uid, account,
- null /*specifiedAttendee*/);
- }
-
- static public EmailContent.Message createMessageForEntity(Context context, Entity entity,
- int messageFlag, String uid, Account account, String specifiedAttendee) {
- ContentValues entityValues = entity.getEntityValues();
- ArrayList<NamedContentValues> subValues = entity.getSubValues();
- boolean isException = entityValues.containsKey(Events.ORIGINAL_SYNC_ID);
- boolean isReply = false;
-
- EmailContent.Message msg = new EmailContent.Message();
- msg.mFlags = messageFlag;
- msg.mTimeStamp = System.currentTimeMillis();
-
- String method;
- if ((messageFlag & EmailContent.Message.FLAG_OUTGOING_MEETING_INVITE) != 0) {
- method = "REQUEST";
- } else if ((messageFlag & EmailContent.Message.FLAG_OUTGOING_MEETING_CANCEL) != 0) {
- method = "CANCEL";
- } else {
- method = "REPLY";
- isReply = true;
- }
-
- try {
- // Create our iCalendar writer and start generating tags
- SimpleIcsWriter ics = new SimpleIcsWriter();
- ics.writeTag("BEGIN", "VCALENDAR");
- ics.writeTag("METHOD", method);
- ics.writeTag("PRODID", "AndroidEmail");
- ics.writeTag("VERSION", "2.0");
-
- // Our default vcalendar time zone is UTC, but this will change (below) if we're
- // sending a recurring event, in which case we use local time
- TimeZone vCalendarTimeZone = sGmtTimeZone;
- String vCalendarDateSuffix = "";
-
- // Check for all day event
- boolean allDayEvent = false;
- if (entityValues.containsKey(Events.ALL_DAY)) {
- Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
- allDayEvent = (ade != null) && (ade == 1);
- if (allDayEvent) {
- // Example: DTSTART;VALUE=DATE:20100331 (all day event)
- vCalendarDateSuffix = ";VALUE=DATE";
- }
- }
-
- // If we're inviting people and the meeting is recurring, we need to send our time zone
- // information and make sure to send DTSTART/DTEND in local time (unless, of course,
- // this is an all-day event). Recurring, for this purpose, includes exceptions to
- // recurring events
- if (!isReply && !allDayEvent &&
- (entityValues.containsKey(Events.RRULE) ||
- entityValues.containsKey(Events.ORIGINAL_SYNC_ID))) {
- vCalendarTimeZone = TimeZone.getDefault();
- // Write the VTIMEZONE block to the writer
- timeZoneToVTimezone(vCalendarTimeZone, ics);
- // Example: DTSTART;TZID=US/Pacific:20100331T124500
- vCalendarDateSuffix = ";TZID=" + vCalendarTimeZone.getID();
- }
-
- ics.writeTag("BEGIN", "VEVENT");
- if (uid == null) {
- uid = entityValues.getAsString(Events.SYNC_DATA2);
- }
- if (uid != null) {
- ics.writeTag("UID", uid);
- }
-
- if (entityValues.containsKey("DTSTAMP")) {
- ics.writeTag("DTSTAMP", entityValues.getAsString("DTSTAMP"));
- } else {
- ics.writeTag("DTSTAMP", millisToEasDateTime(System.currentTimeMillis()));
- }
-
- long startTime = entityValues.getAsLong(Events.DTSTART);
- if (startTime != 0) {
- ics.writeTag("DTSTART" + vCalendarDateSuffix,
- millisToEasDateTime(startTime, vCalendarTimeZone, !allDayEvent));
- }
-
- // If this is an Exception, we send the recurrence-id, which is just the original
- // instance time
- if (isException) {
- long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
- ics.writeTag("RECURRENCE-ID" + vCalendarDateSuffix,
- millisToEasDateTime(originalTime, vCalendarTimeZone, !allDayEvent));
- }
-
- if (!entityValues.containsKey(Events.DURATION)) {
- if (entityValues.containsKey(Events.DTEND)) {
- ics.writeTag("DTEND" + vCalendarDateSuffix,
- millisToEasDateTime(
- entityValues.getAsLong(Events.DTEND), vCalendarTimeZone,
- !allDayEvent));
- }
- } else {
- // Convert this into millis and add it to DTSTART for DTEND
- // We'll use 1 hour as a default
- long durationMillis = HOURS;
- Duration duration = new Duration();
- try {
- duration.parse(entityValues.getAsString(Events.DURATION));
- durationMillis = duration.getMillis();
- } catch (ParseException e) {
- // We'll use the default in this case
- }
- ics.writeTag("DTEND" + vCalendarDateSuffix,
- millisToEasDateTime(
- startTime + durationMillis, vCalendarTimeZone, !allDayEvent));
- }
-
- String location = null;
- if (entityValues.containsKey(Events.EVENT_LOCATION)) {
- location = entityValues.getAsString(Events.EVENT_LOCATION);
- ics.writeTag("LOCATION", location);
- }
-
- String sequence = entityValues.getAsString(SYNC_VERSION);
- if (sequence == null) {
- sequence = "0";
- }
-
- // We'll use 0 to mean a meeting invitation
- int titleId = 0;
- switch (messageFlag) {
- case Message.FLAG_OUTGOING_MEETING_INVITE:
- if (!sequence.equals("0")) {
- titleId = R.string.meeting_updated;
- }
- break;
- case Message.FLAG_OUTGOING_MEETING_ACCEPT:
- titleId = R.string.meeting_accepted;
- break;
- case Message.FLAG_OUTGOING_MEETING_DECLINE:
- titleId = R.string.meeting_declined;
- break;
- case Message.FLAG_OUTGOING_MEETING_TENTATIVE:
- titleId = R.string.meeting_tentative;
- break;
- case Message.FLAG_OUTGOING_MEETING_CANCEL:
- titleId = R.string.meeting_canceled;
- break;
- }
- Resources resources = context.getResources();
- String title = entityValues.getAsString(Events.TITLE);
- if (title == null) {
- title = "";
- }
- ics.writeTag("SUMMARY", title);
- // For meeting invitations just use the title
- if (titleId == 0) {
- msg.mSubject = title;
- } else {
- // Otherwise, use the additional text
- msg.mSubject = resources.getString(titleId, title);
- }
-
- // Build the text for the message, starting with an initial line describing the
- // exception (if this is one)
- StringBuilder sb = new StringBuilder();
- if (isException && !isReply) {
- // Add the line, depending on whether this is a cancellation or update
- Date date = new Date(entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME));
- String dateString = DateFormat.getDateInstance().format(date);
- if (titleId == R.string.meeting_canceled) {
- sb.append(resources.getString(R.string.exception_cancel, dateString));
- } else {
- sb.append(resources.getString(R.string.exception_updated, dateString));
- }
- sb.append("\n\n");
- }
- String text =
- CalendarUtilities.buildMessageTextFromEntityValues(context, entityValues, sb);
-
- if (text.length() > 0) {
- ics.writeTag("DESCRIPTION", text);
- }
- // And store the message text
- msg.mText = text;
- if (!isReply) {
- if (entityValues.containsKey(Events.ALL_DAY)) {
- Integer ade = entityValues.getAsInteger(Events.ALL_DAY);
- ics.writeTag("X-MICROSOFT-CDO-ALLDAYEVENT", ade == 0 ? "FALSE" : "TRUE");
- }
-
- String rrule = entityValues.getAsString(Events.RRULE);
- if (rrule != null) {
- ics.writeTag("RRULE", rrule);
- }
-
- // If we decide to send alarm information in the meeting request ics file,
- // handle it here by looping through the subvalues
- }
-
- // Handle attendee data here; determine "to" list and add ATTENDEE tags to ics
- String organizerName = null;
- String organizerEmail = null;
- ArrayList<Address> toList = new ArrayList<Address>();
- for (NamedContentValues ncv: subValues) {
- Uri ncvUri = ncv.uri;
- ContentValues ncvValues = ncv.values;
- if (ncvUri.equals(Attendees.CONTENT_URI)) {
- Integer relationship =
- ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
- // If there's no relationship, we can't create this for EAS
- // Similarly, we need an attendee email for each invitee
- if (relationship != null &&
- ncvValues.containsKey(Attendees.ATTENDEE_EMAIL)) {
- // Organizer isn't among attendees in EAS
- if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
- organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
- organizerEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
- continue;
- }
- String attendeeEmail = ncvValues.getAsString(Attendees.ATTENDEE_EMAIL);
- String attendeeName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
-
- // This shouldn't be possible, but allow for it
- if (attendeeEmail == null) continue;
- // If we only want to send to the specifiedAttendee, eliminate others here
- if ((specifiedAttendee != null) &&
- !attendeeEmail.equalsIgnoreCase(specifiedAttendee)) {
- continue;
- }
-
- addAttendeeToMessage(ics, toList, attendeeName, attendeeEmail, messageFlag,
- account);
- }
- }
- }
-
- // Manually add the specifiedAttendee if he wasn't added in the Attendees loop
- if (toList.isEmpty() && (specifiedAttendee != null)) {
- addAttendeeToMessage(ics, toList, null, specifiedAttendee, messageFlag, account);
- }
-
- // Create the organizer tag for ical
- if (organizerEmail != null) {
- String icalTag = "ORGANIZER";
- // We should be able to find this, assuming the Email is the user's email
- // TODO Find this in the account
- if (organizerName != null) {
- icalTag += ";CN=" + SimpleIcsWriter.quoteParamValue(organizerName);
- }
- ics.writeTag(icalTag, "MAILTO:" + organizerEmail);
- if (isReply) {
- toList.add(organizerName == null ? new Address(organizerEmail) :
- new Address(organizerEmail, organizerName));
- }
- }
-
- // If we have no "to" list, we're done
- if (toList.isEmpty()) return null;
-
- // Write out the "to" list
- Address[] toArray = new Address[toList.size()];
- int i = 0;
- for (Address address: toList) {
- toArray[i++] = address;
- }
- msg.mTo = Address.pack(toArray);
-
- ics.writeTag("CLASS", "PUBLIC");
- ics.writeTag("STATUS", (messageFlag == Message.FLAG_OUTGOING_MEETING_CANCEL) ?
- "CANCELLED" : "CONFIRMED");
- ics.writeTag("TRANSP", "OPAQUE"); // What Exchange uses
- ics.writeTag("PRIORITY", "5"); // 1 to 9, 5 = medium
- ics.writeTag("SEQUENCE", sequence);
- ics.writeTag("END", "VEVENT");
- ics.writeTag("END", "VCALENDAR");
-
- // Create the ics attachment using the "content" field
- Attachment att = new Attachment();
- att.mContentBytes = ics.getBytes();
- att.mMimeType = "text/calendar; method=" + method;
- att.mFileName = "invite.ics";
- att.mSize = att.mContentBytes.length;
- // We don't send content-disposition with this attachment
- att.mFlags = Attachment.FLAG_ICS_ALTERNATIVE_PART;
-
- // Add the attachment to the message
- msg.mAttachments = new ArrayList<Attachment>();
- msg.mAttachments.add(att);
- } catch (IOException e) {
- Log.w(TAG, "IOException in createMessageForEntity");
- return null;
- }
-
- // Return the new Message to caller
- return msg;
- }
-
- /**
- * Create a Message for an Event that can be retrieved from CalendarProvider
- * by its unique id
- *
- * @param cr a content resolver that can be used to query for the Event
- * @param eventId the unique id of the Event
- * @param messageFlag the Message.FLAG_XXX constant indicating the type of
- * email to be sent
- * @param the unique id of this Event, or null if it can be retrieved from
- * the Event
- * @param the user's account
- * @param requireAddressees if true (the default), no Message is returned if
- * there aren't any addressees; if false, return the Message
- * regardless (addressees will be filled in later)
- * @return a Message with many fields pre-filled (more later)
- * @throws RemoteException if there is an issue retrieving the Event from
- * CalendarProvider
- */
- static public EmailContent.Message createMessageForEventId(Context context, long eventId,
- int messageFlag, String uid, Account account) throws RemoteException {
- return createMessageForEventId(context, eventId, messageFlag, uid, account,
- null /* specifiedAttendee */);
- }
-
- static public EmailContent.Message createMessageForEventId(Context context, long eventId,
- int messageFlag, String uid, Account account, String specifiedAttendee)
- throws RemoteException {
- ContentResolver cr = context.getContentResolver();
- EntityIterator eventIterator = EventsEntity.newEntityIterator(cr.query(
- ContentUris.withAppendedId(Events.CONTENT_URI, eventId), null, null, null, null),
- cr);
- try {
- while (eventIterator.hasNext()) {
- Entity entity = eventIterator.next();
- return createMessageForEntity(context, entity, messageFlag, uid, account,
- specifiedAttendee);
- }
- } finally {
- eventIterator.close();
- }
- return null;
- }
-
- /**
- * Return a boolean value for an integer ContentValues column
- * @param values a ContentValues object
- * @param columnName the name of a column to be found in the ContentValues
- * @return a boolean representation of the value of columnName in values; null and 0 = false,
- * other integers = true
- */
- static public boolean getIntegerValueAsBoolean(ContentValues values, String columnName) {
- Integer intValue = values.getAsInteger(columnName);
- return (intValue != null && intValue != 0);
- }
-}
diff --git a/src/com/android/exchange/utility/Duration.java b/src/com/android/exchange/utility/Duration.java
deleted file mode 100644
index f6e6525..0000000
--- a/src/com/android/exchange/utility/Duration.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/* Copyright 2010, The Android Open Source Project
- **
- ** Licensed under the Apache License, Version 2.0 (the "License");
- ** you may not use this file except in compliance with the License.
- ** You may obtain a copy of the License at
- **
- ** http://www.apache.org/licenses/LICENSE-2.0
- **
- ** Unless required by applicable law or agreed to in writing, software
- ** distributed under the License is distributed on an "AS IS" BASIS,
- ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ** See the License for the specific language governing permissions and
- ** limitations under the License.
- */
-
-package com.android.exchange.utility;
-
-import java.text.ParseException;
-import java.util.Calendar;
-
-/**
- * Note: This class was simply copied from the class in CalendarProvider, since we don't have access
- * to it from the Email app. I reformated some lines, but otherwise haven't altered the code.
- */
-public class Duration {
- public int sign; // 1 or -1
- public int weeks;
- public int days;
- public int hours;
- public int minutes;
- public int seconds;
-
- public Duration() {
- sign = 1;
- }
-
- /**
- * Parse according to RFC2445 ss4.3.6. (It's actually a little loose with
- * its parsing, for better or for worse)
- */
- public void parse(String str) throws ParseException {
- sign = 1;
- weeks = 0;
- days = 0;
- hours = 0;
- minutes = 0;
- seconds = 0;
-
- int len = str.length();
- int index = 0;
- char c;
-
- if (len < 1) {
- return;
- }
-
- c = str.charAt(0);
- if (c == '-') {
- sign = -1;
- index++;
- } else if (c == '+') {
- index++;
- }
-
- if (len < index) {
- return;
- }
-
- c = str.charAt(index);
- if (c != 'P') {
- throw new ParseException (
- "Duration.parse(str='" + str + "') expected 'P' at index="
- + index, index);
- }
- index++;
-
- int n = 0;
- for (; index < len; index++) {
- c = str.charAt(index);
- if (c >= '0' && c <= '9') {
- n *= 10;
- n += (c - '0');
- } else if (c == 'W') {
- weeks = n;
- n = 0;
- } else if (c == 'H') {
- hours = n;
- n = 0;
- } else if (c == 'M') {
- minutes = n;
- n = 0;
- } else if (c == 'S') {
- seconds = n;
- n = 0;
- } else if (c == 'D') {
- days = n;
- n = 0;
- } else if (c == 'T') {
- } else {
- throw new ParseException (
- "Duration.parse(str='" + str + "') unexpected char '"
- + c + "' at index=" + index, index);
- }
- }
- }
-
- /**
- * Add this to the calendar provided, in place, in the calendar.
- */
- public void addTo(Calendar cal) {
- cal.add(Calendar.DAY_OF_MONTH, sign*weeks*7);
- cal.add(Calendar.DAY_OF_MONTH, sign*days);
- cal.add(Calendar.HOUR, sign*hours);
- cal.add(Calendar.MINUTE, sign*minutes);
- cal.add(Calendar.SECOND, sign*seconds);
- }
-
- public long addTo(long dt) {
- return dt + getMillis();
- }
-
- public long getMillis() {
- long factor = 1000 * sign;
- return factor * ((7*24*60*60*weeks) + (24*60*60*days) + (60*60*hours) + (60*minutes) +
- seconds);
- }
-}
diff --git a/src/com/android/exchange/utility/FileLogger.java b/src/com/android/exchange/utility/FileLogger.java
deleted file mode 100644
index bb17151..0000000
--- a/src/com/android/exchange/utility/FileLogger.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.utility;
-
-import android.content.Context;
-import android.os.Environment;
-
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.util.Date;
-
-public class FileLogger {
- private static FileLogger LOGGER = null;
- private static FileWriter sLogWriter = null;
- public static String LOG_FILE_NAME =
- Environment.getExternalStorageDirectory() + "/emaillog.txt";
-
- public synchronized static FileLogger getLogger (Context c) {
- LOGGER = new FileLogger();
- return LOGGER;
- }
-
- private FileLogger() {
- try {
- sLogWriter = new FileWriter(LOG_FILE_NAME, true);
- } catch (IOException e) {
- // Doesn't matter
- }
- }
-
- static public synchronized void close() {
- if (sLogWriter != null) {
- try {
- sLogWriter.close();
- } catch (IOException e) {
- // Doesn't matter
- }
- sLogWriter = null;
- }
- }
-
- static public synchronized void log(Exception e) {
- if (sLogWriter != null) {
- log("Exception", "Stack trace follows...");
- PrintWriter pw = new PrintWriter(sLogWriter);
- e.printStackTrace(pw);
- pw.flush();
- }
- }
-
- @SuppressWarnings("deprecation")
- static public synchronized void log(String prefix, String str) {
- if (LOGGER == null) {
- LOGGER = new FileLogger();
- log("Logger", "\r\n\r\n --- New Log ---");
- }
- Date d = new Date();
- int hr = d.getHours();
- int min = d.getMinutes();
- int sec = d.getSeconds();
-
- // I don't use DateFormat here because (in my experience), it's much slower
- StringBuffer sb = new StringBuffer(256);
- sb.append('[');
- sb.append(hr);
- sb.append(':');
- if (min < 10)
- sb.append('0');
- sb.append(min);
- sb.append(':');
- if (sec < 10) {
- sb.append('0');
- }
- sb.append(sec);
- sb.append("] ");
- if (prefix != null) {
- sb.append(prefix);
- sb.append("| ");
- }
- sb.append(str);
- sb.append("\r\n");
- String s = sb.toString();
-
- if (sLogWriter != null) {
- try {
- sLogWriter.write(s);
- sLogWriter.flush();
- } catch (IOException e) {
- // Something might have happened to the sdcard
- if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
- // If the card is mounted and we can create the writer, retry
- LOGGER = new FileLogger();
- if (sLogWriter != null) {
- try {
- log("FileLogger", "Exception writing log; recreating...");
- log(prefix, str);
- } catch (Exception e1) {
- // Nothing to do at this point
- }
- }
- }
- }
- }
- }
-}
diff --git a/src/com/android/exchange/utility/SimpleIcsWriter.java b/src/com/android/exchange/utility/SimpleIcsWriter.java
deleted file mode 100644
index 1bb8463..0000000
--- a/src/com/android/exchange/utility/SimpleIcsWriter.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/* Copyright 2010, The Android Open Source Project
- **
- ** Licensed under the Apache License, Version 2.0 (the "License");
- ** you may not use this file except in compliance with the License.
- ** You may obtain a copy of the License at
- **
- ** http://www.apache.org/licenses/LICENSE-2.0
- **
- ** Unless required by applicable law or agreed to in writing, software
- ** distributed under the License is distributed on an "AS IS" BASIS,
- ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ** See the License for the specific language governing permissions and
- ** limitations under the License.
- */
-
-package com.android.exchange.utility;
-
-import com.android.emailcommon.utility.Utility;
-
-import android.text.TextUtils;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-
-/**
- * Class to generate iCalender object (*.ics) per RFC 5545.
- */
-public class SimpleIcsWriter {
- private static final int MAX_LINE_LENGTH = 75; // In bytes, excluding CRLF
- private static final int CHAR_MAX_BYTES_IN_UTF8 = 4; // Used to be 6, but RFC3629 limited it.
- private final ByteArrayOutputStream mOut = new ByteArrayOutputStream();
-
- public SimpleIcsWriter() {
- }
-
- /**
- * Low level method to write a line, performing line-folding if necessary.
- */
- /* package for testing */ void writeLine(String string) {
- int numBytes = 0;
- for (byte b : Utility.toUtf8(string)) {
- // Fold it when necessary.
- // To make it simple, we assume all chars are 4 bytes.
- // If not (and usually it's not), we end up wrapping earlier than necessary, but that's
- // completely fine.
- if (numBytes > (MAX_LINE_LENGTH - CHAR_MAX_BYTES_IN_UTF8)
- && Utility.isFirstUtf8Byte(b)) { // Only wrappable if it's before the first byte
- mOut.write((byte) '\r');
- mOut.write((byte) '\n');
- mOut.write((byte) '\t');
- numBytes = 1; // for TAB
- }
- mOut.write(b);
- numBytes++;
- }
- mOut.write((byte) '\r');
- mOut.write((byte) '\n');
- }
-
- /**
- * Write a tag with a value.
- */
- public void writeTag(String name, String value) {
- // Belt and suspenders here; don't crash on null value; just return
- if (TextUtils.isEmpty(value)) {
- return;
- }
-
- // The following properties take a TEXT value, which need to be escaped.
- // (These property names should be all interned, so using equals() should be faster than
- // using a hash table.)
-
- // TODO make constants for these literals
- if ("CALSCALE".equals(name)
- || "METHOD".equals(name)
- || "PRODID".equals(name)
- || "VERSION".equals(name)
- || "CATEGORIES".equals(name)
- || "CLASS".equals(name)
- || "COMMENT".equals(name)
- || "DESCRIPTION".equals(name)
- || "LOCATION".equals(name)
- || "RESOURCES".equals(name)
- || "STATUS".equals(name)
- || "SUMMARY".equals(name)
- || "TRANSP".equals(name)
- || "TZID".equals(name)
- || "TZNAME".equals(name)
- || "CONTACT".equals(name)
- || "RELATED-TO".equals(name)
- || "UID".equals(name)
- || "ACTION".equals(name)
- || "REQUEST-STATUS".equals(name)
- || "X-LIC-LOCATION".equals(name)
- ) {
- value = escapeTextValue(value);
- }
- writeLine(name + ":" + value);
- }
-
- /**
- * For debugging
- */
- @Override
- public String toString() {
- return Utility.fromUtf8(getBytes());
- }
-
- /**
- * @return the entire iCalendar invitation object.
- */
- public byte[] getBytes() {
- try {
- mOut.flush();
- } catch (IOException wonthappen) {
- }
- return mOut.toByteArray();
- }
-
- /**
- * Quote a param-value string, according to RFC 5545, section 3.1
- */
- public static String quoteParamValue(String paramValue) {
- if (paramValue == null) {
- return null;
- }
- // Wrap with double quotes.
- // The spec doesn't allow putting double-quotes in a param value, so let's use single quotes
- // as a substitute.
- // It's not the smartest implementation. e.g. we don't have to wrap an empty string with
- // double quotes. But it works.
- return "\"" + paramValue.replace("\"", "'") + "\"";
- }
-
- /**
- * Escape a TEXT value per RFC 5545 section 3.3.11
- */
- /* package for testing */ static String escapeTextValue(String s) {
- StringBuilder sb = new StringBuilder(s.length());
- for (int i = 0; i < s.length(); i++) {
- char ch = s.charAt(i);
- if (ch == '\n') {
- sb.append("\\n");
- } else if (ch == '\r') {
- // Remove CR
- } else if (ch == ',' || ch == ';' || ch == '\\') {
- sb.append('\\');
- sb.append(ch);
- } else {
- sb.append(ch);
- }
- }
- return sb.toString();
- }
-}
diff --git a/src/com/android/exchange/utility/UriCodec.java b/src/com/android/exchange/utility/UriCodec.java
deleted file mode 100644
index d8811c6..0000000
--- a/src/com/android/exchange/utility/UriCodec.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange.utility;
-
-import java.io.ByteArrayOutputStream;
-import java.net.URISyntaxException;
-import java.nio.charset.Charset;
-import java.nio.charset.Charsets;
-
-// Note: This class copied verbatim from libcore.net
-
-/**
- * Encodes and decodes {@code application/x-www-form-urlencoded} content.
- * Subclasses define exactly which characters are legal.
- *
- * <p>By default, UTF-8 is used to encode escaped characters. A single input
- * character like "\u0080" may be encoded to multiple octets like %C2%80.
- */
-public abstract class UriCodec {
-
- /**
- * Returns true if {@code c} does not need to be escaped.
- */
- protected abstract boolean isRetained(char c);
-
- /**
- * Throws if {@code s} is invalid according to this encoder.
- */
- public final String validate(String uri, int start, int end, String name)
- throws URISyntaxException {
- for (int i = start; i < end; ) {
- char ch = uri.charAt(i);
- if ((ch >= 'a' && ch <= 'z')
- || (ch >= 'A' && ch <= 'Z')
- || (ch >= '0' && ch <= '9')
- || isRetained(ch)) {
- i++;
- } else if (ch == '%') {
- if (i + 2 >= end) {
- throw new URISyntaxException(uri, "Incomplete % sequence in " + name, i);
- }
- int d1 = hexToInt(uri.charAt(i + 1));
- int d2 = hexToInt(uri.charAt(i + 2));
- if (d1 == -1 || d2 == -1) {
- throw new URISyntaxException(uri, "Invalid % sequence: "
- + uri.substring(i, i + 3) + " in " + name, i);
- }
- i += 3;
- } else {
- throw new URISyntaxException(uri, "Illegal character in " + name, i);
- }
- }
- return uri.substring(start, end);
- }
-
- /**
- * Throws if {@code s} contains characters that are not letters, digits or
- * in {@code legal}.
- */
- public static void validateSimple(String s, String legal)
- throws URISyntaxException {
- for (int i = 0; i < s.length(); i++) {
- char ch = s.charAt(i);
- if (!((ch >= 'a' && ch <= 'z')
- || (ch >= 'A' && ch <= 'Z')
- || (ch >= '0' && ch <= '9')
- || legal.indexOf(ch) > -1)) {
- throw new URISyntaxException(s, "Illegal character", i);
- }
- }
- }
-
- /**
- * Encodes {@code s} and appends the result to {@code builder}.
- *
- * @param isPartiallyEncoded true to fix input that has already been
- * partially or fully encoded. For example, input of "hello%20world" is
- * unchanged with isPartiallyEncoded=true but would be double-escaped to
- * "hello%2520world" otherwise.
- */
- private void appendEncoded(StringBuilder builder, String s, Charset charset,
- boolean isPartiallyEncoded) {
- if (s == null) {
- throw new NullPointerException();
- }
-
- int escapeStart = -1;
- for (int i = 0; i < s.length(); i++) {
- char c = s.charAt(i);
- if ((c >= 'a' && c <= 'z')
- || (c >= 'A' && c <= 'Z')
- || (c >= '0' && c <= '9')
- || isRetained(c)
- || (c == '%' && isPartiallyEncoded)) {
- if (escapeStart != -1) {
- appendHex(builder, s.substring(escapeStart, i), charset);
- escapeStart = -1;
- }
- if (c == '%' && isPartiallyEncoded) {
- // this is an encoded 3-character sequence like "%20"
- builder.append(s, i, i + 3);
- i += 2;
- } else if (c == ' ') {
- builder.append('+');
- } else {
- builder.append(c);
- }
- } else if (escapeStart == -1) {
- escapeStart = i;
- }
- }
- if (escapeStart != -1) {
- appendHex(builder, s.substring(escapeStart, s.length()), charset);
- }
- }
-
- public final String encode(String s, Charset charset) {
- // Guess a bit larger for encoded form
- StringBuilder builder = new StringBuilder(s.length() + 16);
- appendEncoded(builder, s, charset, false);
- return builder.toString();
- }
-
- public final void appendEncoded(StringBuilder builder, String s) {
- appendEncoded(builder, s, Charsets.UTF_8, false);
- }
-
- public final void appendPartiallyEncoded(StringBuilder builder, String s) {
- appendEncoded(builder, s, Charsets.UTF_8, true);
- }
-
- /**
- * @param convertPlus true to convert '+' to ' '.
- */
- public static String decode(String s, boolean convertPlus, Charset charset) {
- if (s.indexOf('%') == -1 && (!convertPlus || s.indexOf('+') == -1)) {
- return s;
- }
-
- StringBuilder result = new StringBuilder(s.length());
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- for (int i = 0; i < s.length();) {
- char c = s.charAt(i);
- if (c == '%') {
- do {
- if (i + 2 >= s.length()) {
- throw new IllegalArgumentException("Incomplete % sequence at: " + i);
- }
- int d1 = hexToInt(s.charAt(i + 1));
- int d2 = hexToInt(s.charAt(i + 2));
- if (d1 == -1 || d2 == -1) {
- throw new IllegalArgumentException("Invalid % sequence " +
- s.substring(i, i + 3) + " at " + i);
- }
- out.write((byte) ((d1 << 4) + d2));
- i += 3;
- } while (i < s.length() && s.charAt(i) == '%');
- result.append(new String(out.toByteArray(), charset));
- out.reset();
- } else {
- if (convertPlus && c == '+') {
- c = ' ';
- }
- result.append(c);
- i++;
- }
- }
- return result.toString();
- }
-
- /**
- * Like {@link Character#digit}, but without support for non-ASCII
- * characters.
- */
- private static int hexToInt(char c) {
- if ('0' <= c && c <= '9') {
- return c - '0';
- } else if ('a' <= c && c <= 'f') {
- return 10 + (c - 'a');
- } else if ('A' <= c && c <= 'F') {
- return 10 + (c - 'A');
- } else {
- return -1;
- }
- }
-
- public static String decode(String s) {
- return decode(s, false, Charsets.UTF_8);
- }
-
- private static void appendHex(StringBuilder builder, String s, Charset charset) {
- for (byte b : s.getBytes(charset)) {
- appendHex(builder, b);
- }
- }
-
- private static void appendHex(StringBuilder sb, byte b) {
- sb.append('%');
- sb.append(Byte.toHexString(b, true));
- }
-}
diff --git a/src/com/android/exchange/utility/patent_disclaimer.txt b/src/com/android/exchange/utility/patent_disclaimer.txt
deleted file mode 100644
index 8a715a3..0000000
--- a/src/com/android/exchange/utility/patent_disclaimer.txt
+++ /dev/null
@@ -1,9 +0,0 @@
------------------------------
-THIS IS NOT A GRANT OF PATENT RIGHTS.
-
-Google makes no representation or warranty that the source code made available hereunder is
-unencumbered by third-party patents. Those intending to use this source code in hardware or
-software products are advised that implementations of this code, including in open source software
-or shareware, may require patent licenses from the relevant patent holders.
-
------------------------------