diff --git a/CleanSpec.mk b/CleanSpec.mk index 81f5bfd5c..ebb255650 100644 --- a/CleanSpec.mk +++ b/CleanSpec.mk @@ -53,6 +53,7 @@ $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/EmailGoogle_inte $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/Email_intermediates) $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/com.android.emailcommon_intermediates) $(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/com.android.emailcommon_intermediates) +$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/com.android.emailcommon_intermediates) # ************************************************ # NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST diff --git a/emailcommon/src/com/android/emailcommon/provider/EmailContent.aidl b/emailcommon/src/com/android/emailcommon/provider/EmailContent.aidl deleted file mode 100644 index 7505fa6f9..000000000 --- a/emailcommon/src/com/android/emailcommon/provider/EmailContent.aidl +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (C) 2008-2009 Marc Blank - * Licensed to The Android Open Source Project. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.emailcommon.provider; - -parcelable EmailContent.HostAuth; - diff --git a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java index e42bf0138..f20a00d6e 100644 --- a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java +++ b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java @@ -124,6 +124,33 @@ public abstract class EmailContent { return mId != NOT_SAVED; } + + /** + * Restore a subclass of EmailContent from the database + * @param context the caller's context + * @param klass the class to restore + * @param contentUri the content uri of the EmailContent subclass + * @param contentProjection the content projection for the EmailContent subclass + * @param id the unique id of the object + * @return the instantiated object + */ + public static T restoreContentWithId(Context context, + Class klass, Uri contentUri, String[] contentProjection, long id) { + Uri u = ContentUris.withAppendedId(contentUri, id); + Cursor c = context.getContentResolver().query(u, contentProjection, null, null, null); + + try { + if (c.moveToFirst()) { + return (T)getContent(c, klass); + } else { + return null; + } + } finally { + c.close(); + } + } + + // The Content sub class must have a no-arg constructor static public T getContent(Cursor cursor, Class klass) { try { @@ -194,7 +221,7 @@ public abstract class EmailContent { /** * no public constructor since this is a utility class */ - private EmailContent() { + protected EmailContent() { } public interface SyncColumns { @@ -724,25 +751,8 @@ public abstract class EmailContent { } public static Message restoreMessageWithId(Context context, long id) { - Uri u = ContentUris.withAppendedId(Message.CONTENT_URI, id); - if (context == null) { - throw new NullPointerException("context"); - } - ContentResolver resolver = context.getContentResolver(); - if (resolver == null) { - throw new NullPointerException("resolver"); - } - Cursor c = resolver.query(u, Message.CONTENT_PROJECTION, null, null, null); - - try { - if (c.moveToFirst()) { - return getContent(c, Message.class); - } else { - return null; - } - } finally { - c.close(); - } + return EmailContent.restoreContentWithId(context, Message.class, + Message.CONTENT_URI, Message.CONTENT_PROJECTION, id); } @Override @@ -961,12 +971,16 @@ public abstract class EmailContent { public static final String PROTOCOL_VERSION = "protocolVersion"; // The number of new messages (reported by the sync/download engines public static final String NEW_MESSAGE_COUNT = "newMessageCount"; - // Flags defining security (provisioning) requirements of this account + // Legacy flags defining security (provisioning) requirements of this account; this + // information is now found in the Policy table; POLICY_KEY (below) is the foreign key + @Deprecated public static final String SECURITY_FLAGS = "securityFlags"; // Server-based sync key for the security policies currently enforced public static final String SECURITY_SYNC_KEY = "securitySyncKey"; // Signature to use with this account public static final String SIGNATURE = "signature"; + // A foreign key into the Policy table + public static final String POLICY_KEY = "policyKey"; } public static final class Account extends EmailContent implements AccountColumns, Parcelable { @@ -1039,13 +1053,14 @@ public abstract class EmailContent { public String mRingtoneUri; public String mProtocolVersion; public int mNewMessageCount; - public long mSecurityFlags; public String mSecuritySyncKey; public String mSignature; + public long mPolicyKey; // Convenience for creating an account public transient HostAuth mHostAuthRecv; public transient HostAuth mHostAuthSend; + public transient Policy mPolicy; public static final int CONTENT_ID_COLUMN = 0; public static final int CONTENT_DISPLAY_NAME_COLUMN = 1; @@ -1062,9 +1077,9 @@ public abstract class EmailContent { public static final int CONTENT_RINGTONE_URI_COLUMN = 12; public static final int CONTENT_PROTOCOL_VERSION_COLUMN = 13; public static final int CONTENT_NEW_MESSAGE_COUNT_COLUMN = 14; - public static final int CONTENT_SECURITY_FLAGS_COLUMN = 15; - public static final int CONTENT_SECURITY_SYNC_KEY_COLUMN = 16; - public static final int CONTENT_SIGNATURE_COLUMN = 17; + public static final int CONTENT_SECURITY_SYNC_KEY_COLUMN = 15; + public static final int CONTENT_SIGNATURE_COLUMN = 16; + public static final int CONTENT_POLICY_KEY = 17; public static final String[] CONTENT_PROJECTION = new String[] { RECORD_ID, AccountColumns.DISPLAY_NAME, @@ -1073,8 +1088,8 @@ public abstract class EmailContent { AccountColumns.HOST_AUTH_KEY_SEND, AccountColumns.FLAGS, AccountColumns.IS_DEFAULT, AccountColumns.COMPATIBILITY_UUID, AccountColumns.SENDER_NAME, AccountColumns.RINGTONE_URI, AccountColumns.PROTOCOL_VERSION, - AccountColumns.NEW_MESSAGE_COUNT, AccountColumns.SECURITY_FLAGS, - AccountColumns.SECURITY_SYNC_KEY, AccountColumns.SIGNATURE + AccountColumns.NEW_MESSAGE_COUNT, AccountColumns.SECURITY_SYNC_KEY, + AccountColumns.SIGNATURE, AccountColumns.POLICY_KEY }; public static final int CONTENT_MAILBOX_TYPE_COLUMN = 1; @@ -1100,7 +1115,7 @@ public abstract class EmailContent { private static final String UUID_SELECTION = AccountColumns.COMPATIBILITY_UUID + " =?"; public static final String SECURITY_NONZERO_SELECTION = - Account.SECURITY_FLAGS + " IS NOT NULL AND " + Account.SECURITY_FLAGS + "!=0"; + Account.POLICY_KEY + " IS NOT NULL AND " + Account.POLICY_KEY + "!=0"; private static final String FIND_INBOX_SELECTION = MailboxColumns.TYPE + " = " + Mailbox.TYPE_INBOX + @@ -1128,19 +1143,8 @@ public abstract class EmailContent { } public static Account restoreAccountWithId(Context context, long id) { - Uri u = ContentUris.withAppendedId(Account.CONTENT_URI, id); - Cursor c = context.getContentResolver().query(u, Account.CONTENT_PROJECTION, - null, null, null); - - try { - if (c.moveToFirst()) { - return getContent(c, Account.class); - } else { - return null; - } - } finally { - c.close(); - } + return EmailContent.restoreContentWithId(context, Account.class, + Account.CONTENT_URI, Account.CONTENT_PROJECTION, id); } /** @@ -1178,9 +1182,9 @@ public abstract class EmailContent { mRingtoneUri = cursor.getString(CONTENT_RINGTONE_URI_COLUMN); mProtocolVersion = cursor.getString(CONTENT_PROTOCOL_VERSION_COLUMN); mNewMessageCount = cursor.getInt(CONTENT_NEW_MESSAGE_COUNT_COLUMN); - mSecurityFlags = cursor.getLong(CONTENT_SECURITY_FLAGS_COLUMN); mSecuritySyncKey = cursor.getString(CONTENT_SECURITY_SYNC_KEY_COLUMN); mSignature = cursor.getString(CONTENT_SIGNATURE_COLUMN); + mPolicyKey = cursor.getLong(CONTENT_POLICY_KEY); } private long getId(Uri u) { @@ -1639,6 +1643,10 @@ public abstract class EmailContent { */ @Override public int update(Context context, ContentValues cv) { + if (mPolicy != null && mPolicyKey <= 0) { + // If a policy is set and there's no policy, link it to the account + mPolicy.setAccountPolicy(context, this, null); + } if (cv.containsKey(AccountColumns.IS_DEFAULT) && cv.getAsBoolean(AccountColumns.IS_DEFAULT)) { ArrayList ops = new ArrayList(); @@ -1676,13 +1684,15 @@ public abstract class EmailContent { // This logic is in place so I can (a) short circuit the expensive stuff when // possible, and (b) override (and throw) if anyone tries to call save() or update() // directly for Account, which are unsupported. - if (mHostAuthRecv == null && mHostAuthSend == null && mIsDefault == false) { - return super.save(context); + if (mHostAuthRecv == null && mHostAuthSend == null && mIsDefault == false && + mPolicy != null) { + return super.save(context); } int index = 0; int recvIndex = -1; int sendIndex = -1; + int policyIndex = -1; // Create operations for saving the send and recv hostAuths // Also, remember which operation in the array they represent @@ -1699,6 +1709,12 @@ public abstract class EmailContent { .withValues(mHostAuthSend.toContentValues()) .build()); } + if (mPolicy != null) { + policyIndex = index++; + ops.add(ContentProviderOperation.newInsert(mPolicy.mBaseUri) + .withValues(mPolicy.toContentValues()) + .build()); + } // Create operations for making this the only default account // Note, these are always updates because they change existing accounts @@ -1719,6 +1735,9 @@ public abstract class EmailContent { if (sendIndex >= 0) { cv.put(Account.HOST_AUTH_KEY_SEND, sendIndex); } + if (policyIndex >= 0) { + cv.put(Account.POLICY_KEY, policyIndex); + } } ContentProviderOperation.Builder b = ContentProviderOperation.newInsert(mBaseUri); @@ -1742,6 +1761,11 @@ public abstract class EmailContent { mHostAuthKeySend = newId; mHostAuthSend.mId = newId; } + if (policyIndex >= 0) { + long newId = getId(results[policyIndex].uri); + mPolicyKey = newId; + mPolicy.mId = newId; + } Uri u = results[index].uri; mId = getId(u); return u; @@ -1770,9 +1794,9 @@ public abstract class EmailContent { values.put(AccountColumns.RINGTONE_URI, mRingtoneUri); values.put(AccountColumns.PROTOCOL_VERSION, mProtocolVersion); values.put(AccountColumns.NEW_MESSAGE_COUNT, mNewMessageCount); - values.put(AccountColumns.SECURITY_FLAGS, mSecurityFlags); values.put(AccountColumns.SECURITY_SYNC_KEY, mSecuritySyncKey); values.put(AccountColumns.SIGNATURE, mSignature); + values.put(AccountColumns.POLICY_KEY, mPolicyKey); return values; } @@ -1817,9 +1841,9 @@ public abstract class EmailContent { dest.writeString(mRingtoneUri); dest.writeString(mProtocolVersion); dest.writeInt(mNewMessageCount); - dest.writeLong(mSecurityFlags); dest.writeString(mSecuritySyncKey); dest.writeString(mSignature); + dest.writeLong(mPolicyKey); if (mHostAuthRecv != null) { dest.writeByte((byte)1); @@ -1856,9 +1880,9 @@ public abstract class EmailContent { mRingtoneUri = in.readString(); mProtocolVersion = in.readString(); mNewMessageCount = in.readInt(); - mSecurityFlags = in.readLong(); mSecuritySyncKey = in.readString(); mSignature = in.readString(); + mPolicyKey = in.readLong(); mHostAuthRecv = null; if (in.readByte() == 1) { @@ -2004,19 +2028,8 @@ public abstract class EmailContent { * @return the instantiated Attachment */ public static Attachment restoreAttachmentWithId (Context context, long id) { - Uri u = ContentUris.withAppendedId(Attachment.CONTENT_URI, id); - Cursor c = context.getContentResolver().query(u, Attachment.CONTENT_PROJECTION, - null, null, null); - - try { - if (c.moveToFirst()) { - return getContent(c, Attachment.class); - } else { - return null; - } - } finally { - c.close(); - } + return EmailContent.restoreContentWithId(context, Attachment.class, + Attachment.CONTENT_URI, Attachment.CONTENT_PROJECTION, id); } /** @@ -2397,19 +2410,8 @@ public abstract class EmailContent { * @return the instantiated Mailbox */ public static Mailbox restoreMailboxWithId(Context context, long id) { - Uri u = ContentUris.withAppendedId(Mailbox.CONTENT_URI, id); - Cursor c = context.getContentResolver().query(u, Mailbox.CONTENT_PROJECTION, - null, null, null); - - try { - if (c.moveToFirst()) { - return getContent(c, Mailbox.class); - } else { - return null; - } - } finally { - c.close(); - } + return EmailContent.restoreContentWithId(context, Mailbox.class, + Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION, id); } /** @@ -2737,19 +2739,8 @@ public abstract class EmailContent { * @return the instantiated HostAuth */ public static HostAuth restoreHostAuthWithId(Context context, long id) { - Uri u = ContentUris.withAppendedId(EmailContent.HostAuth.CONTENT_URI, id); - Cursor c = context.getContentResolver().query(u, HostAuth.CONTENT_PROJECTION, - null, null, null); - - try { - if (c.moveToFirst()) { - return getContent(c, HostAuth.class); - } else { - return null; - } - } finally { - c.close(); - } + return EmailContent.restoreContentWithId(context, HostAuth.class, + HostAuth.CONTENT_URI, HostAuth.CONTENT_PROJECTION, id); } @@ -3015,4 +3006,18 @@ public abstract class EmailContent { && Utility.areStringsEqual(mDomain, that.mDomain); } } + + public interface PolicyColumns { + public static final String ID = "_id"; + public static final String PASSWORD_MODE = "passwordMode"; + public static final String PASSWORD_MIN_LENGTH = "passwordMinLength"; + public static final String PASSWORD_EXPIRATION_DAYS = "passwordExpirationDays"; + public static final String PASSWORD_HISTORY = "passwordHistory"; + public static final String PASSWORD_COMPLEX_CHARS = "passwordComplexChars"; + public static final String PASSWORD_MAX_FAILS = "passwordMaxFails"; + public static final String MAX_SCREEN_LOCK_TIME = "maxScreenLockTime"; + public static final String REQUIRE_REMOTE_WIPE = "requireRemoteWipe"; + public static final String REQUIRE_ENCRYPTION = "requireEncryption"; + public static final String REQUIRE_ENCRYPTION_EXTERNAL = "requireEncryptionExternal"; + } } diff --git a/emailcommon/src/com/android/emailcommon/service/PolicySet.aidl b/emailcommon/src/com/android/emailcommon/provider/Policy.aidl similarity index 90% rename from emailcommon/src/com/android/emailcommon/service/PolicySet.aidl rename to emailcommon/src/com/android/emailcommon/provider/Policy.aidl index e825c62c3..02be51b9a 100644 --- a/emailcommon/src/com/android/emailcommon/service/PolicySet.aidl +++ b/emailcommon/src/com/android/emailcommon/provider/Policy.aidl @@ -13,7 +13,7 @@ * limitations under the License. */ -package com.android.emailcommon.service; +package com.android.emailcommon.provider; -parcelable PolicySet; +parcelable Policy; diff --git a/emailcommon/src/com/android/emailcommon/provider/Policy.java b/emailcommon/src/com/android/emailcommon/provider/Policy.java new file mode 100644 index 000000000..4f85997db --- /dev/null +++ b/emailcommon/src/com/android/emailcommon/provider/Policy.java @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.emailcommon.provider; +import com.android.emailcommon.utility.Utility; + +import android.app.admin.DevicePolicyManager; +import android.content.ContentProviderOperation; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.util.Log; + +import java.util.ArrayList; + +/** + * The Policy class represents a set of security requirements that are associated with an Account. + * The requirements may be either device-specific (e.g. password) or application-specific (e.g. + * a limit on the sync window for the Account) + */ +public final class Policy extends EmailContent implements EmailContent.PolicyColumns, Parcelable { + // STOPSHIP Change to false after a few days of debugging + public static final boolean DEBUG_POLICY = true; // DO NOT SUBMIT WITH THIS SET TO FALSE + public static final String TAG = "Email/Policy"; + + public static final String TABLE_NAME = "Policy"; + @SuppressWarnings("hiding") + public static final Uri CONTENT_URI = Uri.parse(EmailContent.CONTENT_URI + "/policy"); + + /* Convert days to mSec (used for password expiration) */ + private static final long DAYS_TO_MSEC = 24 * 60 * 60 * 1000; + /* Small offset (2 minutes) added to policy expiration to make user testing easier. */ + private static final long EXPIRATION_OFFSET_MSEC = 2 * 60 * 1000; + + public static final int PASSWORD_MODE_NONE = 0; + public static final int PASSWORD_MODE_SIMPLE = 1; + public static final int PASSWORD_MODE_STRONG = 2; + + public int mPasswordMode; + public int mPasswordMinLength; + public int mPasswordMaxFails; + public int mPasswordExpirationDays; + public int mPasswordHistory; + public int mPasswordComplexChars; + public int mMaxScreenLockTime; + public boolean mRequireRemoteWipe; + public boolean mRequireEncryption; + public boolean mRequireEncryptionExternal; + + public static final int CONTENT_ID_COLUMN = 0; + public static final int CONTENT_PASSWORD_MODE_COLUMN = 1; + public static final int CONTENT_PASSWORD_MIN_LENGTH_COLUMN = 2; + public static final int CONTENT_PASSWORD_EXPIRATION_DAYS_COLUMN = 3; + public static final int CONTENT_PASSWORD_HISTORY_COLUMN = 4; + public static final int CONTENT_PASSWORD_COMPLEX_CHARS_COLUMN = 5; + public static final int CONTENT_PASSWORD_MAX_FAILS_COLUMN = 6; + public static final int CONTENT_MAX_SCREEN_LOCK_TIME_COLUMN = 7; + public static final int CONTENT_REQUIRE_REMOTE_WIPE_COLUMN = 8; + public static final int CONTENT_REQUIRE_ENCRYPTION_COLUMN = 9; + public static final int CONTENT_REQUIRE_ENCRYPTION_EXTERNAL_COLUMN = 10; + + public static final String[] CONTENT_PROJECTION = new String[] {RECORD_ID, + PolicyColumns.PASSWORD_MODE, PolicyColumns.PASSWORD_MIN_LENGTH, + PolicyColumns.PASSWORD_EXPIRATION_DAYS, PolicyColumns.PASSWORD_HISTORY, + PolicyColumns.PASSWORD_COMPLEX_CHARS, PolicyColumns.PASSWORD_MAX_FAILS, + PolicyColumns.MAX_SCREEN_LOCK_TIME, PolicyColumns.REQUIRE_REMOTE_WIPE, + PolicyColumns.REQUIRE_ENCRYPTION, PolicyColumns.REQUIRE_ENCRYPTION_EXTERNAL + }; + + public static final Policy NO_POLICY = new Policy(); + + public Policy() { + mBaseUri = CONTENT_URI; + // By default, the password mode is "none" + mPasswordMode = PASSWORD_MODE_NONE; + // All server policies require the ability to wipe the device + mRequireRemoteWipe = true; + } + + public static Policy restorePolicyWithId(Context context, long id) { + return EmailContent.restoreContentWithId(context, Policy.class, Policy.CONTENT_URI, + Policy.CONTENT_PROJECTION, id); + } + + public static long getAccountIdWithPolicyKey(Context context, long id) { + return Utility.getFirstRowLong(context, Account.CONTENT_URI, Account.ID_PROJECTION, + AccountColumns.POLICY_KEY + "=?", new String[] {Long.toString(id)}, null, + Account.ID_PROJECTION_COLUMN); + } + + // We override this method to insure that we never write invalid policy data to the provider + public Uri save(Context context) { + normalize(); + return super.save(context); + } + + /** + * Associate the policy with an account; this also removes any other policy associated with + * the account and sets the policy key for the account. This is all done atomically + * @param context the caller's context + * @param account the account whose policy is to be set + * @param securitySyncKey the current security sync key for this account + */ + public void setAccountPolicy(Context context, Account account, String securitySyncKey) { + if (DEBUG_POLICY) { + Log.d(TAG, "Set policy for account " + account.mDisplayName + ": " + toString()); + } + // Make sure this is a valid policy set + normalize(); + ArrayList ops = new ArrayList(); + // Add the new policy (no account will yet reference this) + ops.add(ContentProviderOperation.newInsert( + Policy.CONTENT_URI).withValues(toContentValues()).build()); + // Delete the previous policy associated with this account, if any + if (account.mPolicyKey > 0) { + ops.add(ContentProviderOperation.newDelete( + ContentUris.withAppendedId(Policy.CONTENT_URI, account.mPolicyKey)).build()); + } + // Make the policyKey of the account our newly created policy, and set the sync key + ops.add(ContentProviderOperation.newUpdate( + ContentUris.withAppendedId(Account.CONTENT_URI, account.mId)) + .withValueBackReference(AccountColumns.POLICY_KEY, 0) + .withValue(AccountColumns.SECURITY_SYNC_KEY, securitySyncKey) + .build()); + try { + context.getContentResolver().applyBatch(EmailContent.AUTHORITY, ops); + } catch (RemoteException e) { + // This is fatal to a remote process + throw new IllegalStateException("Exception setting account policy."); + } catch (OperationApplicationException e) { + // Can't happen; our provider doesn't throw this exception + } + } + + /** + * Clear any existing policy for a given account and clear the account's security sync key, + * and do so atomically + * @param context the caller's context + * @param account the account whose policy is to be cleared + */ + public static void clearAccountPolicy(Context context, Account account) { + if (DEBUG_POLICY) { + Log.d(TAG, "Clearing policy for account: " + account.mDisplayName); + } + ArrayList ops = new ArrayList(); + // Delete the previous policy associated with this account, if any + if (account.mPolicyKey > 0) { + ops.add(ContentProviderOperation.newDelete( + ContentUris.withAppendedId(Policy.CONTENT_URI, account.mPolicyKey)).build()); + } + // Clear the security sync key and policy key + ops.add(ContentProviderOperation.newUpdate( + ContentUris.withAppendedId(Account.CONTENT_URI, account.mId)) + .withValue(AccountColumns.SECURITY_SYNC_KEY, null) + .withValue(AccountColumns.POLICY_KEY, 0) + .build()); + try { + context.getContentResolver().applyBatch(EmailContent.AUTHORITY, ops); + } catch (RemoteException e) { + // This is fatal to a remote process + throw new IllegalStateException("Exception setting account policy."); + } catch (OperationApplicationException e) { + // Can't happen; our provider doesn't throw this exception + } + } + + /** + * Normalize the Policy. If the password mode is "none", zero out all password-related fields; + * zero out complex characters for simple passwords. + */ + public void normalize() { + if (mPasswordMode == PASSWORD_MODE_NONE) { + mPasswordMaxFails = 0; + mMaxScreenLockTime = 0; + mPasswordMinLength = 0; + mPasswordComplexChars = 0; + mPasswordHistory = 0; + mPasswordExpirationDays = 0; + } else { + if ((mPasswordMode != PASSWORD_MODE_SIMPLE) && + (mPasswordMode != PASSWORD_MODE_STRONG)) { + throw new IllegalArgumentException("password mode"); + } + // If we're only requiring a simple password, set complex chars to zero; note + // that EAS can erroneously send non-zero values in this case + if (mPasswordMode == PASSWORD_MODE_SIMPLE) { + mPasswordComplexChars = 0; + } + } + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Policy)) return false; + Policy otherPolicy = (Policy)other; + if (mRequireEncryption != otherPolicy.mRequireEncryption) return false; + if (mRequireEncryptionExternal != otherPolicy.mRequireEncryptionExternal) return false; + if (mRequireRemoteWipe != otherPolicy.mRequireRemoteWipe) return false; + if (mMaxScreenLockTime != otherPolicy.mMaxScreenLockTime) return false; + if (mPasswordComplexChars != otherPolicy.mPasswordComplexChars) return false; + if (mPasswordExpirationDays != otherPolicy.mPasswordExpirationDays) return false; + if (mPasswordHistory != otherPolicy.mPasswordHistory) return false; + if (mPasswordMaxFails != otherPolicy.mPasswordMaxFails) return false; + if (mPasswordMinLength != otherPolicy.mPasswordMinLength) return false; + if (mPasswordMode != otherPolicy.mPasswordMode) return false; + return true; + } + + @Override + public int hashCode() { + int code = mRequireEncryption ? 1 : 0; + code += (mRequireEncryptionExternal ? 1 : 0) << 1; + code += (mRequireRemoteWipe ? 1 : 0) << 2; + code += (mMaxScreenLockTime << 3); + code += (mPasswordComplexChars << 6); + code += (mPasswordExpirationDays << 12); + code += (mPasswordHistory << 15); + code += (mPasswordMaxFails << 18); + code += (mPasswordMinLength << 22); + code += (mPasswordMode << 26); + return code; + } + + @Override + public void restore(Cursor cursor) { + mBaseUri = CONTENT_URI; + mId = cursor.getLong(CONTENT_ID_COLUMN); + mPasswordMode = cursor.getInt(CONTENT_PASSWORD_MODE_COLUMN); + mPasswordMinLength = cursor.getInt(CONTENT_PASSWORD_MIN_LENGTH_COLUMN); + mPasswordMaxFails = cursor.getInt(CONTENT_PASSWORD_MAX_FAILS_COLUMN); + mPasswordHistory = cursor.getInt(CONTENT_PASSWORD_HISTORY_COLUMN); + mPasswordExpirationDays = cursor.getInt(CONTENT_PASSWORD_EXPIRATION_DAYS_COLUMN); + mPasswordComplexChars = cursor.getInt(CONTENT_PASSWORD_COMPLEX_CHARS_COLUMN); + mMaxScreenLockTime = cursor.getInt(CONTENT_MAX_SCREEN_LOCK_TIME_COLUMN); + mRequireRemoteWipe = cursor.getInt(CONTENT_REQUIRE_REMOTE_WIPE_COLUMN) == 1; + mRequireEncryption = cursor.getInt(CONTENT_REQUIRE_ENCRYPTION_COLUMN) == 1; + mRequireEncryptionExternal = + cursor.getInt(CONTENT_REQUIRE_ENCRYPTION_EXTERNAL_COLUMN) == 1; + } + + @Override + public ContentValues toContentValues() { + ContentValues values = new ContentValues(); + values.put(PolicyColumns.PASSWORD_MODE, mPasswordMode); + values.put(PolicyColumns.PASSWORD_MIN_LENGTH, mPasswordMinLength); + values.put(PolicyColumns.PASSWORD_MAX_FAILS, mPasswordMaxFails); + values.put(PolicyColumns.PASSWORD_HISTORY, mPasswordHistory); + values.put(PolicyColumns.PASSWORD_EXPIRATION_DAYS, mPasswordExpirationDays); + values.put(PolicyColumns.PASSWORD_COMPLEX_CHARS, mPasswordComplexChars); + values.put(PolicyColumns.MAX_SCREEN_LOCK_TIME, mMaxScreenLockTime); + values.put(PolicyColumns.REQUIRE_REMOTE_WIPE, mRequireRemoteWipe); + values.put(PolicyColumns.REQUIRE_ENCRYPTION, mRequireEncryption); + values.put(PolicyColumns.REQUIRE_ENCRYPTION_EXTERNAL, mRequireEncryptionExternal); + return values; + } + + /** + * Helper to map our internal encoding to DevicePolicyManager password modes. + */ + public int getDPManagerPasswordQuality() { + switch (mPasswordMode) { + case PASSWORD_MODE_SIMPLE: + return DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; + case PASSWORD_MODE_STRONG: + if (mPasswordComplexChars == 0) { + return DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; + } else { + return DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; + } + default: + return DevicePolicyManager .PASSWORD_QUALITY_UNSPECIFIED; + } + } + + /** + * Helper to map expiration times to the millisecond values used by DevicePolicyManager. + */ + public long getDPManagerPasswordExpirationTimeout() { + long result = mPasswordExpirationDays * DAYS_TO_MSEC; + // Add a small offset to the password expiration. This makes it easier to test + // by changing (for example) 1 day to 1 day + 5 minutes. If you set an expiration + // that is within the warning period, you should get a warning fairly quickly. + if (result > 0) { + result += EXPIRATION_OFFSET_MSEC; + } + return result; + } + + private void appendPolicy(StringBuilder sb, String code, int value) { + sb.append(code); + sb.append(":"); + sb.append(value); + sb.append(" "); + } + + public String toString() { + StringBuilder sb = new StringBuilder("["); + if (this.equals(NO_POLICY)) { + sb.append("No policies]"); + } else { + if (mPasswordMode == PASSWORD_MODE_NONE) { + sb.append("Pwd no "); + } else { + appendPolicy(sb, "Pwd strong", mPasswordMode == PASSWORD_MODE_STRONG ? 1 : 0); + appendPolicy(sb, "len", mPasswordMinLength); + appendPolicy(sb, "cmpx", mPasswordComplexChars); + appendPolicy(sb, "expy", mPasswordExpirationDays); + appendPolicy(sb, "hist", mPasswordHistory); + appendPolicy(sb, "fail", mPasswordMaxFails); + appendPolicy(sb, "idle", mMaxScreenLockTime); + } + appendPolicy(sb, "crypt", mRequireEncryption ? 1 : 0); + appendPolicy(sb, "crypt/ex", mRequireEncryptionExternal ? 1 : 0); + sb.append("]"); + } + return sb.toString(); + } + + /** + * Supports Parcelable + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Supports Parcelable + */ + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public Policy createFromParcel(Parcel in) { + return new Policy(in); + } + + public Policy[] newArray(int size) { + return new Policy[size]; + } + }; + + /** + * Supports Parcelable + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + // mBaseUri is not parceled + dest.writeLong(mId); + dest.writeInt(mPasswordMode); + dest.writeInt(mPasswordMinLength); + dest.writeInt(mPasswordMaxFails); + dest.writeInt(mPasswordHistory); + dest.writeInt(mPasswordExpirationDays); + dest.writeInt(mPasswordComplexChars); + dest.writeInt(mMaxScreenLockTime); + dest.writeInt(mRequireRemoteWipe ? 1 : 0); + dest.writeInt(mRequireEncryption ? 1 : 0); + dest.writeInt(mRequireEncryptionExternal ? 1 : 0); + } + + /** + * Supports Parcelable + */ + public Policy(Parcel in) { + mBaseUri = CONTENT_URI; + mId = in.readLong(); + mPasswordMode = in.readInt(); + mPasswordMinLength = in.readInt(); + mPasswordMaxFails = in.readInt(); + mPasswordHistory = in.readInt(); + mPasswordExpirationDays = in.readInt(); + mPasswordComplexChars = in.readInt(); + mMaxScreenLockTime = in.readInt(); + mRequireRemoteWipe = in.readInt() == 1; + mRequireEncryption = in.readInt() == 1; + mRequireEncryptionExternal = in.readInt() == 1; + } +} \ No newline at end of file diff --git a/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java b/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java index cc41505e5..3a274f50f 100644 --- a/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java +++ b/emailcommon/src/com/android/emailcommon/service/EmailServiceProxy.java @@ -20,6 +20,7 @@ import com.android.emailcommon.Api; import com.android.emailcommon.Device; import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.provider.EmailContent.HostAuth; +import com.android.emailcommon.provider.Policy; import android.content.Context; import android.content.Intent; @@ -212,7 +213,7 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { return bundle; } else { Bundle bundle = (Bundle) mReturn; - bundle.setClassLoader(PolicySet.class.getClassLoader()); + bundle.setClassLoader(Policy.class.getClassLoader()); Log.v(TAG, "validate returns " + bundle.getInt(VALIDATE_BUNDLE_RESULT_CODE)); return bundle; } diff --git a/emailcommon/src/com/android/emailcommon/service/IPolicyService.aidl b/emailcommon/src/com/android/emailcommon/service/IPolicyService.aidl index 646abf353..e9bcf42dd 100644 --- a/emailcommon/src/com/android/emailcommon/service/IPolicyService.aidl +++ b/emailcommon/src/com/android/emailcommon/service/IPolicyService.aidl @@ -15,16 +15,16 @@ */ package com.android.emailcommon.service; -import com.android.emailcommon.service.PolicySet; +import com.android.emailcommon.provider.Policy; interface IPolicyService { - boolean isActive(in PolicySet policies); + boolean isActive(in Policy policies); void policiesRequired(long accountId); - void updatePolicies(long accountId); + void policiesUpdated(long accountId); void setAccountHoldFlag(long accountId, boolean newState); boolean isActiveAdmin(); // This is about as oneway as you can get oneway void remoteWipe(); - boolean isSupported(in PolicySet policies); - PolicySet clearUnsupportedPolicies(in PolicySet policies); + boolean isSupported(in Policy policies); + Policy clearUnsupportedPolicies(in Policy policies); } \ No newline at end of file diff --git a/emailcommon/src/com/android/emailcommon/service/LegacyPolicySet.java b/emailcommon/src/com/android/emailcommon/service/LegacyPolicySet.java new file mode 100644 index 000000000..e5d443688 --- /dev/null +++ b/emailcommon/src/com/android/emailcommon/service/LegacyPolicySet.java @@ -0,0 +1,87 @@ +/* Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.emailcommon.service; + +import com.android.emailcommon.provider.Policy; + +/** + * Legacy class for policy storage as a bit field of flags + */ +public class LegacyPolicySet { + + // Security (provisioning) flags + // bits 0..4: password length (0=no password required) + public static final int PASSWORD_LENGTH_MASK = 31; + public static final int PASSWORD_LENGTH_SHIFT = 0; + public static final int PASSWORD_LENGTH_MAX = 30; + // bits 5..8: password mode + public static final int PASSWORD_MODE_SHIFT = 5; + public static final int PASSWORD_MODE_MASK = 15 << PASSWORD_MODE_SHIFT; + public static final int PASSWORD_MODE_NONE = 0 << PASSWORD_MODE_SHIFT; + public static final int PASSWORD_MODE_SIMPLE = 1 << PASSWORD_MODE_SHIFT; + public static final int PASSWORD_MODE_STRONG = 2 << PASSWORD_MODE_SHIFT; + // bits 9..13: password failures -> wipe device (0=disabled) + public static final int PASSWORD_MAX_FAILS_SHIFT = 9; + public static final int PASSWORD_MAX_FAILS_MASK = 31 << PASSWORD_MAX_FAILS_SHIFT; + public static final int PASSWORD_MAX_FAILS_MAX = 31; + // bits 14..24: seconds to screen lock (0=not required) + public static final int SCREEN_LOCK_TIME_SHIFT = 14; + public static final int SCREEN_LOCK_TIME_MASK = 2047 << SCREEN_LOCK_TIME_SHIFT; + public static final int SCREEN_LOCK_TIME_MAX = 2047; + // bit 25: remote wipe capability required + public static final int REQUIRE_REMOTE_WIPE = 1 << 25; + // bit 26..35: password expiration (days; 0=not required) + public static final int PASSWORD_EXPIRATION_SHIFT = 26; + public static final long PASSWORD_EXPIRATION_MASK = 1023L << PASSWORD_EXPIRATION_SHIFT; + public static final int PASSWORD_EXPIRATION_MAX = 1023; + // bit 36..43: password history (length; 0=not required) + public static final int PASSWORD_HISTORY_SHIFT = 36; + public static final long PASSWORD_HISTORY_MASK = 255L << PASSWORD_HISTORY_SHIFT; + public static final int PASSWORD_HISTORY_MAX = 255; + // bit 44..48: min complex characters (0=not required) + public static final int PASSWORD_COMPLEX_CHARS_SHIFT = 44; + public static final long PASSWORD_COMPLEX_CHARS_MASK = 31L << PASSWORD_COMPLEX_CHARS_SHIFT; + public static final int PASSWORD_COMPLEX_CHARS_MAX = 31; + // bit 49: requires device encryption (internal) + public static final long REQUIRE_ENCRYPTION = 1L << 49; + // bit 50: requires external storage encryption + public static final long REQUIRE_ENCRYPTION_EXTERNAL = 1L << 50; + + /** + * Convert legacy policy flags to a Policy + * @param flags legacy policy flags + * @return a Policy representing the legacy policy flag + */ + public static Policy flagsToPolicy(long flags) { + Policy policy = new Policy(); + policy.mPasswordMode = ((int) (flags & PASSWORD_MODE_MASK)) >> PASSWORD_MODE_SHIFT; + policy.mPasswordMinLength = (int) ((flags & PASSWORD_LENGTH_MASK) >> PASSWORD_LENGTH_SHIFT); + policy.mPasswordMaxFails = + (int) ((flags & PASSWORD_MAX_FAILS_MASK) >> PASSWORD_MAX_FAILS_SHIFT); + policy.mPasswordComplexChars = + (int) ((flags & PASSWORD_COMPLEX_CHARS_MASK) >> PASSWORD_COMPLEX_CHARS_SHIFT); + policy.mPasswordHistory = (int) ((flags & PASSWORD_HISTORY_MASK) >> PASSWORD_HISTORY_SHIFT); + policy.mPasswordExpirationDays = + (int) ((flags & PASSWORD_EXPIRATION_MASK) >> PASSWORD_EXPIRATION_SHIFT); + policy.mMaxScreenLockTime = + (int) ((flags & SCREEN_LOCK_TIME_MASK) >> SCREEN_LOCK_TIME_SHIFT); + policy.mRequireRemoteWipe = 0 != (flags & REQUIRE_REMOTE_WIPE); + policy.mRequireEncryption = 0 != (flags & REQUIRE_ENCRYPTION); + policy.mRequireEncryptionExternal = 0 != (flags & REQUIRE_ENCRYPTION_EXTERNAL); + return policy; + } +} + diff --git a/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java b/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java index 45e868a2e..53d7a24ac 100644 --- a/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java +++ b/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java @@ -17,6 +17,7 @@ package com.android.emailcommon.service; import com.android.emailcommon.provider.EmailContent.Account; +import com.android.emailcommon.provider.Policy; import android.content.Context; import android.content.Intent; @@ -48,7 +49,7 @@ public class PolicyServiceProxy extends ServiceProxy implements IPolicyService { } @Override - public PolicySet clearUnsupportedPolicies(final PolicySet arg0) throws RemoteException { + public Policy clearUnsupportedPolicies(final Policy arg0) throws RemoteException { setTask(new ProxyTask() { public void run() throws RemoteException { mReturn = mService.clearUnsupportedPolicies(arg0); @@ -62,12 +63,12 @@ public class PolicyServiceProxy extends ServiceProxy implements IPolicyService { // Can this happen? return null; } else { - return (PolicySet)mReturn; + return (Policy)mReturn; } } @Override - public boolean isActive(final PolicySet arg0) throws RemoteException { + public boolean isActive(final Policy arg0) throws RemoteException { setTask(new ProxyTask() { public void run() throws RemoteException { mReturn = mService.isActive(arg0); @@ -105,7 +106,7 @@ public class PolicyServiceProxy extends ServiceProxy implements IPolicyService { } @Override - public boolean isSupported(final PolicySet arg0) throws RemoteException { + public boolean isSupported(final Policy arg0) throws RemoteException { setTask(new ProxyTask() { public void run() throws RemoteException { mReturn = mService.isSupported(arg0); @@ -151,16 +152,16 @@ public class PolicyServiceProxy extends ServiceProxy implements IPolicyService { } @Override - public void updatePolicies(final long arg0) throws RemoteException { + public void policiesUpdated(final long arg0) throws RemoteException { setTask(new ProxyTask() { public void run() throws RemoteException { - mService.updatePolicies(arg0); + mService.policiesUpdated(arg0); } - }, "updatePolicies"); + }, "policiesUpdated"); } // Static methods that encapsulate the proxy calls above - public static boolean isActive(Context context, PolicySet policies) { + public static boolean isActive(Context context, Policy policies) { try { return new PolicyServiceProxy(context).isActive(policies); } catch (RemoteException e) { @@ -176,9 +177,9 @@ public class PolicyServiceProxy extends ServiceProxy implements IPolicyService { } } - public static void updatePolicies(Context context, long accountId) { + public static void policiesUpdated(Context context, long accountId) { try { - new PolicyServiceProxy(context).updatePolicies(accountId); + new PolicyServiceProxy(context).policiesUpdated(accountId); } catch (RemoteException e) { throw new IllegalStateException("PolicyService transaction failed"); } @@ -208,17 +209,17 @@ public class PolicyServiceProxy extends ServiceProxy implements IPolicyService { } } - public static boolean isSupported(Context context, PolicySet policies) { + public static boolean isSupported(Context context, Policy policy) { try { - return new PolicyServiceProxy(context).isSupported(policies); + return new PolicyServiceProxy(context).isSupported(policy); } catch (RemoteException e) { } return false; } - public static PolicySet clearUnsupportedPolicies(Context context, PolicySet policies) { + public static Policy clearUnsupportedPolicies(Context context, Policy policy) { try { - return new PolicyServiceProxy(context).clearUnsupportedPolicies(policies); + return new PolicyServiceProxy(context).clearUnsupportedPolicies(policy); } catch (RemoteException e) { } throw new IllegalStateException("PolicyService transaction failed"); diff --git a/emailcommon/src/com/android/emailcommon/service/PolicySet.java b/emailcommon/src/com/android/emailcommon/service/PolicySet.java deleted file mode 100644 index 70538b440..000000000 --- a/emailcommon/src/com/android/emailcommon/service/PolicySet.java +++ /dev/null @@ -1,375 +0,0 @@ -/* Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.emailcommon.service; - -import com.android.emailcommon.provider.EmailContent.Account; -import com.android.emailcommon.provider.EmailContent.AccountColumns; - -import android.app.admin.DevicePolicyManager; -import android.content.ContentValues; -import android.content.Context; -import android.os.Parcel; -import android.os.Parcelable; - - -/** - * Class for tracking policies and reading/writing into accounts - */ -public class PolicySet implements Parcelable { - - // Security (provisioning) flags - // bits 0..4: password length (0=no password required) - private static final int PASSWORD_LENGTH_MASK = 31; - private static final int PASSWORD_LENGTH_SHIFT = 0; - public static final int PASSWORD_LENGTH_MAX = 30; - // bits 5..8: password mode - private static final int PASSWORD_MODE_SHIFT = 5; - private static final int PASSWORD_MODE_MASK = 15 << PASSWORD_MODE_SHIFT; - public static final int PASSWORD_MODE_NONE = 0 << PASSWORD_MODE_SHIFT; - public static final int PASSWORD_MODE_SIMPLE = 1 << PASSWORD_MODE_SHIFT; - public static final int PASSWORD_MODE_STRONG = 2 << PASSWORD_MODE_SHIFT; - // bits 9..13: password failures -> wipe device (0=disabled) - private static final int PASSWORD_MAX_FAILS_SHIFT = 9; - private static final int PASSWORD_MAX_FAILS_MASK = 31 << PASSWORD_MAX_FAILS_SHIFT; - public static final int PASSWORD_MAX_FAILS_MAX = 31; - // bits 14..24: seconds to screen lock (0=not required) - private static final int SCREEN_LOCK_TIME_SHIFT = 14; - private static final int SCREEN_LOCK_TIME_MASK = 2047 << SCREEN_LOCK_TIME_SHIFT; - public static final int SCREEN_LOCK_TIME_MAX = 2047; - // bit 25: remote wipe capability required - private static final int REQUIRE_REMOTE_WIPE = 1 << 25; - // bit 26..35: password expiration (days; 0=not required) - private static final int PASSWORD_EXPIRATION_SHIFT = 26; - private static final long PASSWORD_EXPIRATION_MASK = 1023L << PASSWORD_EXPIRATION_SHIFT; - public static final int PASSWORD_EXPIRATION_MAX = 1023; - // bit 36..43: password history (length; 0=not required) - private static final int PASSWORD_HISTORY_SHIFT = 36; - private static final long PASSWORD_HISTORY_MASK = 255L << PASSWORD_HISTORY_SHIFT; - public static final int PASSWORD_HISTORY_MAX = 255; - // bit 44..48: min complex characters (0=not required) - private static final int PASSWORD_COMPLEX_CHARS_SHIFT = 44; - private static final long PASSWORD_COMPLEX_CHARS_MASK = 31L << PASSWORD_COMPLEX_CHARS_SHIFT; - public static final int PASSWORD_COMPLEX_CHARS_MAX = 31; - // bit 49: requires device encryption (internal) - private static final long REQUIRE_ENCRYPTION = 1L << 49; - // bit 50: requires external storage encryption - private static final long REQUIRE_ENCRYPTION_EXTERNAL = 1L << 50; - - /* Convert days to mSec (used for password expiration) */ - private static final long DAYS_TO_MSEC = 24 * 60 * 60 * 1000; - /* Small offset (2 minutes) added to policy expiration to make user testing easier. */ - private static final long EXPIRATION_OFFSET_MSEC = 2 * 60 * 1000; - - public final int mMinPasswordLength; - public final int mPasswordMode; - public final int mMaxPasswordFails; - public final int mMaxScreenLockTime; - public final boolean mRequireRemoteWipe; - public final int mPasswordExpirationDays; - public final int mPasswordHistory; - public final int mPasswordComplexChars; - public final boolean mRequireEncryption; - public final boolean mRequireEncryptionExternal; - - public int getMinPasswordLengthForTest() { - return mMinPasswordLength; - } - - public int getPasswordModeForTest() { - return mPasswordMode; - } - - public int getMaxPasswordFailsForTest() { - return mMaxPasswordFails; - } - - public int getMaxScreenLockTimeForTest() { - return mMaxScreenLockTime; - } - - public boolean isRequireRemoteWipeForTest() { - return mRequireRemoteWipe; - } - - public boolean isRequireEncryptionForTest() { - return mRequireEncryption; - } - - public boolean isRequireEncryptionExternalForTest() { - return mRequireEncryptionExternal; - } - - /** - * Create from raw values. - * @param minPasswordLength (0=not enforced) - * @param passwordMode - * @param maxPasswordFails (0=not enforced) - * @param maxScreenLockTime in seconds (0=not enforced) - * @param requireRemoteWipe - * @param passwordExpirationDays in days (0=not enforced) - * @param passwordHistory (0=not enforced) - * @param passwordComplexChars (0=not enforced) - * @param requireEncryption - * @param requireEncryptionExternal - * @throws IllegalArgumentException for illegal arguments. - */ - public PolicySet(int minPasswordLength, int passwordMode, int maxPasswordFails, - int maxScreenLockTime, boolean requireRemoteWipe, int passwordExpirationDays, - int passwordHistory, int passwordComplexChars, boolean requireEncryption, - boolean requireEncryptionExternal) throws IllegalArgumentException { - // If we're not enforcing passwords, make sure we clean up related values, since EAS - // can send non-zero values for any or all of these - if (passwordMode == PASSWORD_MODE_NONE) { - maxPasswordFails = 0; - maxScreenLockTime = 0; - minPasswordLength = 0; - passwordComplexChars = 0; - passwordHistory = 0; - passwordExpirationDays = 0; - } else { - if ((passwordMode != PASSWORD_MODE_SIMPLE) && - (passwordMode != PASSWORD_MODE_STRONG)) { - throw new IllegalArgumentException("password mode"); - } - // If we're only requiring a simple password, set complex chars to zero; note - // that EAS can erroneously send non-zero values in this case - if (passwordMode == PASSWORD_MODE_SIMPLE) { - passwordComplexChars = 0; - } - // The next four values have hard limits which cannot be supported if exceeded. - if (minPasswordLength > PASSWORD_LENGTH_MAX) { - throw new IllegalArgumentException("password length"); - } - if (passwordExpirationDays > PASSWORD_EXPIRATION_MAX) { - throw new IllegalArgumentException("password expiration"); - } - if (passwordHistory > PASSWORD_HISTORY_MAX) { - throw new IllegalArgumentException("password history"); - } - if (passwordComplexChars > PASSWORD_COMPLEX_CHARS_MAX) { - throw new IllegalArgumentException("complex chars"); - } - // This value can be reduced (which actually increases security) if necessary - if (maxPasswordFails > PASSWORD_MAX_FAILS_MAX) { - maxPasswordFails = PASSWORD_MAX_FAILS_MAX; - } - // This value can be reduced (which actually increases security) if necessary - if (maxScreenLockTime > SCREEN_LOCK_TIME_MAX) { - maxScreenLockTime = SCREEN_LOCK_TIME_MAX; - } - } - mMinPasswordLength = minPasswordLength; - mPasswordMode = passwordMode; - mMaxPasswordFails = maxPasswordFails; - mMaxScreenLockTime = maxScreenLockTime; - mRequireRemoteWipe = requireRemoteWipe; - mPasswordExpirationDays = passwordExpirationDays; - mPasswordHistory = passwordHistory; - mPasswordComplexChars = passwordComplexChars; - mRequireEncryption = requireEncryption; - mRequireEncryptionExternal = requireEncryptionExternal; - } - - /** - * Create from values encoded in an account - * @param account - */ - public PolicySet(Account account) { - this(account.mSecurityFlags); - } - - /** - * Create from values encoded in an account flags int - */ - public PolicySet(long flags) { - mMinPasswordLength = - (int) ((flags & PASSWORD_LENGTH_MASK) >> PASSWORD_LENGTH_SHIFT); - mPasswordMode = - (int) (flags & PASSWORD_MODE_MASK); - mMaxPasswordFails = - (int) ((flags & PASSWORD_MAX_FAILS_MASK) >> PASSWORD_MAX_FAILS_SHIFT); - mMaxScreenLockTime = - (int) ((flags & SCREEN_LOCK_TIME_MASK) >> SCREEN_LOCK_TIME_SHIFT); - mRequireRemoteWipe = 0 != (flags & REQUIRE_REMOTE_WIPE); - mPasswordExpirationDays = - (int) ((flags & PASSWORD_EXPIRATION_MASK) >> PASSWORD_EXPIRATION_SHIFT); - mPasswordHistory = - (int) ((flags & PASSWORD_HISTORY_MASK) >> PASSWORD_HISTORY_SHIFT); - mPasswordComplexChars = - (int) ((flags & PASSWORD_COMPLEX_CHARS_MASK) >> PASSWORD_COMPLEX_CHARS_SHIFT); - mRequireEncryption = 0 != (flags & REQUIRE_ENCRYPTION); - mRequireEncryptionExternal = 0 != (flags & REQUIRE_ENCRYPTION_EXTERNAL); - } - - /** - * Helper to map our internal encoding to DevicePolicyManager password modes. - */ - public int getDPManagerPasswordQuality() { - switch (mPasswordMode) { - case PASSWORD_MODE_SIMPLE: - return DevicePolicyManager.PASSWORD_QUALITY_NUMERIC; - case PASSWORD_MODE_STRONG: - if (mPasswordComplexChars == 0) { - return DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC; - } else { - return DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; - } - default: - return DevicePolicyManager .PASSWORD_QUALITY_UNSPECIFIED; - } - } - - /** - * Helper to map expiration times to the millisecond values used by DevicePolicyManager. - */ - public long getDPManagerPasswordExpirationTimeout() { - long result = mPasswordExpirationDays * DAYS_TO_MSEC; - // Add a small offset to the password expiration. This makes it easier to test - // by changing (for example) 1 day to 1 day + 5 minutes. If you set an expiration - // that is within the warning period, you should get a warning fairly quickly. - if (result > 0) { - result += EXPIRATION_OFFSET_MSEC; - } - return result; - } - - /** - * 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 boolean writeAccount(Account account, String syncKey, boolean update, - Context context) { - long newFlags = getSecurityCode(); - 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 - public boolean equals(Object o) { - if (o instanceof PolicySet) { - PolicySet other = (PolicySet)o; - return (this.getSecurityCode() == other.getSecurityCode()); - } - return false; - } - - /** - * Supports Parcelable - */ - public int describeContents() { - return 0; - } - - /** - * Supports Parcelable - */ - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { - public PolicySet createFromParcel(Parcel in) { - return new PolicySet(in); - } - - public PolicySet[] newArray(int size) { - return new PolicySet[size]; - } - }; - - /** - * Supports Parcelable - */ - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mMinPasswordLength); - dest.writeInt(mPasswordMode); - dest.writeInt(mMaxPasswordFails); - dest.writeInt(mMaxScreenLockTime); - dest.writeInt(mRequireRemoteWipe ? 1 : 0); - dest.writeInt(mPasswordExpirationDays); - dest.writeInt(mPasswordHistory); - dest.writeInt(mPasswordComplexChars); - dest.writeInt(mRequireEncryption ? 1 : 0); - dest.writeInt(mRequireEncryptionExternal ? 1 : 0); - } - - /** - * Supports Parcelable - */ - public PolicySet(Parcel in) { - mMinPasswordLength = in.readInt(); - mPasswordMode = in.readInt(); - mMaxPasswordFails = in.readInt(); - mMaxScreenLockTime = in.readInt(); - mRequireRemoteWipe = in.readInt() == 1; - mPasswordExpirationDays = in.readInt(); - mPasswordHistory = in.readInt(); - mPasswordComplexChars = in.readInt(); - mRequireEncryption = in.readInt() == 1; - mRequireEncryptionExternal = in.readInt() == 1; - } - - @Override - public int hashCode() { - long code = getSecurityCode(); - return (int) code; - } - - public long getSecurityCode() { - long flags = 0; - flags = (long)mMinPasswordLength << PASSWORD_LENGTH_SHIFT; - flags |= mPasswordMode; - flags |= (long)mMaxPasswordFails << PASSWORD_MAX_FAILS_SHIFT; - flags |= (long)mMaxScreenLockTime << SCREEN_LOCK_TIME_SHIFT; - if (mRequireRemoteWipe) flags |= REQUIRE_REMOTE_WIPE; - flags |= (long)mPasswordHistory << PASSWORD_HISTORY_SHIFT; - flags |= (long)mPasswordExpirationDays << PASSWORD_EXPIRATION_SHIFT; - flags |= (long)mPasswordComplexChars << PASSWORD_COMPLEX_CHARS_SHIFT; - if (mRequireEncryption) flags |= REQUIRE_ENCRYPTION; - if (mRequireEncryptionExternal) flags |= REQUIRE_ENCRYPTION_EXTERNAL; - return flags; - } - - @Override - public String toString() { - return "{ " + "pw-len-min=" + mMinPasswordLength + " pw-mode=" + mPasswordMode - + " pw-fails-max=" + mMaxPasswordFails + " screenlock-max=" - + mMaxScreenLockTime + " remote-wipe-req=" + mRequireRemoteWipe - + " pw-expiration=" + mPasswordExpirationDays - + " pw-history=" + mPasswordHistory - + " pw-complex-chars=" + mPasswordComplexChars - + " require-encryption=" + mRequireEncryption - + " require-encryptionExternal=" + mRequireEncryptionExternal + "}"; - } -} - diff --git a/src/com/android/email/AccountBackupRestore.java b/src/com/android/email/AccountBackupRestore.java index d545e7b4a..a0d38523c 100644 --- a/src/com/android/email/AccountBackupRestore.java +++ b/src/com/android/email/AccountBackupRestore.java @@ -71,7 +71,7 @@ public class AccountBackupRestore { // after restoring accounts, register services appropriately Log.w(Logging.LOG_TAG, "Register services after restoring accounts"); // update security profile - SecurityPolicy.getInstance(context).updatePolicies(-1); + SecurityPolicy.getInstance(context).policiesUpdated(-1); // enable/disable other email services as necessary Email.setServicesEnabledSync(context); ExchangeUtils.startExchangeService(context); diff --git a/src/com/android/email/Controller.java b/src/com/android/email/Controller.java index 280b6eac7..ed130e316 100644 --- a/src/com/android/email/Controller.java +++ b/src/com/android/email/Controller.java @@ -37,6 +37,7 @@ import com.android.emailcommon.service.IEmailServiceCallback; import com.android.emailcommon.utility.AttachmentUtilities; import com.android.emailcommon.utility.EmailAsyncTask; import com.android.emailcommon.utility.Utility; +import com.google.common.annotations.VisibleForTesting; import android.app.Service; import android.content.ContentResolver; @@ -1005,6 +1006,15 @@ public class Controller { }); } + /** + * Backup our accounts; define this here so that unit tests can override the behavior + * @param context the caller's context + */ + @VisibleForTesting + protected void backupAccounts(Context context) { + AccountBackupRestore.backupAccounts(context); + } + /** * Delete an account synchronously. */ @@ -1030,8 +1040,7 @@ public class Controller { EmailContent.Account.CONTENT_URI, accountId); context.getContentResolver().delete(uri, null, null); - // Update the backup (side copy) of the accounts - AccountBackupRestore.backupAccounts(context); + backupAccounts(context); // Release or relax device administration, if relevant SecurityPolicy.getInstance(context).reducePolicies(); diff --git a/src/com/android/email/LegacyConversions.java b/src/com/android/email/LegacyConversions.java index c937fbc38..10ec6d33b 100644 --- a/src/com/android/email/LegacyConversions.java +++ b/src/com/android/email/LegacyConversions.java @@ -26,9 +26,9 @@ import com.android.emailcommon.internet.TextBody; import com.android.emailcommon.mail.Address; import com.android.emailcommon.mail.Flag; import com.android.emailcommon.mail.Message; +import com.android.emailcommon.mail.Message.RecipientType; import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.mail.Part; -import com.android.emailcommon.mail.Message.RecipientType; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.Attachment; import com.android.emailcommon.provider.EmailContent.AttachmentColumns; @@ -475,7 +475,6 @@ public class LegacyConversions { result.setRingtone(fromAccount.mRingtoneUri); result.mProtocolVersion = fromAccount.mProtocolVersion; // int fromAccount.mNewMessageCount = will be reset on next sync - result.mSecurityFlags = fromAccount.mSecurityFlags; result.mSignature = fromAccount.mSignature; // Use the existing conversions from HostAuth <-> Uri @@ -518,10 +517,9 @@ public class LegacyConversions { result.setRingtone(fromAccount.getRingtone()); result.mProtocolVersion = fromAccount.mProtocolVersion; result.mNewMessageCount = 0; - result.mSecurityFlags = fromAccount.mSecurityFlags; result.mSecuritySyncKey = null; + result.mPolicyKey = 0; result.mSignature = fromAccount.mSignature; - try { HostAuth recvAuth = result.getOrCreateHostAuthRecv(context); Utility.setHostAuthFromString(recvAuth, fromAccount.getStoreUri()); diff --git a/src/com/android/email/SecurityPolicy.java b/src/com/android/email/SecurityPolicy.java index 557156b7e..f8c482683 100644 --- a/src/com/android/email/SecurityPolicy.java +++ b/src/com/android/email/SecurityPolicy.java @@ -16,13 +16,15 @@ package com.android.email; -import com.android.email.activity.setup.AccountSecurity; import com.android.email.service.EmailBroadcastProcessorService; import com.android.emailcommon.Logging; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.Account; import com.android.emailcommon.provider.EmailContent.AccountColumns; -import com.android.emailcommon.service.PolicySet; +import com.android.emailcommon.provider.EmailContent.PolicyColumns; +import com.android.emailcommon.provider.Policy; +import com.android.emailcommon.utility.Utility; +import com.google.common.annotations.VisibleForTesting; import android.app.admin.DeviceAdminInfo; import android.app.admin.DeviceAdminReceiver; @@ -41,24 +43,12 @@ import android.util.Log; * into and out of various security states. */ public class SecurityPolicy { - private static final String TAG = "SecurityPolicy"; + private static final String TAG = "Email/SecurityPolicy"; private static SecurityPolicy sInstance = null; private Context mContext; private DevicePolicyManager mDPM; private ComponentName mAdminName; - private PolicySet mAggregatePolicy; - - /* package */ static final PolicySet NO_POLICY_SET = - new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false, 0, 0, 0, false, false); - - /** - * This projection on Account is for scanning/reading - */ - private static final String[] ACCOUNT_SECURITY_PROJECTION = new String[] { - AccountColumns.ID, AccountColumns.SECURITY_FLAGS - }; - private static final int ACCOUNT_SECURITY_COLUMN_ID = 0; - private static final int ACCOUNT_SECURITY_COLUMN_FLAGS = 1; + private Policy mAggregatePolicy; // Messages used for DevicePolicyManager callbacks private static final int DEVICE_ADMIN_MESSAGE_ENABLED = 1; @@ -66,6 +56,9 @@ public class SecurityPolicy { private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_CHANGED = 3; private static final int DEVICE_ADMIN_MESSAGE_PASSWORD_EXPIRING = 4; + private static final String HAS_PASSWORD_EXPIRATION = + PolicyColumns.PASSWORD_EXPIRATION_DAYS + ">0"; + /** * Get the security policy instance */ @@ -111,77 +104,86 @@ public class SecurityPolicy { * @return a policy representing the strongest aggregate. If no policy sets are defined, * a lightweight "nothing required" policy will be returned. Never null. */ - /*package*/ PolicySet computeAggregatePolicy() { + /*package*/ Policy computeAggregatePolicy() { boolean policiesFound = false; + Policy aggregate = new Policy(); + aggregate.mPasswordMinLength = Integer.MIN_VALUE; + aggregate.mPasswordMode = Integer.MIN_VALUE; + aggregate.mPasswordMaxFails = Integer.MAX_VALUE; + aggregate.mPasswordHistory = Integer.MIN_VALUE; + aggregate.mPasswordExpirationDays = Integer.MAX_VALUE; + aggregate.mPasswordComplexChars = Integer.MIN_VALUE; + aggregate.mMaxScreenLockTime = Integer.MAX_VALUE; + aggregate.mRequireRemoteWipe = false; + aggregate.mRequireEncryption = false; + aggregate.mRequireEncryptionExternal = false; - int minPasswordLength = Integer.MIN_VALUE; - int passwordMode = Integer.MIN_VALUE; - int maxPasswordFails = Integer.MAX_VALUE; - int maxScreenLockTime = Integer.MAX_VALUE; - boolean requireRemoteWipe = false; - int passwordHistory = Integer.MIN_VALUE; - int passwordExpirationDays = Integer.MAX_VALUE; - int passwordComplexChars = Integer.MIN_VALUE; - boolean requireEncryption = false; - boolean requireEncryptionExternal = false; - - Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI, - ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null); + Cursor c = mContext.getContentResolver().query(Policy.CONTENT_URI, + Policy.CONTENT_PROJECTION, null, null, null); + Policy policy = new Policy(); try { while (c.moveToNext()) { - long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS); - if (flags != 0) { - PolicySet p = new PolicySet(flags); - minPasswordLength = Math.max(p.mMinPasswordLength, minPasswordLength); - passwordMode = Math.max(p.mPasswordMode, passwordMode); - if (p.mMaxPasswordFails > 0) { - maxPasswordFails = Math.min(p.mMaxPasswordFails, maxPasswordFails); - } - if (p.mMaxScreenLockTime > 0) { - maxScreenLockTime = Math.min(p.mMaxScreenLockTime, maxScreenLockTime); - } - if (p.mPasswordHistory > 0) { - passwordHistory = Math.max(p.mPasswordHistory, passwordHistory); - } - if (p.mPasswordExpirationDays > 0) { - passwordExpirationDays = - Math.min(p.mPasswordExpirationDays, passwordExpirationDays); - } - if (p.mPasswordComplexChars > 0) { - passwordComplexChars = Math.max(p.mPasswordComplexChars, - passwordComplexChars); - } - requireRemoteWipe |= p.mRequireRemoteWipe; - requireEncryption |= p.mRequireEncryption; - requireEncryptionExternal |= p.mRequireEncryptionExternal; - policiesFound = true; + policy.restore(c); + if (Email.DEBUG) { + Log.d(TAG, "Aggregate from: " + policy); } + aggregate.mPasswordMinLength = + Math.max(policy.mPasswordMinLength, aggregate.mPasswordMinLength); + aggregate.mPasswordMode = Math.max(policy.mPasswordMode, aggregate.mPasswordMode); + if (policy.mPasswordMaxFails > 0) { + aggregate.mPasswordMaxFails = + Math.min(policy.mPasswordMaxFails, aggregate.mPasswordMaxFails); + } + if (policy.mMaxScreenLockTime > 0) { + aggregate.mMaxScreenLockTime = Math.min(policy.mMaxScreenLockTime, + aggregate.mMaxScreenLockTime); + } + if (policy.mPasswordHistory > 0) { + aggregate.mPasswordHistory = + Math.max(policy.mPasswordHistory, aggregate.mPasswordHistory); + } + if (policy.mPasswordExpirationDays > 0) { + aggregate.mPasswordExpirationDays = + Math.min(policy.mPasswordExpirationDays, aggregate.mPasswordExpirationDays); + } + if (policy.mPasswordComplexChars > 0) { + aggregate.mPasswordComplexChars = Math.max(policy.mPasswordComplexChars, + aggregate.mPasswordComplexChars); + } + aggregate.mRequireRemoteWipe |= policy.mRequireRemoteWipe; + aggregate.mRequireEncryption |= policy.mRequireEncryption; + aggregate.mRequireEncryptionExternal |= policy.mRequireEncryptionExternal; + policiesFound = true; } } finally { 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; - if (passwordHistory == Integer.MIN_VALUE) passwordHistory = 0; - if (passwordExpirationDays == Integer.MAX_VALUE) passwordExpirationDays = 0; - if (passwordComplexChars == Integer.MIN_VALUE) passwordComplexChars = 0; - - return new PolicySet(minPasswordLength, passwordMode, maxPasswordFails, - maxScreenLockTime, requireRemoteWipe, passwordExpirationDays, passwordHistory, - passwordComplexChars, requireEncryption, requireEncryptionExternal); - } else { - return NO_POLICY_SET; + if (aggregate.mPasswordMinLength == Integer.MIN_VALUE) aggregate.mPasswordMinLength = 0; + if (aggregate.mPasswordMode == Integer.MIN_VALUE) aggregate.mPasswordMode = 0; + if (aggregate.mPasswordMaxFails == Integer.MAX_VALUE) aggregate.mPasswordMaxFails = 0; + if (aggregate.mMaxScreenLockTime == Integer.MAX_VALUE) aggregate.mMaxScreenLockTime = 0; + if (aggregate.mPasswordHistory == Integer.MIN_VALUE) aggregate.mPasswordHistory = 0; + if (aggregate.mPasswordExpirationDays == Integer.MAX_VALUE) + aggregate.mPasswordExpirationDays = 0; + if (aggregate.mPasswordComplexChars == Integer.MIN_VALUE) + aggregate.mPasswordComplexChars = 0; + if (Email.DEBUG) { + Log.d(TAG, "Calculated Aggregate: " + aggregate); + } + return aggregate; } + if (Email.DEBUG) { + Log.d(TAG, "Calculated Aggregate: no policy"); + } + return Policy.NO_POLICY; } /** * Return updated aggregate policy, from cached value if possible */ - public synchronized PolicySet getAggregatePolicy() { + public synchronized Policy getAggregatePolicy() { if (mAggregatePolicy == null) { mAggregatePolicy = computeAggregatePolicy(); } @@ -202,7 +204,7 @@ 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, -1 if unknown/deleted */ - public synchronized void updatePolicies(long accountId) { + public synchronized void policiesUpdated(long accountId) { mAggregatePolicy = null; } @@ -213,7 +215,10 @@ public class SecurityPolicy { * rollbacks. */ public void reducePolicies() { - updatePolicies(-1); + if (Email.DEBUG) { + Log.d(TAG, "reducePolicies"); + } + policiesUpdated(-1); setActivePolicies(); } @@ -223,20 +228,20 @@ public class SecurityPolicy { * @param policies the polices that were requested * @return boolean if supported */ - public boolean isSupported(PolicySet policies) { + public boolean isSupported(Policy policy) { // IMPLEMENTATION: At this time, the only policy which might not be supported is // encryption (which requires low-level systems support). Other policies are fully // supported by the framework and do not need to be checked. - if (policies.mRequireEncryption) { + if (policy.mRequireEncryption) { int encryptionStatus = getDPM().getStorageEncryptionStatus(); if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) { return false; } } - if (policies.mRequireEncryptionExternal) { + if (policy.mRequireEncryptionExternal) { // At this time, we only support "external encryption" when it is provided by virtue // of emulating the external storage inside an encrypted device. - if (!policies.mRequireEncryption) return false; + if (!policy.mRequireEncryption) return false; if (Environment.isExternalStorageRemovable()) return false; if (!Environment.isExternalStorageEmulated()) return false; } @@ -253,34 +258,25 @@ public class SecurityPolicy { * @return the same PolicySet if all are supported; A replacement PolicySet if any * unsupported policies were removed */ - public PolicySet clearUnsupportedPolicies(PolicySet policies) { - PolicySet result = policies; + public Policy clearUnsupportedPolicies(Policy policy) { // IMPLEMENTATION: At this time, the only policy which might not be supported is // encryption (which requires low-level systems support). Other policies are fully // supported by the framework and do not need to be checked. - if (policies.mRequireEncryption) { + if (policy.mRequireEncryption) { int encryptionStatus = getDPM().getStorageEncryptionStatus(); if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) { - // Make new PolicySet w/o encryption - result = new PolicySet(policies.mMinPasswordLength, policies.mPasswordMode, - policies.mMaxPasswordFails, policies.mMaxScreenLockTime, - policies.mRequireRemoteWipe, policies.mPasswordExpirationDays, - policies.mPasswordHistory, policies.mPasswordComplexChars, false, false); + policy.mRequireEncryption = false; } } // At this time, we only support "external encryption" when it is provided by virtue // of emulating the external storage inside an encrypted device. - if (policies.mRequireEncryptionExternal) { + if (policy.mRequireEncryptionExternal) { if (Environment.isExternalStorageRemovable() || !Environment.isExternalStorageEmulated()) { - // Make new PolicySet w/o encryption - result = new PolicySet(policies.mMinPasswordLength, policies.mPasswordMode, - policies.mMaxPasswordFails, policies.mMaxScreenLockTime, - policies.mRequireRemoteWipe, policies.mPasswordExpirationDays, - policies.mPasswordHistory, policies.mPasswordComplexChars, false, false); + policy.mRequireEncryptionExternal = false; } } - return result; + return policy; } /** @@ -290,8 +286,29 @@ public class SecurityPolicy { * @param policies the policies requested, or null to check aggregate stored policies * @return true if the requested policies are active, false if not. */ - public boolean isActive(PolicySet policies) { - int reasons = getInactiveReasons(policies); + public boolean isActive(Policy policy) { + int reasons = getInactiveReasons(policy); + if (Email.DEBUG && (reasons != 0)) { + StringBuilder sb = new StringBuilder("isActive for " + policy + ": "); + if (reasons == 0) { + sb.append("true"); + } else { + sb.append("FALSE -> "); + } + if ((reasons & INACTIVE_NEED_ACTIVATION) != 0) { + sb.append("no_admin "); + } + if ((reasons & INACTIVE_NEED_CONFIGURATION) != 0) { + sb.append("config "); + } + if ((reasons & INACTIVE_NEED_PASSWORD) != 0) { + sb.append("password "); + } + if ((reasons & INACTIVE_NEED_ENCRYPTION) != 0) { + sb.append("encryption "); + } + Log.d(TAG, sb.toString()); + } return reasons == 0; } @@ -335,43 +352,43 @@ public class SecurityPolicy { * @return zero if the requested policies are active, non-zero bits indicates that more work * is needed (typically, by the user) before the required security polices are fully active. */ - public int getInactiveReasons(PolicySet policies) { + public int getInactiveReasons(Policy policy) { // select aggregate set if needed - if (policies == null) { - policies = getAggregatePolicy(); + if (policy == null) { + policy = getAggregatePolicy(); } // quick check for the "empty set" of no policies - if (policies == NO_POLICY_SET) { + if (policy == Policy.NO_POLICY) { return 0; } int reasons = 0; DevicePolicyManager dpm = getDPM(); if (isActiveAdmin()) { // check each policy explicitly - if (policies.mMinPasswordLength > 0) { - if (dpm.getPasswordMinimumLength(mAdminName) < policies.mMinPasswordLength) { + if (policy.mPasswordMinLength > 0) { + if (dpm.getPasswordMinimumLength(mAdminName) < policy.mPasswordMinLength) { reasons |= INACTIVE_NEED_PASSWORD; } } - if (policies.mPasswordMode > 0) { - if (dpm.getPasswordQuality(mAdminName) < policies.getDPManagerPasswordQuality()) { + if (policy.mPasswordMode > 0) { + if (dpm.getPasswordQuality(mAdminName) < policy.getDPManagerPasswordQuality()) { reasons |= INACTIVE_NEED_PASSWORD; } if (!dpm.isActivePasswordSufficient()) { reasons |= INACTIVE_NEED_PASSWORD; } } - if (policies.mMaxScreenLockTime > 0) { + if (policy.mMaxScreenLockTime > 0) { // Note, we use seconds, dpm uses milliseconds - if (dpm.getMaximumTimeToLock(mAdminName) > policies.mMaxScreenLockTime * 1000) { + if (dpm.getMaximumTimeToLock(mAdminName) > policy.mMaxScreenLockTime * 1000) { reasons |= INACTIVE_NEED_CONFIGURATION; } } - if (policies.mPasswordExpirationDays > 0) { + if (policy.mPasswordExpirationDays > 0) { // confirm that expirations are currently set long currentTimeout = dpm.getPasswordExpirationTimeout(mAdminName); if (currentTimeout == 0 - || currentTimeout > policies.getDPManagerPasswordExpirationTimeout()) { + || currentTimeout > policy.getDPManagerPasswordExpirationTimeout()) { reasons |= INACTIVE_NEED_PASSWORD; } // confirm that the current password hasn't expired @@ -382,17 +399,17 @@ public class SecurityPolicy { reasons |= INACTIVE_NEED_PASSWORD; } } - if (policies.mPasswordHistory > 0) { - if (dpm.getPasswordHistoryLength(mAdminName) < policies.mPasswordHistory) { + if (policy.mPasswordHistory > 0) { + if (dpm.getPasswordHistoryLength(mAdminName) < policy.mPasswordHistory) { reasons |= INACTIVE_NEED_PASSWORD; } } - if (policies.mPasswordComplexChars > 0) { - if (dpm.getPasswordMinimumNonLetter(mAdminName) < policies.mPasswordComplexChars) { + if (policy.mPasswordComplexChars > 0) { + if (dpm.getPasswordMinimumNonLetter(mAdminName) < policy.mPasswordComplexChars) { reasons |= INACTIVE_NEED_PASSWORD; } } - if (policies.mRequireEncryption) { + if (policy.mRequireEncryption) { int encryptionStatus = getDPM().getStorageEncryptionStatus(); if (encryptionStatus != DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE) { reasons |= INACTIVE_NEED_ENCRYPTION; @@ -421,24 +438,30 @@ public class SecurityPolicy { public void setActivePolicies() { DevicePolicyManager dpm = getDPM(); // compute aggregate set of policies - PolicySet policies = getAggregatePolicy(); + Policy aggregatePolicy = getAggregatePolicy(); // if empty set, detach from policy manager - if (policies == NO_POLICY_SET) { + if (aggregatePolicy == Policy.NO_POLICY) { + if (Email.DEBUG) { + Log.d(TAG, "setActivePolicies: none, remove admin"); + } dpm.removeActiveAdmin(mAdminName); } else if (isActiveAdmin()) { + if (Email.DEBUG) { + Log.d(TAG, "setActivePolicies: " + aggregatePolicy); + } // set each policy in the policy manager // password mode & length - dpm.setPasswordQuality(mAdminName, policies.getDPManagerPasswordQuality()); - dpm.setPasswordMinimumLength(mAdminName, policies.mMinPasswordLength); + dpm.setPasswordQuality(mAdminName, aggregatePolicy.getDPManagerPasswordQuality()); + dpm.setPasswordMinimumLength(mAdminName, aggregatePolicy.mPasswordMinLength); // screen lock time - dpm.setMaximumTimeToLock(mAdminName, policies.mMaxScreenLockTime * 1000); + dpm.setMaximumTimeToLock(mAdminName, aggregatePolicy.mMaxScreenLockTime * 1000); // local wipe (failed passwords limit) - dpm.setMaximumFailedPasswordsForWipe(mAdminName, policies.mMaxPasswordFails); + dpm.setMaximumFailedPasswordsForWipe(mAdminName, aggregatePolicy.mPasswordMaxFails); // password expiration (days until a password expires). API takes mSec. dpm.setPasswordExpirationTimeout(mAdminName, - policies.getDPManagerPasswordExpirationTimeout()); + aggregatePolicy.getDPManagerPasswordExpirationTimeout()); // password history length (number of previous passwords that may not be reused) - dpm.setPasswordHistoryLength(mAdminName, policies.mPasswordHistory); + dpm.setPasswordHistoryLength(mAdminName, aggregatePolicy.mPasswordHistory); // password minimum complex characters. // Note, in Exchange, "complex chars" simply means "non alpha", but in the DPM, // setting the quality to complex also defaults min symbols=1 and min numeric=1. @@ -446,9 +469,9 @@ public class SecurityPolicy { // configuration in which we explicitly require a minimum number of digits or symbols.) dpm.setPasswordMinimumSymbols(mAdminName, 0); dpm.setPasswordMinimumNumeric(mAdminName, 0); - dpm.setPasswordMinimumNonLetter(mAdminName, policies.mPasswordComplexChars); + dpm.setPasswordMinimumNonLetter(mAdminName, aggregatePolicy.mPasswordComplexChars); // encryption required - dpm.setStorageEncryption(mAdminName, policies.mRequireEncryption); + dpm.setStorageEncryption(mAdminName, aggregatePolicy.mRequireEncryption); // TODO: If we ever support external storage encryption as a first-class feature, // it will need to be set here. For now, if there is a policy request for // external storage encryption, it's sufficient that we've activated internal @@ -497,6 +520,18 @@ public class SecurityPolicy { Account account = EmailContent.Account.restoreAccountWithId(mContext, accountId); // In case the account has been deleted, just return if (account == null) return; + if (Email.DEBUG) { + if (account.mPolicyKey == 0) { + Log.d(TAG, "policiesRequired for " + account.mDisplayName + ": none"); + } else { + Policy policy = Policy.restorePolicyWithId(mContext, account.mPolicyKey); + if (policy == null) { + Log.w(TAG, "No policy??"); + } else { + Log.d(TAG, "policiesRequired for " + account.mDisplayName + ": " + policy); + } + } + } // Mark the account as "on hold". setAccountHoldFlag(mContext, account, true); @@ -559,7 +594,7 @@ public class SecurityPolicy { ContentResolver cr = context.getContentResolver(); // Find all accounts with security and delete them Cursor c = cr.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION, - AccountColumns.SECURITY_FLAGS + "!=0", null, null); + Account.SECURITY_NONZERO_SELECTION, null, null); try { Log.w(TAG, "Email administration disabled; deleting " + c.getCount() + " secured account(s)"); @@ -570,7 +605,7 @@ public class SecurityPolicy { } finally { c.close(); } - updatePolicies(-1); + policiesUpdated(-1); } /** @@ -626,27 +661,13 @@ public class SecurityPolicy { * the account that forces the password to be refreshed. * @return -1 if no expirations, or accountId if one is found */ - /* package */ static long findShortestExpiration(Context context) { - long nextExpiringAccountId = -1; - long shortestExpiration = Long.MAX_VALUE; - Cursor c = context.getContentResolver().query(Account.CONTENT_URI, - ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null); - try { - while (c.moveToNext()) { - long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS); - if (flags != 0) { - PolicySet p = new PolicySet(flags); - if (p.mPasswordExpirationDays > 0 && - p.mPasswordExpirationDays < shortestExpiration) { - nextExpiringAccountId = c.getLong(ACCOUNT_SECURITY_COLUMN_ID); - shortestExpiration = p.mPasswordExpirationDays; - } - } - } - } finally { - c.close(); - } - return nextExpiringAccountId; + @VisibleForTesting + /*package*/ static long findShortestExpiration(Context context) { + long policyId = Utility.getFirstRowLong(context, Policy.CONTENT_URI, Policy.ID_PROJECTION, + HAS_PASSWORD_EXPIRATION, null, PolicyColumns.PASSWORD_EXPIRATION_DAYS + " ASC", + EmailContent.ID_PROJECTION_COLUMN, -1L); + if (policyId < 0) return -1L; + return Policy.getAccountIdWithPolicyKey(context, policyId); } /** @@ -656,27 +677,24 @@ public class SecurityPolicy { * @param controller * @return true if one or more accounts were wiped */ - /* package */ static boolean wipeExpiredAccounts(Context context, Controller controller) { + @VisibleForTesting + /*package*/ static boolean wipeExpiredAccounts(Context context, Controller controller) { boolean result = false; - Cursor c = context.getContentResolver().query(Account.CONTENT_URI, - ACCOUNT_SECURITY_PROJECTION, Account.SECURITY_NONZERO_SELECTION, null, null); + Cursor c = context.getContentResolver().query(Policy.CONTENT_URI, + Policy.ID_PROJECTION, HAS_PASSWORD_EXPIRATION, null, null); try { while (c.moveToNext()) { - long flags = c.getLong(ACCOUNT_SECURITY_COLUMN_FLAGS); - if (flags != 0) { - PolicySet p = new PolicySet(flags); - if (p.mPasswordExpirationDays > 0) { - long accountId = c.getLong(ACCOUNT_SECURITY_COLUMN_ID); - Account account = Account.restoreAccountWithId(context, accountId); - if (account != null) { - // Mark the account as "on hold". - setAccountHoldFlag(context, account, true); - // Erase data - controller.deleteSyncedDataSync(accountId); - // Report one or more were found - result = true; - } - } + long policyId = c.getLong(Policy.ID_PROJECTION_COLUMN); + long accountId = Policy.getAccountIdWithPolicyKey(context, policyId); + if (accountId < 0) continue; + Account account = Account.restoreAccountWithId(context, accountId); + if (account != null) { + // Mark the account as "on hold". + setAccountHoldFlag(context, account, true); + // Erase data + controller.deleteSyncedDataSync(accountId); + // Report one or more were found + result = true; } } } finally { diff --git a/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java b/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java index 6de156211..b0c721fc3 100644 --- a/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java +++ b/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java @@ -23,8 +23,8 @@ import com.android.emailcommon.Logging; import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.provider.EmailContent.Account; import com.android.emailcommon.provider.EmailContent.HostAuth; +import com.android.emailcommon.provider.Policy; import com.android.emailcommon.service.EmailServiceProxy; -import com.android.emailcommon.service.PolicySet; import com.android.emailcommon.utility.Utility; import android.app.Activity; @@ -456,7 +456,7 @@ public class AccountCheckSettingsFragment extends Fragment { EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE); } if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED) { - SetupData.setPolicySet((PolicySet)bundle.getParcelable( + SetupData.setPolicy((Policy)bundle.getParcelable( EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET)); return new MessagingException(resultCode, mStoreHost); } else if (resultCode == MessagingException.SECURITY_POLICIES_UNSUPPORTED) { diff --git a/src/com/android/email/activity/setup/AccountSecurity.java b/src/com/android/email/activity/setup/AccountSecurity.java index ec15cb38d..7f066a570 100644 --- a/src/com/android/email/activity/setup/AccountSecurity.java +++ b/src/com/android/email/activity/setup/AccountSecurity.java @@ -16,6 +16,7 @@ package com.android.email.activity.setup; +import com.android.email.Email; import com.android.email.R; import com.android.email.SecurityPolicy; import com.android.email.activity.ActivityHelper; @@ -35,6 +36,7 @@ import android.content.Intent; import android.content.res.Resources; import android.os.AsyncTask; import android.os.Bundle; +import android.util.Log; /** * Psuedo-activity (no UI) to bootstrap the user up to a higher desired security level. This @@ -48,6 +50,7 @@ import android.os.Bundle; * 6. If necessary, request for user to activate device encryption */ public class AccountSecurity extends Activity { + private static final String TAG = "Email/AccountSecurity"; private static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID"; private static final String EXTRA_SHOW_DIALOG = "SHOW_DIALOG"; @@ -139,7 +142,7 @@ public class AccountSecurity extends Activity { return; } // Otherwise, handle normal security settings flow - if (mAccount.mSecurityFlags != 0) { + if (mAccount.mPolicyKey != 0) { // This account wants to control security if (showDialog) { // Show dialog first, unless already showing (e.g. after rotation) @@ -184,10 +187,12 @@ public class AccountSecurity extends Activity { */ private void tryAdvanceSecurity(Account account) { SecurityPolicy security = SecurityPolicy.getInstance(this); - // Step 1. Check if we are an active device administrator, and stop here to activate if (!security.isActiveAdmin()) { if (mTriedAddAdministrator) { + if (Email.DEBUG) { + Log.d(TAG, "Not active admin: repost notification"); + } repostNotification(account, security); finish(); } else { @@ -195,9 +200,15 @@ public class AccountSecurity extends Activity { // retrieve name of server for the format string HostAuth hostAuth = HostAuth.restoreHostAuthWithId(this, account.mHostAuthKeyRecv); if (hostAuth == null) { + if (Email.DEBUG) { + Log.d(TAG, "No HostAuth: repost notification"); + } repostNotification(account, security); finish(); } else { + if (Email.DEBUG) { + Log.d(TAG, "Not active admin: post initial notification"); + } // try to become active - must happen here in activity, to get result Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN); intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, @@ -214,6 +225,9 @@ public class AccountSecurity extends Activity { // Step 2. Check if the current aggregate security policy is being satisfied by the // DevicePolicyManager (the current system security level). if (security.isActive(null)) { + if (Email.DEBUG) { + Log.d(TAG, "Security active; clear holds"); + } Account.clearSecurityHoldOnAllAccounts(this); finish(); return; @@ -229,9 +243,15 @@ public class AccountSecurity extends Activity { // Step 5. If password is needed, try to have the user set it if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_PASSWORD) != 0) { if (mTriedSetPassword) { + if (Email.DEBUG) { + Log.d(TAG, "Password needed; repost notification"); + } repostNotification(account, security); finish(); } else { + if (Email.DEBUG) { + Log.d(TAG, "Password needed; request it via DPM"); + } mTriedSetPassword = true; // launch the activity to have the user set a new password. Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); @@ -243,10 +263,16 @@ public class AccountSecurity extends Activity { // Step 6. If encryption is needed, try to have the user set it if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_ENCRYPTION) != 0) { if (mTriedSetEncryption) { + if (Email.DEBUG) { + Log.d(TAG, "Encryption needed; repost notification"); + } repostNotification(account, security); finish(); } else { - mTriedSetEncryption = true; + if (Email.DEBUG) { + Log.d(TAG, "Encryption needed; request it via DPM"); + } + mTriedSetEncryption = true; // launch the activity to start up encryption. Intent intent = new Intent(DevicePolicyManager.ACTION_START_ENCRYPTION); startActivityForResult(intent, REQUEST_ENCRYPTION); @@ -255,6 +281,9 @@ public class AccountSecurity extends Activity { } // Step 7. No problems were found, so clear holds and exit + if (Email.DEBUG) { + Log.d(TAG, "Policies enforced; clear holds"); + } Account.clearSecurityHoldOnAllAccounts(this); finish(); } @@ -304,6 +333,9 @@ public class AccountSecurity extends Activity { b.setMessage(res.getString(R.string.account_security_dialog_content_fmt, accountName)); b.setPositiveButton(R.string.okay_action, this); b.setNegativeButton(R.string.cancel_action, this); + if (Email.DEBUG) { + Log.d(TAG, "Posting security needed dialog"); + } return b.create(); } @@ -318,9 +350,15 @@ public class AccountSecurity extends Activity { } switch (which) { case DialogInterface.BUTTON_POSITIVE: + if (Email.DEBUG) { + Log.d(TAG, "User accepts; advance to next step"); + } activity.tryAdvanceSecurity(activity.mAccount); break; case DialogInterface.BUTTON_NEGATIVE: + if (Email.DEBUG) { + Log.d(TAG, "User declines; repost notification"); + } activity.repostNotification( activity.mAccount, SecurityPolicy.getInstance(activity)); activity.finish(); diff --git a/src/com/android/email/activity/setup/AccountSettingsUtils.java b/src/com/android/email/activity/setup/AccountSettingsUtils.java index 3cadc45e9..0db2b9604 100644 --- a/src/com/android/email/activity/setup/AccountSettingsUtils.java +++ b/src/com/android/email/activity/setup/AccountSettingsUtils.java @@ -59,8 +59,8 @@ public class AccountSettingsUtils { } /** - * Returns a set of content values to commit account changes (not including HostAuth) to - * the database. Does not actually commit anything. + * Returns a set of content values to commit account changes (not including the foreign keys + * for the two host auth's and policy) to the database. Does not actually commit anything. */ public static ContentValues getAccountContentValues(EmailContent.Account account) { ContentValues cv = new ContentValues(); @@ -72,7 +72,6 @@ public class AccountSettingsUtils { cv.put(AccountColumns.RINGTONE_URI, account.mRingtoneUri); cv.put(AccountColumns.FLAGS, account.mFlags); cv.put(AccountColumns.SYNC_LOOKBACK, account.mSyncLookback); - cv.put(AccountColumns.SECURITY_FLAGS, account.mSecurityFlags); cv.put(AccountColumns.SECURITY_SYNC_KEY, account.mSecuritySyncKey); return cv; } diff --git a/src/com/android/email/activity/setup/AccountSetupOptions.java b/src/com/android/email/activity/setup/AccountSetupOptions.java index 8009856ec..e842cf755 100644 --- a/src/com/android/email/activity/setup/AccountSetupOptions.java +++ b/src/com/android/email/activity/setup/AccountSetupOptions.java @@ -26,7 +26,6 @@ import com.android.email.service.MailService; import com.android.emailcommon.Logging; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.Account; -import com.android.emailcommon.service.PolicySet; import com.android.emailcommon.service.SyncWindow; import com.android.emailcommon.utility.Utility; @@ -238,11 +237,9 @@ public class AccountSetupOptions extends AccountSetupActivity implements OnClick boolean contacts = false; boolean email = mSyncEmailView.isChecked(); if (account.mHostAuthRecv.mProtocol.equals("eas")) { - // Set security hold if necessary to prevent sync until policies are accepted - PolicySet policySet = SetupData.getPolicySet(); - if (policySet != null && policySet.getSecurityCode() != 0) { - account.mSecurityFlags = policySet.getSecurityCode(); + if (SetupData.getPolicy() != null) { account.mFlags |= Account.FLAGS_SECURITY_HOLD; + account.mPolicy = SetupData.getPolicy(); } // Get flags for contacts/calendar sync contacts = mSyncContactsView.isChecked(); diff --git a/src/com/android/email/activity/setup/SetupData.java b/src/com/android/email/activity/setup/SetupData.java index 3d9575620..685d0462d 100644 --- a/src/com/android/email/activity/setup/SetupData.java +++ b/src/com/android/email/activity/setup/SetupData.java @@ -17,7 +17,7 @@ package com.android.email.activity.setup; import com.android.emailcommon.provider.EmailContent.Account; -import com.android.emailcommon.service.PolicySet; +import com.android.emailcommon.provider.Policy; import android.accounts.AccountAuthenticatorResponse; import android.os.Bundle; @@ -56,7 +56,7 @@ public class SetupData implements Parcelable { private String mPassword; private int mCheckSettingsMode = 0; private boolean mAllowAutodiscover = true; - private PolicySet mPolicySet; + private Policy mPolicy; private boolean mAutoSetup = false; private boolean mDefault = false; private AccountAuthenticatorResponse mAccountAuthenticatorResponse = null; @@ -130,28 +130,30 @@ public class SetupData implements Parcelable { getInstance().mAllowAutodiscover = mAllowAutodiscover; } - static public PolicySet getPolicySet() { - return getInstance().mPolicySet; + static public Policy getPolicy() { + return getInstance().mPolicy; } - static public void setPolicySet(PolicySet mPolicySet) { - getInstance().mPolicySet = mPolicySet; + static public void setPolicy(Policy policy) { + SetupData data = getInstance(); + data.mPolicy = policy; + data.mAccount.mPolicy = policy; } static public boolean isAutoSetup() { return getInstance().mAutoSetup; } - static public void setAutoSetup(boolean mAutoSetup) { - getInstance().mAutoSetup = mAutoSetup; + static public void setAutoSetup(boolean autoSetup) { + getInstance().mAutoSetup = autoSetup; } static public boolean isDefault() { return getInstance().mDefault; } - static public void setDefault(boolean mDefault) { - getInstance().mDefault = mDefault; + static public void setDefault(boolean _default) { + getInstance().mDefault = _default; } static public AccountAuthenticatorResponse getAccountAuthenticatorResponse() { @@ -176,7 +178,7 @@ public class SetupData implements Parcelable { } void commonInit() { - mPolicySet = null; + mPolicy = null; mAutoSetup = false; mAllowAutodiscover = true; mCheckSettingsMode = 0; @@ -210,7 +212,7 @@ public class SetupData implements Parcelable { dest.writeString(mPassword); dest.writeInt(mCheckSettingsMode); dest.writeInt(mAllowAutodiscover ? 1 : 0); - dest.writeParcelable(mPolicySet, 0); + dest.writeParcelable(mPolicy, 0); dest.writeInt(mAutoSetup ? 1 : 0); dest.writeInt(mDefault ? 1 : 0); dest.writeParcelable(mAccountAuthenticatorResponse, 0); @@ -224,7 +226,7 @@ public class SetupData implements Parcelable { mPassword = in.readString(); mCheckSettingsMode = in.readInt(); mAllowAutodiscover = in.readInt() == 1; - mPolicySet = in.readParcelable(loader); + mPolicy = in.readParcelable(loader); mAutoSetup = in.readInt() == 1; mDefault = in.readInt() == 1; mAccountAuthenticatorResponse = in.readParcelable(loader); @@ -262,7 +264,7 @@ public class SetupData implements Parcelable { if (SetupData.isCheckIncoming()) sb.append("in+"); if (SetupData.isCheckOutgoing()) sb.append("out+"); if (SetupData.isCheckAutodiscover()) sb.append("a/d"); - sb.append(":policy=" + (data.mPolicySet == null ? "none" : "exists")); + sb.append(":policy=" + (data.mPolicy == null ? "none" : "exists")); return sb.toString(); } } diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index 75eae3b3d..f0d47ae74 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -33,7 +33,10 @@ import com.android.emailcommon.provider.EmailContent.Mailbox; import com.android.emailcommon.provider.EmailContent.MailboxColumns; import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.provider.EmailContent.MessageColumns; +import com.android.emailcommon.provider.EmailContent.PolicyColumns; import com.android.emailcommon.provider.EmailContent.SyncColumns; +import com.android.emailcommon.provider.Policy; +import com.android.emailcommon.service.LegacyPolicySet; import com.google.common.annotations.VisibleForTesting; import android.accounts.AccountManager; @@ -53,6 +56,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; +import android.os.Debug; import android.util.Log; import java.io.File; @@ -93,7 +97,9 @@ public class EmailProvider extends ContentProvider { /*package*/ static final ContentCache sCacheMailbox = new ContentCache("Mailbox", Mailbox.CONTENT_PROJECTION, 8); private static final ContentCache sCacheMessage = - new ContentCache("Message", Message.CONTENT_PROJECTION, 3); + new ContentCache("Message", Message.CONTENT_PROJECTION, 8); + private static final ContentCache sCachePolicy = + new ContentCache("Policy", Policy.CONTENT_PROJECTION, 4); // Any changes to the database format *must* include update-in-place code. // Original version: 3 @@ -114,7 +120,9 @@ public class EmailProvider extends ContentProvider { // Version 17: Add parentKey to Mailbox table // Version 18: Copy Mailbox.displayName to Mailbox.serverId for all IMAP & POP3 mailboxes. // Column Mailbox.serverId is used for the server-side pathname of a mailbox. - public static final int DATABASE_VERSION = 18; + // Version 19: Add Policy table; add policyKey to Account table and trigger to delete an + // Account's policy when the Account is deleted + public static final int DATABASE_VERSION = 19; // Any changes to the database format *must* include update-in-place code. // Original version: 2 @@ -158,8 +166,12 @@ public class EmailProvider extends ContentProvider { private static final int DELETED_MESSAGE = DELETED_MESSAGE_BASE; private static final int DELETED_MESSAGE_ID = DELETED_MESSAGE_BASE + 1; + private static final int POLICY_BASE = 0x7000; + private static final int POLICY = POLICY_BASE; + private static final int POLICY_ID = POLICY_BASE + 1; + // MUST ALWAYS EQUAL THE LAST OF THE PREVIOUS BASE CONSTANTS - private static final int LAST_EMAIL_PROVIDER_DB_BASE = DELETED_MESSAGE_BASE; + private static final int LAST_EMAIL_PROVIDER_DB_BASE = POLICY_BASE; // DO NOT CHANGE BODY_BASE!! private static final int BODY_BASE = LAST_EMAIL_PROVIDER_DB_BASE + 0x1000; @@ -178,6 +190,7 @@ public class EmailProvider extends ContentProvider { EmailContent.HostAuth.TABLE_NAME, EmailContent.Message.UPDATED_TABLE_NAME, EmailContent.Message.DELETED_TABLE_NAME, + Policy.TABLE_NAME, EmailContent.Body.TABLE_NAME }; @@ -186,11 +199,13 @@ public class EmailProvider extends ContentProvider { sCacheAccount, sCacheMailbox, sCacheMessage, - null, + null, // Attachment sCacheHostAuth, - null, - null, - null}; + null, // Updated message + null, // Deleted message + sCachePolicy, + null // Body + }; private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); @@ -230,6 +245,18 @@ public class EmailProvider extends ContentProvider { " where " + MessageColumns.MAILBOX_KEY + "=old." + EmailContent.RECORD_ID + "; end"; + private static final String TRIGGER_ACCOUNT_DELETE = + "create trigger account_delete before delete on " + Account.TABLE_NAME + + " begin delete from " + Mailbox.TABLE_NAME + + " where " + MailboxColumns.ACCOUNT_KEY + "=old." + EmailContent.RECORD_ID + + "; delete from " + HostAuth.TABLE_NAME + + " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_RECV + + "; delete from " + HostAuth.TABLE_NAME + + " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_SEND + + "; delete from " + Policy.TABLE_NAME + + " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.POLICY_KEY + + "; end"; + private static final ContentValues CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT; public static final String MESSAGE_URI_PARAMETER_MAILBOX_ID = "mailboxId"; @@ -312,6 +339,9 @@ public class EmailProvider extends ContentProvider { CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT = new ContentValues(); CONTENT_VALUES_RESET_NEW_MESSAGE_COUNT.put(Account.NEW_MESSAGE_COUNT, 0); + + matcher.addURI(EmailContent.AUTHORITY, "policy", POLICY); + matcher.addURI(EmailContent.AUTHORITY, "policy/#", POLICY_ID); } @@ -505,18 +535,12 @@ public class EmailProvider extends ContentProvider { + AccountColumns.NEW_MESSAGE_COUNT + " integer, " + AccountColumns.SECURITY_FLAGS + " integer, " + AccountColumns.SECURITY_SYNC_KEY + " text, " - + AccountColumns.SIGNATURE + " text " + + AccountColumns.SIGNATURE + " text, " + + AccountColumns.POLICY_KEY + " integer" + ");"; db.execSQL("create table " + Account.TABLE_NAME + s); // Deleting an account deletes associated Mailboxes and HostAuth's - db.execSQL("create trigger account_delete before delete on " + Account.TABLE_NAME + - " begin delete from " + Mailbox.TABLE_NAME + - " where " + MailboxColumns.ACCOUNT_KEY + "=old." + EmailContent.RECORD_ID + - "; delete from " + HostAuth.TABLE_NAME + - " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_RECV + - "; delete from " + HostAuth.TABLE_NAME + - " where " + EmailContent.RECORD_ID + "=old." + AccountColumns.HOST_AUTH_KEY_SEND + - "; end"); + db.execSQL(TRIGGER_ACCOUNT_DELETE); } static void resetAccountTable(SQLiteDatabase db, int oldVersion, int newVersion) { @@ -527,6 +551,22 @@ public class EmailProvider extends ContentProvider { createAccountTable(db); } + static void createPolicyTable(SQLiteDatabase db) { + String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " + + PolicyColumns.PASSWORD_MODE + " integer, " + + PolicyColumns.PASSWORD_MIN_LENGTH + " integer, " + + PolicyColumns.PASSWORD_EXPIRATION_DAYS + " integer, " + + PolicyColumns.PASSWORD_HISTORY + " integer, " + + PolicyColumns.PASSWORD_COMPLEX_CHARS + " integer, " + + PolicyColumns.PASSWORD_MAX_FAILS + " integer, " + + PolicyColumns.MAX_SCREEN_LOCK_TIME + " integer, " + + PolicyColumns.REQUIRE_REMOTE_WIPE + " integer, " + + PolicyColumns.REQUIRE_ENCRYPTION + " integer, " + + PolicyColumns.REQUIRE_ENCRYPTION_EXTERNAL + " integer" + + ");"; + db.execSQL("create table " + Policy.TABLE_NAME + s); + } + static void createHostAuthTable(SQLiteDatabase db) { String s = " (" + EmailContent.RECORD_ID + " integer primary key autoincrement, " + HostAuthColumns.PROTOCOL + " text, " @@ -783,6 +823,7 @@ public class EmailProvider extends ContentProvider { createMailboxTable(db); createHostAuthTable(db); createAccountTable(db); + createPolicyTable(db); } @Override @@ -951,6 +992,21 @@ public class EmailProvider extends ContentProvider { upgradeFromVersion17ToVersion18(db); oldVersion = 18; } + if (oldVersion == 18) { + Debug.waitForDebugger(); + try { + db.execSQL("alter table " + Account.TABLE_NAME + + " add column " + Account.POLICY_KEY + " integer;"); + db.execSQL("drop trigger account_delete;"); + db.execSQL(TRIGGER_ACCOUNT_DELETE); + createPolicyTable(db); + convertPolicyFlagsToPolicyTable(db); + } catch (SQLException e) { + // Shouldn't be needed unless we're debugging and interrupt the process + Log.w(TAG, "Exception upgrading EmailProvider.db from 18 to 19 " + e); + } + oldVersion = 19; + } } @Override @@ -1009,6 +1065,7 @@ public class EmailProvider extends ContentProvider { case MAILBOX_ID: case ACCOUNT_ID: case HOSTAUTH_ID: + case POLICY_ID: id = uri.getPathSegments().get(1); if (match == SYNCED_MESSAGE_ID) { // For synced messages, first copy the old message to the deleted table and @@ -1062,6 +1119,7 @@ public class EmailProvider extends ContentProvider { case MAILBOX: case ACCOUNT: case HOSTAUTH: + case POLICY: switch(match) { // See the comments above for deletion of ACCOUNT_ID, etc case ACCOUNT: @@ -1185,6 +1243,7 @@ public class EmailProvider extends ContentProvider { case MAILBOX: case ACCOUNT: case HOSTAUTH: + case POLICY: id = db.insert(TABLE_NAMES[table], "foo", values); resultUri = ContentUris.withAppendedId(uri, id); // Clients shouldn't normally be adding rows to these tables, as they are @@ -1298,6 +1357,7 @@ public class EmailProvider extends ContentProvider { case MAILBOX_ID: case ACCOUNT_ID: case HOSTAUTH_ID: + case POLICY_ID: return new MatrixCursor(projection, 0); } } @@ -1331,6 +1391,7 @@ public class EmailProvider extends ContentProvider { case MAILBOX: case ACCOUNT: case HOSTAUTH: + case POLICY: c = db.query(tableName, projection, selection, selectionArgs, null, null, sortOrder, limit); break; @@ -1342,6 +1403,7 @@ public class EmailProvider extends ContentProvider { case MAILBOX_ID: case ACCOUNT_ID: case HOSTAUTH_ID: + case POLICY_ID: id = uri.getPathSegments().get(1); if (cache != null) { c = cache.getCachedCursor(id, projection); @@ -1617,6 +1679,25 @@ public class EmailProvider extends ContentProvider { Mailbox.TABLE_NAME + "." + EmailContent.RECORD_ID + ")"); } + @VisibleForTesting + void convertPolicyFlagsToPolicyTable(SQLiteDatabase db) { + Debug.waitForDebugger(); + Cursor c = db.query(Account.TABLE_NAME, + new String[] {EmailContent.RECORD_ID /*0*/, AccountColumns.SECURITY_FLAGS /*1*/}, + AccountColumns.SECURITY_FLAGS + ">0", null, null, null, null); + ContentValues cv = new ContentValues(); + String[] args = new String[1]; + while (c.moveToNext()) { + long securityFlags = c.getLong(1 /*SECURITY_FLAGS*/); + Policy policy = LegacyPolicySet.flagsToPolicy(securityFlags); + long policyId = db.insert(Policy.TABLE_NAME, null, policy.toContentValues()); + cv.put(AccountColumns.POLICY_KEY, policyId); + cv.putNull(AccountColumns.SECURITY_FLAGS); + args[0] = Long.toString(c.getLong(0 /*RECORD_ID*/)); + db.update(Account.TABLE_NAME, cv, EmailContent.RECORD_ID + "=?", args); + } + } + /** Upgrades the database from v17 to v18 */ @VisibleForTesting static void upgradeFromVersion17ToVersion18(SQLiteDatabase db) { diff --git a/src/com/android/email/service/PolicyService.java b/src/com/android/email/service/PolicyService.java index ace65a716..a97f65954 100644 --- a/src/com/android/email/service/PolicyService.java +++ b/src/com/android/email/service/PolicyService.java @@ -17,8 +17,8 @@ package com.android.email.service; import com.android.email.SecurityPolicy; +import com.android.emailcommon.provider.Policy; import com.android.emailcommon.service.IPolicyService; -import com.android.emailcommon.service.PolicySet; import android.app.Service; import android.content.Context; @@ -31,16 +31,16 @@ public class PolicyService extends Service { private Context mContext; private final IPolicyService.Stub mBinder = new IPolicyService.Stub() { - public boolean isActive(PolicySet policies) { - return mSecurityPolicy.isActive(policies); + public boolean isActive(Policy policy) { + return mSecurityPolicy.isActive(policy); } public void policiesRequired(long accountId) { mSecurityPolicy.policiesRequired(accountId); } - public void updatePolicies(long accountId) { - mSecurityPolicy.updatePolicies(accountId); + public void policiesUpdated(long accountId) { + mSecurityPolicy.policiesUpdated(accountId); } public void setAccountHoldFlag(long accountId, boolean newState) { @@ -55,12 +55,12 @@ public class PolicyService extends Service { mSecurityPolicy.remoteWipe(); } - public boolean isSupported(PolicySet policies) { - return mSecurityPolicy.isSupported(policies); + public boolean isSupported(Policy policy) { + return mSecurityPolicy.isSupported(policy); } - public PolicySet clearUnsupportedPolicies(PolicySet policies) { - return mSecurityPolicy.clearUnsupportedPolicies(policies); + public Policy clearUnsupportedPolicies(Policy policy) { + return mSecurityPolicy.clearUnsupportedPolicies(policy); } }; diff --git a/tests/src/com/android/email/LegacyConversionsTests.java b/tests/src/com/android/email/LegacyConversionsTests.java index 4bc0954ff..6d31892a4 100644 --- a/tests/src/com/android/email/LegacyConversionsTests.java +++ b/tests/src/com/android/email/LegacyConversionsTests.java @@ -687,7 +687,6 @@ public class LegacyConversionsTests extends ProviderTestCase2 { assertEquals(tag + " ringtone", expect.getRingtone(), actual.mRingtoneUri); assertEquals(tag + " proto vers", expect.mProtocolVersion, actual.mProtocolVersion); assertEquals(tag + " new count", 0, actual.mNewMessageCount); - assertEquals(tag + " security", expect.mSecurityFlags, actual.mSecurityFlags); assertEquals(tag + " sec sync key", null, actual.mSecuritySyncKey); assertEquals(tag + " signature", expect.mSignature, actual.mSignature); } @@ -728,7 +727,6 @@ public class LegacyConversionsTests extends ProviderTestCase2 { assertEquals(tag + " backup flags", 0, actual.mBackupFlags); assertEquals(tag + " proto vers", expect.mProtocolVersion, actual.mProtocolVersion); assertEquals(tag + " delete policy", expect.getDeletePolicy(), actual.getDeletePolicy()); - assertEquals(tag + " security", expect.mSecurityFlags, actual.mSecurityFlags); assertEquals(tag + " signature", expect.mSignature, actual.mSignature); } } diff --git a/tests/src/com/android/email/SecurityPolicyTests.java b/tests/src/com/android/email/SecurityPolicyTests.java index f1f8039c0..53247ef7e 100644 --- a/tests/src/com/android/email/SecurityPolicyTests.java +++ b/tests/src/com/android/email/SecurityPolicyTests.java @@ -21,17 +21,14 @@ import com.android.email.provider.EmailProvider; import com.android.email.provider.ProviderTestUtils; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.Account; -import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.provider.EmailContent.Mailbox; import com.android.emailcommon.provider.EmailContent.Message; -import com.android.emailcommon.service.PolicySet; +import com.android.emailcommon.provider.Policy; +import com.android.emailcommon.service.LegacyPolicySet; import android.app.admin.DevicePolicyManager; -import android.content.ContentUris; -import android.content.ContentValues; import android.content.Context; import android.content.ContextWrapper; -import android.net.Uri; import android.test.ProviderTestCase2; import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.SmallTest; @@ -47,18 +44,18 @@ import android.test.suitebuilder.annotation.SmallTest; public class SecurityPolicyTests extends ProviderTestCase2 { private Context mMockContext; - - private static final PolicySet EMPTY_POLICY_SET = - new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false, 0, 0, 0, false, false); + private SecurityPolicy mSecurityPolicy; public SecurityPolicyTests() { super(EmailProvider.class, EmailContent.AUTHORITY); } + private static final Policy EMPTY_POLICY = new Policy(); + @Override protected void setUp() throws Exception { super.setUp(); - mMockContext = new MockContext2(getMockContext(), this.mContext); + mMockContext = new MockContext2(getMockContext(), mContext); // Invalidate all caches, since we reset the database for each test ContentCache.invalidateAllCachesForTest(); } @@ -102,88 +99,56 @@ public class SecurityPolicyTests extends ProviderTestCase2 { } /** - * Retrieve the security policy object, and inject the mock context so it works as expected + * Create a Policy using the arguments formerly used to create a PolicySet; this minimizes the + * changes needed for re-using the PolicySet unit test logic */ - private SecurityPolicy getSecurityPolicy() { - SecurityPolicy sp = SecurityPolicy.getInstance(mMockContext); - sp.setContext(mMockContext); - return sp; - } - - public void testPolicySetConstructor() { - // We know that EMPTY_POLICY_SET doesn't generate an Exception or we wouldn't be here - // Try some illegal parameters - try { - new PolicySet(100, PolicySet.PASSWORD_MODE_SIMPLE, 0, 0, false, 0, 0, 0, false, false); - fail("Too-long password allowed"); - } catch (IllegalArgumentException e) { - } - try { - new PolicySet(0, PolicySet.PASSWORD_MODE_STRONG + 1, 0, 0, false, 0, 0, 0, false, - false); - fail("Illegal password mode allowed"); - } catch (IllegalArgumentException e) { - } - - PolicySet ps = new PolicySet(0, PolicySet.PASSWORD_MODE_SIMPLE, 0, - PolicySet.SCREEN_LOCK_TIME_MAX + 1, false, 0, 0, 0, false, false); - assertEquals(PolicySet.SCREEN_LOCK_TIME_MAX, ps.getMaxScreenLockTimeForTest()); - - ps = new PolicySet(0, PolicySet.PASSWORD_MODE_SIMPLE, - PolicySet.PASSWORD_MAX_FAILS_MAX + 1, 0, false, 0, 0, 0, false, false); - assertEquals(PolicySet.PASSWORD_MAX_FAILS_MAX, ps.getMaxPasswordFailsForTest()); - // All password related fields should be zero when password mode is NONE - // Illegal values for these fields should be ignored - ps = new PolicySet(999/*length*/, PolicySet.PASSWORD_MODE_NONE, - 999/*fails*/, 9999/*screenlock*/, false, 999/*expir*/, 999/*history*/, - 999/*complex*/, false, false); - assertEquals(0, ps.mMinPasswordLength); - assertEquals(0, ps.mMaxScreenLockTime); - assertEquals(0, ps.mMaxPasswordFails); - assertEquals(0, ps.mPasswordExpirationDays); - assertEquals(0, ps.mPasswordHistory); - assertEquals(0, ps.mPasswordComplexChars); - - // With a simple password, we should set complex chars to zero - ps = new PolicySet(4/*length*/, PolicySet.PASSWORD_MODE_SIMPLE, - 0, 0, false, 0, 0, 3/*complex*/, false, false); - assertEquals(4, ps.mMinPasswordLength); - assertEquals(0, ps.mPasswordComplexChars); + private Policy setupPolicy(int minPasswordLength, int passwordMode, int maxPasswordFails, + int maxScreenLockTime, boolean requireRemoteWipe, int passwordExpirationDays, + int passwordHistory, int passwordComplexChars, boolean requireEncryption, + boolean requireEncryptionExternal) throws IllegalArgumentException { + Policy policy = new Policy(); + policy.mPasswordMinLength = minPasswordLength; + policy.mPasswordMode = passwordMode; + policy.mPasswordMaxFails = maxPasswordFails; + policy.mMaxScreenLockTime = maxScreenLockTime; + policy.mRequireRemoteWipe = requireRemoteWipe; + policy.mPasswordExpirationDays = passwordExpirationDays; + policy.mPasswordHistory = passwordHistory; + policy.mPasswordComplexChars = passwordComplexChars; + policy.mRequireEncryption = requireEncryption; + policy.mRequireEncryptionExternal = requireEncryptionExternal; + return policy; } /** * Test business logic of aggregating accounts with policies */ public void testAggregator() { - SecurityPolicy sp = getSecurityPolicy(); + mSecurityPolicy = SecurityPolicy.getInstance(mMockContext); // with no accounts, should return empty set - assertEquals(EMPTY_POLICY_SET, sp.computeAggregatePolicy()); + assertEquals(EMPTY_POLICY, mSecurityPolicy.computeAggregatePolicy()); // with accounts having no security, empty set - Account a1 = ProviderTestUtils.setupAccount("no-sec-1", false, mMockContext); - a1.mSecurityFlags = 0; - a1.save(mMockContext); - Account a2 = ProviderTestUtils.setupAccount("no-sec-2", false, mMockContext); - a2.mSecurityFlags = 0; - a2.save(mMockContext); - assertEquals(EMPTY_POLICY_SET, sp.computeAggregatePolicy()); + ProviderTestUtils.setupAccount("no-sec-1", true, mMockContext); + ProviderTestUtils.setupAccount("no-sec-2", true, mMockContext); + assertEquals(EMPTY_POLICY, mSecurityPolicy.computeAggregatePolicy()); // with a single account in security mode, should return same security as in account // first test with partially-populated policies - Account a3 = ProviderTestUtils.setupAccount("sec-3", false, mMockContext); - PolicySet p3ain = new PolicySet(10, PolicySet.PASSWORD_MODE_SIMPLE, 0, 0, false, 0, 0, 0, + Account a3 = ProviderTestUtils.setupAccount("sec-3", true, mMockContext); + Policy p3ain = setupPolicy(10, Policy.PASSWORD_MODE_SIMPLE, 0, 0, false, 0, 0, 0, false, false); - p3ain.writeAccount(a3, null, true, mMockContext); - PolicySet p3aout = sp.computeAggregatePolicy(); + p3ain.setAccountPolicy(mMockContext, a3, "0"); + Policy p3aout = mSecurityPolicy.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, 6, 2, 3, + Policy p3bin = setupPolicy(10, Policy.PASSWORD_MODE_SIMPLE, 15, 16, false, 6, 2, 3, false, false); - p3bin.writeAccount(a3, null, true, mMockContext); - PolicySet p3bout = sp.computeAggregatePolicy(); + p3bin.setAccountPolicy(mMockContext, a3, "0"); + Policy p3bout = mSecurityPolicy.computeAggregatePolicy(); assertNotNull(p3bout); assertEquals(p3bin, p3bout); @@ -195,15 +160,15 @@ public class SecurityPolicyTests extends ProviderTestCase2 { // max complex chars - max logic - will change // encryption required - OR logic - will *not* change here because false // encryption external req'd - OR logic - will *not* change here because false - PolicySet p4in = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false, 0, 5, 7, + Policy p4in = setupPolicy(20, Policy.PASSWORD_MODE_STRONG, 25, 26, false, 0, 5, 7, false, false); - Account a4 = ProviderTestUtils.setupAccount("sec-4", false, mMockContext); - p4in.writeAccount(a4, null, true, mMockContext); - PolicySet p4out = sp.computeAggregatePolicy(); + Account a4 = ProviderTestUtils.setupAccount("sec-4", true, mMockContext); + p4in.setAccountPolicy(mMockContext, a4, "0"); + Policy p4out = mSecurityPolicy.computeAggregatePolicy(); assertNotNull(p4out); - assertEquals(20, p4out.mMinPasswordLength); - assertEquals(PolicySet.PASSWORD_MODE_STRONG, p4out.mPasswordMode); - assertEquals(15, p4out.mMaxPasswordFails); + assertEquals(20, p4out.mPasswordMinLength); + assertEquals(Policy.PASSWORD_MODE_STRONG, p4out.mPasswordMode); + assertEquals(15, p4out.mPasswordMaxFails); assertEquals(16, p4out.mMaxScreenLockTime); assertEquals(6, p4out.mPasswordExpirationDays); assertEquals(5, p4out.mPasswordHistory); @@ -220,15 +185,15 @@ public class SecurityPolicyTests extends ProviderTestCase2 { // history & complex chars - will not change because 0 (unspecified) // encryption required - OR logic - will change here because true // encryption external req'd - OR logic - will *not* change here because false - PolicySet p5in = new PolicySet(4, PolicySet.PASSWORD_MODE_SIMPLE, 5, 6, true, 1, 0, 0, + Policy p5in = setupPolicy(4, Policy.PASSWORD_MODE_SIMPLE, 5, 6, true, 1, 0, 0, true, false); - Account a5 = ProviderTestUtils.setupAccount("sec-5", false, mMockContext); - p5in.writeAccount(a5, null, true, mMockContext); - PolicySet p5out = sp.computeAggregatePolicy(); + Account a5 = ProviderTestUtils.setupAccount("sec-5", true, mMockContext); + p5in.setAccountPolicy(mMockContext, a5, "0"); + Policy p5out = mSecurityPolicy.computeAggregatePolicy(); assertNotNull(p5out); - assertEquals(20, p5out.mMinPasswordLength); - assertEquals(PolicySet.PASSWORD_MODE_STRONG, p5out.mPasswordMode); - assertEquals(5, p5out.mMaxPasswordFails); + assertEquals(20, p5out.mPasswordMinLength); + assertEquals(Policy.PASSWORD_MODE_STRONG, p5out.mPasswordMode); + assertEquals(5, p5out.mPasswordMaxFails); assertEquals(6, p5out.mMaxScreenLockTime); assertEquals(1, p5out.mPasswordExpirationDays); assertEquals(5, p5out.mPasswordHistory); @@ -238,203 +203,27 @@ public class SecurityPolicyTests extends ProviderTestCase2 { // add another account that continues to mutate fields // encryption external req'd - OR logic - will change here because true - PolicySet p6in = new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false, 0, 0, 0, + Policy p6in = setupPolicy(0, Policy.PASSWORD_MODE_NONE, 0, 0, false, 0, 0, 0, false, true); - Account a6 = ProviderTestUtils.setupAccount("sec-6", false, mMockContext); - p6in.writeAccount(a6, null, true, mMockContext); - PolicySet p6out = sp.computeAggregatePolicy(); + Account a6 = ProviderTestUtils.setupAccount("sec-6", true, mMockContext); + p6in.setAccountPolicy(mMockContext, a6, "0"); + Policy p6out = mSecurityPolicy.computeAggregatePolicy(); assertNotNull(p6out); assertTrue(p6out.mRequireEncryptionExternal); } - /** - * Make sure aggregator (and any other direct DB accessors) handle the case of upgraded - * accounts properly (where the security flags will be NULL instead of zero). - */ - public void testNullFlags() { - SecurityPolicy sp = getSecurityPolicy(); - - Account a1 = ProviderTestUtils.setupAccount("null-sec-1", true, mMockContext); - ContentValues cv = new ContentValues(); - cv.putNull(AccountColumns.SECURITY_FLAGS); - Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, a1.mId); - mMockContext.getContentResolver().update(uri, cv, null, null); - - Account a2 = ProviderTestUtils.setupAccount("no-sec-2", false, mMockContext); - a2.mSecurityFlags = 0; - a2.save(mMockContext); - assertEquals(EMPTY_POLICY_SET, sp.computeAggregatePolicy()); - } - - /** - * Make sure the fields are encoded properly for their max ranges. This is looking - * for any encoding mask/shift errors, which would cause bits to overflow into other fields. - */ - @SmallTest - public void testFieldIsolation() { - // Check PASSWORD_LENGTH - PolicySet p = new PolicySet(PolicySet.PASSWORD_LENGTH_MAX, PolicySet.PASSWORD_MODE_SIMPLE, - 0, 0, false, 0, 0 ,0, false, false); - assertEquals(PolicySet.PASSWORD_MODE_SIMPLE, p.mPasswordMode); - assertEquals(PolicySet.PASSWORD_LENGTH_MAX, p.mMinPasswordLength); - assertEquals(0, p.mMaxPasswordFails); - assertEquals(0, p.mMaxScreenLockTime); - assertEquals(0, p.mPasswordExpirationDays); - assertEquals(0, p.mPasswordHistory); - assertEquals(0, p.mPasswordComplexChars); - assertFalse(p.mRequireRemoteWipe); - assertFalse(p.mRequireEncryption); - assertFalse(p.mRequireEncryptionExternal); - - // Check PASSWORD_MODE - p = new PolicySet(0, PolicySet.PASSWORD_MODE_STRONG, 0, 0, false, 0, 0, 0, false, false); - assertEquals(PolicySet.PASSWORD_MODE_STRONG, p.mPasswordMode); - assertEquals(0, p.mMinPasswordLength); - assertEquals(0, p.mMaxPasswordFails); - assertEquals(0, p.mMaxScreenLockTime); - assertEquals(0, p.mPasswordExpirationDays); - assertEquals(0, p.mPasswordHistory); - assertEquals(0, p.mPasswordComplexChars); - assertFalse(p.mRequireRemoteWipe); - assertFalse(p.mRequireEncryption); - assertFalse(p.mRequireEncryptionExternal); - - // Check PASSWORD_FAILS (note, mode must be set for this to be non-zero) - p = new PolicySet(0, PolicySet.PASSWORD_MODE_SIMPLE, PolicySet.PASSWORD_MAX_FAILS_MAX, 0, - false, 0, 0, 0, false, false); - assertEquals(PolicySet.PASSWORD_MODE_SIMPLE, p.mPasswordMode); - assertEquals(0, p.mMinPasswordLength); - assertEquals(PolicySet.PASSWORD_MAX_FAILS_MAX, p.mMaxPasswordFails); - assertEquals(0, p.mMaxScreenLockTime); - assertEquals(0, p.mPasswordExpirationDays); - assertEquals(0, p.mPasswordHistory); - assertEquals(0, p.mPasswordComplexChars); - assertFalse(p.mRequireRemoteWipe); - assertFalse(p.mRequireEncryption); - assertFalse(p.mRequireEncryptionExternal); - - // Check SCREEN_LOCK_TIME (note, mode must be set for this to be non-zero) - p = new PolicySet(0, PolicySet.PASSWORD_MODE_SIMPLE, 0, PolicySet.SCREEN_LOCK_TIME_MAX, - false, 0, 0, 0, false, false); - assertEquals(PolicySet.PASSWORD_MODE_SIMPLE, p.mPasswordMode); - assertEquals(0, p.mMinPasswordLength); - assertEquals(0, p.mMaxPasswordFails); - assertEquals(PolicySet.SCREEN_LOCK_TIME_MAX, p.mMaxScreenLockTime); - assertEquals(0, p.mPasswordExpirationDays); - assertEquals(0, p.mPasswordHistory); - assertEquals(0, p.mPasswordComplexChars); - assertFalse(p.mRequireRemoteWipe); - assertFalse(p.mRequireEncryption); - assertFalse(p.mRequireEncryptionExternal); - - // Check REQUIRE_REMOTE_WIPE - p = new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, true, 0, 0, 0, false, false); - assertEquals(PolicySet.PASSWORD_MODE_NONE, p.mPasswordMode); - assertEquals(0, p.mMinPasswordLength); - assertEquals(0, p.mMaxPasswordFails); - assertEquals(0, p.mMaxScreenLockTime); - assertEquals(0, p.mPasswordExpirationDays); - assertEquals(0, p.mPasswordHistory); - assertEquals(0, p.mPasswordComplexChars); - assertTrue(p.mRequireRemoteWipe); - assertFalse(p.mRequireEncryption); - assertFalse(p.mRequireEncryptionExternal); - - // Check PASSWORD_EXPIRATION (note, mode must be set for this to be non-zero) - p = new PolicySet(0, PolicySet.PASSWORD_MODE_SIMPLE, 0, 0, false, - PolicySet.PASSWORD_EXPIRATION_MAX, 0, 0, false, false); - assertEquals(PolicySet.PASSWORD_MODE_SIMPLE, p.mPasswordMode); - assertEquals(0, p.mMinPasswordLength); - assertEquals(0, p.mMaxPasswordFails); - assertEquals(0, p.mMaxScreenLockTime); - assertEquals(PolicySet.PASSWORD_EXPIRATION_MAX, p.mPasswordExpirationDays); - assertEquals(0, p.mPasswordHistory); - assertEquals(0, p.mPasswordComplexChars); - assertFalse(p.mRequireRemoteWipe); - assertFalse(p.mRequireEncryption); - assertFalse(p.mRequireEncryptionExternal); - - // Check PASSWORD_HISTORY (note, mode must be set for this to be non-zero) - p = new PolicySet(0, PolicySet.PASSWORD_MODE_SIMPLE, 0, 0, false, 0, - PolicySet.PASSWORD_HISTORY_MAX, 0, false, false); - assertEquals(PolicySet.PASSWORD_MODE_SIMPLE, p.mPasswordMode); - assertEquals(0, p.mMinPasswordLength); - assertEquals(0, p.mMaxPasswordFails); - assertEquals(0, p.mMaxScreenLockTime); - assertEquals(0, p.mPasswordExpirationDays); - assertEquals(PolicySet.PASSWORD_HISTORY_MAX, p.mPasswordHistory); - assertEquals(0, p.mPasswordComplexChars); - assertFalse(p.mRequireRemoteWipe); - assertFalse(p.mRequireEncryption); - assertFalse(p.mRequireEncryptionExternal); - - // Check PASSWORD_COMPLEX_CHARS (note, mode must be set for this to be non-zero) - p = new PolicySet(0, PolicySet.PASSWORD_MODE_STRONG, 0, 0, false, 0, 0, - PolicySet.PASSWORD_COMPLEX_CHARS_MAX, false, false); - assertEquals(PolicySet.PASSWORD_MODE_STRONG, p.mPasswordMode); - assertEquals(0, p.mMinPasswordLength); - assertEquals(0, p.mMaxPasswordFails); - assertEquals(0, p.mMaxScreenLockTime); - assertEquals(0, p.mPasswordExpirationDays); - assertEquals(0, p.mPasswordHistory); - assertEquals(PolicySet.PASSWORD_COMPLEX_CHARS_MAX, p.mPasswordComplexChars); - assertFalse(p.mRequireRemoteWipe); - assertFalse(p.mRequireEncryption); - assertFalse(p.mRequireEncryptionExternal); - - // Check REQUIRE_ENCRYPTION - p = new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false, 0, 0, 0, true, false); - assertEquals(PolicySet.PASSWORD_MODE_NONE, p.mPasswordMode); - assertEquals(0, p.mMinPasswordLength); - assertEquals(0, p.mMaxPasswordFails); - assertEquals(0, p.mMaxScreenLockTime); - assertEquals(0, p.mPasswordExpirationDays); - assertEquals(0, p.mPasswordHistory); - assertEquals(0, p.mPasswordComplexChars); - assertFalse(p.mRequireRemoteWipe); - assertTrue(p.mRequireEncryption); - assertFalse(p.mRequireEncryptionExternal); - - // Check REQUIRE_ENCRYPTION_EXTERNAL - p = new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false, 0, 0, 0, false, true); - assertEquals(PolicySet.PASSWORD_MODE_NONE, p.mPasswordMode); - assertEquals(0, p.mMinPasswordLength); - assertEquals(0, p.mMaxPasswordFails); - assertEquals(0, p.mMaxScreenLockTime); - assertEquals(0, p.mPasswordExpirationDays); - assertEquals(0, p.mPasswordHistory); - assertEquals(0, p.mPasswordComplexChars); - assertFalse(p.mRequireRemoteWipe); - assertFalse(p.mRequireEncryption); - assertTrue(p.mRequireEncryptionExternal); - } - - /** - * Test encoding into an Account and out again - */ - @SmallTest - public void testAccountEncoding() { - PolicySet p1 = - new PolicySet(1, PolicySet.PASSWORD_MODE_STRONG, 3, 4, true, 7, 8, 9, false, false); - Account a = new Account(); - final String SYNC_KEY = "test_sync_key"; - p1.writeAccount(a, SYNC_KEY, false, null); - PolicySet p2 = new PolicySet(a); - assertEquals(p1, p2); - } - /** * Test equality. Note, the tests for inequality are poor, as each field should * be tested individually. */ @SmallTest public void testEquals() { - PolicySet p1 = - new PolicySet(1, PolicySet.PASSWORD_MODE_STRONG, 3, 4, true, 7, 8, 9, false, false); - PolicySet p2 = - new PolicySet(1, PolicySet.PASSWORD_MODE_STRONG, 3, 4, true, 7, 8, 9, false, false); - PolicySet p3 = - new PolicySet(2, PolicySet.PASSWORD_MODE_SIMPLE, 5, 6, true, 7, 8, 9, false, false); + Policy p1 = + setupPolicy(1, Policy.PASSWORD_MODE_STRONG, 3, 4, true, 7, 8, 9, false, false); + Policy p2 = + setupPolicy(1, Policy.PASSWORD_MODE_STRONG, 3, 4, true, 7, 8, 9, false, false); + Policy p3 = + setupPolicy(2, Policy.PASSWORD_MODE_SIMPLE, 5, 6, true, 7, 8, 9, false, false); assertTrue(p1.equals(p2)); assertFalse(p2.equals(p3)); } @@ -443,8 +232,6 @@ public class SecurityPolicyTests extends ProviderTestCase2 { * Test the API to set/clear policy hold flags in an account */ public void testSetClearHoldFlag() { - SecurityPolicy sp = getSecurityPolicy(); - Account a1 = ProviderTestUtils.setupAccount("holdflag-1", false, mMockContext); a1.mFlags = Account.FLAGS_NOTIFY_NEW_MAIL; a1.save(mMockContext); @@ -455,7 +242,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 { // confirm clear until set Account a1a = Account.restoreAccountWithId(mMockContext, a1.mId); assertEquals(Account.FLAGS_NOTIFY_NEW_MAIL, a1a.mFlags); - sp.setAccountHoldFlag(mMockContext, a1, true); + SecurityPolicy.setAccountHoldFlag(mMockContext, a1, true); assertEquals(Account.FLAGS_NOTIFY_NEW_MAIL | Account.FLAGS_SECURITY_HOLD, a1.mFlags); Account a1b = Account.restoreAccountWithId(mMockContext, a1.mId); assertEquals(Account.FLAGS_NOTIFY_NEW_MAIL | Account.FLAGS_SECURITY_HOLD, a1b.mFlags); @@ -463,108 +250,111 @@ public class SecurityPolicyTests extends ProviderTestCase2 { // confirm set until cleared Account a2a = Account.restoreAccountWithId(mMockContext, a2.mId); assertEquals(Account.FLAGS_VIBRATE_ALWAYS | Account.FLAGS_SECURITY_HOLD, a2a.mFlags); - sp.setAccountHoldFlag(mMockContext, a2, false); + SecurityPolicy.setAccountHoldFlag(mMockContext, a2, false); assertEquals(Account.FLAGS_VIBRATE_ALWAYS, a2.mFlags); Account a2b = Account.restoreAccountWithId(mMockContext, a2.mId); assertEquals(Account.FLAGS_VIBRATE_ALWAYS, a2b.mFlags); } -// private static class MockController extends Controller { -// protected MockController(Context context) { -// super(context); -// } -// } + private static class MockController extends Controller { + protected MockController(Context context) { + super(context); + } + + protected void backupAccounts(Context context) { + // For testing, we don't want to back up our accounts + } + } /** * Test the response to disabling DeviceAdmin status - * - * TODO: Reenable the 2nd portion of this test - it fails because it gets into the Controller - * and spins up an account backup on another thread. */ public void testDisableAdmin() { - Account a1 = ProviderTestUtils.setupAccount("disable-1", false, mMockContext); - PolicySet p1 = new PolicySet(10, PolicySet.PASSWORD_MODE_SIMPLE, 0, 0, false, 0, 0, 0, + Account a1 = ProviderTestUtils.setupAccount("disable-1", true, mMockContext); + Policy p1 = setupPolicy(10, Policy.PASSWORD_MODE_SIMPLE, 0, 0, false, 0, 0, 0, false, false); - p1.writeAccount(a1, "sync-key-1", true, mMockContext); + p1.setAccountPolicy(mMockContext, a1, "security-sync-key-1"); - Account a2 = ProviderTestUtils.setupAccount("disable-2", false, mMockContext); - PolicySet p2 = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false, 0, 0, 0, + Account a2 = ProviderTestUtils.setupAccount("disable-2", true, mMockContext); + Policy p2 = setupPolicy(20, Policy.PASSWORD_MODE_STRONG, 25, 26, false, 0, 0, 0, false, false); - p2.writeAccount(a2, "sync-key-2", true, mMockContext); + p2.setAccountPolicy(mMockContext, a2, "security-sync-key-2"); - Account a3 = ProviderTestUtils.setupAccount("disable-3", false, mMockContext); - a3.mSecurityFlags = 0; - a3.mSecuritySyncKey = null; - a3.save(mMockContext); + Account a3 = ProviderTestUtils.setupAccount("disable-3", true, mMockContext); + Policy.clearAccountPolicy(mMockContext, a3); - SecurityPolicy sp = getSecurityPolicy(); + mSecurityPolicy = SecurityPolicy.getInstance(mMockContext); - // Confirm that "enabling" device admin does not change security status (flags & sync key) - PolicySet before = sp.getAggregatePolicy(); - sp.onAdminEnabled(true); // "enabled" should not change anything - PolicySet after1 = sp.getAggregatePolicy(); + // Confirm that "enabling" device admin does not change security status (policy & sync key) + Policy before = mSecurityPolicy.getAggregatePolicy(); + mSecurityPolicy.onAdminEnabled(true); // "enabled" should not change anything + Policy after1 = mSecurityPolicy.getAggregatePolicy(); assertEquals(before, after1); Account a1a = Account.restoreAccountWithId(mMockContext, a1.mId); assertNotNull(a1a.mSecuritySyncKey); + assertTrue(a1a.mPolicyKey > 0); Account a2a = Account.restoreAccountWithId(mMockContext, a2.mId); assertNotNull(a2a.mSecuritySyncKey); + assertTrue(a2a.mPolicyKey > 0); Account a3a = Account.restoreAccountWithId(mMockContext, a3.mId); assertNull(a3a.mSecuritySyncKey); + assertTrue(a3a.mPolicyKey == 0); // Simulate revoke of device admin; directly call deleteSecuredAccounts, which is normally // called from a background thread -// MockController mockController = new MockController(mMockContext); -// Controller.injectMockControllerForTest(mockController); -// try { -// sp.deleteSecuredAccounts(mMockContext); -// PolicySet after2 = sp.getAggregatePolicy(); -// assertEquals(SecurityPolicy.NO_POLICY_SET, after2); -// Account a1b = Account.restoreAccountWithId(mMockContext, a1.mId); -// assertNull(a1b); -// Account a2b = Account.restoreAccountWithId(mMockContext, a2.mId); -// assertNull(a2b); -// Account a3b = Account.restoreAccountWithId(mMockContext, a3.mId); -// assertNull(a3b.mSecuritySyncKey); -// } finally { -// Controller.injectMockControllerForTest(null); -// } + MockController mockController = new MockController(mMockContext); + Controller.injectMockControllerForTest(mockController); + try { + mSecurityPolicy.deleteSecuredAccounts(mMockContext); + Policy after2 = mSecurityPolicy.getAggregatePolicy(); + assertEquals(EMPTY_POLICY, after2); + Account a1b = Account.restoreAccountWithId(mMockContext, a1.mId); + assertNull(a1b); + Account a2b = Account.restoreAccountWithId(mMockContext, a2.mId); + assertNull(a2b); + Account a3b = Account.restoreAccountWithId(mMockContext, a3.mId); + assertNull(a3b.mSecuritySyncKey); + } finally { + Controller.injectMockControllerForTest(null); + } } /** * Test the scanner that finds expiring accounts */ public void testFindExpiringAccount() { - Account a1 = ProviderTestUtils.setupAccount("expiring-1", true, mMockContext); + ProviderTestUtils.setupAccount("expiring-1", true, mMockContext); // With no expiring accounts, this should return null. long nextExpiringAccountId = SecurityPolicy.findShortestExpiration(mMockContext); assertEquals(-1, nextExpiringAccountId); // Add a single expiring account - Account a2 = ProviderTestUtils.setupAccount("expiring-2", false, mMockContext); - PolicySet p2 = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false, 30, 0, 0, + Account a2 = + ProviderTestUtils.setupAccount("expiring-2", true, mMockContext); + Policy p2 = setupPolicy(20, Policy.PASSWORD_MODE_STRONG, 25, 26, false, 30, 0, 0, false, false); - p2.writeAccount(a2, "sync-key-2", true, mMockContext); + p2.setAccountPolicy(mMockContext, a2, "0"); // The expiring account should be returned nextExpiringAccountId = SecurityPolicy.findShortestExpiration(mMockContext); assertEquals(a2.mId, nextExpiringAccountId); // Add an account with a longer expiration - Account a3 = ProviderTestUtils.setupAccount("expiring-3", false, mMockContext); - PolicySet p3 = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false, 60, 0, 0, + Account a3 = ProviderTestUtils.setupAccount("expiring-3", true, mMockContext); + Policy p3 = setupPolicy(20, Policy.PASSWORD_MODE_STRONG, 25, 26, false, 60, 0, 0, false, false); - p3.writeAccount(a3, "sync-key-3", true, mMockContext); + p3.setAccountPolicy(mMockContext, a3, "0"); // The original expiring account (a2) should be returned nextExpiringAccountId = SecurityPolicy.findShortestExpiration(mMockContext); assertEquals(a2.mId, nextExpiringAccountId); // Add an account with a shorter expiration - Account a4 = ProviderTestUtils.setupAccount("expiring-4", false, mMockContext); - PolicySet p4 = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false, 15, 0, 0, + Account a4 = ProviderTestUtils.setupAccount("expiring-4", true, mMockContext); + Policy p4 = setupPolicy(20, Policy.PASSWORD_MODE_STRONG, 25, 26, false, 15, 0, 0, false, false); - p4.writeAccount(a4, "sync-key-4", true, mMockContext); + p4.setAccountPolicy(mMockContext, a4, "0"); // The new expiring account (a4) should be returned nextExpiringAccountId = SecurityPolicy.findShortestExpiration(mMockContext); @@ -586,15 +376,15 @@ public class SecurityPolicyTests extends ProviderTestCase2 { * Test the scanner that wipes expiring accounts */ public void testWipeExpiringAccounts() { - SecurityPolicy sp = getSecurityPolicy(); + mSecurityPolicy = SecurityPolicy.getInstance(mMockContext); TestController testController = new TestController(mMockContext, getContext()); // Two accounts - a1 is normal, a2 has security (but no expiration) Account a1 = ProviderTestUtils.setupAccount("expired-1", true, mMockContext); - Account a2 = ProviderTestUtils.setupAccount("expired-2", false, mMockContext); - PolicySet p2 = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false, 0, 0, 0, + Account a2 = ProviderTestUtils.setupAccount("expired-2", true, mMockContext); + Policy p2 = setupPolicy(20, Policy.PASSWORD_MODE_STRONG, 25, 26, false, 0, 0, 0, false, false); - p2.writeAccount(a2, "sync-key-2", true, mMockContext); + p2.setAccountPolicy(mMockContext, a2, "0"); // Add a mailbox & messages to each account long account1Id = a1.mId; @@ -609,7 +399,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 { ProviderTestUtils.setupMessage("message4", account2Id, box2Id, false, true, mMockContext); // Run the expiration code - should do nothing - boolean wiped = sp.wipeExpiredAccounts(mMockContext, testController); + boolean wiped = SecurityPolicy.wipeExpiredAccounts(mMockContext, testController); assertFalse(wiped); // check mailboxes & messages not wiped assertEquals(2, EmailContent.count(mMockContext, Account.CONTENT_URI)); @@ -617,10 +407,10 @@ public class SecurityPolicyTests extends ProviderTestCase2 { assertEquals(4, EmailContent.count(mMockContext, Message.CONTENT_URI)); // Add 3rd account that really expires - Account a3 = ProviderTestUtils.setupAccount("expired-3", false, mMockContext); - PolicySet p3 = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false, 30, 0, 0, + Account a3 = ProviderTestUtils.setupAccount("expired-3", true, mMockContext); + Policy p3 = setupPolicy(20, Policy.PASSWORD_MODE_STRONG, 25, 26, false, 30, 0, 0, false, false); - p3.writeAccount(a3, "sync-key-3", true, mMockContext); + p3.setAccountPolicy(mMockContext, a3, "0"); // Add mailbox & messages to 3rd account long account3Id = a3.mId; @@ -635,7 +425,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 { assertEquals(6, EmailContent.count(mMockContext, Message.CONTENT_URI)); // Run the expiration code - wipe acct #3 - wiped = sp.wipeExpiredAccounts(mMockContext, testController); + wiped = SecurityPolicy.wipeExpiredAccounts(mMockContext, testController); assertTrue(wiped); // check new counts - account survives but data is wiped assertEquals(3, EmailContent.count(mMockContext, Account.CONTENT_URI)); @@ -656,21 +446,21 @@ public class SecurityPolicyTests extends ProviderTestCase2 { * TODO inject a mock DPM so we can directly control & test all cases, no matter what device */ public void testClearUnsupportedPolicies() { - PolicySet p1 = - new PolicySet(1, PolicySet.PASSWORD_MODE_STRONG, 3, 4, true, 7, 8, 9, false, false); - PolicySet p2 = - new PolicySet(1, PolicySet.PASSWORD_MODE_STRONG, 3, 4, true, 7, 8, 9, true, false); - PolicySet p3 = - new PolicySet(1, PolicySet.PASSWORD_MODE_STRONG, 3, 4, true, 7, 8, 9, false, true); + Policy p1 = + setupPolicy(1, Policy.PASSWORD_MODE_STRONG, 3, 4, true, 7, 8, 9, false, false); + Policy p2 = + setupPolicy(1, Policy.PASSWORD_MODE_STRONG, 3, 4, true, 7, 8, 9, true, false); + Policy p3 = + setupPolicy(1, Policy.PASSWORD_MODE_STRONG, 3, 4, true, 7, 8, 9, false, true); - SecurityPolicy sp = getSecurityPolicy(); - DevicePolicyManager dpm = sp.getDPM(); + mSecurityPolicy = SecurityPolicy.getInstance(mMockContext); + DevicePolicyManager dpm = mSecurityPolicy.getDPM(); boolean hasEncryption = dpm.getStorageEncryptionStatus() != DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED; - PolicySet p1Result = sp.clearUnsupportedPolicies(p1); - PolicySet p2Result = sp.clearUnsupportedPolicies(p2); - PolicySet p3Result = sp.clearUnsupportedPolicies(p3); + Policy p1Result = mSecurityPolicy.clearUnsupportedPolicies(p1); + Policy p2Result = mSecurityPolicy.clearUnsupportedPolicies(p2); + Policy p3Result = mSecurityPolicy.clearUnsupportedPolicies(p3); // No changes expected when encryptionRequested bits were false assertEquals(p1, p1Result); @@ -682,8 +472,9 @@ public class SecurityPolicyTests extends ProviderTestCase2 { assertEquals(p3, p3Result); } else { // If encryption is unsupported, encryption policy bits are cleared - PolicySet policyExpect = - new PolicySet(1, PolicySet.PASSWORD_MODE_STRONG, 3, 4, true, 7, 8, 9, false, false); + Policy policyExpect = + setupPolicy(1, Policy.PASSWORD_MODE_STRONG, 3, 4, true, 7, 8, 9, false, + false); assertEquals(policyExpect, p2Result); assertEquals(policyExpect, p3Result); } @@ -693,28 +484,101 @@ public class SecurityPolicyTests extends ProviderTestCase2 { * Test the code that converts from exchange-style quality to DPM/Lockscreen style quality. */ public void testGetDPManagerPasswordQuality() { - // PolicySet.PASSWORD_MODE_NONE -> DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED - PolicySet p1 = new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, + // Policy.PASSWORD_MODE_NONE -> DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED + Policy p1 = setupPolicy(0, Policy.PASSWORD_MODE_NONE, 0, 0, false, 0, 0, 0, false, false); assertEquals(DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, p1.getDPManagerPasswordQuality()); // PASSWORD_MODE_SIMPLE -> PASSWORD_QUALITY_NUMERIC - PolicySet p2 = new PolicySet(4, PolicySet.PASSWORD_MODE_SIMPLE, + Policy p2 = setupPolicy(4, Policy.PASSWORD_MODE_SIMPLE, 0, 0, false, 0, 0, 0, false, false); assertEquals(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC, p2.getDPManagerPasswordQuality()); // PASSWORD_MODE_STRONG -> PASSWORD_QUALITY_ALPHANUMERIC - PolicySet p3 = new PolicySet(4, PolicySet.PASSWORD_MODE_STRONG, + Policy p3 = setupPolicy(4, Policy.PASSWORD_MODE_STRONG, 0, 0, false, 0, 0, 0, false, false); assertEquals(DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, p3.getDPManagerPasswordQuality()); // PASSWORD_MODE_STRONG + complex chars -> PASSWORD_QUALITY_COMPLEX - PolicySet p4 = new PolicySet(4, PolicySet.PASSWORD_MODE_STRONG, + Policy p4 = setupPolicy(4, Policy.PASSWORD_MODE_STRONG, 0, 0, false, 0, 0 , 2, false, false); assertEquals(DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, p4.getDPManagerPasswordQuality()); } + + private boolean policySetEqualsPolicy(PolicySet ps, Policy policy) { + if ((ps.mPasswordMode >> LegacyPolicySet.PASSWORD_MODE_SHIFT) != policy.mPasswordMode) { + return false; + } + if (ps.mMinPasswordLength != policy.mPasswordMinLength) return false; + if (ps.mPasswordComplexChars != policy.mPasswordComplexChars) return false; + if (ps.mPasswordHistory != policy.mPasswordHistory) return false; + if (ps.mPasswordExpirationDays != policy.mPasswordExpirationDays) return false; + if (ps.mMaxPasswordFails != policy.mPasswordMaxFails) return false; + if (ps.mMaxScreenLockTime != policy.mMaxScreenLockTime) return false; + if (ps.mRequireRemoteWipe != policy.mRequireRemoteWipe) return false; + if (ps.mRequireEncryption != policy.mRequireEncryption) return false; + if (ps.mRequireEncryptionExternal != policy.mRequireEncryptionExternal) return false; + return true; + } + + public void testPolicyFlagsToPolicy() { + // Policy flags; the three sets included here correspond to policies for three test + // accounts that, between them, use all of the possible policies + long flags = 67096612L; + PolicySet ps = new PolicySet(flags); + Policy policy = LegacyPolicySet.flagsToPolicy(flags); + assertTrue(policySetEqualsPolicy(ps, policy)); + flags = 52776591691846L; + ps = new PolicySet(flags); + policy = LegacyPolicySet.flagsToPolicy(flags); + assertTrue(policySetEqualsPolicy(ps, policy)); + flags = 1689605957029924L; + ps = new PolicySet(flags); + policy = LegacyPolicySet.flagsToPolicy(flags); + assertTrue(policySetEqualsPolicy(ps, policy)); + } + + /** + * The old PolicySet class fields and constructor; we use this to test conversion to the + * new Policy table scheme + */ + private static class PolicySet { + private final int mMinPasswordLength; + private final int mPasswordMode; + private final int mMaxPasswordFails; + private final int mMaxScreenLockTime; + private final boolean mRequireRemoteWipe; + private final int mPasswordExpirationDays; + private final int mPasswordHistory; + private final int mPasswordComplexChars; + private final boolean mRequireEncryption; + private final boolean mRequireEncryptionExternal; + + /** + * Create from values encoded in an account flags int + */ + private PolicySet(long flags) { + mMinPasswordLength = (int) ((flags & LegacyPolicySet.PASSWORD_LENGTH_MASK) + >> LegacyPolicySet.PASSWORD_LENGTH_SHIFT); + mPasswordMode = + (int) (flags & LegacyPolicySet.PASSWORD_MODE_MASK); + mMaxPasswordFails = (int) ((flags & LegacyPolicySet.PASSWORD_MAX_FAILS_MASK) + >> LegacyPolicySet.PASSWORD_MAX_FAILS_SHIFT); + mMaxScreenLockTime = (int) ((flags & LegacyPolicySet.SCREEN_LOCK_TIME_MASK) + >> LegacyPolicySet.SCREEN_LOCK_TIME_SHIFT); + mRequireRemoteWipe = 0 != (flags & LegacyPolicySet.REQUIRE_REMOTE_WIPE); + mPasswordExpirationDays = (int) ((flags & LegacyPolicySet.PASSWORD_EXPIRATION_MASK) + >> LegacyPolicySet.PASSWORD_EXPIRATION_SHIFT); + mPasswordHistory = (int) ((flags & LegacyPolicySet.PASSWORD_HISTORY_MASK) + >> LegacyPolicySet.PASSWORD_HISTORY_SHIFT); + mPasswordComplexChars = (int) ((flags & LegacyPolicySet.PASSWORD_COMPLEX_CHARS_MASK) + >> LegacyPolicySet.PASSWORD_COMPLEX_CHARS_SHIFT); + mRequireEncryption = 0 != (flags & LegacyPolicySet.REQUIRE_ENCRYPTION); + mRequireEncryptionExternal = 0 != (flags & LegacyPolicySet.REQUIRE_ENCRYPTION_EXTERNAL); + } + } } diff --git a/tests/src/com/android/email/provider/PolicyTests.java b/tests/src/com/android/email/provider/PolicyTests.java new file mode 100644 index 000000000..8cb319ba8 --- /dev/null +++ b/tests/src/com/android/email/provider/PolicyTests.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.email.provider; + +import com.android.emailcommon.provider.EmailContent; +import com.android.emailcommon.provider.EmailContent.Account; +import com.android.emailcommon.provider.Policy; + +import android.content.Context; +import android.os.Parcel; +import android.test.ProviderTestCase2; +import android.test.suitebuilder.annotation.MediumTest; + +/** + * This is a series of unit tests for the Policy class + * + * You can run this entire test case with: + * runtest -c com.android.email.provider.PolicyTests email + */ + +@MediumTest +public class PolicyTests extends ProviderTestCase2 { + + private Context mMockContext; + + public PolicyTests() { + super(EmailProvider.class, EmailContent.AUTHORITY); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + mMockContext = getMockContext(); + // Invalidate all caches, since we reset the database for each test + ContentCache.invalidateAllCachesForTest(); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + } + + public void testGetAccountIdWithPolicyKey() { + String securitySyncKey = "key"; + // Setup two accounts with policies + Account account1 = ProviderTestUtils.setupAccount("acct1", true, mMockContext); + Policy policy1 = new Policy(); + policy1.setAccountPolicy(mMockContext, account1, securitySyncKey); + Account account2 = ProviderTestUtils.setupAccount("acct2", true, mMockContext); + Policy policy2 = new Policy(); + policy2.setAccountPolicy(mMockContext, account2, securitySyncKey); + // Get the accounts back from the database + account1.refresh(mMockContext); + account2.refresh(mMockContext); + // Both should have valid policies + assertTrue(account1.mPolicyKey > 0); + // And they should be findable via getAccountIdWithPolicyKey + assertTrue(account2.mPolicyKey > 0); + assertEquals(account1.mId, Policy.getAccountIdWithPolicyKey(mMockContext, + account1.mPolicyKey)); + assertEquals(account2.mId, Policy.getAccountIdWithPolicyKey(mMockContext, + account2.mPolicyKey)); + } + + public void testSetAndClearAccountPolicy() { + String securitySyncKey = "key"; + Account account = ProviderTestUtils.setupAccount("acct", true, mMockContext); + // Nothing up my sleeve + assertEquals(0, account.mPolicyKey); + assertEquals(0, EmailContent.count(mMockContext, Policy.CONTENT_URI)); + Policy policy = new Policy(); + policy.setAccountPolicy(mMockContext, account, securitySyncKey); + account.refresh(mMockContext); + // We should have a policyKey now + assertTrue(account.mPolicyKey > 0); + Policy dbPolicy = Policy.restorePolicyWithId(mMockContext, account.mPolicyKey); + // The policy should exist in the database + assertNotNull(dbPolicy); + // And it should be the same as the original + assertEquals(policy, dbPolicy); + // The account should have the security sync key set + assertEquals(securitySyncKey, account.mSecuritySyncKey); + Policy.clearAccountPolicy(mMockContext, account); + account.refresh(mMockContext); + // Make sure policyKey is cleared and policy is deleted + assertEquals(0, account.mPolicyKey); + assertEquals(0, EmailContent.count(mMockContext, Policy.CONTENT_URI)); + account.refresh(mMockContext); + // The account's security sync key should also be null + assertNull(account.mSecuritySyncKey); + } + + public void testParcel() { + Policy policy = new Policy(); + policy.mPasswordMode = Policy.PASSWORD_MODE_STRONG; + policy.mPasswordMinLength = 6; + policy.mPasswordComplexChars = 5; + policy.mPasswordExpirationDays = 4; + policy.mPasswordHistory = 3; + policy.mPasswordMaxFails = 8; + policy.mMaxScreenLockTime = 600; + policy.mRequireRemoteWipe = true; + policy.mRequireEncryption = true; + policy.mRequireEncryptionExternal = true; + Parcel parcel = Parcel.obtain(); + policy.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + Policy readPolicy = Policy.CREATOR.createFromParcel(parcel); + assertEquals(policy, readPolicy); + } +} diff --git a/tests/src/com/android/email/provider/ProviderTestUtils.java b/tests/src/com/android/email/provider/ProviderTestUtils.java index f0e9853cc..674295aab 100644 --- a/tests/src/com/android/email/provider/ProviderTestUtils.java +++ b/tests/src/com/android/email/provider/ProviderTestUtils.java @@ -64,7 +64,7 @@ public class ProviderTestUtils extends Assert { account.mRingtoneUri = "content://ringtone-" + name; account.mProtocolVersion = "2.5" + name; account.mNewMessageCount = 5 + name.length(); - account.mSecurityFlags = 7; + account.mPolicyKey = 0; account.mSecuritySyncKey = "sec-sync-key-" + name; account.mSignature = "signature-" + name; if (saveIt) { @@ -319,7 +319,7 @@ public class ProviderTestUtils extends Assert { actual.mProtocolVersion); assertEquals(caller + " mNewMessageCount", expect.mNewMessageCount, actual.mNewMessageCount); - assertEquals(caller + " mSecurityFlags", expect.mSecurityFlags, actual.mSecurityFlags); + assertEquals(caller + " mPolicyKey", expect.mPolicyKey, actual.mPolicyKey); assertEquals(caller + " mSecuritySyncKey", expect.mSecuritySyncKey, actual.mSecuritySyncKey); assertEquals(caller + " mSignature", expect.mSignature, actual.mSignature);