diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b7cb8640c..caf95926b 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -193,6 +193,20 @@
+
+
+
+
+
+
+
+
The AccountManager could not create the Account; please try again.
+
+
+
+ Email Device Administrator
+
+ Email Device Administrator - Long Description
+
diff --git a/res/xml/device_admin.xml b/res/xml/device_admin.xml
new file mode 100644
index 000000000..cbb8641b7
--- /dev/null
+++ b/res/xml/device_admin.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/com/android/email/SecurityPolicy.java b/src/com/android/email/SecurityPolicy.java
index f543a81e5..1a54def2c 100644
--- a/src/com/android/email/SecurityPolicy.java
+++ b/src/com/android/email/SecurityPolicy.java
@@ -19,6 +19,8 @@ package com.android.email;
import com.android.email.provider.EmailContent.Account;
import android.app.DeviceAdmin;
+import android.app.DevicePolicyManager;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
@@ -28,8 +30,18 @@ import android.database.Cursor;
*/
public class SecurityPolicy {
+ /** STOPSHIP - ok to check in true for now, but must be false for shipping */
+ /** DO NOT CHECK IN WHILE 'true' */
+ private static final boolean DEBUG_ALWAYS_ACTIVE = true;
+
private static SecurityPolicy sInstance = null;
private Context mContext;
+ private DevicePolicyManager mDPM;
+ private ComponentName mAdminName;
+ private PolicySet mAggregatePolicy;
+
+ private static final PolicySet NO_POLICY_SET =
+ new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false);
/**
* This projection on Account is for scanning/reading
@@ -42,6 +54,15 @@ public class SecurityPolicy {
private static final String WHERE_ACCOUNT_SECURITY_NONZERO =
Account.SECURITY_FLAGS + " IS NOT NULL AND " + Account.SECURITY_FLAGS + "!=0";
+ /**
+ * These are hardcoded limits based on knowledge of the current DevicePolicyManager
+ * and screen lock mechanisms. Wherever possible, these should be replaced with queries of
+ * dynamic capabilities of the device (e.g. what password modes are supported?)
+ */
+ private static final int LIMIT_MIN_PASSWORD_LENGTH = 16;
+ private static final int LIMIT_PASSWORD_MODE = PolicySet.PASSWORD_MODE_STRONG;
+ private static final int LIMIT_SCREENLOCK_TIME = PolicySet.SCREEN_LOCK_TIME_MAX;
+
/**
* Get the security policy instance
*/
@@ -57,6 +78,9 @@ public class SecurityPolicy {
*/
private SecurityPolicy(Context context) {
mContext = context;
+ mDPM = null;
+ mAdminName = new ComponentName(context, PolicyAdmin.class);
+ mAggregatePolicy = null;
}
/**
@@ -76,7 +100,8 @@ public class SecurityPolicy {
* max screen lock time take the min
* require remote wipe take the max (logical or)
*
- * @return a policy representing the strongest aggregate, or null if none are defined
+ * @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() {
boolean policiesFound = false;
@@ -117,39 +142,107 @@ public class SecurityPolicy {
return new PolicySet(minPasswordLength, passwordMode, maxPasswordFails,
maxScreenLockTime, requireRemoteWipe);
} else {
- return null;
+ return NO_POLICY_SET;
}
}
/**
- * Query used to determine if a given policy is "possible" (irrespective of current
+ * Get the dpm. This mainly allows us to make some utility calls without it, for testing.
+ */
+ private synchronized DevicePolicyManager getDPM() {
+ if (mDPM == null) {
+ mDPM = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
+ }
+ return mDPM;
+ }
+
+ /**
+ * API: Query used to determine if a given policy is "possible" (irrespective of current
* device state. This is used when creating new accounts.
*
- * TO BE IMPLEMENTED
+ * TODO: This is hardcoded based on knowledge of the current DevicePolicyManager
+ * and screen lock mechanisms. It would be nice to replace these tests with something
+ * more dynamic.
*
* @param policies the policies requested
* @return true if the policies are supported, false if not supported
*/
public boolean isSupported(PolicySet policies) {
+ if (policies.mMinPasswordLength > LIMIT_MIN_PASSWORD_LENGTH) {
+ return false;
+ }
+ if (policies.mPasswordMode > LIMIT_PASSWORD_MODE ) {
+ return false;
+ }
+ // No limit on password fail count
+ if (policies.mMaxScreenLockTime > LIMIT_SCREENLOCK_TIME ) {
+ return false;
+ }
+ // No limit on remote wipe capable
+
return true;
}
/**
- * Query used to determine if a given policy is "active" (the device is operating at
- * the required security level). This is used when creating new accounts.
- *
- * TO BE IMPLEMENTED
+ * API: Report that policies may have been updated due to rewriting values in an Account.
+ */
+ public synchronized void updatePolicies() {
+ mAggregatePolicy = null;
+ }
+
+ /**
+ * API: Query used to determine if a given policy is "active" (the device is operating at
+ * the required security level). This is used when creating new accounts. This method
+ * is for queries only, and does not trigger any change in device state.
*
* @param policies the policies requested
* @return true if the policies are active, false if not active
*/
public boolean isActive(PolicySet policies) {
- return true;
+ DevicePolicyManager dpm = getDPM();
+ if (dpm.isAdminActive(mAdminName)) {
+ // check each policy
+ PolicySet aggregate;
+ synchronized (this) {
+ if (mAggregatePolicy == null) {
+ mAggregatePolicy = computeAggregatePolicy();
+ }
+ aggregate = mAggregatePolicy;
+ }
+ // quick check for the "empty set" of no policies
+ if (aggregate == NO_POLICY_SET) {
+ return true;
+ }
+ // check each policy explicitly
+ if (aggregate.mMinPasswordLength > 0) {
+ if (dpm.getPasswordMinimumLength(mAdminName) < aggregate.mMinPasswordLength) {
+ return false;
+ }
+ }
+ if (aggregate.mPasswordMode > 0) {
+ if (dpm.getPasswordQuality(mAdminName) < aggregate.getDPManagerPasswordMode()) {
+ return false;
+ }
+ if (!dpm.isActivePasswordSufficient()) {
+ return false;
+ }
+ }
+ if (aggregate.mMaxScreenLockTime > 0) {
+ // Note, we use seconds, dpm uses milliseconds
+ if (dpm.getMaximumTimeToLock(mAdminName) > aggregate.mMaxScreenLockTime * 1000) {
+ return false;
+ }
+ }
+ // password failures are counted locally - no test required here
+ // no check required for remote wipe (it's supported, if we're the admin)
+ }
+ // return false, not active - unless debugging enabled
+ return DEBUG_ALWAYS_ACTIVE;
}
/**
* Sync service should call this any time a sync fails due to isActive() returning false.
- * This will kick off the acquire-admin-state process and/or increase the security level.
+ * This will kick off the notify-acquire-admin-state process and/or increase the security level.
* The caller needs to write the required policies into this account before making this call.
*
* @param accountId the account for which sync cannot proceed
@@ -196,7 +289,7 @@ public class SecurityPolicy {
* @param minPasswordLength (0=not enforced)
* @param passwordMode
* @param maxPasswordFails (0=not enforced)
- * @param maxScreenLockTime (0=not enforced)
+ * @param maxScreenLockTime in seconds (0=not enforced)
* @param requireRemoteWipe
*/
public PolicySet(int minPasswordLength, int passwordMode, int maxPasswordFails,
@@ -245,6 +338,20 @@ public class SecurityPolicy {
mRequireRemoteWipe = 0 != (flags & REQUIRE_REMOTE_WIPE);
}
+ /**
+ * Helper to map DevicePolicyManager password modes to our internal encoding.
+ */
+ public int getDPManagerPasswordMode() {
+ switch (mPasswordMode) {
+ case PASSWORD_MODE_SIMPLE:
+ return DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
+ case PASSWORD_MODE_STRONG:
+ return DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
+ default:
+ return DevicePolicyManager .PASSWORD_QUALITY_UNSPECIFIED;
+ }
+ }
+
/**
* Record flags (and a sync key for the flags) into an Account
* Note: the hash code is defined as the encoding used in Account
diff --git a/tests/src/com/android/email/SecurityPolicyTests.java b/tests/src/com/android/email/SecurityPolicyTests.java
index 81ad940c2..a7c6f2586 100644
--- a/tests/src/com/android/email/SecurityPolicyTests.java
+++ b/tests/src/com/android/email/SecurityPolicyTests.java
@@ -25,6 +25,7 @@ import com.android.email.provider.EmailContent.AccountColumns;
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;
@@ -38,6 +39,9 @@ public class SecurityPolicyTests extends ProviderTestCase2 {
private Context mMockContext;
+ private static final PolicySet EMPTY_POLICY_SET =
+ new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false);
+
public SecurityPolicyTests() {
super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
}
@@ -46,7 +50,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 {
protected void setUp() throws Exception {
super.setUp();
- mMockContext = getMockContext();
+ mMockContext = new MockContext2(getMockContext(), this.mContext);
}
/**
@@ -57,6 +61,24 @@ public class SecurityPolicyTests extends ProviderTestCase2 {
super.tearDown();
}
+ /**
+ * Private context wrapper used to add back getPackageName() for these tests
+ */
+ private static class MockContext2 extends ContextWrapper {
+
+ private final Context mRealContext;
+
+ public MockContext2(Context mockContext, Context realContext) {
+ super(mockContext);
+ mRealContext = realContext;
+ }
+
+ @Override
+ public String getPackageName() {
+ return mRealContext.getPackageName();
+ }
+ }
+
/**
* Retrieve the security policy object, and inject the mock context so it works as expected
*/
@@ -72,17 +94,17 @@ public class SecurityPolicyTests extends ProviderTestCase2 {
public void testAggregator() {
SecurityPolicy sp = getSecurityPolicy();
- // with no accounts, should return null
- assertNull(sp.computeAggregatePolicy());
+ // with no accounts, should return empty set
+ assertTrue(EMPTY_POLICY_SET.equals(sp.computeAggregatePolicy()));
- // with accounts having no security, return null
+ // 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);
- assertNull(sp.computeAggregatePolicy());
+ assertTrue(EMPTY_POLICY_SET.equals(sp.computeAggregatePolicy()));
// with a single account in security mode, should return same security as in account
PolicySet p3in = new PolicySet(10, PolicySet.PASSWORD_MODE_SIMPLE, 15, 16, false);
@@ -142,7 +164,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 {
Account a2 = ProviderTestUtils.setupAccount("no-sec-2", false, mMockContext);
a2.mSecurityFlags = 0;
a2.save(mMockContext);
- assertNull(sp.computeAggregatePolicy());
+ assertTrue(EMPTY_POLICY_SET.equals(sp.computeAggregatePolicy()));
}
/**