Logic to move phone into security-admin mode

* Create notification to display when syncs fail due to security
* Create psuedo-activity (no UI) to manage device admin state transitions
* Clean up and flesh out SecurityPolicy APIs'
* Add placeholders in EasSyncService showing how to react when policies
    are not met and sync cannot continue.

Note:  There are some STOPSHIP todo's at the top of SecurityPolicy.java.
These should explain any code that you might think is "missing".
This commit is contained in:
Andrew Stadler 2010-02-05 11:10:39 -08:00
parent 4d2a701844
commit 3d2b3b3b35
8 changed files with 417 additions and 77 deletions

View File

@ -115,6 +115,11 @@
android:label="@string/account_settings_action"
>
</activity>
<activity
android:name=".activity.setup.AccountSecurity"
android.label="@string/account_security_title"
>
</activity>
<activity
android:name=".activity.Debug"

View File

@ -502,6 +502,19 @@
<string name="account_setup_failed_security_policies_unsupported">
This server requires security features your phone does not support.</string>
<!-- Notification ticker when device security required -->
<string name="security_notification_ticker_fmt">
Account \"<xliff:g id="account">%s</xliff:g>\" requires security settings update.
</string>
<!-- Notification content title when device security required -->
<string name="security_notification_content_title">Update Security Settings</string>
<!-- Title of the activity that dispatches changes to device security. Not normally seen. -->
<string name="account_security_title">Device Security</string>
<!-- Additional diagnostic text when the email app asserts control of the phone. -->
<string name="account_security_policy_explanation_fmt">
The server <xliff:g id="server">%s</xliff:g> requires that you allow it to remotely control
some security features of your phone.</string>
<!-- "Setup could not finish" dialog action button -->
<string name="account_setup_failed_dlg_edit_details_action">Edit details</string>
<!-- On Settings screen, section heading -->
@ -585,9 +598,9 @@
<string name="system_account_create_failed">The AccountManager could not create the Account; please try again.</string>
<!-- Strings that support the DeviceAdmin / DevicePolicyManager API -->
<!-- Name of the DeviceAdmin (seen in settings - anywhere else?) -->
<string name="device_admin_label">Email Device Administrator</string>
<!-- Long-form description of the DeviceAdmin (seen in settings - anywhere else?) -->
<string name="device_admin_description">Email Device Administrator - Long Description</string>
<!-- Name of the DeviceAdmin (seen in settings & in user confirmation screen) -->
<string name="device_admin_label">Email</string>
<!-- Long-form description of the DeviceAdmin (2nd line in settings & in user conf. screen) -->
<string name="device_admin_description">Enables server-specified security policies</string>
</resources>

View File

@ -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);
}
/**

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -107,13 +107,20 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
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<EmailProvider> {
// 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<EmailProvider> {
// 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<EmailProvider> {
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);
}