am a3e021d1: am 3b811ae4: resolved conflicts for merge of fb060de6 to gingerbread
Merge commit 'a3e021d12c96d10f758fb6af3b7a05e85d0d8eeb'
* commit 'a3e021d12c96d10f758fb6af3b7a05e85d0d8eeb':
Explicitly verify certificate hostname on SSL connections
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a21a23d..ecbefed 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -143,7 +143,8 @@
</activity>
<activity
android:name=".activity.AccountFolderList"
- android:launchMode="singleTop" >
+ android:launchMode="singleTop"
+ android:theme="@android:style/Theme.WithActionBar" >
</activity>
<activity
@@ -159,7 +160,7 @@
<activity
android:name=".activity.MailboxList"
- android:theme="@style/ThemeNoTitleBar">
+ android:theme="@android:style/Theme.WithActionBar" >
</activity>
<activity
@@ -191,6 +192,12 @@
<activity
android:name=".activity.MessageView"
android:theme="@android:style/Theme.NoTitleBar" >
+ <intent-filter android:label="@string/app_name">
+ <action android:name="android.intent.action.VIEW" />
+ <data android:mimeType="application/eml" />
+ <data android:mimeType="message/rfc822" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
</activity>
<activity
android:name=".activity.MessageCompose"
@@ -216,26 +223,18 @@
</intent-filter>
</activity>
<!--EXCHANGE-REMOVE-SECTION-START-->
- <receiver android:name="com.android.exchange.EmailSyncAlarmReceiver"/>
- <receiver android:name="com.android.exchange.MailboxAlarmReceiver"/>
- <receiver android:name="com.android.exchange.BootReceiver" android:enabled="true">
- <intent-filter>
- <action android:name="android.intent.action.BOOT_COMPLETED" />
- </intent-filter>
- </receiver>
+ <receiver android:name="com.android.exchange.EmailSyncAlarmReceiver"/>
+ <receiver android:name="com.android.exchange.MailboxAlarmReceiver"/>
<!--EXCHANGE-REMOVE-SECTION-END-->
- <receiver android:name=".service.BootReceiver" android:enabled="true">
+ <receiver android:name=".service.EmailBroadcastReceiver" android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
- </intent-filter>
- <intent-filter>
<action android:name="android.intent.action.DEVICE_STORAGE_LOW" />
- </intent-filter>
- <intent-filter>
<action android:name="android.intent.action.DEVICE_STORAGE_OK" />
</intent-filter>
</receiver>
+ <service android:name=".service.EmailBroadcastProcessorService" />
<!-- Support for DeviceAdmin / DevicePolicyManager. See SecurityPolicy class for impl. -->
<receiver
@@ -250,15 +249,6 @@
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
-
- <receiver
- android:name=".OneTimeInitializer"
- android:enabled="true"
- >
- <intent-filter>
- <action android:name="android.intent.action.BOOT_COMPLETED" />
- </intent-filter>
- </receiver>
<service
android:name=".service.MailService"
@@ -349,12 +339,20 @@
<!--EXCHANGE-REMOVE-SECTION-START-->
<!-- In this release, GAL information is used locally only, so we used the same
strict permissions. -->
+ <!-- NOTE: ExchangeDirectoryProvider will replace ExchangeProvider after integration with
+ the new GAL/contacts implementation -->
<provider
android:name="com.android.exchange.provider.ExchangeProvider"
android:authorities="com.android.exchange.provider"
android:multiprocess="true"
android:permission="com.android.email.permission.ACCESS_PROVIDER"
/>
+ <provider
+ android:name="com.android.exchange.provider.ExchangeDirectoryProvider"
+ android:authorities="com.android.exchange.directory.provider"
+ android:readPermission="android.permission.READ_CONTACTS"
+ android:multiprocess="false"
+ />
<!--EXCHANGE-REMOVE-SECTION-END-->
</application>
diff --git a/proguard.flags b/proguard.flags
index bbffc8a..f3b7e57 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -23,9 +23,9 @@
-keep class * extends org.apache.james.mime4j.util.TempStorage
-
# Keep names that are used only by unit tests
+# Any methods whose name is '*ForTest' are preserved.
-keep class ** {
*** *ForTest(...);
}
@@ -172,3 +172,7 @@
-keep class org.apache.james.mime4j.message.Message {
*;
}
+
+-keepclasseswithmembers class org.apache.commons.io.IOUtils {
+ *** toByteArray(...);
+}
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 95b4e9e..c7167c3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -278,9 +278,8 @@
<string name="message_view_fetching_attachment_toast">Fetching attachment.</string>
<!-- Appears progress dialog for fetching attachment -->
<string name="message_view_fetching_attachment_progress">Fetching attachment <xliff:g id="filename">%s</xliff:g></string>
- <!-- Calendar invitation, link to calendar.
- Preserve the chevron (unicode ») untranslated -->
- <string name="message_view_invite_view">View in Calendar »</string>
+ <!-- Calendar invitation, label of the button to open in calendar -->
+ <string name="message_view_invite_view">View in Calendar</string>
<!-- String shown with a calendar invitation. -->
<string name="message_view_invite_title">Calendar Invite</string>
<!-- String shown with a calendar invitation. -->
@@ -309,6 +308,8 @@
<string name="message_saved_toast">Message saved as draft.</string>
<!-- String that is displayed when the attachment could not be displayed. -->
<string name="message_view_display_attachment_toast">This attachment cannot be displayed.</string>
+ <!-- String that is displayed when a long message is being parsed. -->
+ <string name="message_view_parse_message_toast">Opening message\u2026</string>
<!-- Title of screen when setting up new email account -->
<string name="account_setup_basics_title">Set up email</string>
@@ -445,6 +446,8 @@
<string name="account_setup_exchange_ssl_label">Use secure connection (SSL)</string>
<!-- On "Exchange" setup screen, the trust ssl certificates checkbox label -->
<string name="account_setup_exchange_trust_certificates_label">Accept all SSL certificates</string>
+ <!-- On "Exchange" setup screen, the exchange device-id label -->
+ <string name="account_setup_exchange_device_id_label">Mobile Device ID</string>
<!-- In Account setup options screen, Activity title -->
<string name="account_setup_options_title">Account options</string>
diff --git a/src/com/android/exchange/AbstractSyncService.java b/src/com/android/exchange/AbstractSyncService.java
index f1fe834..1a2d958 100644
--- a/src/com/android/exchange/AbstractSyncService.java
+++ b/src/com/android/exchange/AbstractSyncService.java
@@ -30,6 +30,7 @@
import android.net.NetworkInfo;
import android.net.Uri;
import android.net.NetworkInfo.DetailedState;
+import android.os.Bundle;
import android.util.Log;
import java.util.ArrayList;
@@ -109,10 +110,12 @@
* @param port
* @param ssl
* @param context
+ * @return a Bundle containing a result code and, depending on the result, a PolicySet or an
+ * error message
* @throws MessagingException
*/
- public abstract void validateAccount(String host, String userName, String password, int port,
- boolean ssl, boolean trustCertificates, Context context) throws MessagingException;
+ public abstract Bundle validateAccount(String host, String userName, String password, int port,
+ boolean ssl, boolean trustCertificates, Context context);
public AbstractSyncService(Context _context, Mailbox _mailbox) {
mContext = _context;
@@ -137,21 +140,22 @@
* @param port
* @param ssl
* @param context
+ * @return a Bundle containing a result code and, depending on the result, a PolicySet or an
+ * error message
* @throws MessagingException
*/
- static public void validate(Class<? extends AbstractSyncService> klass, String host,
+ static public Bundle validate(Class<? extends AbstractSyncService> klass, String host,
String userName, String password, int port, boolean ssl, boolean trustCertificates,
- Context context)
- throws MessagingException {
+ Context context) {
AbstractSyncService svc;
try {
svc = klass.newInstance();
- svc.validateAccount(host, userName, password, port, ssl, trustCertificates, context);
+ return svc.validateAccount(host, userName, password, port, ssl, trustCertificates,
+ context);
} catch (IllegalAccessException e) {
- throw new MessagingException("internal error", e);
} catch (InstantiationException e) {
- throw new MessagingException("internal error", e);
}
+ return null;
}
public static class ValidationResult {
diff --git a/src/com/android/exchange/BootReceiver.java b/src/com/android/exchange/BootReceiver.java
deleted file mode 100644
index 1ebfa7b..0000000
--- a/src/com/android/exchange/BootReceiver.java
+++ /dev/null
@@ -1,32 +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.email.ExchangeUtils;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-public class BootReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- Log.d("Exchange", "BootReceiver onReceive");
- ExchangeUtils.startExchangeService(context);
- }
-}
diff --git a/src/com/android/exchange/Eas.java b/src/com/android/exchange/Eas.java
index 8e561e7..14dc537 100644
--- a/src/com/android/exchange/Eas.java
+++ b/src/com/android/exchange/Eas.java
@@ -45,6 +45,8 @@
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 DEFAULT_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_EX2003;
// From EAS spec
@@ -91,5 +93,16 @@
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;
+ }
+ throw new IllegalArgumentException("illegal protocol version");
+ }
}
diff --git a/src/com/android/exchange/EasOutboxService.java b/src/com/android/exchange/EasOutboxService.java
index 34b6d7f..b0b39b7 100644
--- a/src/com/android/exchange/EasOutboxService.java
+++ b/src/com/android/exchange/EasOutboxService.java
@@ -36,6 +36,7 @@
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
+import android.net.Uri;
import android.os.RemoteException;
import java.io.File;
@@ -71,6 +72,11 @@
}
}
+ /*package*/ String generateSmartSendCmd(boolean reply, String itemId, String collectionId) {
+ return (reply ? "SmartReply" : "SmartForward") + "&ItemId=" + Uri.encode(itemId) +
+ "&CollectionId=" + Uri.encode(collectionId);
+ }
+
/**
* Send a single message via EAS
* Note that we mark messages SEND_FAILED when there is a permanent failure, rather than an
@@ -130,11 +136,12 @@
new InputStreamEntity(inputStream, tmpFile.length());
// Create the appropriate command and POST it to the server
- String cmd = "SendMail&SaveInSent=T";
+ String cmd = "SendMail";
if (smartSend) {
- cmd = reply ? "SmartReply" : "SmartForward";
- cmd += "&ItemId=" + itemId + "&CollectionId=" + collectionId + "&SaveInSent=T";
+ cmd = generateSmartSendCmd(reply, itemId, collectionId);
}
+ cmd += "&SaveInSent=T";
+
userLog("Send cmd: " + cmd);
HttpResponse resp = sendHttpClientPost(cmd, inputEntity, SEND_MAIL_TIMEOUT);
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index 3eb48c6..aeeb90b 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -21,7 +21,6 @@
import com.android.email.Utility;
import com.android.email.SecurityPolicy.PolicySet;
import com.android.email.mail.Address;
-import com.android.email.mail.AuthenticationFailedException;
import com.android.email.mail.MeetingInfo;
import com.android.email.mail.MessagingException;
import com.android.email.mail.PackedString;
@@ -78,6 +77,7 @@
import android.content.Context;
import android.content.Entity;
import android.database.Cursor;
+import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.SystemClock;
@@ -95,7 +95,6 @@
import java.io.InputStream;
import java.lang.Thread.State;
import java.net.URI;
-import java.net.URLEncoder;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.HashMap;
@@ -175,13 +174,18 @@
// MSFT's custom HTTP result code indicating the need to provision
static private final int HTTP_NEED_PROVISIONING = 449;
+ // 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";
+
// Reasonable default
public String mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
public Double mProtocolVersionDouble;
protected String mDeviceId = null;
- private String mDeviceType = "Android";
- private String mAuthString = null;
- private String mCmdString = null;
+ /*package*/ String mDeviceType = "Android";
+ /*package*/ String mAuthString = null;
+ /*package*/ String mCmdString = null;
public String mHostAddress;
public String mUserName;
public String mPassword;
@@ -352,7 +356,8 @@
// 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) ||
+ version.equals(Eas.SUPPORTED_PROTOCOL_EX2007_SP1)) {
ourVersion = version;
}
}
@@ -363,7 +368,7 @@
throw new MessagingException(MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
} else {
service.mProtocolVersion = ourVersion;
- service.mProtocolVersionDouble = Double.parseDouble(ourVersion);
+ service.mProtocolVersionDouble = Eas.getProtocolVersionDouble(ourVersion);
if (service.mAccount != null) {
service.mAccount.mProtocolVersion = ourVersion;
}
@@ -371,8 +376,10 @@
}
@Override
- public void validateAccount(String hostAddress, String userName, String password, int port,
- boolean ssl, boolean trustCertificates, Context context) throws MessagingException {
+ public Bundle validateAccount(String hostAddress, String userName, String password, int port,
+ boolean ssl, boolean trustCertificates, Context context) {
+ Bundle bundle = new Bundle();
+ int resultCode = MessagingException.NO_ERROR;
try {
userLog("Testing EAS: ", hostAddress, ", ", userName, ", ssl = ", ssl ? "1" : "0");
EasSyncService svc = new EasSyncService("%TestAccount%");
@@ -392,60 +399,75 @@
// No exception means successful validation
Header commands = resp.getFirstHeader("MS-ASProtocolCommands");
Header versions = resp.getFirstHeader("ms-asprotocolversions");
- if (commands == null || versions == null) {
- userLog("OPTIONS response without commands or versions; reporting I/O error");
- throw new MessagingException(MessagingException.IOERROR);
+ // 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(svc, versions);
+ } catch (MessagingException e) {
+ bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE,
+ MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
+ return bundle;
}
- // Make sure we've got the right protocol version set up
- setupProtocolVersion(svc, versions);
-
- // Run second test here for provisioning failures...
- Serializer s = new Serializer();
+ // Run second test here for provisioning failures using FolderSync
userLog("Try folder sync");
- s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text("0")
+ // Send "0" as the sync key for new accounts; otherwise we'll use the current key
+ String syncKey = "0";
+ Account existingAccount =
+ Utility.findExistingAccount(context, -1L, hostAddress, userName);
+ 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 = svc.sendHttpClientPost("FolderSync", s.toByteArray());
code = resp.getStatusLine().getStatusCode();
// We'll get one of the following responses if policies are required by the server
if (code == HttpStatus.SC_FORBIDDEN || code == HTTP_NEED_PROVISIONING) {
// Get the policies and see if we are able to support them
- if (svc.canProvision() != null) {
- // If so, send the advisory Exception (the account may be created later)
- throw new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
+ ProvisionParser pp = svc.canProvision();
+ if (pp != null) {
+ // If so, 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.getPolicySet());
} else
- // If not, send the unsupported Exception (the account won't be created)
- throw new MessagingException(
- MessagingException.SECURITY_POLICIES_UNSUPPORTED);
+ // If not, set the proper code (the account will not be created)
+ resultCode = MessagingException.SECURITY_POLICIES_UNSUPPORTED;
} else if (code == HttpStatus.SC_NOT_FOUND) {
// We get a 404 from OWA addresses (which are NOT EAS addresses)
- throw new MessagingException(MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
+ resultCode = MessagingException.PROTOCOL_VERSION_UNSUPPORTED;
} else if (code != HttpStatus.SC_OK) {
// Fail generically with anything other than success
userLog("Unexpected response for FolderSync: ", code);
- throw new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION);
+ resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
+ } else {
+ userLog("Validation successful");
}
- userLog("Validation successful");
- return;
- }
- if (isAuthError(code)) {
+ } else if (isAuthError(code)) {
userLog("Authentication failed");
- throw new AuthenticationFailedException("Validation failed");
+ resultCode = MessagingException.AUTHENTICATION_FAILED;
} else {
// TODO Need to catch other kinds of errors (e.g. policy) For now, report the code.
userLog("Validation failed, reporting I/O error: ", code);
- throw new MessagingException(MessagingException.IOERROR);
+ resultCode = MessagingException.IOERROR;
}
} catch (IOException e) {
Throwable cause = e.getCause();
if (cause != null && cause instanceof CertificateException) {
userLog("CertificateException caught: ", e.getMessage());
- throw new MessagingException(MessagingException.GENERAL_SECURITY);
+ resultCode = MessagingException.GENERAL_SECURITY;
}
userLog("IOException caught: ", e.getMessage());
- throw new MessagingException(MessagingException.IOERROR);
+ resultCode = MessagingException.IOERROR;
}
-
+ bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, resultCode);
+ return bundle;
}
/**
@@ -766,19 +788,27 @@
* @param context caller's context
* @param accountId the account Id to search
* @param filter the characters entered so far
- * @return a result record
+ * @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)
- {
+ static public GalResult searchGal(Context context, long accountId, String filter) {
Account acct = SyncManager.getAccountById(accountId);
if (acct != null) {
HostAuth ha = HostAuth.restoreHostAuthWithId(context, acct.mHostAuthKeyRecv);
EasSyncService svc = new EasSyncService("%GalLookupk%");
try {
+ // If there's no protocol version set up, we haven't successfully started syncing
+ // so we can't use GAL yet
+ String protocolVersion = acct.mProtocolVersion;
+ if (protocolVersion == null) {
+ return null;
+ } else {
+ svc.mProtocolVersion = protocolVersion;
+ svc.mProtocolVersionDouble = Eas.getProtocolVersionDouble(protocolVersion);
+ }
svc.mContext = context;
svc.mHostAddress = ha.mAddress;
svc.mUserName = ha.mLogin;
@@ -1069,17 +1099,16 @@
* in all HttpPost commands. This should be called if these strings are null, or if mUserName
* and/or mPassword are changed
*/
- @SuppressWarnings("deprecation")
private void cacheAuthAndCmdString() {
- String safeUserName = URLEncoder.encode(mUserName);
+ String safeUserName = Uri.encode(mUserName);
String cs = mUserName + ':' + mPassword;
mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP);
mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId +
"&DeviceType=" + mDeviceType;
}
- private String makeUriString(String cmd, String extra) throws IOException {
- // Cache the authentication string and the command string
+ /*package*/ String makeUriString(String cmd, String extra) throws IOException {
+ // Cache the authentication string and the command string
if (mAuthString == null || mCmdString == null) {
cacheAuthAndCmdString();
}
@@ -1099,18 +1128,21 @@
* @param method the method we are going to send
* @param usePolicyKey whether or not a policy key should be sent in the headers
*/
- private void setHeaders(HttpRequestBase method, boolean usePolicyKey) {
+ /*package*/ void setHeaders(HttpRequestBase method, boolean usePolicyKey) {
method.setHeader("Authorization", mAuthString);
method.setHeader("MS-ASProtocolVersion", mProtocolVersion);
method.setHeader("Connection", "keep-alive");
method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION);
- if (usePolicyKey && (mAccount != null)) {
- String key = mAccount.mSecuritySyncKey;
- if (key == null || key.length() == 0) {
- return;
- }
- if (Eas.PARSER_LOG) {
- userLog("Policy key: " , key);
+ 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);
}
@@ -1277,7 +1309,7 @@
} else if (sp.isActive(ps)) {
// See if the required policies are in force; if they are, acknowledge the policies
// to the server and get the final policy key
- String policyKey = acknowledgeProvision(pp.getPolicyKey());
+ String policyKey = acknowledgeProvision(pp.getPolicyKey(), PROVISION_STATUS_OK);
if (policyKey != null) {
// Write the final policy key to the Account and say we've been successful
ps.writeAccount(mAccount, policyKey, true, mContext);
@@ -1314,15 +1346,20 @@
InputStream is = resp.getEntity().getContent();
ProvisionParser pp = new ProvisionParser(is, this);
if (pp.parse()) {
- // If true, we received policies from the server; see if they are supported by
- // the framework; if so, return the ProvisionParser containing the policy set and
- // temporary key
- PolicySet ps = pp.getPolicySet();
- // The PolicySet can be null if there are policies we don't know about (e.g. ones
- // from Exchange 12.1) If we have a PolicySet, then we ask whether the device can
- // support the actual parameters of those policies.
- if ((ps != null) && SecurityPolicy.getInstance(mContext).isSupported(ps)) {
+ // The PolicySet in the ProvisionParser will have the requirements for all KNOWN
+ // policies. If others are required, hasSupportablePolicySet will be false
+ if (pp.hasSupportablePolicySet()) {
+ // If the policies are supportable (in this context, meaning that there are no
+ // completely unimplemented policies required), just return the parser itself
return pp;
+ } else {
+ // 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
+ String policyKey = acknowledgeProvision(pp.getPolicyKey(),
+ PROVISION_STATUS_PARTIAL);
+ // Return either the parser (success) or null (failure)
+ return (policyKey != null) ? pp : null;
}
}
}
@@ -1338,14 +1375,15 @@
* @throws IOException
*/
private void acknowledgeRemoteWipe(String tempKey) throws IOException {
- acknowledgeProvisionImpl(tempKey, true);
+ acknowledgeProvisionImpl(tempKey, PROVISION_STATUS_OK, true);
}
- private String acknowledgeProvision(String tempKey) throws IOException {
- return acknowledgeProvisionImpl(tempKey, false);
+ private String acknowledgeProvision(String tempKey, String result) throws IOException {
+ return acknowledgeProvisionImpl(tempKey, result, false);
}
- private String acknowledgeProvisionImpl(String tempKey, boolean remoteWipe) throws IOException {
+ private String acknowledgeProvisionImpl(String tempKey, String status,
+ boolean remoteWipe) throws IOException {
Serializer s = new Serializer();
s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES);
s.start(Tags.PROVISION_POLICY);
@@ -1354,11 +1392,11 @@
s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType());
s.data(Tags.PROVISION_POLICY_KEY, tempKey);
- s.data(Tags.PROVISION_STATUS, "1");
+ 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, "1");
+ s.data(Tags.PROVISION_STATUS, PROVISION_STATUS_OK);
s.end();
}
s.end().done(); // PROVISION_PROVISION
@@ -1368,7 +1406,7 @@
InputStream is = resp.getEntity().getContent();
ProvisionParser pp = new ProvisionParser(is, this);
if (pp.parse()) {
- // Return the final polic key from the ProvisionParser
+ // Return the final policy key from the ProvisionParser
return pp.getPolicyKey();
}
}
@@ -1384,7 +1422,7 @@
*/
public void runAccountMailbox() throws IOException, EasParserException {
// Initialize exit status to success
- mExitStatus = EmailServiceStatus.SUCCESS;
+ mExitStatus = EXIT_DONE;
try {
try {
SyncManager.callback()
@@ -1393,6 +1431,12 @@
// Don't care if this fails
}
+ // Don't run if we're being held
+ if ((mAccount.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
+ mExitStatus = EXIT_SECURITY_FAILURE;
+ return;
+ }
+
if (mAccount.mSyncKey == null) {
mAccount.mSyncKey = "0";
userLog("Account syncKey INIT to 0");
@@ -1979,9 +2023,12 @@
userLog("sync, sending ", className, " syncKey: ", syncKey);
s.start(Tags.SYNC_SYNC)
.start(Tags.SYNC_COLLECTIONS)
- .start(Tags.SYNC_COLLECTION)
- .data(Tags.SYNC_CLASS, className)
- .data(Tags.SYNC_SYNC_KEY, syncKey)
+ .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
@@ -2028,6 +2075,24 @@
timeout);
int code = resp.getStatusLine().getStatusCode();
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
+ Header header = resp.getFirstHeader("content-length");
+ if (header != null && header.getValue().equals("0")) {
+ // If this happens, exit cleanly, and change the interval from push to ping
+ // if necessary
+ mExitStatus = EXIT_DONE;
+ 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);
+ }
+ return;
+ }
InputStream is = resp.getEntity().getContent();
if (is != null) {
moreAvailable = target.parse(is);
@@ -2084,7 +2149,7 @@
if (mProtocolVersion == null) {
mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
}
- mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
+ mProtocolVersionDouble = Eas.getProtocolVersionDouble(mProtocolVersion);
return true;
}
diff --git a/src/com/android/exchange/MailboxAlarmReceiver.java b/src/com/android/exchange/MailboxAlarmReceiver.java
index f8551e4..565b158 100644
--- a/src/com/android/exchange/MailboxAlarmReceiver.java
+++ b/src/com/android/exchange/MailboxAlarmReceiver.java
@@ -29,9 +29,14 @@
public class MailboxAlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- long mid = intent.getLongExtra("mailbox", -1);
- SyncManager.log("Alarm received for: " + SyncManager.alarmOwner(mid));
- SyncManager.alert(context, mid);
+ long mailboxId = intent.getLongExtra("mailbox", SyncManager.SYNC_MANAGER_ID);
+ // SYNC_MANAGER_SERVICE_ID tells us that the service is asking to be started
+ if (mailboxId == SyncManager.SYNC_MANAGER_SERVICE_ID) {
+ context.startService(new Intent(context, SyncManager.class));
+ } else {
+ SyncManager.log("Alarm received for: " + SyncManager.alarmOwner(mailboxId));
+ SyncManager.alert(context, mailboxId);
+ }
}
}
diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java
index 7a56cbb..890a99a 100644
--- a/src/com/android/exchange/SyncManager.java
+++ b/src/com/android/exchange/SyncManager.java
@@ -21,7 +21,6 @@
import com.android.email.Email;
import com.android.email.SecurityPolicy;
import com.android.email.Utility;
-import com.android.email.mail.MessagingException;
import com.android.email.mail.transport.SSLUtils;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.Account;
@@ -114,7 +113,8 @@
private static final String TAG = "EAS SyncManager";
// The SyncManager's mailbox "id"
- private static final int SYNC_MANAGER_ID = -1;
+ protected static final int SYNC_MANAGER_ID = -1;
+ protected static final int SYNC_MANAGER_SERVICE_ID = 0;
private static final int SECONDS = 1000;
private static final int MINUTES = 60*SECONDS;
@@ -182,8 +182,6 @@
// All threads can use this lock to wait for connectivity
public static final Object sConnectivityLock = new Object();
public static boolean sConnectivityHold = false;
- // Keep our cached list of active Accounts here
- public static final AccountList sAccountList = new AccountList();
// Keeps track of running services (by mailbox id)
private HashMap<Long, AbstractSyncService> mServiceMap =
@@ -196,6 +194,8 @@
private HashMap<Long, PendingIntent> mPendingIntents = new HashMap<Long, PendingIntent>();
// The actual WakeLock obtained by SyncManager
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 Handler mHandler = new Handler();
@@ -204,6 +204,7 @@
private SyncedMessageObserver mSyncedMessageObserver;
private MessageObserver mMessageObserver;
private EasSyncStatusObserver mSyncStatusObserver;
+ private Object mStatusChangeListener;
private EasAccountsUpdatedListener mAccountsUpdatedListener;
private HashMap<Long, CalendarObserver> mCalendarObservers =
@@ -221,7 +222,7 @@
// Count of ClientConnectionManager shutdowns
private static volatile int sClientConnectionManagerShutdownCount = 0;
- private boolean mStop = false;
+ private static volatile boolean sStop = false;
// The reason for SyncManager's next wakeup call
private String mNextWaitReason;
@@ -285,15 +286,10 @@
*/
private final IEmailService.Stub mBinder = new IEmailService.Stub() {
- public int validate(String protocol, String host, String userName, String password,
+ public Bundle validate(String protocol, String host, String userName, String password,
int port, boolean ssl, boolean trustCertificates) throws RemoteException {
- try {
- AbstractSyncService.validate(EasSyncService.class, host, userName, password, port,
- ssl, trustCertificates, SyncManager.this);
- return MessagingException.NO_ERROR;
- } catch (MessagingException e) {
- return e.getExceptionType();
- }
+ return AbstractSyncService.validate(EasSyncService.class, host, userName, password,
+ port, ssl, trustCertificates, SyncManager.this);
}
public Bundle autoDiscover(String userName, String password) throws RemoteException {
@@ -424,6 +420,15 @@
}
return null;
}
+
+ public Account getByName(String accountName) {
+ for (Account account : this) {
+ if (account.mEmailAddress.equalsIgnoreCase(accountName)) {
+ return account;
+ }
+ }
+ return null;
+ }
}
class AccountObserver extends ContentObserver {
@@ -434,18 +439,18 @@
super(handler);
// At startup, we want to see what EAS accounts exist and cache them
Context context = getContext();
- synchronized (sAccountList) {
+ synchronized (mAccountList) {
Cursor c = getContentResolver().query(Account.CONTENT_URI,
Account.CONTENT_PROJECTION, null, null, null);
// Build the account list from the cursor
try {
- collectEasAccounts(c, sAccountList);
+ collectEasAccounts(c, mAccountList);
} finally {
c.close();
}
// Create an account mailbox for any account without one
- for (Account account : sAccountList) {
+ for (Account account : mAccountList) {
int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, "accountKey="
+ account.mId, null);
if (cnt == 0) {
@@ -464,8 +469,8 @@
if (mSyncableEasMailboxSelector == null) {
StringBuilder sb = new StringBuilder(WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN);
boolean first = true;
- synchronized (sAccountList) {
- for (Account account : sAccountList) {
+ synchronized (mAccountList) {
+ for (Account account : mAccountList) {
if (!first) {
sb.append(',');
} else {
@@ -489,8 +494,8 @@
if (mEasAccountSelector == null) {
StringBuilder sb = new StringBuilder(ACCOUNT_KEY_IN);
boolean first = true;
- synchronized (sAccountList) {
- for (Account account : sAccountList) {
+ synchronized (mAccountList) {
+ for (Account account : mAccountList) {
if (!first) {
sb.append(',');
} else {
@@ -520,14 +525,14 @@
Account.CONTENT_PROJECTION, null, null, null);
try {
collectEasAccounts(c, currentAccounts);
- synchronized (sAccountList) {
- for (Account account : sAccountList) {
- // Ignore accounts not fully created
- if ((account.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
- log("Account observer noticed incomplete account; ignoring");
- continue;
- } else if (!currentAccounts.contains(account.mId)) {
- // This is a deletion; shut down any account-related syncs
+ 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) {
+ // Shut down any account-related syncs
stopAccountSyncs(account.mId, true);
// Delete this from AccountManager...
android.accounts.Account acct = new android.accounts.Account(
@@ -536,9 +541,9 @@
mSyncableEasMailboxSelector = null;
mEasAccountSelector = null;
} else {
- // An account has changed
- Account updatedAccount = Account.restoreAccountWithId(context,
- account.mId);
+ // 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) {
@@ -568,7 +573,7 @@
}
// Look for new accounts
for (Account account : currentAccounts) {
- if (!sAccountList.contains(account.mId)) {
+ if (!mAccountList.contains(account.mId)) {
// Don't forget to cache the HostAuth
HostAuth ha = HostAuth.restoreHostAuthWithId(getContext(),
account.mHostAuthKeyRecv);
@@ -577,14 +582,14 @@
// This is an addition; create our magic hidden mailbox...
log("Account observer found new account: " + account.mDisplayName);
addAccountMailbox(account.mId);
- sAccountList.add(account);
+ mAccountList.add(account);
mSyncableEasMailboxSelector = null;
mEasAccountSelector = null;
}
}
// Finally, make sure our account list is up to date
- sAccountList.clear();
- sAccountList.addAll(currentAccounts);
+ mAccountList.clear();
+ mAccountList.addAll(currentAccounts);
}
} finally {
c.close();
@@ -599,7 +604,7 @@
new Thread(new Runnable() {
public void run() {
onAccountChanged();
- }}).start();
+ }}, "Account Observer").start();
}
private void collectEasAccounts(Cursor c, ArrayList<Account> accounts) {
@@ -773,7 +778,7 @@
} finally {
c.close();
}
- }}).start();
+ }}, "Calendar Observer").start();
}
}
}
@@ -831,9 +836,25 @@
}
static public Account getAccountById(long accountId) {
- synchronized (sAccountList) {
- return sAccountList.getById(accountId);
+ SyncManager syncManager = INSTANCE;
+ if (syncManager != null) {
+ AccountList accountList = syncManager.mAccountList;
+ synchronized (accountList) {
+ return accountList.getById(accountId);
+ }
}
+ return null;
+ }
+
+ static public Account getAccountByName(String accountName) {
+ SyncManager syncManager = INSTANCE;
+ if (syncManager != null) {
+ AccountList accountList = syncManager.mAccountList;
+ synchronized (accountList) {
+ return accountList.getByName(accountName);
+ }
+ }
+ return null;
}
static public String getEasAccountSelector() {
@@ -963,13 +984,13 @@
public void run() {
android.accounts.Account[] accountMgrList = AccountManager.get(syncManager)
.getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
- synchronized (sAccountList) {
+ synchronized (mAccountList) {
// Make sure we have an up-to-date sAccountList. If not (for example, if the
// service has been destroyed), we would be reconciling against an empty account
// list, which would cause the deletion of all of our accounts
if (mAccountObserver != null) {
mAccountObserver.onAccountChanged();
- reconcileAccountsWithAccountManager(syncManager, sAccountList,
+ reconcileAccountsWithAccountManager(syncManager, mAccountList,
accountMgrList, false, mResolver);
}
}
@@ -1055,113 +1076,6 @@
return mBinder;
}
- /**
- * Note that there are two ways the EAS SyncManager service can be created:
- *
- * 1) as a background service instantiated via startService (which happens on boot, when the
- * first EAS account is created, etc), in which case the service thread is spun up, mailboxes
- * sync, etc. and
- * 2) to execute an RPC call from the UI, in which case the background service will already be
- * running most of the time (unless we're creating a first EAS account)
- *
- * If the running background service detects that there are no EAS accounts (on boot, if none
- * were created, or afterward if the last remaining EAS account is deleted), it will call
- * stopSelf() to terminate operation.
- *
- * The goal is to ensure that the background service is running at all times when there is at
- * least one EAS account in existence
- *
- * Because there are edge cases in which our process can crash (typically, this has been seen
- * in UI crashes, ANR's, etc.), it's possible for the UI to start up again without the
- * background service having been started. We explicitly try to start the service in Welcome
- * (to handle the case of the app having been reloaded). We also start the service on any
- * startSync call (if it isn't already running)
- */
- @Override
- public void onCreate() {
- alwaysLog("!!! EAS SyncManager, onCreate");
- if (INSTANCE == null) {
- INSTANCE = this;
- mResolver = getContentResolver();
- mAccountObserver = new AccountObserver(mHandler);
- mResolver.registerContentObserver(Account.CONTENT_URI, true, mAccountObserver);
- mMailboxObserver = new MailboxObserver(mHandler);
- mSyncedMessageObserver = new SyncedMessageObserver(mHandler);
- mMessageObserver = new MessageObserver(mHandler);
- mSyncStatusObserver = new EasSyncStatusObserver();
- } else {
- alwaysLog("!!! EAS SyncManager onCreated, but INSTANCE not null??");
- }
- if (sDeviceId == null) {
- try {
- getDeviceId(this);
- } catch (IOException e) {
- // We can't run in this situation
- throw new RuntimeException();
- }
- }
- // Run the reconciler and clean up any mismatched accounts - if we weren't running when
- // accounts were deleted, it won't have been called.
- runAccountReconciler();
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- alwaysLog("!!! EAS SyncManager, onStartCommand");
-
- // Restore accounts, if it has not happened already
- AccountBackupRestore.restoreAccountsIfNeeded(this);
-
- maybeStartSyncManagerThread();
- if (sServiceThread == null) {
- alwaysLog("!!! EAS SyncManager, stopping self");
- stopSelf();
- }
- return Service.START_STICKY;
- }
-
- @Override
- public void onDestroy() {
- alwaysLog("!!! EAS SyncManager, onDestroy");
- if (INSTANCE != null) {
- INSTANCE = null;
- mResolver.unregisterContentObserver(mAccountObserver);
- unregisterCalendarObservers();
- mResolver = null;
- mAccountObserver = null;
- mMailboxObserver = null;
- mSyncedMessageObserver = null;
- mMessageObserver = null;
- mSyncStatusObserver = null;
- mAccountsUpdatedListener = null;
- }
- }
-
- void maybeStartSyncManagerThread() {
- // Start our thread...
- // See if there are any EAS accounts; otherwise, just go away
- if (EmailContent.count(this, HostAuth.CONTENT_URI, WHERE_PROTOCOL_EAS, null) > 0) {
- if (sServiceThread == null || !sServiceThread.isAlive()) {
- log(sServiceThread == null ? "Starting thread..." : "Restarting thread...");
- sServiceThread = new Thread(this, "SyncManager");
- sServiceThread.start();
- }
- }
- }
-
- static void checkSyncManagerServiceRunning() {
- // Get the service thread running if it isn't
- // This is a stopgap for cases in which SyncManager died (due to a crash somewhere in
- // com.android.email) and hasn't been restarted
- // See the comment for onCreate for details
- SyncManager syncManager = INSTANCE;
- if (syncManager == null) return;
- if (sServiceThread == null) {
- alwaysLog("!!! checkSyncManagerServiceRunning; starting service...");
- syncManager.startService(new Intent(syncManager, SyncManager.class));
- }
- }
-
static public ConnPerRoute sConnPerRoute = new ConnPerRoute() {
public int getMaxForRoute(HttpRoute route) {
return 8;
@@ -1453,6 +1367,10 @@
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 = "SyncManager Alert: ";
+ if (service.mMailbox != null) {
+ threadName += service.mMailbox.mDisplayName;
+ }
new Thread(new Runnable() {
public void run() {
Mailbox m = Mailbox.restoreMailboxWithId(syncManager, id);
@@ -1484,7 +1402,7 @@
SyncManager.shutdownConnectionManager();
}
}
- }}).start();
+ }}, threadName).start();
}
}
}
@@ -1541,8 +1459,8 @@
* Make our sync settings match those of AccountManager
*/
private void checkPIMSyncSettings() {
- synchronized (sAccountList) {
- for (Account account : sAccountList) {
+ synchronized (mAccountList) {
+ for (Account account : mAccountList) {
updatePIMSyncSettings(account, Mailbox.TYPE_CONTACTS, ContactsContract.AUTHORITY);
updatePIMSyncSettings(account, Mailbox.TYPE_CALENDAR, Calendar.AUTHORITY);
}
@@ -1668,8 +1586,8 @@
// Otherwise, stop all syncs
} else {
log("Background data off: stop all syncs");
- synchronized (sAccountList) {
- for (Account account : sAccountList)
+ synchronized (mAccountList) {
+ for (Account account : mAccountList)
SyncManager.stopAccountSyncs(account.mId);
}
}
@@ -1723,7 +1641,7 @@
private void requestSync(Mailbox m, int reason, Request req) {
// Don't sync if there's no connectivity
- if (sConnectivityHold || (m == null)) return;
+ if (sConnectivityHold || (m == null) || sStop) return;
synchronized (sSyncLock) {
Account acct = Account.restoreAccountWithId(this, m.mAccountKey);
if (acct != null) {
@@ -1770,7 +1688,7 @@
boolean waiting = false;
ConnectivityManager cm =
(ConnectivityManager)this.getSystemService(Context.CONNECTIVITY_SERVICE);
- while (!mStop) {
+ while (!sStop) {
NetworkInfo info = cm.getActiveNetworkInfo();
if (info != null) {
// We're done if there's an active network
@@ -1807,9 +1725,111 @@
}
}
- public void run() {
- mStop = false;
+ /**
+ * Note that there are two ways the EAS SyncManager service can be created:
+ *
+ * 1) as a background service instantiated via startService (which happens on boot, when the
+ * first EAS account is created, etc), in which case the service thread is spun up, mailboxes
+ * sync, etc. and
+ * 2) to execute an RPC call from the UI, in which case the background service will already be
+ * running most of the time (unless we're creating a first EAS account)
+ *
+ * If the running background service detects that there are no EAS accounts (on boot, if none
+ * were created, or afterward if the last remaining EAS account is deleted), it will call
+ * stopSelf() to terminate operation.
+ *
+ * The goal is to ensure that the background service is running at all times when there is at
+ * least one EAS account in existence
+ *
+ * Because there are edge cases in which our process can crash (typically, this has been seen
+ * in UI crashes, ANR's, etc.), it's possible for the UI to start up again without the
+ * background service having been started. We explicitly try to start the service in Welcome
+ * (to handle the case of the app having been reloaded). We also start the service on any
+ * startSync call (if it isn't already running)
+ */
+ @Override
+ public void onCreate() {
+ synchronized (sSyncLock) {
+ alwaysLog("!!! EAS SyncManager, onCreate");
+ if (sStop) {
+ return;
+ }
+ if (sDeviceId == null) {
+ try {
+ getDeviceId(this);
+ } catch (IOException e) {
+ // We can't run in this situation
+ throw new RuntimeException();
+ }
+ }
+ // Run the reconciler and clean up any mismatched accounts - if we weren't running when
+ // accounts were deleted, it won't have been called.
+ runAccountReconciler();
+ }
+ }
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ synchronized (sSyncLock) {
+ alwaysLog("!!! EAS SyncManager, onStartCommand");
+ // Restore accounts, if it has not happened already
+ AccountBackupRestore.restoreAccountsIfNeeded(this);
+ maybeStartSyncManagerThread();
+ if (sServiceThread == null) {
+ alwaysLog("!!! EAS SyncManager, stopping self");
+ stopSelf();
+ } else if (sStop) {
+ // If we were in the middle of trying to stop, attempt a restart in 5 seconds
+ setAlarm(SYNC_MANAGER_SERVICE_ID, 5*SECONDS);
+ }
+ return Service.START_STICKY;
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ synchronized(sSyncLock) {
+ alwaysLog("!!! EAS SyncManager, onDestroy");
+ // Stop the sync manager thread and return
+ synchronized (sSyncLock) {
+ sStop = true;
+ if (sServiceThread != null) {
+ sServiceThread.interrupt();
+ }
+ }
+ }
+ }
+
+ void maybeStartSyncManagerThread() {
+ // Start our thread...
+ // See if there are any EAS accounts; otherwise, just go away
+ if (EmailContent.count(this, HostAuth.CONTENT_URI, WHERE_PROTOCOL_EAS, null) > 0) {
+ if (sServiceThread == null || !sServiceThread.isAlive()) {
+ log(sServiceThread == null ? "Starting thread..." : "Restarting thread...");
+ sServiceThread = new Thread(this, "SyncManager");
+ INSTANCE = this;
+ sServiceThread.start();
+ }
+ }
+ }
+
+ /**
+ * Start up the SyncManager service if it's not already running
+ * This is a stopgap for cases in which SyncManager died (due to a crash somewhere in
+ * com.android.email) and hasn't been restarted. See the comment for onCreate for details
+ */
+ static void checkSyncManagerServiceRunning() {
+ SyncManager syncManager = INSTANCE;
+ if (syncManager == null) return;
+ if (sServiceThread == null) {
+ alwaysLog("!!! checkSyncManagerServiceRunning; starting service...");
+ syncManager.startService(new Intent(syncManager, SyncManager.class));
+ }
+ }
+
+ public void run() {
+ sStop = false;
+ alwaysLog("!!! SyncManager thread running");
// If we're really debugging, turn on all logging
if (Eas.DEBUG) {
Eas.USER_LOG = true;
@@ -1822,41 +1842,56 @@
Debug.waitForDebugger();
}
- // Set up our observers; we need them to know when to start/stop various syncs based
- // on the insert/delete/update of mailboxes and accounts
- // We also observe synced messages to trigger upsyncs at the appropriate time
- mResolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver);
- mResolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true, mSyncedMessageObserver);
- mResolver.registerContentObserver(Message.CONTENT_URI, true, mMessageObserver);
- ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS,
- mSyncStatusObserver);
- mAccountsUpdatedListener = new EasAccountsUpdatedListener();
- // TODO Find and fix root cause of duplication
- try {
- AccountManager.get(getApplication())
- .addOnAccountsUpdatedListener(mAccountsUpdatedListener, mHandler, true);
- } catch (IllegalStateException e1) {
- // This exception is more of a warning; we shouldn't be in the state in which we
- // already have a listener.
+ // 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.CONTENT_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);
+ mMessageObserver = new MessageObserver(mHandler);
+ mResolver.registerContentObserver(Message.CONTENT_URI, true, mMessageObserver);
+ mSyncStatusObserver = new EasSyncStatusObserver();
+ mStatusChangeListener =
+ ContentResolver.addStatusChangeListener(
+ ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, mSyncStatusObserver);
+
+ // Set up our observer for AccountManager
+ mAccountsUpdatedListener = new EasAccountsUpdatedListener();
+ AccountManager.get(getApplication()).addOnAccountsUpdatedListener(
+ mAccountsUpdatedListener, mHandler, true);
+
+ // 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();
+
+ // See if any settings have changed while we weren't running...
+ checkPIMSyncSettings();
+ }
}
- // Set up receivers for ConnectivityManager
- 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 background data setting; we'll keep track of it with the receiver
- ConnectivityManager cm =
- (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
- mBackgroundData = cm.getBackgroundDataSetting();
-
- // See if any settings have changed while we weren't running...
- checkPIMSyncSettings();
-
try {
- while (!mStop) {
+ // Loop indefinitely until we're shut down
+ while (!sStop) {
runAwake(SYNC_MANAGER_ID);
waitForConnectivity();
mNextWaitReason = "Heartbeat";
@@ -1877,6 +1912,7 @@
}
} catch (InterruptedException e) {
// Needs to be caught, but causes no problem
+ log("SyncManager interrupted");
} finally {
synchronized (this) {
if (mKicked) {
@@ -1891,51 +1927,78 @@
Log.e(TAG, "RuntimeException in SyncManager", e);
throw e;
} finally {
- log("Finishing SyncManager");
- // Lots of cleanup here
- // Stop our running syncs
- stopServiceThreads();
-
- // Stop receivers and content observers
- if (mConnectivityReceiver != null) {
- unregisterReceiver(mConnectivityReceiver);
- }
- if (mBackgroundDataSettingReceiver != null) {
- unregisterReceiver(mBackgroundDataSettingReceiver);
- }
-
- if (INSTANCE != null) {
- ContentResolver resolver = getContentResolver();
- resolver.unregisterContentObserver(mAccountObserver);
- resolver.unregisterContentObserver(mMailboxObserver);
- resolver.unregisterContentObserver(mSyncedMessageObserver);
- resolver.unregisterContentObserver(mMessageObserver);
- unregisterCalendarObservers();
- }
- // Don't leak the Intent associated with this listener
- if (mAccountsUpdatedListener != null) {
- AccountManager.get(this).removeOnAccountsUpdatedListener(mAccountsUpdatedListener);
- mAccountsUpdatedListener = null;
- }
-
- // Clear pending alarms and associated Intents
- clearAlarms();
-
- // Release our wake lock, if we have one
- synchronized (mWakeLocks) {
- if (mWakeLock != null) {
- mWakeLock.release();
- mWakeLock = null;
- }
- }
-
- log("Goodbye");
+ shutdown();
}
+ }
- if (!mStop) {
- // If this wasn't intentional, try to restart the service
- throw new RuntimeException("EAS SyncManager crash; please restart me...");
- }
+ private void shutdown() {
+ synchronized (sSyncLock) {
+ // If INSTANCE is null, we've already been shut down
+ if (INSTANCE != null) {
+ log("SyncManager 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 (mMessageObserver != null) {
+ resolver.unregisterContentObserver(mMessageObserver);
+ mMessageObserver = null;
+ }
+ if (mAccountObserver != null) {
+ resolver.unregisterContentObserver(mAccountObserver);
+ mAccountObserver = null;
+ }
+ if (mMailboxObserver != null) {
+ resolver.unregisterContentObserver(mMailboxObserver);
+ mMailboxObserver = null;
+ }
+ unregisterCalendarObservers();
+
+ // Remove account listener (registered with AccountManager)
+ if (mAccountsUpdatedListener != null) {
+ AccountManager.get(this).removeOnAccountsUpdatedListener(
+ mAccountsUpdatedListener);
+ mAccountsUpdatedListener = null;
+ }
+
+ // Remove the sync status change listener (and null out the observer)
+ if (mStatusChangeListener != null) {
+ ContentResolver.removeStatusChangeListener(mStatusChangeListener);
+ mStatusChangeListener = null;
+ mSyncStatusObserver = null;
+ }
+
+ // Clear pending alarms and associated Intents
+ clearAlarms();
+
+ // Release our wake lock, if we have one
+ synchronized (mWakeLocks) {
+ if (mWakeLock != null) {
+ mWakeLock.release();
+ mWakeLock = null;
+ }
+ }
+
+ INSTANCE = null;
+ sServiceThread = null;
+ sStop = false;
+ log("Goodbye");
+ }
+ }
}
private void releaseMailbox(long mailboxId) {
@@ -2112,7 +2175,7 @@
long requestTime = service.mRequestTime;
if (requestTime > 0) {
long timeToRequest = requestTime - now;
- if (service instanceof AbstractSyncService && timeToRequest <= 0) {
+ if (timeToRequest <= 0) {
service.mRequestTime = 0;
service.alarm();
} else if (requestTime > 0 && timeToRequest < nextWait) {
@@ -2137,15 +2200,24 @@
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
+ */
+ static /*package*/ boolean isSyncable(Mailbox m) {
+ if (m == null || m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX ||
+ m.mType >= Mailbox.TYPE_NOT_SYNCABLE) {
+ return false;
+ }
+ return true;
+ }
+
static public void serviceRequest(long mailboxId, long ms, int reason) {
SyncManager syncManager = INSTANCE;
if (syncManager == null) return;
Mailbox m = Mailbox.restoreMailboxWithId(syncManager, mailboxId);
- // Never allow manual start of Drafts or Outbox via serviceRequest
- if (m == null || m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) {
- log("Ignoring serviceRequest for drafts/outbox/null mailbox");
- return;
- }
+ if (!isSyncable(m)) return;
try {
AbstractSyncService service = syncManager.mServiceMap.get(mailboxId);
if (service != null) {
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
index 25b6913..59a9823 100644
--- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -1251,7 +1251,7 @@
}
}
- private class CalendarOperations extends ArrayList<ContentProviderOperation> {
+ protected class CalendarOperations extends ArrayList<ContentProviderOperation> {
private static final long serialVersionUID = 1L;
public int mCount = 0;
private ContentProviderResult[] mResults = null;
@@ -1649,7 +1649,7 @@
}
// Send exception deleted flag if necessary
- Integer deleted = entityValues.getAsInteger(Events.DELETED);
+ Integer deleted = entityValues.getAsInteger(Calendar.EventsColumns.DELETED);
boolean isDeleted = deleted != null && deleted == 1;
Integer eventStatus = entityValues.getAsInteger(Events.STATUS);
boolean isCanceled = eventStatus != null && eventStatus.equals(Events.STATUS_CANCELED);
@@ -1789,7 +1789,7 @@
cr.update(ContentUris.withAppendedId(EVENTS_URI, eventId), cidValues,
null, null);
} else {
- if (entityValues.getAsInteger(Events.DELETED) == 1) {
+ if (entityValues.getAsInteger(Calendar.EventsColumns.DELETED) == 1) {
userLog("Deleting event with serverId: ", serverId);
s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
mDeletedIdList.add(eventId);
@@ -1855,7 +1855,7 @@
exEntity.addSubValue(ncv.uri, ncv.values);
}
- if ((getInt(exValues, Events.DELETED) == 1) ||
+ if ((getInt(exValues, Calendar.EventsColumns.DELETED) == 1) ||
(getInt(exValues, Events.STATUS) ==
Events.STATUS_CANCELED)) {
flag = Message.FLAG_OUTGOING_MEETING_CANCEL;
diff --git a/src/com/android/exchange/adapter/ContactsSyncAdapter.java b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
index 8b4adba..c05a820 100644
--- a/src/com/android/exchange/adapter/ContactsSyncAdapter.java
+++ b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
@@ -1071,8 +1071,8 @@
// 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);
+ for (NamedContentValues values : result) {
+ list.remove(values);
}
// Return the row found (or null)
diff --git a/src/com/android/exchange/adapter/ProvisionParser.java b/src/com/android/exchange/adapter/ProvisionParser.java
index af1ecdd..bbb3464 100644
--- a/src/com/android/exchange/adapter/ProvisionParser.java
+++ b/src/com/android/exchange/adapter/ProvisionParser.java
@@ -37,11 +37,11 @@
PolicySet mPolicySet = null;
String mPolicyKey = null;
boolean mRemoteWipe = false;
+ boolean mIsSupportable = true;
public ProvisionParser(InputStream in, EasSyncService service) throws IOException {
super(in);
mService = service;
- setDebug(true);
}
public PolicySet getPolicySet() {
@@ -56,14 +56,22 @@
return mRemoteWipe;
}
- public void parseProvisionDocWbxml() throws IOException {
+ public boolean hasSupportablePolicySet() {
+ return (mPolicySet != null) && mIsSupportable;
+ }
+
+ private void parseProvisionDocWbxml() throws IOException {
int minPasswordLength = 0;
int passwordMode = PolicySet.PASSWORD_MODE_NONE;
int maxPasswordFails = 0;
int maxScreenLockTime = 0;
+ int passwordExpiration = 0;
+ int passwordHistory = 0;
+ int passwordComplexChars = 0;
boolean canSupport = true;
while (nextTag(Tags.PROVISION_EAS_PROVISION_DOC) != END) {
+ boolean supported = true;
switch (tag) {
case Tags.PROVISION_DEVICE_PASSWORD_ENABLED:
if (getValueInt() == 1) {
@@ -87,36 +95,138 @@
case Tags.PROVISION_MAX_DEVICE_PASSWORD_FAILED_ATTEMPTS:
maxPasswordFails = getValueInt();
break;
+ case Tags.PROVISION_DEVICE_PASSWORD_EXPIRATION:
+ passwordExpiration = getValueInt();
+ // We don't yet support this
+ if (passwordExpiration > 0) {
+ supported = false;
+ }
+ break;
+ case Tags.PROVISION_DEVICE_PASSWORD_HISTORY:
+ passwordHistory = getValueInt();
+ break;
case Tags.PROVISION_ALLOW_SIMPLE_DEVICE_PASSWORD:
// Ignore this unless there's any MSFT documentation for what this means
// Hint: I haven't seen any that's more specific than "simple"
getValue();
break;
- // The following policy, if false, can't be supported at the moment
+ // The following policies, if false, can't be supported at the moment
case Tags.PROVISION_ATTACHMENTS_ENABLED:
+ case Tags.PROVISION_ALLOW_STORAGE_CARD:
+ case Tags.PROVISION_ALLOW_CAMERA:
+ 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) {
- canSupport = false;
+ supported = false;
+ }
+ break;
+ // Bluetooth: 0 = no bluetooth; 1 = only hands-free; 2 = allowed
+ case Tags.PROVISION_ALLOW_BLUETOOTH:
+ if (getValueInt() != 2) {
+ supported = false;
}
break;
// The following policies, if true, can't be supported at the moment
case Tags.PROVISION_DEVICE_ENCRYPTION_ENABLED:
case Tags.PROVISION_PASSWORD_RECOVERY_ENABLED:
- case Tags.PROVISION_DEVICE_PASSWORD_EXPIRATION:
- case Tags.PROVISION_DEVICE_PASSWORD_HISTORY:
- case Tags.PROVISION_MAX_ATTACHMENT_SIZE:
+ case Tags.PROVISION_REQUIRE_DEVICE_ENCRYPTION:
+ 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:
+ case Tags.PROVISION_REQUIRE_MANUAL_SYNC_WHEN_ROAMING:
if (getValueInt() == 1) {
- canSupport = false;
+ supported = false;
+ }
+ break;
+ // The following, if greater than zero, can't be supported at the moment
+ case Tags.PROVISION_MAX_ATTACHMENT_SIZE:
+ if (getValueInt() > 0) {
+ supported = false;
+ }
+ break;
+ // Complex character setting is only used if we're in "strong" (alphanumeric) mode
+ case Tags.PROVISION_MIN_DEVICE_PASSWORD_COMPLEX_CHARS:
+ passwordComplexChars = getValueInt();
+ if ((passwordMode == PolicySet.PASSWORD_MODE_STRONG) &&
+ (passwordComplexChars > 0)) {
+ supported = false;
+ }
+ 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)) {
+ supported = false;
+ }
+ break;
+ // NOTE: We can support these entirely within the email application if we choose
+ case Tags.PROVISION_MAX_CALENDAR_AGE_FILTER:
+ case Tags.PROVISION_MAX_EMAIL_AGE_FILTER:
+ // 0 indicates no specified filter
+ if (getValueInt() != 0) {
+ supported = false;
+ }
+ break;
+ // NOTE: We can support these entirely within the email application if we choose
+ 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")) {
+ supported = false;
}
break;
default:
skipTag();
}
+
+ if (!supported) {
+ log("Policy not supported: " + tag);
+ mIsSupportable = false;
+ }
}
- if (canSupport) {
- mPolicySet = new SecurityPolicy.PolicySet(minPasswordLength, passwordMode,
- maxPasswordFails, maxScreenLockTime, true);
+ mPolicySet = new SecurityPolicy.PolicySet(minPasswordLength, passwordMode,
+ maxPasswordFails, maxScreenLockTime, true, passwordExpiration, passwordHistory,
+ passwordComplexChars);
+ }
+
+ /**
+ * 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;
}
class ShadowPolicySet {
@@ -124,9 +234,12 @@
int mPasswordMode = PolicySet.PASSWORD_MODE_NONE;
int mMaxPasswordFails = 0;
int mMaxScreenLockTime = 0;
+ int mPasswordExpiration = 0;
+ int mPasswordHistory = 0;
+ int mPasswordComplexChars = 0;
}
- public void parseProvisionDocXml(String doc) throws IOException {
+ /*package*/ void parseProvisionDocXml(String doc) throws IOException {
ShadowPolicySet sps = new ShadowPolicySet();
try {
@@ -148,13 +261,14 @@
}
mPolicySet = new PolicySet(sps.mMinPasswordLength, sps.mPasswordMode, sps.mMaxPasswordFails,
- sps.mMaxScreenLockTime, true);
+ sps.mMaxScreenLockTime, true, sps.mPasswordExpiration, sps.mPasswordHistory,
+ sps.mPasswordComplexChars);
}
/**
* Return true if password is required; otherwise false.
*/
- boolean parseSecurityPolicy(XmlPullParser parser, ShadowPolicySet sps)
+ private boolean parseSecurityPolicy(XmlPullParser parser, ShadowPolicySet sps)
throws XmlPullParserException, IOException {
boolean passwordRequired = true;
while (true) {
@@ -177,7 +291,7 @@
return passwordRequired;
}
- void parseCharacteristic(XmlPullParser parser, ShadowPolicySet sps)
+ private void parseCharacteristic(XmlPullParser parser, ShadowPolicySet sps)
throws XmlPullParserException, IOException {
boolean enforceInactivityTimer = true;
while (true) {
@@ -219,7 +333,7 @@
}
}
- void parseRegistry(XmlPullParser parser, ShadowPolicySet sps)
+ private void parseRegistry(XmlPullParser parser, ShadowPolicySet sps)
throws XmlPullParserException, IOException {
while (true) {
int type = parser.nextTag();
@@ -234,7 +348,7 @@
}
}
- void parseWapProvisioningDoc(XmlPullParser parser, ShadowPolicySet sps)
+ private void parseWapProvisioningDoc(XmlPullParser parser, ShadowPolicySet sps)
throws XmlPullParserException, IOException {
while (true) {
int type = parser.nextTag();
@@ -258,7 +372,7 @@
}
}
- public void parseProvisionData() throws IOException {
+ private void parseProvisionData() throws IOException {
while (nextTag(Tags.PROVISION_DATA) != END) {
if (tag == Tags.PROVISION_EAS_PROVISION_DOC) {
parseProvisionDocWbxml();
@@ -268,7 +382,7 @@
}
}
- public void parsePolicy() throws IOException {
+ private void parsePolicy() throws IOException {
String policyType = null;
while (nextTag(Tags.PROVISION_POLICY) != END) {
switch (tag) {
@@ -297,7 +411,7 @@
}
}
- public void parsePolicies() throws IOException {
+ private void parsePolicies() throws IOException {
while (nextTag(Tags.PROVISION_POLICIES) != END) {
if (tag == Tags.PROVISION_POLICY) {
parsePolicy();
@@ -318,10 +432,10 @@
case Tags.PROVISION_STATUS:
int status = getValueInt();
mService.userLog("Provision status: ", status);
+ res = (status == 1);
break;
case Tags.PROVISION_POLICIES:
parsePolicies();
- res = (mPolicySet != null) || (mPolicyKey != null);
break;
case Tags.PROVISION_REMOTE_WIPE:
// Indicate remote wipe command received
diff --git a/src/com/android/exchange/adapter/Tags.java b/src/com/android/exchange/adapter/Tags.java
index ef70981..2745e33 100644
--- a/src/com/android/exchange/adapter/Tags.java
+++ b/src/com/android/exchange/adapter/Tags.java
@@ -596,7 +596,7 @@
"MinDevicePasswordComplexCharacters", "AllowWiFi", "AllowTextMessaging",
"AllowPOPIMAPEmail", "AllowBluetooth", "AllowIrDA", "RequireManualSyncWhenRoaming",
"AllowDesktopSync",
- "MaxCalendarAgeFilder", "AllowHTMLEmail", "MaxEmailAgeFilder",
+ "MaxCalendarAgeFilder", "AllowHTMLEmail", "MaxEmailAgeFilter",
"MaxEmailBodyTruncationSize", "MaxEmailHTMLBodyTruncationSize",
"RequireSignedSMIMEMessages", "RequireEncryptedSMIMEMessages",
"RequireSignedSMIMEAlgorithm", "RequireEncryptionSMIMEAlgorithm",
diff --git a/src/com/android/exchange/provider/ExchangeDirectoryProvider.java b/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
new file mode 100644
index 0000000..5d6121f
--- /dev/null
+++ b/src/com/android/exchange/provider/ExchangeDirectoryProvider.java
@@ -0,0 +1,159 @@
+/*
+ * 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.email.provider.EmailContent.Account;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.SyncManager;
+import com.android.exchange.provider.GalResult.GalData;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+
+/**
+ * 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 GAL_BASE = 0;
+ private static final int GAL_FILTER = GAL_BASE;
+
+ private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ static {
+ sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/filter/*", GAL_FILTER);
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
+ if (accountName == null) {
+ return null;
+ }
+
+ Account account = SyncManager.getAccountByName(accountName);
+ if (account == null) {
+ return null;
+ }
+
+ int match = sURIMatcher.match(uri);
+ switch (match) {
+ case GAL_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;
+ }
+ long callingId = Binder.clearCallingIdentity();
+ try {
+ // Get results from the Exchange account
+ GalResult galResult = EasSyncService.searchGal(getContext(), account.mId,
+ filter);
+ if (galResult != null) {
+ return buildGalResultCursor(projection, galResult);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
+ }
+ break;
+ }
+
+ return null;
+ }
+
+ /*package*/ Cursor buildGalResultCursor(String[] projection, GalResult galResult) {
+ int displayNameIndex = -1;
+ int emailIndex = -1;
+ boolean alternateDisplayName = false;
+
+ 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)) {
+ displayNameIndex = i;
+ alternateDisplayName = true;
+
+ } else if (CommonDataKinds.Email.ADDRESS.equals(column)) {
+ emailIndex = i;
+ }
+ // TODO other fields
+ }
+
+ 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);
+ if (displayNameIndex != -1) {
+ row[displayNameIndex] = galDataRow.displayName;
+ // TODO Handle alternate display name here
+ }
+ if (emailIndex != -1) {
+ row[emailIndex] = galDataRow.emailAddress;
+ }
+ 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/tests/src/com/android/exchange/EasOutboxServiceTests.java b/tests/src/com/android/exchange/EasOutboxServiceTests.java
new file mode 100644
index 0000000..df6bd6c
--- /dev/null
+++ b/tests/src/com/android/exchange/EasOutboxServiceTests.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import com.android.email.provider.EmailContent.Mailbox;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+
+/**
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.EasOutboxServiceTests email
+ */
+
+public class EasOutboxServiceTests extends AndroidTestCase {
+
+ Context mMockContext;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mMockContext = getContext();
+ }
+
+ public void testGenerateSmartSendCmd() {
+ EasOutboxService svc = new EasOutboxService(mMockContext, new Mailbox());
+ // Test encoding of collection id
+ String cmd = svc.generateSmartSendCmd(true, "1339085683659694034", "Mail:^f");
+ assertEquals("SmartReply&ItemId=1339085683659694034&CollectionId=Mail%3A%5Ef", cmd);
+ // Test encoding of item id
+ cmd = svc.generateSmartSendCmd(false, "14:3", "6");
+ assertEquals("SmartForward&ItemId=14%3A3&CollectionId=6", cmd);
+ }
+}
diff --git a/tests/src/com/android/exchange/EasSyncServiceTests.java b/tests/src/com/android/exchange/EasSyncServiceTests.java
index d4372a5..320a1d1 100644
--- a/tests/src/com/android/exchange/EasSyncServiceTests.java
+++ b/tests/src/com/android/exchange/EasSyncServiceTests.java
@@ -17,13 +17,31 @@
package com.android.exchange;
+import com.android.email.provider.EmailContent.Account;
+
+import org.apache.http.Header;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpRequestBase;
+
import android.content.Context;
import android.test.AndroidTestCase;
+import android.util.Base64;
import java.io.File;
import java.io.IOException;
+/**
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.EasSyncServiceTests email
+ */
+
public class EasSyncServiceTests extends AndroidTestCase {
+ static private final String USER = "user";
+ static private final String PASSWORD = "password";
+ static private final String HOST = "xxx.host.zzz";
+ static private final String ID = "id";
+ static private final String TYPE = "type";
+
Context mMockContext;
@Override
@@ -73,4 +91,85 @@
}
}
}
+
+ public void testAddHeaders() {
+ HttpRequestBase method = new HttpPost();
+ EasSyncService svc = new EasSyncService();
+ svc.mAuthString = "auth";
+ svc.mProtocolVersion = "12.1";
+ svc.mDeviceType = "android";
+ svc.mAccount = null;
+ // With second argument false, there should be no header
+ svc.setHeaders(method, false);
+ Header[] headers = method.getHeaders("X-MS-PolicyKey");
+ assertEquals(0, headers.length);
+ // With second argument true, there should always be a header
+ // The value will be "0" without an account
+ method.removeHeaders("X-MS-PolicyKey");
+ svc.setHeaders(method, true);
+ headers = method.getHeaders("X-MS-PolicyKey");
+ assertEquals(1, headers.length);
+ assertEquals("0", headers[0].getValue());
+ // With an account, but null security key, the header's value should be "0"
+ Account account = new Account();
+ account.mSecuritySyncKey = null;
+ svc.mAccount = account;
+ method.removeHeaders("X-MS-PolicyKey");
+ svc.setHeaders(method, true);
+ headers = method.getHeaders("X-MS-PolicyKey");
+ assertEquals(1, headers.length);
+ assertEquals("0", headers[0].getValue());
+ // With an account and security key, the header's value should be the security key
+ account.mSecuritySyncKey = "key";
+ svc.mAccount = account;
+ method.removeHeaders("X-MS-PolicyKey");
+ svc.setHeaders(method, true);
+ headers = method.getHeaders("X-MS-PolicyKey");
+ assertEquals(1, headers.length);
+ assertEquals("key", headers[0].getValue());
+ }
+
+ public void testGetProtocolVersionDouble() {
+ assertEquals(Eas.SUPPORTED_PROTOCOL_EX2003_DOUBLE,
+ Eas.getProtocolVersionDouble(Eas.SUPPORTED_PROTOCOL_EX2003));
+ assertEquals(Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE,
+ Eas.getProtocolVersionDouble(Eas.SUPPORTED_PROTOCOL_EX2007));
+ assertEquals(Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE,
+ Eas.getProtocolVersionDouble(Eas.SUPPORTED_PROTOCOL_EX2007_SP1));
+ }
+
+ private EasSyncService setupService(String user) {
+ EasSyncService svc = new EasSyncService();
+ svc.mUserName = user;
+ svc.mPassword = PASSWORD;
+ svc.mDeviceId = ID;
+ svc.mDeviceType = TYPE;
+ svc.mHostAddress = HOST;
+ return svc;
+ }
+
+ public void testMakeUriString() throws IOException {
+ // Simple user name and command
+ EasSyncService svc = setupService(USER);
+ String uriString = svc.makeUriString("OPTIONS", null);
+ // These next two should now be cached
+ assertNotNull(svc.mAuthString);
+ assertNotNull(svc.mCmdString);
+ assertEquals("Basic " + Base64.encodeToString((USER+":"+PASSWORD).getBytes(),
+ Base64.NO_WRAP), svc.mAuthString);
+ assertEquals("&User=" + USER + "&DeviceId=" + ID + "&DeviceType=" + TYPE, svc.mCmdString);
+ assertEquals("https://" + HOST + "/Microsoft-Server-ActiveSync?Cmd=OPTIONS" +
+ svc.mCmdString, uriString);
+ // User name that requires encoding
+ String user = "name_with_underscore@foo%bar.com";
+ svc = setupService(user);
+ uriString = svc.makeUriString("OPTIONS", null);
+ assertEquals("Basic " + Base64.encodeToString((user+":"+PASSWORD).getBytes(),
+ Base64.NO_WRAP), svc.mAuthString);
+ String safeUserName = "name_with_underscore%40foo%25bar.com";
+ assertEquals("&User=" + safeUserName + "&DeviceId=" + ID + "&DeviceType=" + TYPE,
+ svc.mCmdString);
+ assertEquals("https://" + HOST + "/Microsoft-Server-ActiveSync?Cmd=OPTIONS" +
+ svc.mCmdString, uriString);
+ }
}
diff --git a/tests/src/com/android/exchange/SyncManagerAccountTests.java b/tests/src/com/android/exchange/SyncManagerAccountTests.java
index 3b67dc1..6a325b5 100644
--- a/tests/src/com/android/exchange/SyncManagerAccountTests.java
+++ b/tests/src/com/android/exchange/SyncManagerAccountTests.java
@@ -211,4 +211,24 @@
assertEquals(0, errorMap.keySet().size());
}
+ public void testIsSyncable() {
+ Context context = mMockContext;
+ Account acct1 = ProviderTestUtils.setupAccount("acct1", true, context);
+ Mailbox box1 = ProviderTestUtils.setupMailbox("box1", acct1.mId, true, context,
+ Mailbox.TYPE_DRAFTS);
+ Mailbox box2 = ProviderTestUtils.setupMailbox("box2", acct1.mId, true, context,
+ Mailbox.TYPE_OUTBOX);
+ Mailbox box3 = ProviderTestUtils.setupMailbox("box2", acct1.mId, true, context,
+ Mailbox.TYPE_ATTACHMENT);
+ Mailbox box4 = ProviderTestUtils.setupMailbox("box2", acct1.mId, true, context,
+ Mailbox.TYPE_NOT_SYNCABLE + 64);
+ Mailbox box5 = ProviderTestUtils.setupMailbox("box2", acct1.mId, true, context,
+ Mailbox.TYPE_MAIL);
+ assertFalse(SyncManager.isSyncable(null));
+ assertFalse(SyncManager.isSyncable(box1));
+ assertFalse(SyncManager.isSyncable(box2));
+ assertFalse(SyncManager.isSyncable(box3));
+ assertFalse(SyncManager.isSyncable(box4));
+ assertTrue(SyncManager.isSyncable(box5));
+ }
}
diff --git a/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
index eb87ccd..bef9938 100644
--- a/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
+++ b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
@@ -16,13 +16,17 @@
package com.android.exchange.adapter;
+import com.android.exchange.adapter.CalendarSyncAdapter.CalendarOperations;
import com.android.exchange.adapter.CalendarSyncAdapter.EasCalendarSyncParser;
+import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.provider.Calendar.Events;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.GregorianCalendar;
+import java.util.List;
import java.util.TimeZone;
/**
@@ -173,4 +177,129 @@
cv.put(Events.DURATION, "P1D");
assertTrue(p.isValidEventValues(cv));
}
+
+ private void addAttendeesToSerializer(Serializer s, int num) throws IOException {
+ for (int i = 0; i < num; i++) {
+ s.start(Tags.CALENDAR_ATTENDEE);
+ s.data(Tags.CALENDAR_ATTENDEE_EMAIL, "frederick" + num +
+ ".flintstone@this.that.verylongservername.com");
+ s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1");
+ s.data(Tags.CALENDAR_ATTENDEE_NAME, "Frederick" + num + " Flintstone, III");
+ s.end();
+ }
+ }
+
+ private int countInsertOperationsForTable(CalendarOperations ops, String tableName) {
+ int cnt = 0;
+ for (ContentProviderOperation op: ops) {
+ List<String> segments = op.getUri().getPathSegments();
+ if (segments.get(0).equalsIgnoreCase(tableName) &&
+ op.getType() == ContentProviderOperation.TYPE_INSERT) {
+ cnt++;
+ }
+ }
+ return cnt;
+ }
+
+ /**
+ * Note that there is no way to access the ContentValues inside of a ContentProviderOperation,
+ * which limits the extent to which we can test the result of parsing events. We can count
+ * the number of objects to be created, but we can't examine them.
+ */
+ // TODO Try to convince fredq to allow access to the ContentValues of a CPO
+ public void testAddEvent() throws IOException {
+ CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
+ EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
+
+ // Set up an input stream with new event data
+ Serializer s = new Serializer(false);
+ s.start(Tags.SYNC_APPLICATION_DATA);
+ s.data(Tags.CALENDAR_TIME_ZONE, "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByA" +
+ "GQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAY" +
+ "QBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==");
+ s.data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
+ s.data(Tags.CALENDAR_START_TIME, "20100518T220000Z");
+ s.data(Tags.CALENDAR_SUBJECT, "Documentation");
+ s.data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
+ s.data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
+ s.data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
+ s.start(Tags.CALENDAR_ATTENDEES);
+ addAttendeesToSerializer(s, 10);
+ s.end(); // CALENDAR_ATTENDEES
+ s.data(Tags.CALENDAR_LOCATION, "CR SF 601T2/North Shore Presentation Self Service (16)");
+ s.data(Tags.CALENDAR_END_TIME, "20100518T223000Z");
+ s.start(Tags.BASE_BODY);
+ s.data(Tags.BASE_BODY_PREFERENCE, "1");
+ s.data(Tags.BASE_ESTIMATED_DATA_SIZE, "69105"); // The number is ignored by the parser
+ s.data(Tags.BASE_DATA, "This is the event description; we should probably make it longer");
+ s.end(); // BASE_BODY
+ s.data(Tags.CALENDAR_SENSITIVITY, "0");
+ s.data(Tags.CALENDAR_BUSY_STATUS, "2");
+ s.data(Tags.CALENDAR_ALL_DAY_EVENT, "0");
+ s.data(Tags.CALENDAR_MEETING_STATUS, "3");
+ s.data(Tags.BASE_NATIVE_BODY_TYPE, "3");
+ s.end().done(); // SYNC_APPLICATION_DATA
+
+ // Set up our parser's input and eat the initial tag
+ byte[] bytes = s.toByteArray();
+ p.resetInput(new ByteArrayInputStream(bytes));
+ p.nextTag(0);
+
+ p.addEvent(p.mOps, "1:1", false);
+ // There should be 1 event
+ assertEquals(1, countInsertOperationsForTable(p.mOps, "events"));
+ // Two attendees (organizer and 10 attendees)
+ assertEquals(11, countInsertOperationsForTable(p.mOps, "attendees"));
+ // dtstamp, meeting status, attendees, attendees redacted, and upsync prohibited
+ assertEquals(5, countInsertOperationsForTable(p.mOps, "extendedproperties"));
+ }
+
+ public void testAddEventRedactedAttendees() throws IOException {
+ CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
+ EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
+
+ // Set up an input stream with new event data
+ Serializer s = new Serializer(false);
+ s.start(Tags.SYNC_APPLICATION_DATA);
+ s.data(Tags.CALENDAR_TIME_ZONE, "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByA" +
+ "GQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAY" +
+ "QBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==");
+ s.data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
+ s.data(Tags.CALENDAR_START_TIME, "20100518T220000Z");
+ s.data(Tags.CALENDAR_SUBJECT, "Documentation");
+ s.data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
+ s.data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
+ s.data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
+ s.start(Tags.CALENDAR_ATTENDEES);
+ addAttendeesToSerializer(s, 100);
+ s.end(); // CALENDAR_ATTENDEES
+ s.data(Tags.CALENDAR_LOCATION, "CR SF 601T2/North Shore Presentation Self Service (16)");
+ s.data(Tags.CALENDAR_END_TIME, "20100518T223000Z");
+ s.start(Tags.BASE_BODY);
+ s.data(Tags.BASE_BODY_PREFERENCE, "1");
+ s.data(Tags.BASE_ESTIMATED_DATA_SIZE, "69105"); // The number is ignored by the parser
+ s.data(Tags.BASE_DATA, "This is the event description; we should probably make it longer");
+ s.end(); // BASE_BODY
+ s.data(Tags.CALENDAR_SENSITIVITY, "0");
+ s.data(Tags.CALENDAR_BUSY_STATUS, "2");
+ s.data(Tags.CALENDAR_ALL_DAY_EVENT, "0");
+ s.data(Tags.CALENDAR_MEETING_STATUS, "3");
+ s.data(Tags.BASE_NATIVE_BODY_TYPE, "3");
+ s.end().done(); // SYNC_APPLICATION_DATA
+
+ // Set up our parser's input and eat the initial tag
+ byte[] bytes = s.toByteArray();
+ p.resetInput(new ByteArrayInputStream(bytes));
+ p.nextTag(0);
+
+ p.addEvent(p.mOps, "1:1", false);
+ // There should be 1 event
+ assertEquals(1, countInsertOperationsForTable(p.mOps, "events"));
+ // One attendees (organizer; all others are redacted)
+ assertEquals(1, countInsertOperationsForTable(p.mOps, "attendees"));
+ // dtstamp, meeting status, and attendees redacted
+ assertEquals(3, countInsertOperationsForTable(p.mOps, "extendedproperties"));
+ }
}
diff --git a/tests/src/com/android/exchange/adapter/ProvisionParserTests.java b/tests/src/com/android/exchange/adapter/ProvisionParserTests.java
index 09a9d96..842e4b6 100644
--- a/tests/src/com/android/exchange/adapter/ProvisionParserTests.java
+++ b/tests/src/com/android/exchange/adapter/ProvisionParserTests.java
@@ -113,11 +113,11 @@
PolicySet ps = parser.getPolicySet();
assertNotNull(ps);
// Check the settings to make sure they were parsed correctly
- assertEquals(5*60, ps.mMaxScreenLockTime); // Screen lock time is in seconds
- assertEquals(8, ps.mMinPasswordLength);
- assertEquals(PolicySet.PASSWORD_MODE_STRONG, ps.mPasswordMode);
- assertEquals(20, ps.mMaxPasswordFails);
- assertTrue(ps.mRequireRemoteWipe);
+ assertEquals(5*60, ps.getMaxScreenLockTime()); // Screen lock time is in seconds
+ assertEquals(8, ps.getMinPasswordLength());
+ assertEquals(PolicySet.PASSWORD_MODE_STRONG, ps.getPasswordMode());
+ assertEquals(20, ps.getMaxPasswordFails());
+ assertTrue(ps.isRequireRemoteWipe());
}
public void testWapProvisionParser2() throws IOException {
@@ -126,7 +126,7 @@
PolicySet ps = parser.getPolicySet();
assertNotNull(ps);
// Password should be set to none; others are ignored in this case.
- assertEquals(PolicySet.PASSWORD_MODE_NONE, ps.mPasswordMode);
+ assertEquals(PolicySet.PASSWORD_MODE_NONE, ps.getPasswordMode());
}
public void testWapProvisionParser3() throws IOException {
@@ -135,10 +135,10 @@
PolicySet ps = parser.getPolicySet();
assertNotNull(ps);
// Password should be set to simple
- assertEquals(2*60, ps.mMaxScreenLockTime); // Screen lock time is in seconds
- assertEquals(4, ps.mMinPasswordLength);
- assertEquals(PolicySet.PASSWORD_MODE_SIMPLE, ps.mPasswordMode);
- assertEquals(5, ps.mMaxPasswordFails);
- assertTrue(ps.mRequireRemoteWipe);
+ assertEquals(2*60, ps.getMaxScreenLockTime()); // Screen lock time is in seconds
+ assertEquals(4, ps.getMinPasswordLength());
+ assertEquals(PolicySet.PASSWORD_MODE_SIMPLE, ps.getPasswordMode());
+ assertEquals(5, ps.getMaxPasswordFails());
+ assertTrue(ps.isRequireRemoteWipe());
}
}
diff --git a/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java b/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java
new file mode 100644
index 0000000..a623aed
--- /dev/null
+++ b/tests/src/com/android/exchange/provider/ExchangeDirectoryProviderTests.java
@@ -0,0 +1,61 @@
+/*
+ * 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.exchange.provider.GalResult.GalData;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.test.AndroidTestCase;
+
+/**
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.provider.ExchangeDirectoryProviderTests email
+ */
+public class ExchangeDirectoryProviderTests extends AndroidTestCase {
+
+ // Create a test projection; we should only get back values for display name and email address
+ private static final String[] GAL_RESULT_PROJECTION =
+ new String[] {Contacts.DISPLAY_NAME, CommonDataKinds.Email.ADDRESS, Contacts.CONTENT_TYPE};
+ private static final int GAL_RESULT_DISPLAY_NAME_COLUMN = 0;
+ private static final int GAL_RESULT_EMAIL_ADDRESS_COLUMN = 1;
+ private static final int GAL_RESULT_CONTENT_TYPE_COLUMN = 2;
+
+ public void testBuildGalResultCursor() {
+ GalResult result = new GalResult();
+ result.addGalData(1, "Alice Aardvark", "alice@aardvark.com");
+ result.addGalData(2, "Bob Badger", "bob@badger.com");
+ result.addGalData(3, "Clark Cougar", "clark@cougar.com");
+ result.addGalData(4, "Dan Dolphin", "dan@dolphin.com");
+
+ // Make sure our returned cursor has the expected contents
+ ExchangeDirectoryProvider provider = new ExchangeDirectoryProvider();
+ Cursor c = provider.buildGalResultCursor(GAL_RESULT_PROJECTION, result);
+ assertNotNull(c);
+ assertEquals(MatrixCursor.class, c.getClass());
+ assertEquals(4, c.getCount());
+ for (int i = 0; i < 4; i++) {
+ GalData data = result.galData.get(i);
+ assertTrue(c.moveToNext());
+ assertEquals(data.displayName, c.getString(GAL_RESULT_DISPLAY_NAME_COLUMN));
+ assertEquals(data.emailAddress, c.getString(GAL_RESULT_EMAIL_ADDRESS_COLUMN));
+ assertNull(c.getString(GAL_RESULT_CONTENT_TYPE_COLUMN));
+ }
+ }
+}