From 2736c1a11ce3ecdcd9d19aa9c324fb9ce0910c7b Mon Sep 17 00:00:00 2001 From: Marc Blank Date: Thu, 20 Oct 2011 10:13:02 -0700 Subject: [PATCH] Rewrite of security policy handling and service code * Remove PolicyService APIs policiesRequired, policiesUpdated, isSupported, clearUnsupportedPolicies, and isActiveAdmin * Add PolicyService API setAccountPolicy, which is the sole method by which security policies are promulgated * Add protocolPoliciesEnabled and protocolPoliciesUnsupported to the Policy class; these are packed, localized strings indicating policies that the protocol itself have enabled and/or cannot support (i.e. these are policies that are unknown to the DPM, e.g. don't load attachments) * Differentiate in security notifications between three kinds of policy changes - changes that don't require user intervention (e.g. reducing requirements), changes that require user intervention (the legacy notification), and changes that make the account unsyncable (e.g. the server adding an unsupportable policy). Handle all possible policy changes cleanly. * Make security notifications per account (with multiple accounts, notifications would get arbitrarily munged) * Expose ALL enforced policies via the account settings screen in two categories: policies enforced (including both policies enforced by the DPM and policies enforced by the protocol) and policies unsupported (note that these can only be seen if policies are changed after an account is created; we do not allow the creation of an account when any required policies are unsupported). Add a button that forces a sync attempt, for accounts that are locked out, but whose policies have changed on the server (this would otherwise require a reboot). * Updated unit tests Bug: 5398682 Bug: 5393724 Bug: 5379682 Change-Id: I4a3df823913a809874ed959d228177f0fc799281 --- .../android/emailcommon/provider/Account.java | 4 - .../emailcommon/provider/EmailContent.java | 3 + .../android/emailcommon/provider/Policy.java | 127 +++++------ .../emailcommon/service/IPolicyService.aidl | 7 +- .../service/PolicyServiceProxy.java | 107 +-------- .../emailcommon/utility/TextUtilities.java | 11 + res/values/strings.xml | 53 ++++- res/xml/account_settings_preferences.xml | 20 ++ .../android/email/NotificationController.java | 64 +++++- src/com/android/email/SecurityPolicy.java | 203 ++++++++++++------ src/com/android/email/activity/Welcome.java | 15 +- .../setup/AccountCheckSettingsFragment.java | 6 +- .../email/activity/setup/AccountSecurity.java | 1 + .../setup/AccountSettingsFragment.java | 89 +++++++- .../activity/setup/PolicyListPreference.java | 45 ++++ .../android/email/provider/EmailProvider.java | 21 +- .../android/email/service/PolicyService.java | 28 +-- .../android/email/SecurityPolicyTests.java | 62 ++---- .../android/email/provider/PolicyTests.java | 22 +- .../android/email/provider/ProviderTests.java | 3 +- 20 files changed, 536 insertions(+), 355 deletions(-) mode change 100644 => 100755 emailcommon/src/com/android/emailcommon/provider/Account.java mode change 100644 => 100755 emailcommon/src/com/android/emailcommon/provider/EmailContent.java mode change 100644 => 100755 emailcommon/src/com/android/emailcommon/provider/Policy.java mode change 100644 => 100755 emailcommon/src/com/android/emailcommon/service/IPolicyService.aidl mode change 100644 => 100755 emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java mode change 100644 => 100755 emailcommon/src/com/android/emailcommon/utility/TextUtilities.java mode change 100644 => 100755 res/values/strings.xml mode change 100644 => 100755 res/xml/account_settings_preferences.xml create mode 100644 src/com/android/email/activity/setup/PolicyListPreference.java mode change 100644 => 100755 tests/src/com/android/email/SecurityPolicyTests.java mode change 100644 => 100755 tests/src/com/android/email/provider/PolicyTests.java mode change 100644 => 100755 tests/src/com/android/email/provider/ProviderTests.java diff --git a/emailcommon/src/com/android/emailcommon/provider/Account.java b/emailcommon/src/com/android/emailcommon/provider/Account.java old mode 100644 new mode 100755 index bffba5d5b..757118b10 --- a/emailcommon/src/com/android/emailcommon/provider/Account.java +++ b/emailcommon/src/com/android/emailcommon/provider/Account.java @@ -671,10 +671,6 @@ public final class Account extends EmailContent implements AccountColumns, Parce */ @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 - Policy.setAccountPolicy(context, this, mPolicy, null); - } if (cv.containsKey(AccountColumns.IS_DEFAULT) && cv.getAsBoolean(AccountColumns.IS_DEFAULT)) { ArrayList ops = new ArrayList(); diff --git a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java old mode 100644 new mode 100755 index 45876731b..ac0d659a5 --- a/emailcommon/src/com/android/emailcommon/provider/EmailContent.java +++ b/emailcommon/src/com/android/emailcommon/provider/EmailContent.java @@ -1437,5 +1437,8 @@ public abstract class EmailContent { public static final String MAX_CALENDAR_LOOKBACK = "maxCalendarLookback"; // Indicates that the server allows password recovery, not that we support it public static final String PASSWORD_RECOVERY_ENABLED = "passwordRecoveryEnabled"; + // Tokenized strings indicating protocol specific policies enforced/unsupported + public static final String PROTOCOL_POLICIES_ENFORCED = "protocolPoliciesEnforced"; + public static final String PROTOCOL_POLICIES_UNSUPPORTED = "protocolPoliciesUnsupported"; } } diff --git a/emailcommon/src/com/android/emailcommon/provider/Policy.java b/emailcommon/src/com/android/emailcommon/provider/Policy.java old mode 100644 new mode 100755 index 5c95abec8..d43290f9f --- a/emailcommon/src/com/android/emailcommon/provider/Policy.java +++ b/emailcommon/src/com/android/emailcommon/provider/Policy.java @@ -16,21 +16,17 @@ package com.android.emailcommon.provider; import android.app.admin.DevicePolicyManager; -import android.content.ContentProviderOperation; import android.content.ContentResolver; 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 com.android.emailcommon.utility.TextUtilities; import com.android.emailcommon.utility.Utility; -import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; @@ -56,6 +52,8 @@ public final class Policy extends EmailContent implements EmailContent.PolicyCol public static final int PASSWORD_MODE_SIMPLE = 1; public static final int PASSWORD_MODE_STRONG = 2; + public static final char POLICY_STRING_DELIMITER = '\1'; + public int mPasswordMode; public int mPasswordMinLength; public int mPasswordMaxFails; @@ -76,6 +74,8 @@ public final class Policy extends EmailContent implements EmailContent.PolicyCol public int mMaxEmailLookback; public int mMaxCalendarLookback; public boolean mPasswordRecoveryEnabled; + public String mProtocolPoliciesEnforced; + public String mProtocolPoliciesUnsupported; public static final int CONTENT_ID_COLUMN = 0; public static final int CONTENT_PASSWORD_MODE_COLUMN = 1; @@ -98,6 +98,8 @@ public final class Policy extends EmailContent implements EmailContent.PolicyCol public static final int CONTENT_MAX_EMAIL_LOOKBACK_COLUMN = 18; public static final int CONTENT_MAX_CALENDAR_LOOKBACK_COLUMN = 19; public static final int CONTENT_PASSWORD_RECOVERY_ENABLED_COLUMN = 20; + public static final int CONTENT_PROTOCOL_POLICIES_ENFORCED_COLUMN = 21; + public static final int CONTENT_PROTOCOL_POLICIES_UNSUPPORTED_COLUMN = 22; public static final String[] CONTENT_PROJECTION = new String[] {RECORD_ID, PolicyColumns.PASSWORD_MODE, PolicyColumns.PASSWORD_MIN_LENGTH, @@ -109,7 +111,8 @@ public final class Policy extends EmailContent implements EmailContent.PolicyCol PolicyColumns.DONT_ALLOW_ATTACHMENTS, PolicyColumns.DONT_ALLOW_HTML, PolicyColumns.MAX_ATTACHMENT_SIZE, PolicyColumns.MAX_TEXT_TRUNCATION_SIZE, PolicyColumns.MAX_HTML_TRUNCATION_SIZE, PolicyColumns.MAX_EMAIL_LOOKBACK, - PolicyColumns.MAX_CALENDAR_LOOKBACK, PolicyColumns.PASSWORD_RECOVERY_ENABLED + PolicyColumns.MAX_CALENDAR_LOOKBACK, PolicyColumns.PASSWORD_RECOVERY_ENABLED, + PolicyColumns.PROTOCOL_POLICIES_ENFORCED, PolicyColumns.PROTOCOL_POLICIES_UNSUPPORTED }; public static final Policy NO_POLICY = new Policy(); @@ -139,6 +142,24 @@ public final class Policy extends EmailContent implements EmailContent.PolicyCol Account.ID_PROJECTION_COLUMN, Account.NO_ACCOUNT); } + public static ArrayList addPolicyStringToList(String policyString, + ArrayList policyList) { + if (policyString != null) { + int start = 0; + int len = policyString.length(); + while(start < len) { + int end = policyString.indexOf(POLICY_STRING_DELIMITER, start); + if (end > start) { + policyList.add(policyString.substring(start, end)); + start = end + 1; + } else { + break; + } + } + } + return policyList; + } + // We override this method to insure that we never write invalid policy data to the provider @Override public Uri save(Context context) { @@ -146,76 +167,6 @@ public final class Policy extends EmailContent implements EmailContent.PolicyCol return super.save(context); } - public static void clearAccountPolicy(Context context, Account account) { - setAccountPolicy(context, account, null, null); - } - - /** - * Convenience method for {@link #setAccountPolicy(Context, Account, Policy, String)}. - */ - @VisibleForTesting - public static void setAccountPolicy(Context context, long accountId, Policy policy, - String securitySyncKey) { - setAccountPolicy(context, Account.restoreAccountWithId(context, accountId), - policy, securitySyncKey); - } - - /** - * Set the policy for an account atomically; this also removes any other policy associated with - * the account and sets the policy key for the account. If policy is null, the policyKey is - * set to 0 and the securitySyncKey to null. Also, update the account object to reflect the - * current policyKey and securitySyncKey - * @param context the caller's context - * @param account the account whose policy is to be set - * @param policy the policy to set, or null if we're clearing the policy - * @param securitySyncKey the security sync key for this account (ignored if policy is null) - */ - public static void setAccountPolicy(Context context, Account account, Policy policy, - String securitySyncKey) { - if (DEBUG_POLICY) { - Log.d(TAG, "Set policy for account " + account.mDisplayName + ": " + - ((policy == null) ? "none" : policy.toString())); - } - ArrayList ops = new ArrayList(); - - // Make sure this is a valid policy set - if (policy != null) { - policy.normalize(); - // Add the new policy (no account will yet reference this) - ops.add(ContentProviderOperation.newInsert( - Policy.CONTENT_URI).withValues(policy.toContentValues()).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()); - } else { - ops.add(ContentProviderOperation.newUpdate( - ContentUris.withAppendedId(Account.CONTENT_URI, account.mId)) - .withValue(AccountColumns.SECURITY_SYNC_KEY, null) - .withValue(AccountColumns.POLICY_KEY, 0) - .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()); - } - - try { - context.getContentResolver().applyBatch(EmailContent.AUTHORITY, ops); - account.refresh(context); - } 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 - } - } - /** * Review all attachment records for this account, and reset the "don't allow download" flag * as required by the account's new security policies @@ -286,6 +237,7 @@ public final class Policy extends EmailContent implements EmailContent.PolicyCol public boolean equals(Object other) { if (!(other instanceof Policy)) return false; Policy otherPolicy = (Policy)other; + // Policies here are enforced by the DPM if (mRequireEncryption != otherPolicy.mRequireEncryption) return false; if (mRequireEncryptionExternal != otherPolicy.mRequireEncryptionExternal) return false; if (mRequireRemoteWipe != otherPolicy.mRequireRemoteWipe) return false; @@ -296,10 +248,13 @@ public final class Policy extends EmailContent implements EmailContent.PolicyCol if (mPasswordMaxFails != otherPolicy.mPasswordMaxFails) return false; if (mPasswordMinLength != otherPolicy.mPasswordMinLength) return false; if (mPasswordMode != otherPolicy.mPasswordMode) return false; + if (mDontAllowCamera != otherPolicy.mDontAllowCamera) return false; + + // Policies here are enforced by the Exchange sync manager + // They should eventually be removed from Policy and replaced with some opaque data if (mRequireManualSyncWhenRoaming != otherPolicy.mRequireManualSyncWhenRoaming) { return false; } - if (mDontAllowCamera != otherPolicy.mDontAllowCamera) return false; if (mDontAllowAttachments != otherPolicy.mDontAllowAttachments) return false; if (mDontAllowHtml != otherPolicy.mDontAllowHtml) return false; if (mMaxAttachmentSize != otherPolicy.mMaxAttachmentSize) return false; @@ -308,6 +263,15 @@ public final class Policy extends EmailContent implements EmailContent.PolicyCol if (mMaxEmailLookback != otherPolicy.mMaxEmailLookback) return false; if (mMaxCalendarLookback != otherPolicy.mMaxCalendarLookback) return false; if (mPasswordRecoveryEnabled != otherPolicy.mPasswordRecoveryEnabled) return false; + + if (!TextUtilities.stringOrNullEquals(mProtocolPoliciesEnforced, + otherPolicy.mProtocolPoliciesEnforced)) { + return false; + } + if (!TextUtilities.stringOrNullEquals(mProtocolPoliciesUnsupported, + otherPolicy.mProtocolPoliciesUnsupported)) { + return false; + } return true; } @@ -353,6 +317,9 @@ public final class Policy extends EmailContent implements EmailContent.PolicyCol mMaxEmailLookback = cursor.getInt(CONTENT_MAX_EMAIL_LOOKBACK_COLUMN); mMaxCalendarLookback = cursor.getInt(CONTENT_MAX_CALENDAR_LOOKBACK_COLUMN); mPasswordRecoveryEnabled = cursor.getInt(CONTENT_PASSWORD_RECOVERY_ENABLED_COLUMN) == 1; + mProtocolPoliciesEnforced = cursor.getString(CONTENT_PROTOCOL_POLICIES_ENFORCED_COLUMN); + mProtocolPoliciesUnsupported = + cursor.getString(CONTENT_PROTOCOL_POLICIES_UNSUPPORTED_COLUMN); } @Override @@ -378,6 +345,8 @@ public final class Policy extends EmailContent implements EmailContent.PolicyCol values.put(PolicyColumns.MAX_EMAIL_LOOKBACK, mMaxEmailLookback); values.put(PolicyColumns.MAX_CALENDAR_LOOKBACK, mMaxCalendarLookback); values.put(PolicyColumns.PASSWORD_RECOVERY_ENABLED, mPasswordRecoveryEnabled); + values.put(PolicyColumns.PROTOCOL_POLICIES_ENFORCED, mProtocolPoliciesEnforced); + values.put(PolicyColumns.PROTOCOL_POLICIES_UNSUPPORTED, mProtocolPoliciesUnsupported); return values; } @@ -508,6 +477,8 @@ public final class Policy extends EmailContent implements EmailContent.PolicyCol dest.writeInt(mMaxEmailLookback); dest.writeInt(mMaxCalendarLookback); dest.writeInt(mPasswordRecoveryEnabled ? 1 : 0); + dest.writeString(mProtocolPoliciesEnforced); + dest.writeString(mProtocolPoliciesUnsupported); } /** @@ -536,5 +507,7 @@ public final class Policy extends EmailContent implements EmailContent.PolicyCol mMaxEmailLookback = in.readInt(); mMaxCalendarLookback = in.readInt(); mPasswordRecoveryEnabled = in.readInt() == 1; + mProtocolPoliciesEnforced = in.readString(); + mProtocolPoliciesUnsupported = in.readString(); } } \ No newline at end of file diff --git a/emailcommon/src/com/android/emailcommon/service/IPolicyService.aidl b/emailcommon/src/com/android/emailcommon/service/IPolicyService.aidl old mode 100644 new mode 100755 index e9bcf42dd..9d4be36e4 --- a/emailcommon/src/com/android/emailcommon/service/IPolicyService.aidl +++ b/emailcommon/src/com/android/emailcommon/service/IPolicyService.aidl @@ -19,12 +19,7 @@ import com.android.emailcommon.provider.Policy; interface IPolicyService { boolean isActive(in Policy policies); - void policiesRequired(long accountId); - void policiesUpdated(long accountId); void setAccountHoldFlag(long accountId, boolean newState); - boolean isActiveAdmin(); - // This is about as oneway as you can get + void setAccountPolicy(long accountId, in Policy policy, String securityKey); oneway void remoteWipe(); - boolean isSupported(in Policy policies); - Policy clearUnsupportedPolicies(in Policy policies); } \ No newline at end of file diff --git a/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java b/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java old mode 100644 new mode 100755 index a3b317e26..26e820dee --- a/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java +++ b/emailcommon/src/com/android/emailcommon/service/PolicyServiceProxy.java @@ -48,24 +48,6 @@ public class PolicyServiceProxy extends ServiceProxy implements IPolicyService { return null; } - @Override - public Policy clearUnsupportedPolicies(final Policy arg0) throws RemoteException { - setTask(new ProxyTask() { - public void run() throws RemoteException { - mReturn = mService.clearUnsupportedPolicies(arg0); - } - }, "clearUnsupportedPolicies"); - waitForCompletion(); - if (DEBUG_PROXY) { - Log.v(TAG, "clearUnsupportedPolicies: " + ((mReturn == null) ? "null" : mReturn)); - } - if (mReturn == null) { - throw new ServiceUnavailableException("clearUnsupportedPolicies"); - } else { - return (Policy)mReturn; - } - } - @Override public boolean isActive(final Policy arg0) throws RemoteException { setTask(new ProxyTask() { @@ -85,48 +67,14 @@ public class PolicyServiceProxy extends ServiceProxy implements IPolicyService { } @Override - public boolean isActiveAdmin() throws RemoteException { + public void setAccountPolicy(final long accountId, final Policy policy, + final String securityKey) throws RemoteException { setTask(new ProxyTask() { public void run() throws RemoteException { - mReturn = mService.isActiveAdmin(); + mService.setAccountPolicy(accountId, policy, securityKey); } - }, "isActiveAdmin"); + }, "setAccountPolicy"); waitForCompletion(); - if (DEBUG_PROXY) { - Log.v(TAG, "isActiveAdmin: " + ((mReturn == null) ? "null" : mReturn)); - } - if (mReturn == null) { - throw new ServiceUnavailableException("isActiveAdmin"); - } else { - return (Boolean)mReturn; - } - } - - @Override - public boolean isSupported(final Policy arg0) throws RemoteException { - setTask(new ProxyTask() { - public void run() throws RemoteException { - mReturn = mService.isSupported(arg0); - } - }, "isSupported"); - waitForCompletion(); - if (DEBUG_PROXY) { - Log.v(TAG, "isSupported: " + ((mReturn == null) ? "null" : mReturn)); - } - if (mReturn == null) { - throw new ServiceUnavailableException("isSupported"); - } else { - return (Boolean)mReturn; - } - } - - @Override - public void policiesRequired(final long arg0) throws RemoteException { - setTask(new ProxyTask() { - public void run() throws RemoteException { - mService.policiesRequired(arg0); - } - }, "policiesRequired"); } @Override @@ -147,15 +95,6 @@ public class PolicyServiceProxy extends ServiceProxy implements IPolicyService { }, "setAccountHoldFlag"); } - @Override - public void policiesUpdated(final long arg0) throws RemoteException { - setTask(new ProxyTask() { - public void run() throws RemoteException { - mService.policiesUpdated(arg0); - } - }, "policiesUpdated"); - } - // Static methods that encapsulate the proxy calls above public static boolean isActive(Context context, Policy policies) { try { @@ -165,22 +104,6 @@ public class PolicyServiceProxy extends ServiceProxy implements IPolicyService { return false; } - public static void policiesRequired(Context context, long accountId) { - try { - new PolicyServiceProxy(context).policiesRequired(accountId); - } catch (RemoteException e) { - throw new IllegalStateException("PolicyService transaction failed"); - } - } - - public static void policiesUpdated(Context context, long accountId) { - try { - new PolicyServiceProxy(context).policiesUpdated(accountId); - } catch (RemoteException e) { - throw new IllegalStateException("PolicyService transaction failed"); - } - } - public static void setAccountHoldFlag(Context context, Account account, boolean newState) { try { new PolicyServiceProxy(context).setAccountHoldFlag(account.mId, newState); @@ -189,14 +112,6 @@ public class PolicyServiceProxy extends ServiceProxy implements IPolicyService { } } - public static boolean isActiveAdmin(Context context) { - try { - return new PolicyServiceProxy(context).isActiveAdmin(); - } catch (RemoteException e) { - } - return false; - } - public static void remoteWipe(Context context) { try { new PolicyServiceProxy(context).remoteWipe(); @@ -205,17 +120,11 @@ public class PolicyServiceProxy extends ServiceProxy implements IPolicyService { } } - public static boolean isSupported(Context context, Policy policy) { + public static void setAccountPolicy(Context context, long accountId, Policy policy, + String securityKey) { try { - return new PolicyServiceProxy(context).isSupported(policy); - } catch (RemoteException e) { - } - return false; - } - - public static Policy clearUnsupportedPolicies(Context context, Policy policy) { - try { - return new PolicyServiceProxy(context).clearUnsupportedPolicies(policy); + new PolicyServiceProxy(context).setAccountPolicy(accountId, policy, securityKey); + return; } catch (RemoteException e) { } throw new IllegalStateException("PolicyService transaction failed"); diff --git a/emailcommon/src/com/android/emailcommon/utility/TextUtilities.java b/emailcommon/src/com/android/emailcommon/utility/TextUtilities.java old mode 100644 new mode 100755 index 59f6fd2c0..0aa919098 --- a/emailcommon/src/com/android/emailcommon/utility/TextUtilities.java +++ b/emailcommon/src/com/android/emailcommon/utility/TextUtilities.java @@ -714,4 +714,15 @@ public class TextUtilities { return (CharSequence)sb; } + + /** + * Determine whether two Strings (either of which might be null) are the same; this is true + * when both are null or both are Strings that are equal. + */ + public static boolean stringOrNullEquals(String a, String b) { + if (a == null && b == null) return true; + if (a != null && b != null && a.equals(b)) return true; + return false; + } + } diff --git a/res/values/strings.xml b/res/values/strings.xml old mode 100644 new mode 100755 index 391565a85..1b6276d31 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -856,14 +856,30 @@ as %s. provisioning, just before jumping into system settings such as Device Policy grant, PIN/password, or encryption setup. [CHAR LIMIT=none] --> - %s requires that you update your security settings. + %s requires that you update your security + settings. - + + Account \"%s\" cannot be synced due to security + requirements. + + Account \"%s\" requires security settings update. + + + Account \"%s\" changed its security settings; no user + action is required. + - Security update required + Security update required + + Security policies have + changed + + Security policies cannot be + met Device security Incoming settings - + Username, password, and other incoming server settings Outgoing settings - + Username, password, and other outgoing server settings + Policies enforced + + None + + Unsupported policies + + None + + Attempt sync + + Tap here to attempt to sync this account (i.e. if server settings have changed) + Account name Your name @@ -963,6 +991,8 @@ as %s. Notification settings Data usage + + Security policies Edit quick response @@ -1213,4 +1243,17 @@ as %s. looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong + + + Disallow use of the device\'s camera + + Require device password + + Restrict the reuse of recent passwords + + Require passwords to expire + + Require an idle device to lock its screen + diff --git a/res/xml/account_settings_preferences.xml b/res/xml/account_settings_preferences.xml old mode 100644 new mode 100755 index 7b6fa9bd6..e55386356 --- a/res/xml/account_settings_preferences.xml +++ b/res/xml/account_settings_preferences.xml @@ -135,6 +135,26 @@ android:summary="@string/account_settings_outgoing_summary" /> + + + + + + + + + diff --git a/src/com/android/email/NotificationController.java b/src/com/android/email/NotificationController.java index 5ed3e012f..eaf0213ad 100644 --- a/src/com/android/email/NotificationController.java +++ b/src/com/android/email/NotificationController.java @@ -52,6 +52,7 @@ 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.Mailbox; +import com.android.emailcommon.utility.EmailAsyncTask; import com.android.emailcommon.utility.Utility; import com.google.common.annotations.VisibleForTesting; @@ -62,7 +63,6 @@ import java.util.HashSet; * Class that manages notifications. */ public class NotificationController { - private static final int NOTIFICATION_ID_SECURITY_NEEDED = 1; /** Reserved for {@link com.android.exchange.CalendarSyncEnabler} */ @SuppressWarnings("unused") private static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2; @@ -70,8 +70,11 @@ public class NotificationController { private static final int NOTIFICATION_ID_PASSWORD_EXPIRING = 4; private static final int NOTIFICATION_ID_PASSWORD_EXPIRED = 5; + private static final int NOTIFICATION_ID_BASE_MASK = 0xF0000000; private static final int NOTIFICATION_ID_BASE_NEW_MESSAGES = 0x10000000; private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000; + private static final int NOTIFICATION_ID_BASE_SECURITY_NEEDED = 0x30000000; + private static final int NOTIFICATION_ID_BASE_SECURITY_CHANGED = 0x40000000; /** Selection to retrieve accounts that should we notify user for changes */ private final static String NOTIFIED_ACCOUNT_SELECTION = @@ -144,7 +147,7 @@ public class NotificationController { private boolean needsOngoingNotification(int notificationId) { // "Security needed" must be ongoing so that the user doesn't close it; otherwise, sync will // be prevented until a reboot. Consider also doing this for password expired. - return notificationId == NOTIFICATION_ID_SECURITY_NEEDED; + return (notificationId & NOTIFICATION_ID_BASE_MASK) == NOTIFICATION_ID_BASE_SECURITY_NEEDED; } /** @@ -591,24 +594,67 @@ public class NotificationController { } /** - * Show (or update) a security needed notification. The given account is used to update - * the display text, but, all accounts share the same notification ID. + * Show (or update) a security needed notification. If tapped, the user is taken to a + * dialog asking whether he wants to update his settings. */ public void showSecurityNeededNotification(Account account) { Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, account.mId, true); String accountName = account.getDisplayName(); String ticker = - mContext.getString(R.string.security_notification_ticker_fmt, accountName); - String title = mContext.getString(R.string.security_notification_content_title); + mContext.getString(R.string.security_needed_ticker_fmt, accountName); + String title = mContext.getString(R.string.security_notification_content_update_title); showAccountNotification(account, ticker, title, accountName, intent, - NOTIFICATION_ID_SECURITY_NEEDED); + (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId)); } /** - * Cancels the security needed notification. + * Show (or update) a security changed notification. If tapped, the user is taken to the + * account settings screen where he can view the list of enforced policies + */ + public void showSecurityChangedNotification(Account account) { + Intent intent = AccountSettings.createAccountSettingsIntent(mContext, account.mId, null); + String accountName = account.getDisplayName(); + String ticker = + mContext.getString(R.string.security_changed_ticker_fmt, accountName); + String title = mContext.getString(R.string.security_notification_content_change_title); + showAccountNotification(account, ticker, title, accountName, intent, + (int)(NOTIFICATION_ID_BASE_SECURITY_CHANGED + account.mId)); + } + + /** + * Show (or update) a security unsupported notification. If tapped, the user is taken to the + * account settings screen where he can view the list of unsupported policies + */ + public void showSecurityUnsupportedNotification(Account account) { + Intent intent = AccountSettings.createAccountSettingsIntent(mContext, account.mId, null); + String accountName = account.getDisplayName(); + String ticker = + mContext.getString(R.string.security_unsupported_ticker_fmt, accountName); + String title = mContext.getString(R.string.security_notification_content_unsupported_title); + showAccountNotification(account, ticker, title, accountName, intent, + (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId)); + } + + /** + * Cancels all security needed notifications. */ public void cancelSecurityNeededNotification() { - mNotificationManager.cancel(NOTIFICATION_ID_SECURITY_NEEDED); + EmailAsyncTask.runAsyncParallel(new Runnable() { + @Override + public void run() { + Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI, + Account.ID_PROJECTION, null, null, null); + try { + while (c.moveToNext()) { + long id = c.getLong(Account.ID_PROJECTION_COLUMN); + mNotificationManager.cancel( + (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + id)); + } + } + finally { + c.close(); + } + }}); } /** diff --git a/src/com/android/email/SecurityPolicy.java b/src/com/android/email/SecurityPolicy.java index f571e8ad4..71793a028 100644 --- a/src/com/android/email/SecurityPolicy.java +++ b/src/com/android/email/SecurityPolicy.java @@ -20,11 +20,15 @@ import android.app.admin.DeviceAdminInfo; import android.app.admin.DeviceAdminReceiver; import android.app.admin.DevicePolicyManager; import android.content.ComponentName; +import android.content.ContentProviderOperation; import android.content.ContentResolver; +import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.content.OperationApplicationException; import android.database.Cursor; +import android.os.RemoteException; import android.util.Log; import com.android.email.service.EmailBroadcastProcessorService; @@ -34,9 +38,12 @@ import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.AccountColumns; import com.android.emailcommon.provider.EmailContent.PolicyColumns; import com.android.emailcommon.provider.Policy; +import com.android.emailcommon.utility.TextUtilities; import com.android.emailcommon.utility.Utility; import com.google.common.annotations.VisibleForTesting; +import java.util.ArrayList; + /** * Utility functions to support reading and writing security policies, and handshaking the device * into and out of various security states. @@ -204,11 +211,12 @@ 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 + * API: Report that policies may have been updated due to rewriting values in an Account; we + * clear the aggregate policy (so it can be recomputed) and set the policies in the DPM */ - public synchronized void policiesUpdated(long accountId) { + public synchronized void policiesUpdated() { mAggregatePolicy = null; + setActivePolicies(); } /** @@ -221,58 +229,7 @@ public class SecurityPolicy { if (Email.DEBUG) { Log.d(TAG, "reducePolicies"); } - policiesUpdated(-1); - setActivePolicies(); - } - - /** - * API: Query if the proposed set of policies are supported on the device. - * - * @param policy the polices that were requested - * @return boolean if supported - */ - 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 (policy.mRequireEncryption) { - int encryptionStatus = getDPM().getStorageEncryptionStatus(); - if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) { - return false; - } - } - - // If we ever support devices that can't disable cameras for any reason, we should - // indicate as such in the mDontAllowCamera policy - - return true; - } - - /** - * API: Remove any unsupported policies - * - * This is used when we have a set of polices that have been requested, but the server - * is willing to allow unsupported policies to be considered optional. - * - * @param policy the polices that were requested - * @return the same PolicySet if all are supported; A replacement PolicySet if any - * unsupported policies were removed - */ - 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 (policy.mRequireEncryption) { - int encryptionStatus = getDPM().getStorageEncryptionStatus(); - if (encryptionStatus == DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED) { - policy.mRequireEncryption = false; - } - } - - // If we ever support devices that can't disable cameras for any reason, we should - // clear the mDontAllowCamera policy - - return policy; + policiesUpdated(); } /** @@ -303,6 +260,9 @@ public class SecurityPolicy { if ((reasons & INACTIVE_NEED_ENCRYPTION) != 0) { sb.append("encryption "); } + if ((reasons & INACTIVE_PROTOCOL_POLICIES) != 0) { + sb.append("protocol "); + } Log.d(TAG, sb.toString()); } return reasons == 0; @@ -328,6 +288,11 @@ public class SecurityPolicy { */ public final static int INACTIVE_NEED_ENCRYPTION = 8; + /** + * Return bits from isActive: Protocol-specific policies cannot be enforced + */ + public final static int INACTIVE_PROTOCOL_POLICIES = 16; + /** * API: Query used to determine if a given policy is "active" (the device is operating at * the required security level). @@ -418,6 +383,10 @@ public class SecurityPolicy { // password failures are counted locally - no test required here // no check required for remote wipe (it's supported, if we're the admin) + if (policy.mProtocolPoliciesUnsupported != null) { + reasons |= INACTIVE_PROTOCOL_POLICIES; + } + // If we made it all the way, reasons == 0 here. Otherwise it's a list of grievances. return reasons; } @@ -514,24 +483,122 @@ public class SecurityPolicy { Account account = Account.restoreAccountWithId(mContext, accountId); // In case the account has been deleted, just return if (account == null) return; + if (account.mPolicyKey == 0) return; + Policy policy = Policy.restorePolicyWithId(mContext, account.mPolicyKey); + if (policy == 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); - } - } + Log.d(TAG, "policiesRequired for " + account.mDisplayName + ": " + policy); } // Mark the account as "on hold". setAccountHoldFlag(mContext, account, true); - // Put up a notification - NotificationController.getInstance(mContext).showSecurityNeededNotification(account); + // Put up an appropriate notification + if (policy.mProtocolPoliciesUnsupported == null) { + NotificationController.getInstance(mContext).showSecurityNeededNotification(account); + } else { + NotificationController.getInstance(mContext).showSecurityUnsupportedNotification( + account); + } + } + + public static void clearAccountPolicy(Context context, Account account) { + setAccountPolicy(context, account, null, null); + } + + /** + * Set the policy for an account atomically; this also removes any other policy associated with + * the account and sets the policy key for the account. If policy is null, the policyKey is + * set to 0 and the securitySyncKey to null. Also, update the account object to reflect the + * current policyKey and securitySyncKey + * @param context the caller's context + * @param account the account whose policy is to be set + * @param policy the policy to set, or null if we're clearing the policy + * @param securitySyncKey the security sync key for this account (ignored if policy is null) + */ + public static void setAccountPolicy(Context context, Account account, Policy policy, + String securitySyncKey) { + ArrayList ops = new ArrayList(); + + // Make sure this is a valid policy set + if (policy != null) { + policy.normalize(); + // Add the new policy (no account will yet reference this) + ops.add(ContentProviderOperation.newInsert( + Policy.CONTENT_URI).withValues(policy.toContentValues()).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()); + } else { + ops.add(ContentProviderOperation.newUpdate( + ContentUris.withAppendedId(Account.CONTENT_URI, account.mId)) + .withValue(AccountColumns.SECURITY_SYNC_KEY, null) + .withValue(AccountColumns.POLICY_KEY, 0) + .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()); + } + + try { + context.getContentResolver().applyBatch(EmailContent.AUTHORITY, ops); + account.refresh(context); + } 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 + } + } + + public void setAccountPolicy(long accountId, Policy policy, String securityKey) { + Account account = Account.restoreAccountWithId(mContext, accountId); + Policy oldPolicy = null; + if (account.mPolicyKey > 0) { + oldPolicy = Policy.restorePolicyWithId(mContext, account.mPolicyKey); + } + boolean policyChanged = !oldPolicy.equals(policy); + if (!policyChanged && (TextUtilities.stringOrNullEquals(securityKey, + account.mSecuritySyncKey))) { + Log.d(Logging.LOG_TAG, "setAccountPolicy; policy unchanged"); + } else { + setAccountPolicy(mContext, account, policy, securityKey); + policiesUpdated(); + } + + boolean setHold = false; + if (policy.mProtocolPoliciesUnsupported != null) { + // We can't support this, reasons in unsupportedRemotePolicies + Log.d(Logging.LOG_TAG, + "Notify policies for " + account.mDisplayName + " not supported."); + setHold = true; + NotificationController.getInstance(mContext).showSecurityUnsupportedNotification( + account); + // Erase data + Controller.getInstance(mContext).deleteSyncedDataSync(accountId); + } else if (isActive(policy)) { + if (policyChanged) { + Log.d(Logging.LOG_TAG, "Notify policies for " + account.mDisplayName + " changed."); + // Notify that policies changed + NotificationController.getInstance(mContext).showSecurityChangedNotification( + account); + } + } else { + setHold = true; + Log.d(Logging.LOG_TAG, "Notify policies for " + account.mDisplayName + + " are not being enforced."); + // Put up a notification + NotificationController.getInstance(mContext).showSecurityNeededNotification(account); + } + // Set/clear the account hold. + setAccountHoldFlag(mContext, account, setHold); } /** @@ -600,7 +667,7 @@ public class SecurityPolicy { } finally { c.close(); } - policiesUpdated(-1); + policiesUpdated(); } /** diff --git a/src/com/android/email/activity/Welcome.java b/src/com/android/email/activity/Welcome.java index e77746129..828073263 100644 --- a/src/com/android/email/activity/Welcome.java +++ b/src/com/android/email/activity/Welcome.java @@ -41,6 +41,7 @@ import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.provider.Policy; import com.android.emailcommon.utility.EmailAsyncTask; import com.android.emailcommon.utility.IntentUtilities; import com.android.emailcommon.utility.Utility; @@ -414,7 +415,19 @@ public class Welcome extends Activity { @Override public void onAccountSecurityHold(long accountId) { cleanUp(); - + // If we can't find the account, we know what to do + Account account = Account.restoreAccountWithId(Welcome.this, accountId); + if (account == null) { + onAccountNotFound(); + return; + } + // If there's no policy or it's "unsupported", act like the account doesn't exist + Policy policy = Policy.restorePolicyWithId(Welcome.this, account.mPolicyKey); + if (policy == null || (policy.mProtocolPoliciesUnsupported != null)) { + onAccountNotFound(); + return; + } + // Otherwise, try advancing security ActivityHelper.showSecurityHoldDialog(Welcome.this, accountId); finish(); } diff --git a/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java b/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java index ace59c3fb..a88e4b736 100644 --- a/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java +++ b/src/com/android/email/activity/setup/AccountCheckSettingsFragment.java @@ -469,8 +469,10 @@ public class AccountCheckSettingsFragment extends Fragment { EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET)); return new MessagingException(resultCode, mStoreHost); } else if (resultCode == MessagingException.SECURITY_POLICIES_UNSUPPORTED) { - String[] data = bundle.getStringArray( - EmailServiceProxy.VALIDATE_BUNDLE_UNSUPPORTED_POLICIES); + Policy policy = (Policy)bundle.getParcelable( + EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET); + String unsupported = policy.mProtocolPoliciesUnsupported; + String[] data = unsupported.split("" + Policy.POLICY_STRING_DELIMITER); return new MessagingException(resultCode, mStoreHost, data); } else if (resultCode != MessagingException.NO_ERROR) { String errorMessage = diff --git a/src/com/android/email/activity/setup/AccountSecurity.java b/src/com/android/email/activity/setup/AccountSecurity.java index 8c8ce5a62..ec336d528 100644 --- a/src/com/android/email/activity/setup/AccountSecurity.java +++ b/src/com/android/email/activity/setup/AccountSecurity.java @@ -119,6 +119,7 @@ public class AccountSecurity extends Activity { finish(); return; } + // Special handling for password expiration events if (passwordExpiring || passwordExpired) { FragmentManager fm = getFragmentManager(); diff --git a/src/com/android/email/activity/setup/AccountSettingsFragment.java b/src/com/android/email/activity/setup/AccountSettingsFragment.java index 5f8f18992..1acc55ba2 100644 --- a/src/com/android/email/activity/setup/AccountSettingsFragment.java +++ b/src/com/android/email/activity/setup/AccountSettingsFragment.java @@ -27,6 +27,7 @@ import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; +import android.content.res.Resources; import android.os.AsyncTask; import android.os.Bundle; import android.os.Vibrator; @@ -43,6 +44,7 @@ import android.util.Log; import com.android.email.Email; import com.android.email.R; +import com.android.email.SecurityPolicy; import com.android.email.mail.Sender; import com.android.emailcommon.AccountManagerTypes; import com.android.emailcommon.CalendarProviderStub; @@ -51,8 +53,11 @@ import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.HostAuth; +import com.android.emailcommon.provider.Policy; import com.android.emailcommon.utility.Utility; +import java.util.ArrayList; + /** * Fragment containing the main logic for account settings. This also calls out to other * fragments for server settings. @@ -81,6 +86,10 @@ public class AccountSettingsFragment extends PreferenceFragment { private static final String PREFERENCE_VIBRATE_WHEN = "account_settings_vibrate_when"; private static final String PREFERENCE_RINGTONE = "account_ringtone"; private static final String PREFERENCE_CATEGORY_SERVER = "account_servers"; + private static final String PREFERENCE_CATEGORY_POLICIES = "account_policies"; + private static final String PREFERENCE_POLICIES_ENFORCED = "policies_enforced"; + private static final String PREFERENCE_POLICIES_UNSUPPORTED = "policies_unsupported"; + private static final String PREFERENCE_POLICIES_RETRY_ACCOUNT = "policies_retry_account"; private static final String PREFERENCE_INCOMING = "incoming"; private static final String PREFERENCE_OUTGOING = "outgoing"; private static final String PREFERENCE_SYNC_CONTACTS = "account_sync_contacts"; @@ -351,6 +360,46 @@ public class AccountSettingsFragment extends PreferenceFragment { } } + /** + * From a Policy, create and return an ArrayList of Strings that describe (simply) those + * policies that are supported by the OS. At the moment, the strings are simple (e.g. + * "password required"); we should probably add more information (# characters, etc.), though + */ + private ArrayList getSystemPoliciesList(Policy policy) { + Resources res = mContext.getResources(); + ArrayList policies = new ArrayList(); + if (policy.mPasswordMode != Policy.PASSWORD_MODE_NONE) { + policies.add(res.getString(R.string.policy_require_password)); + } + if (policy.mPasswordHistory > 0) { + policies.add(res.getString(R.string.policy_password_history)); + } + if (policy.mPasswordExpirationDays > 0) { + policies.add(res.getString(R.string.policy_password_expiration)); + } + if (policy.mMaxScreenLockTime > 0) { + policies.add(res.getString(R.string.policy_screen_timeout)); + } + if (policy.mDontAllowCamera) { + policies.add(res.getString(R.string.policy_dont_allow_camera)); + } + return policies; + } + + private void setPolicyListSummary(ArrayList policies, String policiesToAdd, + String preferenceName) { + Policy.addPolicyStringToList(policiesToAdd, policies); + if (policies.size() > 0) { + Preference p = findPreference(preferenceName); + StringBuilder sb = new StringBuilder(); + for (String desc: policies) { + sb.append(desc); + sb.append('\n'); + } + p.setSummary(sb.toString()); + } + } + /** * Load account data into preference UI */ @@ -397,7 +446,6 @@ public class AccountSettingsFragment extends PreferenceFragment { }); mAccountSignature = (EditTextPreference) findPreference(PREFERENCE_SIGNATURE); - String signature = mAccount.getSignature(); mAccountSignature.setText(mAccount.getSignature()); mAccountSignature.setOnPreferenceChangeListener( new Preference.OnPreferenceChangeListener() { @@ -520,6 +568,45 @@ public class AccountSettingsFragment extends PreferenceFragment { notificationsCategory.removePreference(mAccountVibrateWhen); } + final Preference retryAccount = findPreference(PREFERENCE_POLICIES_RETRY_ACCOUNT); + final PreferenceCategory policiesCategory = (PreferenceCategory) findPreference( + PREFERENCE_CATEGORY_POLICIES); + if (mAccount.mPolicyKey > 0) { + // Make sure we have most recent data from account + mAccount.refresh(mContext); + Policy policy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey); + if (policy == null) { + // The account has been deleted? Crazy, but not impossible + return; + } + if (policy.mProtocolPoliciesEnforced != null) { + ArrayList policies = getSystemPoliciesList(policy); + setPolicyListSummary(policies, policy.mProtocolPoliciesEnforced, + PREFERENCE_POLICIES_ENFORCED); + } + if (policy.mProtocolPoliciesUnsupported != null) { + ArrayList policies = new ArrayList(); + setPolicyListSummary(policies, policy.mProtocolPoliciesUnsupported, + PREFERENCE_POLICIES_UNSUPPORTED); + } else { + // Don't show "retry" unless we have unsupported policies + policiesCategory.removePreference(retryAccount); + } + } else { + // Remove the category completely if there are no policies + getPreferenceScreen().removePreference(policiesCategory); + } + + retryAccount.setOnPreferenceClickListener( + new Preference.OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + // Release the account + SecurityPolicy.setAccountHoldFlag(mContext, mAccount, false); + // Remove the preference + policiesCategory.removePreference(retryAccount); + return true; + } + }); findPreference(PREFERENCE_INCOMING).setOnPreferenceClickListener( new Preference.OnPreferenceClickListener() { public boolean onPreferenceClick(Preference preference) { diff --git a/src/com/android/email/activity/setup/PolicyListPreference.java b/src/com/android/email/activity/setup/PolicyListPreference.java new file mode 100644 index 000000000..3a32438e7 --- /dev/null +++ b/src/com/android/email/activity/setup/PolicyListPreference.java @@ -0,0 +1,45 @@ +/* + * 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.activity.setup; + +import android.content.Context; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +/** + * Simple text preference allowing a large number of lines + */ +public class PolicyListPreference extends Preference { + // Arbitrary, but large number (we don't, and won't, have nearly this many) + public static final int MAX_POLICIES = 24; + + public PolicyListPreference(Context ctx, AttributeSet attrs, int defStyle) { + super(ctx, attrs, defStyle); + } + + public PolicyListPreference(Context ctx, AttributeSet attrs) { + super(ctx, attrs); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + ((TextView)view.findViewById(android.R.id.summary)).setMaxLines(MAX_POLICIES); + } +} diff --git a/src/com/android/email/provider/EmailProvider.java b/src/com/android/email/provider/EmailProvider.java index 59acee112..f15b76f0e 100644 --- a/src/com/android/email/provider/EmailProvider.java +++ b/src/com/android/email/provider/EmailProvider.java @@ -160,8 +160,9 @@ public class EmailProvider extends ContentProvider { // Version 26: Update IMAP accounts to add FLAG_SUPPORTS_SEARCH flag // Version 27: Add protocolSearchInfo to Message table // Version 28: Add notifiedMessageId and notifiedMessageCount to Account + // Version 29: Add protocolPoliciesEnforced and protocolPoliciesUnsupported to Policy - public static final int DATABASE_VERSION = 28; + public static final int DATABASE_VERSION = 29; // Any changes to the database format *must* include update-in-place code. // Original version: 2 @@ -651,7 +652,9 @@ public class EmailProvider extends ContentProvider { + PolicyColumns.MAX_HTML_TRUNCATION_SIZE + " integer, " + PolicyColumns.MAX_EMAIL_LOOKBACK + " integer, " + PolicyColumns.MAX_CALENDAR_LOOKBACK + " integer, " - + PolicyColumns.PASSWORD_RECOVERY_ENABLED + " integer" + + PolicyColumns.PASSWORD_RECOVERY_ENABLED + " integer, " + + PolicyColumns.PROTOCOL_POLICIES_ENFORCED + " text, " + + PolicyColumns.PROTOCOL_POLICIES_UNSUPPORTED + " text" + ");"; db.execSQL("create table " + Policy.TABLE_NAME + s); } @@ -1341,10 +1344,22 @@ public class EmailProvider extends ContentProvider { + " add column " + Account.NOTIFIED_MESSAGE_COUNT + " integer;"); } catch (SQLException e) { // Shouldn't be needed unless we're debugging and interrupt the process - Log.w(TAG, "Exception upgrading EmailProvider.db from 27 to 27 " + e); + Log.w(TAG, "Exception upgrading EmailProvider.db from 27 to 28 " + e); } oldVersion = 28; } + if (oldVersion == 28) { + try { + db.execSQL("alter table " + Policy.TABLE_NAME + + " add column " + Policy.PROTOCOL_POLICIES_ENFORCED + " text;"); + db.execSQL("alter table " + Policy.TABLE_NAME + + " add column " + Policy.PROTOCOL_POLICIES_UNSUPPORTED + " text;"); + } catch (SQLException e) { + // Shouldn't be needed unless we're debugging and interrupt the process + Log.w(TAG, "Exception upgrading EmailProvider.db from 28 to 29 " + e); + } + oldVersion = 29; + } } @Override diff --git a/src/com/android/email/service/PolicyService.java b/src/com/android/email/service/PolicyService.java index a97f65954..2394eedd7 100644 --- a/src/com/android/email/service/PolicyService.java +++ b/src/com/android/email/service/PolicyService.java @@ -16,15 +16,15 @@ package com.android.email.service; -import com.android.email.SecurityPolicy; -import com.android.emailcommon.provider.Policy; -import com.android.emailcommon.service.IPolicyService; - import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.IBinder; +import com.android.email.SecurityPolicy; +import com.android.emailcommon.provider.Policy; +import com.android.emailcommon.service.IPolicyService; + public class PolicyService extends Service { private SecurityPolicy mSecurityPolicy; @@ -35,32 +35,16 @@ public class PolicyService extends Service { return mSecurityPolicy.isActive(policy); } - public void policiesRequired(long accountId) { - mSecurityPolicy.policiesRequired(accountId); - } - - public void policiesUpdated(long accountId) { - mSecurityPolicy.policiesUpdated(accountId); - } - public void setAccountHoldFlag(long accountId, boolean newState) { SecurityPolicy.setAccountHoldFlag(mContext, accountId, newState); } - public boolean isActiveAdmin() { - return mSecurityPolicy.isActiveAdmin(); - } - public void remoteWipe() { mSecurityPolicy.remoteWipe(); } - public boolean isSupported(Policy policy) { - return mSecurityPolicy.isSupported(policy); - } - - public Policy clearUnsupportedPolicies(Policy policy) { - return mSecurityPolicy.clearUnsupportedPolicies(policy); + public void setAccountPolicy(long accountId, Policy policy, String securityKey) { + mSecurityPolicy.setAccountPolicy(accountId, policy, securityKey); } }; diff --git a/tests/src/com/android/email/SecurityPolicyTests.java b/tests/src/com/android/email/SecurityPolicyTests.java old mode 100644 new mode 100755 index ed264859a..1a9731be1 --- a/tests/src/com/android/email/SecurityPolicyTests.java +++ b/tests/src/com/android/email/SecurityPolicyTests.java @@ -142,7 +142,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 { Account a3 = ProviderTestUtils.setupAccount("sec-3", true, mMockContext); Policy p3ain = setupPolicy(10, Policy.PASSWORD_MODE_SIMPLE, 0, 0, false, 0, 0, 0, false, false); - Policy.setAccountPolicy(mMockContext, a3, p3ain, null); + SecurityPolicy.setAccountPolicy(mMockContext, a3, p3ain, null); Policy p3aout = mSecurityPolicy.computeAggregatePolicy(); assertNotNull(p3aout); assertEquals(p3ain, p3aout); @@ -150,7 +150,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 { // Repeat that test with fully-populated policies Policy p3bin = setupPolicy(10, Policy.PASSWORD_MODE_SIMPLE, 15, 16, false, 6, 2, 3, false, false); - Policy.setAccountPolicy(mMockContext, a3, p3bin, null); + SecurityPolicy.setAccountPolicy(mMockContext, a3, p3bin, null); Policy p3bout = mSecurityPolicy.computeAggregatePolicy(); assertNotNull(p3bout); assertEquals(p3bin, p3bout); @@ -166,7 +166,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 { Policy p4in = setupPolicy(20, Policy.PASSWORD_MODE_STRONG, 25, 26, false, 0, 5, 7, false, true); Account a4 = ProviderTestUtils.setupAccount("sec-4", true, mMockContext); - Policy.setAccountPolicy(mMockContext, a4, p4in, null); + SecurityPolicy.setAccountPolicy(mMockContext, a4, p4in, null); Policy p4out = mSecurityPolicy.computeAggregatePolicy(); assertNotNull(p4out); assertEquals(20, p4out.mPasswordMinLength); @@ -192,7 +192,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 { Policy p5in = setupPolicy(4, Policy.PASSWORD_MODE_SIMPLE, 5, 6, true, 1, 0, 0, true, false); Account a5 = ProviderTestUtils.setupAccount("sec-5", true, mMockContext); - Policy.setAccountPolicy(mMockContext, a5, p5in, null); + SecurityPolicy.setAccountPolicy(mMockContext, a5, p5in, null); Policy p5out = mSecurityPolicy.computeAggregatePolicy(); assertNotNull(p5out); assertEquals(20, p5out.mPasswordMinLength); @@ -236,17 +236,17 @@ public class SecurityPolicyTests extends ProviderTestCase2 { long accountId = account.mId; Policy initial = setupPolicy(10, Policy.PASSWORD_MODE_SIMPLE, 0, 0, false, 0, 0, 0, false, false); - Policy.setAccountPolicy(mMockContext, accountId, initial, null); + SecurityPolicy.setAccountPolicy(mMockContext, account, initial, null); long oldKey = assertAccountPolicyConsistent(account.mId, 0); Policy updated = setupPolicy(10, Policy.PASSWORD_MODE_SIMPLE, 0, 0, false, 0, 0, 0, false, false); - Policy.setAccountPolicy(mMockContext, accountId, updated, null); + SecurityPolicy.setAccountPolicy(mMockContext, account, updated, null); oldKey = assertAccountPolicyConsistent(account.mId, oldKey); // Remove the policy - Policy.clearAccountPolicy( + SecurityPolicy.clearAccountPolicy( mMockContext, Account.restoreAccountWithId(mMockContext, accountId)); assertNull("old policy not cleaned up", Policy.restorePolicyWithId(mMockContext, oldKey)); @@ -306,15 +306,15 @@ public class SecurityPolicyTests extends ProviderTestCase2 { Account a1 = ProviderTestUtils.setupAccount("disable-1", true, mMockContext); Policy p1 = setupPolicy(10, Policy.PASSWORD_MODE_SIMPLE, 0, 0, false, 0, 0, 0, false, false); - Policy.setAccountPolicy(mMockContext, a1, p1, "security-sync-key-1"); + SecurityPolicy.setAccountPolicy(mMockContext, a1, p1, "security-sync-key-1"); Account a2 = ProviderTestUtils.setupAccount("disable-2", true, mMockContext); Policy p2 = setupPolicy(20, Policy.PASSWORD_MODE_STRONG, 25, 26, false, 0, 0, 0, false, false); - Policy.setAccountPolicy(mMockContext, a2, p2, "security-sync-key-2"); + SecurityPolicy.setAccountPolicy(mMockContext, a2, p2, "security-sync-key-2"); Account a3 = ProviderTestUtils.setupAccount("disable-3", true, mMockContext); - Policy.clearAccountPolicy(mMockContext, a3); + SecurityPolicy.clearAccountPolicy(mMockContext, a3); mSecurityPolicy = SecurityPolicy.getInstance(mMockContext); @@ -359,7 +359,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 { ProviderTestUtils.setupAccount("expiring-2", true, mMockContext); Policy p2 = setupPolicy(20, Policy.PASSWORD_MODE_STRONG, 25, 26, false, 30, 0, 0, false, true); - Policy.setAccountPolicy(mMockContext, a2, p2, null); + SecurityPolicy.setAccountPolicy(mMockContext, a2, p2, null); // The expiring account should be returned nextExpiringAccountId = SecurityPolicy.findShortestExpiration(mMockContext); @@ -369,7 +369,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 { Account a3 = ProviderTestUtils.setupAccount("expiring-3", true, mMockContext); Policy p3 = setupPolicy(20, Policy.PASSWORD_MODE_STRONG, 25, 26, false, 60, 0, 0, false, true); - Policy.setAccountPolicy(mMockContext, a3, p3, null); + SecurityPolicy.setAccountPolicy(mMockContext, a3, p3, null); // The original expiring account (a2) should be returned nextExpiringAccountId = SecurityPolicy.findShortestExpiration(mMockContext); @@ -379,7 +379,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 { Account a4 = ProviderTestUtils.setupAccount("expiring-4", true, mMockContext); Policy p4 = setupPolicy(20, Policy.PASSWORD_MODE_STRONG, 25, 26, false, 15, 0, 0, false, true); - Policy.setAccountPolicy(mMockContext, a4, p4, null); + SecurityPolicy.setAccountPolicy(mMockContext, a4, p4, null); // The new expiring account (a4) should be returned nextExpiringAccountId = SecurityPolicy.findShortestExpiration(mMockContext); @@ -409,7 +409,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 { Account a2 = ProviderTestUtils.setupAccount("expired-2", true, mMockContext); Policy p2 = setupPolicy(20, Policy.PASSWORD_MODE_STRONG, 25, 26, false, 0, 0, 0, false, true); - Policy.setAccountPolicy(mMockContext, a2, p2, null); + SecurityPolicy.setAccountPolicy(mMockContext, a2, p2, null); // Add a mailbox & messages to each account long account1Id = a1.mId; @@ -435,7 +435,7 @@ public class SecurityPolicyTests extends ProviderTestCase2 { Account a3 = ProviderTestUtils.setupAccount("expired-3", true, mMockContext); Policy p3 = setupPolicy(20, Policy.PASSWORD_MODE_STRONG, 25, 26, false, 30, 0, 0, false, true); - Policy.setAccountPolicy(mMockContext, a3, p3, null); + SecurityPolicy.setAccountPolicy(mMockContext, a3, p3, null); // Add mailbox & messages to 3rd account long account3Id = a3.mId; @@ -466,38 +466,6 @@ public class SecurityPolicyTests extends ProviderTestCase2 { assertEquals(Account.FLAGS_SECURITY_HOLD, account.mFlags & Account.FLAGS_SECURITY_HOLD); } - /** - * Test the code that clears unsupported policies - * TODO inject a mock DPM so we can directly control & test all cases, no matter what device - */ - public void testClearUnsupportedPolicies() { - 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); - - mSecurityPolicy = SecurityPolicy.getInstance(mMockContext); - DevicePolicyManager dpm = mSecurityPolicy.getDPM(); - boolean hasEncryption = - dpm.getStorageEncryptionStatus() != DevicePolicyManager.ENCRYPTION_STATUS_UNSUPPORTED; - - Policy p1Result = mSecurityPolicy.clearUnsupportedPolicies(p1); - Policy p2Result = mSecurityPolicy.clearUnsupportedPolicies(p2); - - // No changes expected when encryptionRequested was false - assertEquals(p1, p1Result); - if (hasEncryption) { - // No changes expected - assertEquals(p2, p2Result); - } else { - // If encryption is unsupported, encryption policy bits are cleared - Policy policyExpect = - setupPolicy(1, Policy.PASSWORD_MODE_STRONG, 3, 4, true, 7, 8, 9, false, - false); - assertEquals(policyExpect, p2Result); - } - } - /** * Test the code that converts from exchange-style quality to DPM/Lockscreen style quality. */ diff --git a/tests/src/com/android/email/provider/PolicyTests.java b/tests/src/com/android/email/provider/PolicyTests.java old mode 100644 new mode 100755 index 2903c45b4..e18b2c9e4 --- a/tests/src/com/android/email/provider/PolicyTests.java +++ b/tests/src/com/android/email/provider/PolicyTests.java @@ -16,6 +16,12 @@ package com.android.email.provider; +import android.content.Context; +import android.os.Parcel; +import android.test.ProviderTestCase2; +import android.test.suitebuilder.annotation.MediumTest; + +import com.android.email.SecurityPolicy; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.Attachment; @@ -24,11 +30,6 @@ import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.provider.Mailbox; import com.android.emailcommon.provider.Policy; -import android.content.Context; -import android.os.Parcel; -import android.test.ProviderTestCase2; -import android.test.suitebuilder.annotation.MediumTest; - import java.util.ArrayList; /** @@ -68,10 +69,10 @@ public class PolicyTests extends ProviderTestCase2 { // Setup two accounts with policies Account account1 = ProviderTestUtils.setupAccount("acct1", true, mMockContext); Policy policy1 = new Policy(); - Policy.setAccountPolicy(mMockContext, account1, policy1, securitySyncKey); + SecurityPolicy.setAccountPolicy(mMockContext, account1, policy1, securitySyncKey); Account account2 = ProviderTestUtils.setupAccount("acct2", true, mMockContext); Policy policy2 = new Policy(); - Policy.setAccountPolicy(mMockContext, account2, policy2, securitySyncKey); + SecurityPolicy.setAccountPolicy(mMockContext, account2, policy2, securitySyncKey); // Get the accounts back from the database account1.refresh(mMockContext); account2.refresh(mMockContext); @@ -92,7 +93,7 @@ public class PolicyTests extends ProviderTestCase2 { assertEquals(0, account.mPolicyKey); assertEquals(0, EmailContent.count(mMockContext, Policy.CONTENT_URI)); Policy policy = new Policy(); - Policy.setAccountPolicy(mMockContext, account, policy, securitySyncKey); + SecurityPolicy.setAccountPolicy(mMockContext, account, policy, securitySyncKey); account.refresh(mMockContext); // We should have a policyKey now assertTrue(account.mPolicyKey > 0); @@ -103,7 +104,7 @@ public class PolicyTests extends ProviderTestCase2 { assertEquals(policy, dbPolicy); // The account should have the security sync key set assertEquals(securitySyncKey, account.mSecuritySyncKey); - Policy.clearAccountPolicy(mMockContext, account); + SecurityPolicy.clearAccountPolicy(mMockContext, account); account.refresh(mMockContext); // Make sure policyKey is cleared and policy is deleted assertEquals(0, account.mPolicyKey); @@ -118,11 +119,12 @@ public class PolicyTests extends ProviderTestCase2 { att.mAccountKey = acct.mId; return att; } + public void testSetAttachmentFlagsForNewPolicy() { Account acct = ProviderTestUtils.setupAccount("acct1", true, mMockContext); Policy policy1 = new Policy(); policy1.mDontAllowAttachments = true; - Policy.setAccountPolicy(mMockContext, acct, policy1, null); + SecurityPolicy.setAccountPolicy(mMockContext, acct, policy1, null); Mailbox box = ProviderTestUtils.setupMailbox("box1", acct.mId, true, mMockContext); Message msg1 = ProviderTestUtils.setupMessage("message1", acct.mId, box.mId, false, false, mMockContext); diff --git a/tests/src/com/android/email/provider/ProviderTests.java b/tests/src/com/android/email/provider/ProviderTests.java old mode 100644 new mode 100755 index ebf324399..a33dacaa5 --- a/tests/src/com/android/email/provider/ProviderTests.java +++ b/tests/src/com/android/email/provider/ProviderTests.java @@ -36,6 +36,7 @@ import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.SmallTest; +import com.android.email.SecurityPolicy; import com.android.email.provider.EmailProvider.AttachmentService; import com.android.emailcommon.AccountManagerTypes; import com.android.emailcommon.provider.Account; @@ -2529,7 +2530,7 @@ public class ProviderTests extends ProviderTestCase2 { Policy p2 = new Policy(); p2.save(mMockContext); Policy p3 = new Policy(); - Policy.setAccountPolicy(mMockContext, a.mId, p3, "0"); + SecurityPolicy.setAccountPolicy(mMockContext, a, p3, "0"); // We don't want anything cached or the tests below won't work. Note that // deleteUnlinked is only called by EmailProvider when the caches are empty