Rework of security policy storage

* Replace crazy (and soon to be "full") bit fields stored in an account's
  securityFlags with a row in a newly created Policy table (thus, fully
  expandable)
* Update code from database version 17 to 18; adds Policy table, a
  policyKey row in Account, and a revised trigger that deletes Policy
  information for deleted Accounts
* Update old PolicySet unit tests to work against the new Policy class
* Add test for the conversion of securityFlags to Policy
* Tested in a variety of scenarios; appears to be functionally equivalent

Change-Id: I1505ee75230d6a0d3c2b62a46326f39c2c7f9eb5
This commit is contained in:
Marc Blank 2011-04-27 17:12:06 -07:00
parent 6cd012b92d
commit aeee10e57e
25 changed files with 1301 additions and 1078 deletions

View File

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

View File

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

View File

@ -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 extends EmailContent> T restoreContentWithId(Context context,
Class<T> 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 extends EmailContent> T getContent(Cursor cursor, Class<T> 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<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
@ -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";
}
}

View File

@ -13,7 +13,7 @@
* limitations under the License.
*/
package com.android.emailcommon.service;
package com.android.emailcommon.provider;
parcelable PolicySet;
parcelable Policy;

View File

@ -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<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
// 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<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
// 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<Policy> CREATOR = new Parcelable.Creator<Policy>() {
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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<PolicySet> CREATOR
= new Parcelable.Creator<PolicySet>() {
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 + "}";
}
}

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -687,7 +687,6 @@ public class LegacyConversionsTests extends ProviderTestCase2<EmailProvider> {
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<EmailProvider> {
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);
}
}

View File

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

View File

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

View File

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