diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 76ff91525..8eaa175c3 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -115,6 +115,11 @@
android:label="@string/account_settings_action"
>
+
+
This server requires security features your phone does not support.
+
+
+ Account \"%s\" requires security settings update.
+
+
+ Update Security Settings
+
+ Device Security
+
+
+ The server %s requires that you allow it to remotely control
+ some security features of your phone.
+
Edit details
@@ -585,9 +598,9 @@
The AccountManager could not create the Account; please try again.
-
- Email Device Administrator
-
- Email Device Administrator - Long Description
-
+
+ Email
+
+ Enables server-specified security policies
+
diff --git a/src/com/android/email/SecurityPolicy.java b/src/com/android/email/SecurityPolicy.java
index 1a54def2c..b8abb4902 100644
--- a/src/com/android/email/SecurityPolicy.java
+++ b/src/com/android/email/SecurityPolicy.java
@@ -16,29 +16,48 @@
package com.android.email;
+import com.android.email.activity.setup.AccountSecurity;
+import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.AccountColumns;
+import com.android.email.service.MailService;
import android.app.DeviceAdmin;
import android.app.DevicePolicyManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
import android.content.ComponentName;
+import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
+import android.net.Uri;
/**
* Utility functions to support reading and writing security policies
+ *
+ * STOPSHIP - these TODO items are all part of finishing the feature
+ * TODO: When user sets password, and conditions are now satisfied, restart syncs
+ * TODO: When accounts are deleted, reduce policy and/or give up admin status
+ * TODO: Provide a way to check for policy issues at synchronous times such as entering
+ * message list or folder list.
+ * TODO: Implement local wipe after failed passwords
+ *
*/
public class SecurityPolicy {
/** STOPSHIP - ok to check in true for now, but must be false for shipping */
/** DO NOT CHECK IN WHILE 'true' */
- private static final boolean DEBUG_ALWAYS_ACTIVE = true;
+ private static final boolean DEBUG_ALWAYS_ACTIVE = false;
private static SecurityPolicy sInstance = null;
private Context mContext;
private DevicePolicyManager mDPM;
private ComponentName mAdminName;
private PolicySet mAggregatePolicy;
+ private boolean mNotificationActive;
+ private boolean mAdminEnabled;
private static final PolicySet NO_POLICY_SET =
new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false);
@@ -81,6 +100,7 @@ public class SecurityPolicy {
mDPM = null;
mAdminName = new ComponentName(context, PolicyAdmin.class);
mAggregatePolicy = null;
+ mNotificationActive = false;
}
/**
@@ -119,12 +139,8 @@ public class SecurityPolicy {
int flags = c.getInt(ACCOUNT_SECURITY_COLUMN_FLAGS);
if (flags != 0) {
PolicySet p = new PolicySet(flags);
- if (p.mMinPasswordLength > 0) {
- minPasswordLength = Math.max(p.mMinPasswordLength, minPasswordLength);
- }
- if (p.mPasswordMode > 0) {
- passwordMode = Math.max(p.mPasswordMode, passwordMode);
- }
+ minPasswordLength = Math.max(p.mMinPasswordLength, minPasswordLength);
+ passwordMode = Math.max(p.mPasswordMode, passwordMode);
if (p.mMaxPasswordFails > 0) {
maxPasswordFails = Math.min(p.mMaxPasswordFails, maxPasswordFails);
}
@@ -139,6 +155,12 @@ public class SecurityPolicy {
c.close();
}
if (policiesFound) {
+ // final cleanup pass converts any untouched min/max values to zero (not specified)
+ if (minPasswordLength == Integer.MIN_VALUE) minPasswordLength = 0;
+ if (passwordMode == Integer.MIN_VALUE) passwordMode = 0;
+ if (maxPasswordFails == Integer.MAX_VALUE) maxPasswordFails = 0;
+ if (maxScreenLockTime == Integer.MAX_VALUE) maxScreenLockTime = 0;
+
return new PolicySet(minPasswordLength, passwordMode, maxPasswordFails,
maxScreenLockTime, requireRemoteWipe);
} else {
@@ -185,51 +207,58 @@ public class SecurityPolicy {
/**
* API: Report that policies may have been updated due to rewriting values in an Account.
+ * @param accountId the account that has been updated
*/
- public synchronized void updatePolicies() {
+ public synchronized void updatePolicies(long accountId) {
mAggregatePolicy = null;
}
/**
* API: Query used to determine if a given policy is "active" (the device is operating at
- * the required security level). This is used when creating new accounts. This method
- * is for queries only, and does not trigger any change in device state.
+ * the required security level).
*
- * @param policies the policies requested
+ * This can be used when syncing a specific account, by passing a specific set of policies
+ * for that account. Or, it can be used at any time to compare the device
+ * state against the aggregate set of device policies stored in all accounts.
+ *
+ * This method is for queries only, and does not trigger any change in device state.
+ *
+ * @param policies the policies requested, or null to check aggregate stored policies
* @return true if the policies are active, false if not active
*/
public boolean isActive(PolicySet policies) {
DevicePolicyManager dpm = getDPM();
if (dpm.isAdminActive(mAdminName)) {
- // check each policy
- PolicySet aggregate;
- synchronized (this) {
- if (mAggregatePolicy == null) {
- mAggregatePolicy = computeAggregatePolicy();
+ // select aggregate set if needed
+ if (policies == null) {
+ synchronized (this) {
+ if (mAggregatePolicy == null) {
+ mAggregatePolicy = computeAggregatePolicy();
+ }
+ policies = mAggregatePolicy;
}
- aggregate = mAggregatePolicy;
}
// quick check for the "empty set" of no policies
- if (aggregate == NO_POLICY_SET) {
+ if (policies == NO_POLICY_SET) {
return true;
}
// check each policy explicitly
- if (aggregate.mMinPasswordLength > 0) {
- if (dpm.getPasswordMinimumLength(mAdminName) < aggregate.mMinPasswordLength) {
+ if (policies.mMinPasswordLength > 0) {
+ if (dpm.getPasswordMinimumLength(mAdminName) < policies.mMinPasswordLength) {
return false;
}
}
- if (aggregate.mPasswordMode > 0) {
- if (dpm.getPasswordQuality(mAdminName) < aggregate.getDPManagerPasswordMode()) {
+ if (policies.mPasswordMode > 0) {
+ if (dpm.getPasswordQuality(mAdminName) < policies.getDPManagerPasswordQuality()) {
return false;
}
if (!dpm.isActivePasswordSufficient()) {
return false;
}
}
- if (aggregate.mMaxScreenLockTime > 0) {
+ if (policies.mMaxScreenLockTime > 0) {
// Note, we use seconds, dpm uses milliseconds
- if (dpm.getMaximumTimeToLock(mAdminName) > aggregate.mMaxScreenLockTime * 1000) {
+ if (dpm.getMaximumTimeToLock(mAdminName) > policies.mMaxScreenLockTime * 1000) {
return false;
}
}
@@ -241,14 +270,101 @@ public class SecurityPolicy {
}
/**
- * Sync service should call this any time a sync fails due to isActive() returning false.
+ * Set the requested security level based on the aggregate set of requests
+ */
+ public void setActivePolicies() {
+ DevicePolicyManager dpm = getDPM();
+ if (dpm.isAdminActive(mAdminName)) {
+ // compute aggregate set if needed
+ PolicySet policies;
+ synchronized (this) {
+ if (mAggregatePolicy == null) {
+ mAggregatePolicy = computeAggregatePolicy();
+ }
+ policies = mAggregatePolicy;
+ }
+ // set each policy in the policy manager
+ // password mode & length
+ dpm.setPasswordQuality(mAdminName, policies.getDPManagerPasswordQuality());
+ dpm.setPasswordMinimumLength(mAdminName, policies.mMinPasswordLength);
+ // screen lock time
+ dpm.setMaximumTimeToLock(mAdminName, policies.mMaxScreenLockTime * 1000);
+ // local wipe (failed passwords limit)
+ dpm.setMaximumFailedPasswordsForWipe(mAdminName, policies.mMaxPasswordFails);
+ }
+ }
+
+ /**
+ * API: Sync service should call this any time a sync fails due to isActive() returning false.
* This will kick off the notify-acquire-admin-state process and/or increase the security level.
* The caller needs to write the required policies into this account before making this call.
+ * Should not be called from UI thread - uses DB lookups to prepare new notifications
*
* @param accountId the account for which sync cannot proceed
*/
public void policiesRequired(long accountId) {
- // implement....
+ synchronized (this) {
+ if (mNotificationActive) {
+ // no need to do anything - we've already been notified, and we've already
+ // put up a notification
+ return;
+ } else {
+ // Prepare & post a notification
+ // record that we're watching this one
+ mNotificationActive = true;
+ }
+ }
+ // At this point, we will put up a notification
+ Account account = EmailContent.Account.restoreAccountWithId(mContext, accountId);
+
+ String tickerText = mContext.getString(R.string.security_notification_ticker_fmt,
+ account.getDisplayName());
+ String contentTitle = mContext.getString(R.string.security_notification_content_title);
+ String contentText = account.getDisplayName();
+ String ringtoneString = account.getRingtone();
+ Uri ringTone = (ringtoneString == null) ? null : Uri.parse(ringtoneString);
+ boolean vibrate = 0 != (account.mFlags & Account.FLAGS_VIBRATE);
+
+ Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, accountId);
+ PendingIntent pending =
+ PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ Notification notification = new Notification(R.drawable.stat_notify_email_generic,
+ tickerText, System.currentTimeMillis());
+ notification.setLatestEventInfo(mContext, contentTitle, contentText, pending);
+
+ // Use the account's notification rules for sound & vibrate (but always notify)
+ notification.sound = ringTone;
+ if (vibrate) {
+ notification.defaults |= Notification.DEFAULT_VIBRATE;
+ }
+ notification.flags |= Notification.FLAG_SHOW_LIGHTS;
+ notification.defaults |= Notification.DEFAULT_LIGHTS;
+
+ NotificationManager notificationManager =
+ (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.notify(MailService.NOTIFICATION_ID_SECURITY_NEEDED, notification);
+ }
+
+ /**
+ * Called from the notification's intent receiver to register that the notification can be
+ * cleared now.
+ */
+ public synchronized void clearNotification(long accountId) {
+ NotificationManager notificationManager =
+ (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.cancel(MailService.NOTIFICATION_ID_SECURITY_NEEDED);
+ mNotificationActive = false;
+ }
+
+ /**
+ * API: Remote wipe (from server). This is final, there is no confirmation.
+ */
+ public void remoteWipe(long accountId) {
+ DevicePolicyManager dpm = getDPM();
+ if (dpm.isAdminActive(mAdminName)) {
+ dpm.wipeData(0);
+ }
}
/**
@@ -291,9 +407,10 @@ public class SecurityPolicy {
* @param maxPasswordFails (0=not enforced)
* @param maxScreenLockTime in seconds (0=not enforced)
* @param requireRemoteWipe
+ * @throws IllegalArgumentException when any arguments are outside of legal ranges.
*/
public PolicySet(int minPasswordLength, int passwordMode, int maxPasswordFails,
- int maxScreenLockTime, boolean requireRemoteWipe) {
+ int maxScreenLockTime, boolean requireRemoteWipe) throws IllegalArgumentException {
if (minPasswordLength > PASSWORD_LENGTH_MAX) {
throw new IllegalArgumentException("password length");
}
@@ -339,9 +456,9 @@ public class SecurityPolicy {
}
/**
- * Helper to map DevicePolicyManager password modes to our internal encoding.
+ * Helper to map our internal encoding to DevicePolicyManager password modes.
*/
- public int getDPManagerPasswordMode() {
+ public int getDPManagerPasswordQuality() {
switch (mPasswordMode) {
case PASSWORD_MODE_SIMPLE:
return DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
@@ -355,11 +472,32 @@ public class SecurityPolicy {
/**
* Record flags (and a sync key for the flags) into an Account
* Note: the hash code is defined as the encoding used in Account
+ *
* @param account to write the values mSecurityFlags and mSecuritySyncKey
* @param syncKey the value to write into the account's mSecuritySyncKey
+ * @param update if true, also writes the account back to the provider (updating only
+ * the fields changed by this API)
+ * @param context a context for writing to the provider
+ * @return true if the actual policies changed, false if no change (note, sync key
+ * does not affect this)
*/
- public void writeAccount(Account account, String syncKey) {
- account.mSecurityFlags = hashCode();
+ public boolean writeAccount(Account account, String syncKey, boolean update,
+ Context context) {
+ int newFlags = hashCode();
+ boolean dirty = (newFlags != account.mSecurityFlags);
+ account.mSecurityFlags = newFlags;
+ account.mSecuritySyncKey = syncKey;
+ if (update) {
+ if (account.isSaved()) {
+ ContentValues cv = new ContentValues();
+ cv.put(AccountColumns.SECURITY_FLAGS, account.mSecurityFlags);
+ cv.put(AccountColumns.SECURITY_SYNC_KEY, account.mSecuritySyncKey);
+ account.update(context, cv);
+ } else {
+ account.save(context);
+ }
+ }
+ return dirty;
}
@Override
@@ -400,19 +538,47 @@ public class SecurityPolicy {
}
/**
- * Device Policy administrator. This is primarily a listener for device state changes.
+ * If we are not the active device admin, try to become so.
+ *
+ * @return true if we are already active, false if we are not
*/
- private static class PolicyAdmin extends DeviceAdmin {
+ public boolean isActiveAdmin() {
+ DevicePolicyManager dpm = getDPM();
+ return dpm.isAdminActive(mAdminName);
+ }
- boolean mEnabled = false;
+ /**
+ * Report admin component name - for making calls into device policy manager
+ */
+ public ComponentName getAdminComponent() {
+ return mAdminName;
+ }
+
+ /**
+ * Internal handler for enabled/disabled transitions. Handles DeviceAdmin.onEnabled and
+ * and DeviceAdmin.onDisabled.
+ */
+ private void onAdminEnabled(boolean isEnabled) {
+ if (isEnabled && !mAdminEnabled) {
+ // TODO: transition to enabled state
+ } else if (!isEnabled && mAdminEnabled) {
+ // TODO: transition to disabled state
+ }
+ mAdminEnabled = isEnabled;
+ }
+
+ /**
+ * Device Policy administrator. This is primarily a listener for device state changes.
+ * Note: This is instantiated by incoming messages.
+ */
+ public static class PolicyAdmin extends DeviceAdmin {
/**
* Called after the administrator is first enabled.
*/
@Override
public void onEnabled(Context context, Intent intent) {
- mEnabled = true;
- // do something
+ SecurityPolicy.getInstance(context).onAdminEnabled(true);
}
/**
@@ -420,8 +586,7 @@ public class SecurityPolicy {
*/
@Override
public void onDisabled(Context context, Intent intent) {
- mEnabled = false;
- // do something
+ SecurityPolicy.getInstance(context).onAdminEnabled(false);
}
/**
diff --git a/src/com/android/email/activity/MessageList.java b/src/com/android/email/activity/MessageList.java
index c0f388c6a..6d7e2f60f 100644
--- a/src/com/android/email/activity/MessageList.java
+++ b/src/com/android/email/activity/MessageList.java
@@ -325,7 +325,7 @@ public class MessageList extends ListActivity implements OnItemClickListener, On
// clear notifications here
NotificationManager notificationManager = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.cancel(MailService.NEW_MESSAGE_NOTIFICATION_ID);
+ notificationManager.cancel(MailService.NOTIFICATION_ID_NEW_MESSAGES);
restoreListPosition();
autoRefreshStaleMailbox();
}
diff --git a/src/com/android/email/activity/setup/AccountSecurity.java b/src/com/android/email/activity/setup/AccountSecurity.java
new file mode 100644
index 000000000..da16cb67b
--- /dev/null
+++ b/src/com/android/email/activity/setup/AccountSecurity.java
@@ -0,0 +1,134 @@
+/*
+ * 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.email.activity.setup;
+
+import com.android.email.R;
+import com.android.email.SecurityPolicy;
+import com.android.email.provider.EmailContent.Account;
+
+import android.app.Activity;
+import android.app.DevicePolicyManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * Psuedo-activity (no UI) to bootstrap the user up to a higher desired security level. This
+ * bootstrap requires the following steps.
+ *
+ * 1. Confirm the account of interest has any security policies defined - exit early if not
+ * 2. If not actively administrating the device, ask Device Policy Manager to start that
+ * 3. When we are actively administrating, check current policies and see if they're sufficient
+ * 4. If not, set policies
+ * 5. If necessary, request for user to update device password
+ */
+public class AccountSecurity extends Activity {
+
+ private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity.setup.ACCOUNT_ID";
+
+ private static final int REQUEST_ENABLE = 1;
+
+ /**
+ * Used for generating intent for this activity (which is intended to be launched
+ * from a notification.)
+ *
+ * @param context Calling context for building the intent
+ * @param accountId The account of interest
+ * @return an Intent which can be used to view that account
+ */
+ public static Intent actionUpdateSecurityIntent(Context context, long accountId) {
+ Intent intent = new Intent(context, AccountSecurity.class);
+ intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
+ return intent;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent i = getIntent();
+ long accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1);
+ SecurityPolicy security = SecurityPolicy.getInstance(this);
+ security.clearNotification(accountId);
+ if (accountId != -1) {
+ // TODO: spin up a thread to do this in the background, because of DB ops
+ Account account = Account.restoreAccountWithId(this, accountId);
+ if (account != null) {
+ if (account.mSecurityFlags != 0) {
+ // This account wants to control security
+ if (!security.isActiveAdmin()) {
+ // try to become active - must happen here in this activity, to get result
+ Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
+ intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN,
+ security.getAdminComponent());
+ intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION,
+ this.getString(R.string.account_security_policy_explanation_fmt,
+ account.getDisplayName()));
+ startActivityForResult(intent, REQUEST_ENABLE);
+ // keep this activity on stack to process result
+ return;
+ } else {
+ // already active - try to set actual policies, finish, and return
+ setActivePolicies();
+ }
+ }
+ }
+ }
+ finish();
+ }
+
+ /**
+ * Handle the eventual result of the user allowing us to become an active device admin
+ */
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_ENABLE:
+ if (resultCode == Activity.RESULT_OK) {
+ // now active - try to set actual policies
+ setActivePolicies();
+ } else {
+ // failed - just give up and go away
+ }
+ }
+ finish();
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ /**
+ * Now that we are connected as an active device admin, try to set the device to the
+ * correct security level, and ask for a password if necessary.
+ */
+ private void setActivePolicies() {
+ SecurityPolicy sp = SecurityPolicy.getInstance(this);
+ // check current security level - if sufficient, we're done!
+ if (sp.isActive(null)) {
+ return;
+ }
+ // set current security level
+ sp.setActivePolicies();
+ // check current security level - if sufficient, we're done!
+ if (sp.isActive(null)) {
+ return;
+ }
+ // if not sufficient, launch the activity to have the user set a new password.
+ Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
+ startActivity(intent);
+ }
+
+}
diff --git a/src/com/android/email/service/MailService.java b/src/com/android/email/service/MailService.java
index 3681f7b65..9e356bbb7 100644
--- a/src/com/android/email/service/MailService.java
+++ b/src/com/android/email/service/MailService.java
@@ -53,7 +53,8 @@ public class MailService extends Service {
private static final String LOG_TAG = "Email-MailService";
- public static int NEW_MESSAGE_NOTIFICATION_ID = 1;
+ public static int NOTIFICATION_ID_NEW_MESSAGES = 1;
+ public static int NOTIFICATION_ID_SECURITY_NEEDED = 2;
private static final String ACTION_CHECK_MAIL =
"com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
@@ -201,7 +202,7 @@ public class MailService extends Service {
// but that's an edge condition and this is much safer.
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.cancel(NEW_MESSAGE_NOTIFICATION_ID);
+ notificationManager.cancel(NOTIFICATION_ID_NEW_MESSAGES);
// When called externally, we refresh the sync reports table to pick up
// any changes in the account list or account settings
@@ -705,6 +706,6 @@ public class MailService extends Service {
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.notify(NEW_MESSAGE_NOTIFICATION_ID, notification);
+ notificationManager.notify(NOTIFICATION_ID_NEW_MESSAGES, notification);
}
}
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index b15dac7e2..8766c617a 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -17,6 +17,8 @@
package com.android.exchange;
+import com.android.email.SecurityPolicy;
+import com.android.email.SecurityPolicy.PolicySet;
import com.android.email.codec.binary.Base64;
import com.android.email.mail.AuthenticationFailedException;
import com.android.email.mail.MessagingException;
@@ -964,25 +966,40 @@ public class EasSyncService extends AbstractSyncService {
}
while (!mStop) {
- userLog("Sending Account syncKey: ", mAccount.mSyncKey);
- Serializer s = new Serializer();
- s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY)
- .text(mAccount.mSyncKey).end().end().done();
- HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray());
- if (mStop) break;
- int code = resp.getStatusLine().getStatusCode();
- if (code == HttpStatus.SC_OK) {
- HttpEntity entity = resp.getEntity();
- int len = (int)entity.getContentLength();
- if (len != 0) {
- InputStream is = entity.getContent();
- // Returns true if we need to sync again
- if (new FolderSyncParser(is, new AccountSyncAdapter(mMailbox, this))
- .parse()) {
- continue;
- }
- }
- } else if (isAuthError(code)) {
+ userLog("Sending Account syncKey: ", mAccount.mSyncKey);
+ Serializer s = new Serializer();
+ s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY)
+ .text(mAccount.mSyncKey).end().end().done();
+ HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray());
+ if (mStop) break;
+ int code = resp.getStatusLine().getStatusCode();
+ if (code == HttpStatus.SC_OK) {
+ HttpEntity entity = resp.getEntity();
+ int len = (int)entity.getContentLength();
+ if (len != 0) {
+ InputStream is = entity.getContent();
+ // Returns true if we need to sync again
+ if (new FolderSyncParser(is, new AccountSyncAdapter(mMailbox, this))
+ .parse()) {
+ continue;
+ }
+ }
+ // // EVERYTHING IN THE code==403 BLOCK IS PLACEHOLDER/SAMPLE CODE
+ } else if (code == 403) { // security error
+ // Reality: Find out from server what policies are required
+ // Fake: Hardcode the policies
+ SecurityPolicy sp = SecurityPolicy.getInstance(mContext);
+ PolicySet ps = new PolicySet(4, PolicySet.PASSWORD_MODE_SIMPLE, 0, 0, true);
+ // Update the account
+ if (ps.writeAccount(mAccount, "securitySyncKey", true, mContext)) {
+ sp.updatePolicies(mAccount.mId);
+ }
+ // Notify that we are blocked because of policies
+ sp.policiesRequired(mAccount.mId);
+ // and exit (don't sync in this condition)
+ mExitStatus = EXIT_LOGIN_FAILURE;
+ // END PLACEHOLDER CODE
+ } else if (isAuthError(code)) {
mExitStatus = EXIT_LOGIN_FAILURE;
} else {
userLog("FolderSync response error: ", code);
diff --git a/tests/src/com/android/email/SecurityPolicyTests.java b/tests/src/com/android/email/SecurityPolicyTests.java
index a7c6f2586..fc841ca68 100644
--- a/tests/src/com/android/email/SecurityPolicyTests.java
+++ b/tests/src/com/android/email/SecurityPolicyTests.java
@@ -107,13 +107,20 @@ public class SecurityPolicyTests extends ProviderTestCase2 {
assertTrue(EMPTY_POLICY_SET.equals(sp.computeAggregatePolicy()));
// with a single account in security mode, should return same security as in account
- PolicySet p3in = new PolicySet(10, PolicySet.PASSWORD_MODE_SIMPLE, 15, 16, false);
+ // first test with partially-populated policies
Account a3 = ProviderTestUtils.setupAccount("sec-3", false, mMockContext);
- p3in.writeAccount(a3, null);
- a3.save(mMockContext);
- PolicySet p3out = sp.computeAggregatePolicy();
- assertNotNull(p3out);
- assertEquals(p3in, p3out);
+ PolicySet p3ain = new PolicySet(10, PolicySet.PASSWORD_MODE_SIMPLE, 0, 0, false);
+ p3ain.writeAccount(a3, null, true, mMockContext);
+ PolicySet p3aout = sp.computeAggregatePolicy();
+ assertNotNull(p3aout);
+ assertEquals(p3ain, p3aout);
+
+ // Repeat that test with fully-populated policies
+ PolicySet p3bin = new PolicySet(10, PolicySet.PASSWORD_MODE_SIMPLE, 15, 16, false);
+ p3bin.writeAccount(a3, null, true, mMockContext);
+ PolicySet p3bout = sp.computeAggregatePolicy();
+ assertNotNull(p3bout);
+ assertEquals(p3bin, p3bout);
// add another account which mixes it up (some fields will change, others will not)
// pw length and pw mode - max logic - will change because larger #s here
@@ -121,8 +128,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 {
// wipe required - OR logic - will *not* change here because false
PolicySet p4in = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false);
Account a4 = ProviderTestUtils.setupAccount("sec-4", false, mMockContext);
- p4in.writeAccount(a4, null);
- a4.save(mMockContext);
+ p4in.writeAccount(a4, null, true, mMockContext);
PolicySet p4out = sp.computeAggregatePolicy();
assertNotNull(p4out);
assertEquals(20, p4out.mMinPasswordLength);
@@ -137,8 +143,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 {
// wipe required - OR logic - will change here because true
PolicySet p5in = new PolicySet(4, PolicySet.PASSWORD_MODE_NONE, 5, 6, true);
Account a5 = ProviderTestUtils.setupAccount("sec-5", false, mMockContext);
- p5in.writeAccount(a5, null);
- a5.save(mMockContext);
+ p5in.writeAccount(a5, null, true, mMockContext);
PolicySet p5out = sp.computeAggregatePolicy();
assertNotNull(p5out);
assertEquals(20, p5out.mMinPasswordLength);
@@ -217,7 +222,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 {
PolicySet p1 = new PolicySet(1, PolicySet.PASSWORD_MODE_STRONG, 3, 4, true);
Account a = new Account();
final String SYNC_KEY = "test_sync_key";
- p1.writeAccount(a, SYNC_KEY);
+ p1.writeAccount(a, SYNC_KEY, false, null);
PolicySet p2 = new PolicySet(a);
assertEquals(p1, p2);
}