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
This commit is contained in:
Marc Blank 2011-10-20 10:13:02 -07:00
parent 0e6685c97b
commit 2736c1a11c
20 changed files with 536 additions and 355 deletions

View File

@ -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<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();

View File

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

View File

@ -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<String> addPolicyStringToList(String policyString,
ArrayList<String> 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<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
// 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();
}
}

View File

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

View File

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

View File

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

53
res/values/strings.xml Normal file → Executable file
View File

@ -856,14 +856,30 @@ as <xliff:g id="filename">%s</xliff:g>.</string>
provisioning, just before jumping into system settings such as Device Policy grant,
PIN/password, or encryption setup. [CHAR LIMIT=none] -->
<string name="account_security_dialog_content_fmt">
<xliff:g id="account">%s</xliff:g> requires that you update your security settings.</string>
<xliff:g id="account">%s</xliff:g> requires that you update your security
settings.</string>
<!-- Notification ticker when device security required (note: unused in Holo XL) -->
<string name="security_notification_ticker_fmt">
<string name="security_unsupported_ticker_fmt">
Account \"<xliff:g id="account">%s</xliff:g>\" cannot be synced due to security
requirements.</string>
<!-- Notification ticker when device security required (note: unused in Holo XL) -->
<string name="security_needed_ticker_fmt">
Account \"<xliff:g id="account">%s</xliff:g>\" requires security settings update.
</string>
<!-- Notification ticker when device security required (note: unused in Holo XL) -->
<string name="security_changed_ticker_fmt">
Account \"<xliff:g id="account">%s</xliff:g>\" changed its security settings; no user
action is required.
</string>
<!-- Notification content title when device security required [CHAR_LIMIT=30] -->
<string name="security_notification_content_title">Security update required</string>
<string name="security_notification_content_update_title">Security update required</string>
<!-- Notification content title when device security policies have changed [CHAR_LIMIT=30] -->
<string name="security_notification_content_change_title">Security policies have
changed</string>
<!-- Notification content title when device security policies cannot be met [CHAR_LIMIT=30] -->
<string name="security_notification_content_unsupported_title">Security policies cannot be
met</string>
<!-- Title of the activity that dispatches changes to device security. Not normally seen. -->
<string name="account_security_title">Device security</string>
<!-- Additional diagnostic text when the email app asserts control of the phone.
@ -937,15 +953,27 @@ as <xliff:g id="filename">%s</xliff:g>.</string>
<string name="account_settings_mail_check_frequency_label">Inbox check frequency</string>
<!-- On Settings screen, setting option name -->
<string name="account_settings_incoming_label">Incoming settings</string>
<!-- On Settings screen, setting option summary [CHAR LIMIT=64] -->
<!-- On Settings screen, setting option summary [CHAR LIMIT=64] -->
<string name="account_settings_incoming_summary">
Username, password, and other incoming server settings</string>
<!-- On Settings screen, setting option name -->
<string name="account_settings_outgoing_label">Outgoing settings</string>
<!-- On Settings screen, setting option summary [CHAR LIMIT=64] -->
<!-- On Settings screen, setting option summary [CHAR LIMIT=64] -->
<string name="account_settings_outgoing_summary">
Username, password, and other outgoing server settings</string>
<!-- On Settings screen, setting option name -->
<string name="account_settings_enforced_label">Policies enforced</string>
<!-- On Settings screen, setting option summary [CHAR LIMIT=64] -->
<string name="account_settings_enforced_summary">None</string>
<!-- On Settings screen, setting option name -->
<string name="account_settings_unsupported_label">Unsupported policies</string>
<!-- On Settings screen, setting option summary [CHAR LIMIT=64] -->
<string name="account_settings_unsupported_summary">None</string>
<!-- On Settings screen, label for button that attempts to sync the account -->
<string name="account_settings_retry_label">Attempt sync</string>
<!-- On Settings screen, summmary for button that attempts to sync an account [CHAR LIMIT=64] -->
<string name="account_settings_retry_summary">Tap here to attempt to sync this account (i.e. if server settings have changed)</string>
<!-- On Settings screen, setting option name -->
<string name="account_settings_description_label">Account name</string>
<!-- On Settings screen, setting option name -->
<string name="account_settings_name_label">Your name</string>
@ -963,6 +991,8 @@ as <xliff:g id="filename">%s</xliff:g>.</string>
<string name="account_settings_notifications">Notification settings</string>
<!-- On Settings screen, section heading for data usage [CHAR LIMIT=70] -->
<string name="account_settings_data_usage">Data usage</string>
<!-- On Settings screen, section heading -->
<string name="account_settings_policies">Security policies</string>
<!-- On settings screen, dialog heading informing user to edit a quick response -->
<string name="edit_quick_response_dialog">Edit quick response</string>
@ -1213,4 +1243,17 @@ as <xliff:g id="filename">%s</xliff:g>.</string>
<!-- A long placeholder string to be used in template XML files message_list_item_*.xml.
Used only in layout computation, and never actually exposed to the user. -->
<string name="long_string" translatable="false">looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong</string>
<!-- A policy disallowing the user of the device's camera [CHAR LIMIT=40] -->
<string name="policy_dont_allow_camera">Disallow use of the device\'s camera</string>
<!-- A policy requiring a device lock screen password [CHAR LIMIT=40] -->
<string name="policy_require_password">Require device password</string>
<!-- A policy disallowing the reuse of recent passwords [CHAR LIMIT=40] -->
<string name="policy_password_history">Restrict the reuse of recent passwords</string>
<!-- A policy that forces a password to expire after a set period of time [CHAR LIMIT=40] -->
<string name="policy_password_expiration">Require passwords to expire</string>
<!-- A policy requiring a maximum amount of time the device can sit idle before the lock screen
is activated [CHAR LIMIT=40] -->
<string name="policy_screen_timeout">Require an idle device to lock its screen</string>
</resources>

20
res/xml/account_settings_preferences.xml Normal file → Executable file
View File

@ -135,6 +135,26 @@
android:summary="@string/account_settings_outgoing_summary" />
</PreferenceCategory>
<PreferenceCategory
android:key="account_policies"
android:title="@string/account_settings_policies">
<com.android.email.activity.setup.PolicyListPreference
android:key="policies_enforced"
android:title="@string/account_settings_enforced_label"
android:summary="@string/account_settings_enforced_summary" />
<com.android.email.activity.setup.PolicyListPreference
android:key="policies_unsupported"
android:title="@string/account_settings_unsupported_label"
android:summary="@string/account_settings_unsupported_summary" />
<Preference
android:key="policies_retry_account"
android:title="@string/account_settings_retry_label"
android:summary="@string/account_settings_retry_summary" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/account_settings_category_delete_account">

View File

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

View File

@ -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<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
// 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();
}
/**

View File

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

View File

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

View File

@ -119,6 +119,7 @@ public class AccountSecurity extends Activity {
finish();
return;
}
// Special handling for password expiration events
if (passwordExpiring || passwordExpired) {
FragmentManager fm = getFragmentManager();

View File

@ -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<String> getSystemPoliciesList(Policy policy) {
Resources res = mContext.getResources();
ArrayList<String> policies = new ArrayList<String>();
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<String> 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<String> policies = getSystemPoliciesList(policy);
setPolicyListSummary(policies, policy.mProtocolPoliciesEnforced,
PREFERENCE_POLICIES_ENFORCED);
}
if (policy.mProtocolPoliciesUnsupported != null) {
ArrayList<String> policies = new ArrayList<String>();
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) {

View File

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

View File

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

View File

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

62
tests/src/com/android/email/SecurityPolicyTests.java Normal file → Executable file
View File

@ -142,7 +142,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
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<EmailProvider> {
// 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<EmailProvider> {
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<EmailProvider> {
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<EmailProvider> {
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<EmailProvider> {
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<EmailProvider> {
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<EmailProvider> {
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<EmailProvider> {
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<EmailProvider> {
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<EmailProvider> {
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<EmailProvider> {
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.
*/

22
tests/src/com/android/email/provider/PolicyTests.java Normal file → Executable file
View File

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

View File

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