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