Logic to move phone into security-admin mode
* Create notification to display when syncs fail due to security * Create psuedo-activity (no UI) to manage device admin state transitions * Clean up and flesh out SecurityPolicy APIs' * Add placeholders in EasSyncService showing how to react when policies are not met and sync cannot continue. Note: There are some STOPSHIP todo's at the top of SecurityPolicy.java. These should explain any code that you might think is "missing".
This commit is contained in:
parent
4d2a701844
commit
3d2b3b3b35
|
@ -115,6 +115,11 @@
|
||||||
android:label="@string/account_settings_action"
|
android:label="@string/account_settings_action"
|
||||||
>
|
>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".activity.setup.AccountSecurity"
|
||||||
|
android.label="@string/account_security_title"
|
||||||
|
>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.Debug"
|
android:name=".activity.Debug"
|
||||||
|
|
|
@ -502,6 +502,19 @@
|
||||||
<string name="account_setup_failed_security_policies_unsupported">
|
<string name="account_setup_failed_security_policies_unsupported">
|
||||||
This server requires security features your phone does not support.</string>
|
This server requires security features your phone does not support.</string>
|
||||||
|
|
||||||
|
<!-- Notification ticker when device security required -->
|
||||||
|
<string name="security_notification_ticker_fmt">
|
||||||
|
Account \"<xliff:g id="account">%s</xliff:g>\" requires security settings update.
|
||||||
|
</string>
|
||||||
|
<!-- Notification content title when device security required -->
|
||||||
|
<string name="security_notification_content_title">Update Security Settings</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. -->
|
||||||
|
<string name="account_security_policy_explanation_fmt">
|
||||||
|
The server <xliff:g id="server">%s</xliff:g> requires that you allow it to remotely control
|
||||||
|
some security features of your phone.</string>
|
||||||
|
|
||||||
<!-- "Setup could not finish" dialog action button -->
|
<!-- "Setup could not finish" dialog action button -->
|
||||||
<string name="account_setup_failed_dlg_edit_details_action">Edit details</string>
|
<string name="account_setup_failed_dlg_edit_details_action">Edit details</string>
|
||||||
<!-- On Settings screen, section heading -->
|
<!-- On Settings screen, section heading -->
|
||||||
|
@ -585,9 +598,9 @@
|
||||||
<string name="system_account_create_failed">The AccountManager could not create the Account; please try again.</string>
|
<string name="system_account_create_failed">The AccountManager could not create the Account; please try again.</string>
|
||||||
|
|
||||||
<!-- Strings that support the DeviceAdmin / DevicePolicyManager API -->
|
<!-- Strings that support the DeviceAdmin / DevicePolicyManager API -->
|
||||||
<!-- Name of the DeviceAdmin (seen in settings - anywhere else?) -->
|
<!-- Name of the DeviceAdmin (seen in settings & in user confirmation screen) -->
|
||||||
<string name="device_admin_label">Email Device Administrator</string>
|
<string name="device_admin_label">Email</string>
|
||||||
<!-- Long-form description of the DeviceAdmin (seen in settings - anywhere else?) -->
|
<!-- Long-form description of the DeviceAdmin (2nd line in settings & in user conf. screen) -->
|
||||||
<string name="device_admin_description">Email Device Administrator - Long Description</string>
|
<string name="device_admin_description">Enables server-specified security policies</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -16,29 +16,48 @@
|
||||||
|
|
||||||
package com.android.email;
|
package com.android.email;
|
||||||
|
|
||||||
|
import com.android.email.activity.setup.AccountSecurity;
|
||||||
|
import com.android.email.provider.EmailContent;
|
||||||
import com.android.email.provider.EmailContent.Account;
|
import com.android.email.provider.EmailContent.Account;
|
||||||
|
import com.android.email.provider.EmailContent.AccountColumns;
|
||||||
|
import com.android.email.service.MailService;
|
||||||
|
|
||||||
import android.app.DeviceAdmin;
|
import android.app.DeviceAdmin;
|
||||||
import android.app.DevicePolicyManager;
|
import android.app.DevicePolicyManager;
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility functions to support reading and writing security policies
|
* Utility functions to support reading and writing security policies
|
||||||
|
*
|
||||||
|
* STOPSHIP - these TODO items are all part of finishing the feature
|
||||||
|
* TODO: When user sets password, and conditions are now satisfied, restart syncs
|
||||||
|
* TODO: When accounts are deleted, reduce policy and/or give up admin status
|
||||||
|
* TODO: Provide a way to check for policy issues at synchronous times such as entering
|
||||||
|
* message list or folder list.
|
||||||
|
* TODO: Implement local wipe after failed passwords
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
public class SecurityPolicy {
|
public class SecurityPolicy {
|
||||||
|
|
||||||
/** STOPSHIP - ok to check in true for now, but must be false for shipping */
|
/** STOPSHIP - ok to check in true for now, but must be false for shipping */
|
||||||
/** DO NOT CHECK IN WHILE 'true' */
|
/** DO NOT CHECK IN WHILE 'true' */
|
||||||
private static final boolean DEBUG_ALWAYS_ACTIVE = true;
|
private static final boolean DEBUG_ALWAYS_ACTIVE = false;
|
||||||
|
|
||||||
private static SecurityPolicy sInstance = null;
|
private static SecurityPolicy sInstance = null;
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
private DevicePolicyManager mDPM;
|
private DevicePolicyManager mDPM;
|
||||||
private ComponentName mAdminName;
|
private ComponentName mAdminName;
|
||||||
private PolicySet mAggregatePolicy;
|
private PolicySet mAggregatePolicy;
|
||||||
|
private boolean mNotificationActive;
|
||||||
|
private boolean mAdminEnabled;
|
||||||
|
|
||||||
private static final PolicySet NO_POLICY_SET =
|
private static final PolicySet NO_POLICY_SET =
|
||||||
new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false);
|
new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false);
|
||||||
|
@ -81,6 +100,7 @@ public class SecurityPolicy {
|
||||||
mDPM = null;
|
mDPM = null;
|
||||||
mAdminName = new ComponentName(context, PolicyAdmin.class);
|
mAdminName = new ComponentName(context, PolicyAdmin.class);
|
||||||
mAggregatePolicy = null;
|
mAggregatePolicy = null;
|
||||||
|
mNotificationActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -119,12 +139,8 @@ public class SecurityPolicy {
|
||||||
int flags = c.getInt(ACCOUNT_SECURITY_COLUMN_FLAGS);
|
int flags = c.getInt(ACCOUNT_SECURITY_COLUMN_FLAGS);
|
||||||
if (flags != 0) {
|
if (flags != 0) {
|
||||||
PolicySet p = new PolicySet(flags);
|
PolicySet p = new PolicySet(flags);
|
||||||
if (p.mMinPasswordLength > 0) {
|
minPasswordLength = Math.max(p.mMinPasswordLength, minPasswordLength);
|
||||||
minPasswordLength = Math.max(p.mMinPasswordLength, minPasswordLength);
|
passwordMode = Math.max(p.mPasswordMode, passwordMode);
|
||||||
}
|
|
||||||
if (p.mPasswordMode > 0) {
|
|
||||||
passwordMode = Math.max(p.mPasswordMode, passwordMode);
|
|
||||||
}
|
|
||||||
if (p.mMaxPasswordFails > 0) {
|
if (p.mMaxPasswordFails > 0) {
|
||||||
maxPasswordFails = Math.min(p.mMaxPasswordFails, maxPasswordFails);
|
maxPasswordFails = Math.min(p.mMaxPasswordFails, maxPasswordFails);
|
||||||
}
|
}
|
||||||
|
@ -139,6 +155,12 @@ public class SecurityPolicy {
|
||||||
c.close();
|
c.close();
|
||||||
}
|
}
|
||||||
if (policiesFound) {
|
if (policiesFound) {
|
||||||
|
// final cleanup pass converts any untouched min/max values to zero (not specified)
|
||||||
|
if (minPasswordLength == Integer.MIN_VALUE) minPasswordLength = 0;
|
||||||
|
if (passwordMode == Integer.MIN_VALUE) passwordMode = 0;
|
||||||
|
if (maxPasswordFails == Integer.MAX_VALUE) maxPasswordFails = 0;
|
||||||
|
if (maxScreenLockTime == Integer.MAX_VALUE) maxScreenLockTime = 0;
|
||||||
|
|
||||||
return new PolicySet(minPasswordLength, passwordMode, maxPasswordFails,
|
return new PolicySet(minPasswordLength, passwordMode, maxPasswordFails,
|
||||||
maxScreenLockTime, requireRemoteWipe);
|
maxScreenLockTime, requireRemoteWipe);
|
||||||
} else {
|
} else {
|
||||||
|
@ -185,51 +207,58 @@ public class SecurityPolicy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API: Report that policies may have been updated due to rewriting values in an Account.
|
* API: Report that policies may have been updated due to rewriting values in an Account.
|
||||||
|
* @param accountId the account that has been updated
|
||||||
*/
|
*/
|
||||||
public synchronized void updatePolicies() {
|
public synchronized void updatePolicies(long accountId) {
|
||||||
mAggregatePolicy = null;
|
mAggregatePolicy = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API: Query used to determine if a given policy is "active" (the device is operating at
|
* API: Query used to determine if a given policy is "active" (the device is operating at
|
||||||
* the required security level). This is used when creating new accounts. This method
|
* the required security level).
|
||||||
* is for queries only, and does not trigger any change in device state.
|
|
||||||
*
|
*
|
||||||
* @param policies the policies requested
|
* This can be used when syncing a specific account, by passing a specific set of policies
|
||||||
|
* for that account. Or, it can be used at any time to compare the device
|
||||||
|
* state against the aggregate set of device policies stored in all accounts.
|
||||||
|
*
|
||||||
|
* This method is for queries only, and does not trigger any change in device state.
|
||||||
|
*
|
||||||
|
* @param policies the policies requested, or null to check aggregate stored policies
|
||||||
* @return true if the policies are active, false if not active
|
* @return true if the policies are active, false if not active
|
||||||
*/
|
*/
|
||||||
public boolean isActive(PolicySet policies) {
|
public boolean isActive(PolicySet policies) {
|
||||||
DevicePolicyManager dpm = getDPM();
|
DevicePolicyManager dpm = getDPM();
|
||||||
if (dpm.isAdminActive(mAdminName)) {
|
if (dpm.isAdminActive(mAdminName)) {
|
||||||
// check each policy
|
// select aggregate set if needed
|
||||||
PolicySet aggregate;
|
if (policies == null) {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
if (mAggregatePolicy == null) {
|
if (mAggregatePolicy == null) {
|
||||||
mAggregatePolicy = computeAggregatePolicy();
|
mAggregatePolicy = computeAggregatePolicy();
|
||||||
|
}
|
||||||
|
policies = mAggregatePolicy;
|
||||||
}
|
}
|
||||||
aggregate = mAggregatePolicy;
|
|
||||||
}
|
}
|
||||||
// quick check for the "empty set" of no policies
|
// quick check for the "empty set" of no policies
|
||||||
if (aggregate == NO_POLICY_SET) {
|
if (policies == NO_POLICY_SET) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// check each policy explicitly
|
// check each policy explicitly
|
||||||
if (aggregate.mMinPasswordLength > 0) {
|
if (policies.mMinPasswordLength > 0) {
|
||||||
if (dpm.getPasswordMinimumLength(mAdminName) < aggregate.mMinPasswordLength) {
|
if (dpm.getPasswordMinimumLength(mAdminName) < policies.mMinPasswordLength) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (aggregate.mPasswordMode > 0) {
|
if (policies.mPasswordMode > 0) {
|
||||||
if (dpm.getPasswordQuality(mAdminName) < aggregate.getDPManagerPasswordMode()) {
|
if (dpm.getPasswordQuality(mAdminName) < policies.getDPManagerPasswordQuality()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!dpm.isActivePasswordSufficient()) {
|
if (!dpm.isActivePasswordSufficient()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (aggregate.mMaxScreenLockTime > 0) {
|
if (policies.mMaxScreenLockTime > 0) {
|
||||||
// Note, we use seconds, dpm uses milliseconds
|
// Note, we use seconds, dpm uses milliseconds
|
||||||
if (dpm.getMaximumTimeToLock(mAdminName) > aggregate.mMaxScreenLockTime * 1000) {
|
if (dpm.getMaximumTimeToLock(mAdminName) > policies.mMaxScreenLockTime * 1000) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -241,14 +270,101 @@ public class SecurityPolicy {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync service should call this any time a sync fails due to isActive() returning false.
|
* Set the requested security level based on the aggregate set of requests
|
||||||
|
*/
|
||||||
|
public void setActivePolicies() {
|
||||||
|
DevicePolicyManager dpm = getDPM();
|
||||||
|
if (dpm.isAdminActive(mAdminName)) {
|
||||||
|
// compute aggregate set if needed
|
||||||
|
PolicySet policies;
|
||||||
|
synchronized (this) {
|
||||||
|
if (mAggregatePolicy == null) {
|
||||||
|
mAggregatePolicy = computeAggregatePolicy();
|
||||||
|
}
|
||||||
|
policies = mAggregatePolicy;
|
||||||
|
}
|
||||||
|
// set each policy in the policy manager
|
||||||
|
// password mode & length
|
||||||
|
dpm.setPasswordQuality(mAdminName, policies.getDPManagerPasswordQuality());
|
||||||
|
dpm.setPasswordMinimumLength(mAdminName, policies.mMinPasswordLength);
|
||||||
|
// screen lock time
|
||||||
|
dpm.setMaximumTimeToLock(mAdminName, policies.mMaxScreenLockTime * 1000);
|
||||||
|
// local wipe (failed passwords limit)
|
||||||
|
dpm.setMaximumFailedPasswordsForWipe(mAdminName, policies.mMaxPasswordFails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API: Sync service should call this any time a sync fails due to isActive() returning false.
|
||||||
* This will kick off the notify-acquire-admin-state process and/or increase the security level.
|
* This will kick off the notify-acquire-admin-state process and/or increase the security level.
|
||||||
* The caller needs to write the required policies into this account before making this call.
|
* The caller needs to write the required policies into this account before making this call.
|
||||||
|
* Should not be called from UI thread - uses DB lookups to prepare new notifications
|
||||||
*
|
*
|
||||||
* @param accountId the account for which sync cannot proceed
|
* @param accountId the account for which sync cannot proceed
|
||||||
*/
|
*/
|
||||||
public void policiesRequired(long accountId) {
|
public void policiesRequired(long accountId) {
|
||||||
// implement....
|
synchronized (this) {
|
||||||
|
if (mNotificationActive) {
|
||||||
|
// no need to do anything - we've already been notified, and we've already
|
||||||
|
// put up a notification
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Prepare & post a notification
|
||||||
|
// record that we're watching this one
|
||||||
|
mNotificationActive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// At this point, we will put up a notification
|
||||||
|
Account account = EmailContent.Account.restoreAccountWithId(mContext, accountId);
|
||||||
|
|
||||||
|
String tickerText = mContext.getString(R.string.security_notification_ticker_fmt,
|
||||||
|
account.getDisplayName());
|
||||||
|
String contentTitle = mContext.getString(R.string.security_notification_content_title);
|
||||||
|
String contentText = account.getDisplayName();
|
||||||
|
String ringtoneString = account.getRingtone();
|
||||||
|
Uri ringTone = (ringtoneString == null) ? null : Uri.parse(ringtoneString);
|
||||||
|
boolean vibrate = 0 != (account.mFlags & Account.FLAGS_VIBRATE);
|
||||||
|
|
||||||
|
Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, accountId);
|
||||||
|
PendingIntent pending =
|
||||||
|
PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
|
||||||
|
Notification notification = new Notification(R.drawable.stat_notify_email_generic,
|
||||||
|
tickerText, System.currentTimeMillis());
|
||||||
|
notification.setLatestEventInfo(mContext, contentTitle, contentText, pending);
|
||||||
|
|
||||||
|
// Use the account's notification rules for sound & vibrate (but always notify)
|
||||||
|
notification.sound = ringTone;
|
||||||
|
if (vibrate) {
|
||||||
|
notification.defaults |= Notification.DEFAULT_VIBRATE;
|
||||||
|
}
|
||||||
|
notification.flags |= Notification.FLAG_SHOW_LIGHTS;
|
||||||
|
notification.defaults |= Notification.DEFAULT_LIGHTS;
|
||||||
|
|
||||||
|
NotificationManager notificationManager =
|
||||||
|
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
notificationManager.notify(MailService.NOTIFICATION_ID_SECURITY_NEEDED, notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from the notification's intent receiver to register that the notification can be
|
||||||
|
* cleared now.
|
||||||
|
*/
|
||||||
|
public synchronized void clearNotification(long accountId) {
|
||||||
|
NotificationManager notificationManager =
|
||||||
|
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
notificationManager.cancel(MailService.NOTIFICATION_ID_SECURITY_NEEDED);
|
||||||
|
mNotificationActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API: Remote wipe (from server). This is final, there is no confirmation.
|
||||||
|
*/
|
||||||
|
public void remoteWipe(long accountId) {
|
||||||
|
DevicePolicyManager dpm = getDPM();
|
||||||
|
if (dpm.isAdminActive(mAdminName)) {
|
||||||
|
dpm.wipeData(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -291,9 +407,10 @@ public class SecurityPolicy {
|
||||||
* @param maxPasswordFails (0=not enforced)
|
* @param maxPasswordFails (0=not enforced)
|
||||||
* @param maxScreenLockTime in seconds (0=not enforced)
|
* @param maxScreenLockTime in seconds (0=not enforced)
|
||||||
* @param requireRemoteWipe
|
* @param requireRemoteWipe
|
||||||
|
* @throws IllegalArgumentException when any arguments are outside of legal ranges.
|
||||||
*/
|
*/
|
||||||
public PolicySet(int minPasswordLength, int passwordMode, int maxPasswordFails,
|
public PolicySet(int minPasswordLength, int passwordMode, int maxPasswordFails,
|
||||||
int maxScreenLockTime, boolean requireRemoteWipe) {
|
int maxScreenLockTime, boolean requireRemoteWipe) throws IllegalArgumentException {
|
||||||
if (minPasswordLength > PASSWORD_LENGTH_MAX) {
|
if (minPasswordLength > PASSWORD_LENGTH_MAX) {
|
||||||
throw new IllegalArgumentException("password length");
|
throw new IllegalArgumentException("password length");
|
||||||
}
|
}
|
||||||
|
@ -339,9 +456,9 @@ public class SecurityPolicy {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to map DevicePolicyManager password modes to our internal encoding.
|
* Helper to map our internal encoding to DevicePolicyManager password modes.
|
||||||
*/
|
*/
|
||||||
public int getDPManagerPasswordMode() {
|
public int getDPManagerPasswordQuality() {
|
||||||
switch (mPasswordMode) {
|
switch (mPasswordMode) {
|
||||||
case PASSWORD_MODE_SIMPLE:
|
case PASSWORD_MODE_SIMPLE:
|
||||||
return DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
|
return DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
|
||||||
|
@ -355,11 +472,32 @@ public class SecurityPolicy {
|
||||||
/**
|
/**
|
||||||
* Record flags (and a sync key for the flags) into an Account
|
* Record flags (and a sync key for the flags) into an Account
|
||||||
* Note: the hash code is defined as the encoding used in Account
|
* Note: the hash code is defined as the encoding used in Account
|
||||||
|
*
|
||||||
* @param account to write the values mSecurityFlags and mSecuritySyncKey
|
* @param account to write the values mSecurityFlags and mSecuritySyncKey
|
||||||
* @param syncKey the value to write into the account's mSecuritySyncKey
|
* @param syncKey the value to write into the account's mSecuritySyncKey
|
||||||
|
* @param update if true, also writes the account back to the provider (updating only
|
||||||
|
* the fields changed by this API)
|
||||||
|
* @param context a context for writing to the provider
|
||||||
|
* @return true if the actual policies changed, false if no change (note, sync key
|
||||||
|
* does not affect this)
|
||||||
*/
|
*/
|
||||||
public void writeAccount(Account account, String syncKey) {
|
public boolean writeAccount(Account account, String syncKey, boolean update,
|
||||||
account.mSecurityFlags = hashCode();
|
Context context) {
|
||||||
|
int newFlags = hashCode();
|
||||||
|
boolean dirty = (newFlags != account.mSecurityFlags);
|
||||||
|
account.mSecurityFlags = newFlags;
|
||||||
|
account.mSecuritySyncKey = syncKey;
|
||||||
|
if (update) {
|
||||||
|
if (account.isSaved()) {
|
||||||
|
ContentValues cv = new ContentValues();
|
||||||
|
cv.put(AccountColumns.SECURITY_FLAGS, account.mSecurityFlags);
|
||||||
|
cv.put(AccountColumns.SECURITY_SYNC_KEY, account.mSecuritySyncKey);
|
||||||
|
account.update(context, cv);
|
||||||
|
} else {
|
||||||
|
account.save(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -400,19 +538,47 @@ public class SecurityPolicy {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Device Policy administrator. This is primarily a listener for device state changes.
|
* If we are not the active device admin, try to become so.
|
||||||
|
*
|
||||||
|
* @return true if we are already active, false if we are not
|
||||||
*/
|
*/
|
||||||
private static class PolicyAdmin extends DeviceAdmin {
|
public boolean isActiveAdmin() {
|
||||||
|
DevicePolicyManager dpm = getDPM();
|
||||||
|
return dpm.isAdminActive(mAdminName);
|
||||||
|
}
|
||||||
|
|
||||||
boolean mEnabled = false;
|
/**
|
||||||
|
* Report admin component name - for making calls into device policy manager
|
||||||
|
*/
|
||||||
|
public ComponentName getAdminComponent() {
|
||||||
|
return mAdminName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal handler for enabled/disabled transitions. Handles DeviceAdmin.onEnabled and
|
||||||
|
* and DeviceAdmin.onDisabled.
|
||||||
|
*/
|
||||||
|
private void onAdminEnabled(boolean isEnabled) {
|
||||||
|
if (isEnabled && !mAdminEnabled) {
|
||||||
|
// TODO: transition to enabled state
|
||||||
|
} else if (!isEnabled && mAdminEnabled) {
|
||||||
|
// TODO: transition to disabled state
|
||||||
|
}
|
||||||
|
mAdminEnabled = isEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device Policy administrator. This is primarily a listener for device state changes.
|
||||||
|
* Note: This is instantiated by incoming messages.
|
||||||
|
*/
|
||||||
|
public static class PolicyAdmin extends DeviceAdmin {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called after the administrator is first enabled.
|
* Called after the administrator is first enabled.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onEnabled(Context context, Intent intent) {
|
public void onEnabled(Context context, Intent intent) {
|
||||||
mEnabled = true;
|
SecurityPolicy.getInstance(context).onAdminEnabled(true);
|
||||||
// do something
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -420,8 +586,7 @@ public class SecurityPolicy {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onDisabled(Context context, Intent intent) {
|
public void onDisabled(Context context, Intent intent) {
|
||||||
mEnabled = false;
|
SecurityPolicy.getInstance(context).onAdminEnabled(false);
|
||||||
// do something
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -325,7 +325,7 @@ public class MessageList extends ListActivity implements OnItemClickListener, On
|
||||||
// clear notifications here
|
// clear notifications here
|
||||||
NotificationManager notificationManager = (NotificationManager)
|
NotificationManager notificationManager = (NotificationManager)
|
||||||
getSystemService(Context.NOTIFICATION_SERVICE);
|
getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
notificationManager.cancel(MailService.NEW_MESSAGE_NOTIFICATION_ID);
|
notificationManager.cancel(MailService.NOTIFICATION_ID_NEW_MESSAGES);
|
||||||
restoreListPosition();
|
restoreListPosition();
|
||||||
autoRefreshStaleMailbox();
|
autoRefreshStaleMailbox();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2010 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 com.android.email.R;
|
||||||
|
import com.android.email.SecurityPolicy;
|
||||||
|
import com.android.email.provider.EmailContent.Account;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.DevicePolicyManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Psuedo-activity (no UI) to bootstrap the user up to a higher desired security level. This
|
||||||
|
* bootstrap requires the following steps.
|
||||||
|
*
|
||||||
|
* 1. Confirm the account of interest has any security policies defined - exit early if not
|
||||||
|
* 2. If not actively administrating the device, ask Device Policy Manager to start that
|
||||||
|
* 3. When we are actively administrating, check current policies and see if they're sufficient
|
||||||
|
* 4. If not, set policies
|
||||||
|
* 5. If necessary, request for user to update device password
|
||||||
|
*/
|
||||||
|
public class AccountSecurity extends Activity {
|
||||||
|
|
||||||
|
private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity.setup.ACCOUNT_ID";
|
||||||
|
|
||||||
|
private static final int REQUEST_ENABLE = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for generating intent for this activity (which is intended to be launched
|
||||||
|
* from a notification.)
|
||||||
|
*
|
||||||
|
* @param context Calling context for building the intent
|
||||||
|
* @param accountId The account of interest
|
||||||
|
* @return an Intent which can be used to view that account
|
||||||
|
*/
|
||||||
|
public static Intent actionUpdateSecurityIntent(Context context, long accountId) {
|
||||||
|
Intent intent = new Intent(context, AccountSecurity.class);
|
||||||
|
intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
Intent i = getIntent();
|
||||||
|
long accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1);
|
||||||
|
SecurityPolicy security = SecurityPolicy.getInstance(this);
|
||||||
|
security.clearNotification(accountId);
|
||||||
|
if (accountId != -1) {
|
||||||
|
// TODO: spin up a thread to do this in the background, because of DB ops
|
||||||
|
Account account = Account.restoreAccountWithId(this, accountId);
|
||||||
|
if (account != null) {
|
||||||
|
if (account.mSecurityFlags != 0) {
|
||||||
|
// This account wants to control security
|
||||||
|
if (!security.isActiveAdmin()) {
|
||||||
|
// try to become active - must happen here in this activity, to get result
|
||||||
|
Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
|
||||||
|
intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN,
|
||||||
|
security.getAdminComponent());
|
||||||
|
intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION,
|
||||||
|
this.getString(R.string.account_security_policy_explanation_fmt,
|
||||||
|
account.getDisplayName()));
|
||||||
|
startActivityForResult(intent, REQUEST_ENABLE);
|
||||||
|
// keep this activity on stack to process result
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// already active - try to set actual policies, finish, and return
|
||||||
|
setActivePolicies();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the eventual result of the user allowing us to become an active device admin
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
switch (requestCode) {
|
||||||
|
case REQUEST_ENABLE:
|
||||||
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
|
// now active - try to set actual policies
|
||||||
|
setActivePolicies();
|
||||||
|
} else {
|
||||||
|
// failed - just give up and go away
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now that we are connected as an active device admin, try to set the device to the
|
||||||
|
* correct security level, and ask for a password if necessary.
|
||||||
|
*/
|
||||||
|
private void setActivePolicies() {
|
||||||
|
SecurityPolicy sp = SecurityPolicy.getInstance(this);
|
||||||
|
// check current security level - if sufficient, we're done!
|
||||||
|
if (sp.isActive(null)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// set current security level
|
||||||
|
sp.setActivePolicies();
|
||||||
|
// check current security level - if sufficient, we're done!
|
||||||
|
if (sp.isActive(null)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// if not sufficient, launch the activity to have the user set a new password.
|
||||||
|
Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -53,7 +53,8 @@ public class MailService extends Service {
|
||||||
|
|
||||||
private static final String LOG_TAG = "Email-MailService";
|
private static final String LOG_TAG = "Email-MailService";
|
||||||
|
|
||||||
public static int NEW_MESSAGE_NOTIFICATION_ID = 1;
|
public static int NOTIFICATION_ID_NEW_MESSAGES = 1;
|
||||||
|
public static int NOTIFICATION_ID_SECURITY_NEEDED = 2;
|
||||||
|
|
||||||
private static final String ACTION_CHECK_MAIL =
|
private static final String ACTION_CHECK_MAIL =
|
||||||
"com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
|
"com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
|
||||||
|
@ -201,7 +202,7 @@ public class MailService extends Service {
|
||||||
// but that's an edge condition and this is much safer.
|
// but that's an edge condition and this is much safer.
|
||||||
NotificationManager notificationManager =
|
NotificationManager notificationManager =
|
||||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
notificationManager.cancel(NEW_MESSAGE_NOTIFICATION_ID);
|
notificationManager.cancel(NOTIFICATION_ID_NEW_MESSAGES);
|
||||||
|
|
||||||
// When called externally, we refresh the sync reports table to pick up
|
// When called externally, we refresh the sync reports table to pick up
|
||||||
// any changes in the account list or account settings
|
// any changes in the account list or account settings
|
||||||
|
@ -705,6 +706,6 @@ public class MailService extends Service {
|
||||||
|
|
||||||
NotificationManager notificationManager =
|
NotificationManager notificationManager =
|
||||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
notificationManager.notify(NEW_MESSAGE_NOTIFICATION_ID, notification);
|
notificationManager.notify(NOTIFICATION_ID_NEW_MESSAGES, notification);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
package com.android.exchange;
|
package com.android.exchange;
|
||||||
|
|
||||||
|
import com.android.email.SecurityPolicy;
|
||||||
|
import com.android.email.SecurityPolicy.PolicySet;
|
||||||
import com.android.email.codec.binary.Base64;
|
import com.android.email.codec.binary.Base64;
|
||||||
import com.android.email.mail.AuthenticationFailedException;
|
import com.android.email.mail.AuthenticationFailedException;
|
||||||
import com.android.email.mail.MessagingException;
|
import com.android.email.mail.MessagingException;
|
||||||
|
@ -964,25 +966,40 @@ public class EasSyncService extends AbstractSyncService {
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!mStop) {
|
while (!mStop) {
|
||||||
userLog("Sending Account syncKey: ", mAccount.mSyncKey);
|
userLog("Sending Account syncKey: ", mAccount.mSyncKey);
|
||||||
Serializer s = new Serializer();
|
Serializer s = new Serializer();
|
||||||
s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY)
|
s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY)
|
||||||
.text(mAccount.mSyncKey).end().end().done();
|
.text(mAccount.mSyncKey).end().end().done();
|
||||||
HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray());
|
HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray());
|
||||||
if (mStop) break;
|
if (mStop) break;
|
||||||
int code = resp.getStatusLine().getStatusCode();
|
int code = resp.getStatusLine().getStatusCode();
|
||||||
if (code == HttpStatus.SC_OK) {
|
if (code == HttpStatus.SC_OK) {
|
||||||
HttpEntity entity = resp.getEntity();
|
HttpEntity entity = resp.getEntity();
|
||||||
int len = (int)entity.getContentLength();
|
int len = (int)entity.getContentLength();
|
||||||
if (len != 0) {
|
if (len != 0) {
|
||||||
InputStream is = entity.getContent();
|
InputStream is = entity.getContent();
|
||||||
// Returns true if we need to sync again
|
// Returns true if we need to sync again
|
||||||
if (new FolderSyncParser(is, new AccountSyncAdapter(mMailbox, this))
|
if (new FolderSyncParser(is, new AccountSyncAdapter(mMailbox, this))
|
||||||
.parse()) {
|
.parse()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (isAuthError(code)) {
|
// // EVERYTHING IN THE code==403 BLOCK IS PLACEHOLDER/SAMPLE CODE
|
||||||
|
} else if (code == 403) { // security error
|
||||||
|
// Reality: Find out from server what policies are required
|
||||||
|
// Fake: Hardcode the policies
|
||||||
|
SecurityPolicy sp = SecurityPolicy.getInstance(mContext);
|
||||||
|
PolicySet ps = new PolicySet(4, PolicySet.PASSWORD_MODE_SIMPLE, 0, 0, true);
|
||||||
|
// Update the account
|
||||||
|
if (ps.writeAccount(mAccount, "securitySyncKey", true, mContext)) {
|
||||||
|
sp.updatePolicies(mAccount.mId);
|
||||||
|
}
|
||||||
|
// Notify that we are blocked because of policies
|
||||||
|
sp.policiesRequired(mAccount.mId);
|
||||||
|
// and exit (don't sync in this condition)
|
||||||
|
mExitStatus = EXIT_LOGIN_FAILURE;
|
||||||
|
// END PLACEHOLDER CODE
|
||||||
|
} else if (isAuthError(code)) {
|
||||||
mExitStatus = EXIT_LOGIN_FAILURE;
|
mExitStatus = EXIT_LOGIN_FAILURE;
|
||||||
} else {
|
} else {
|
||||||
userLog("FolderSync response error: ", code);
|
userLog("FolderSync response error: ", code);
|
||||||
|
|
|
@ -107,13 +107,20 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
|
||||||
assertTrue(EMPTY_POLICY_SET.equals(sp.computeAggregatePolicy()));
|
assertTrue(EMPTY_POLICY_SET.equals(sp.computeAggregatePolicy()));
|
||||||
|
|
||||||
// with a single account in security mode, should return same security as in account
|
// with a single account in security mode, should return same security as in account
|
||||||
PolicySet p3in = new PolicySet(10, PolicySet.PASSWORD_MODE_SIMPLE, 15, 16, false);
|
// first test with partially-populated policies
|
||||||
Account a3 = ProviderTestUtils.setupAccount("sec-3", false, mMockContext);
|
Account a3 = ProviderTestUtils.setupAccount("sec-3", false, mMockContext);
|
||||||
p3in.writeAccount(a3, null);
|
PolicySet p3ain = new PolicySet(10, PolicySet.PASSWORD_MODE_SIMPLE, 0, 0, false);
|
||||||
a3.save(mMockContext);
|
p3ain.writeAccount(a3, null, true, mMockContext);
|
||||||
PolicySet p3out = sp.computeAggregatePolicy();
|
PolicySet p3aout = sp.computeAggregatePolicy();
|
||||||
assertNotNull(p3out);
|
assertNotNull(p3aout);
|
||||||
assertEquals(p3in, p3out);
|
assertEquals(p3ain, p3aout);
|
||||||
|
|
||||||
|
// Repeat that test with fully-populated policies
|
||||||
|
PolicySet p3bin = new PolicySet(10, PolicySet.PASSWORD_MODE_SIMPLE, 15, 16, false);
|
||||||
|
p3bin.writeAccount(a3, null, true, mMockContext);
|
||||||
|
PolicySet p3bout = sp.computeAggregatePolicy();
|
||||||
|
assertNotNull(p3bout);
|
||||||
|
assertEquals(p3bin, p3bout);
|
||||||
|
|
||||||
// add another account which mixes it up (some fields will change, others will not)
|
// add another account which mixes it up (some fields will change, others will not)
|
||||||
// pw length and pw mode - max logic - will change because larger #s here
|
// pw length and pw mode - max logic - will change because larger #s here
|
||||||
|
@ -121,8 +128,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
|
||||||
// wipe required - OR logic - will *not* change here because false
|
// wipe required - OR logic - will *not* change here because false
|
||||||
PolicySet p4in = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false);
|
PolicySet p4in = new PolicySet(20, PolicySet.PASSWORD_MODE_STRONG, 25, 26, false);
|
||||||
Account a4 = ProviderTestUtils.setupAccount("sec-4", false, mMockContext);
|
Account a4 = ProviderTestUtils.setupAccount("sec-4", false, mMockContext);
|
||||||
p4in.writeAccount(a4, null);
|
p4in.writeAccount(a4, null, true, mMockContext);
|
||||||
a4.save(mMockContext);
|
|
||||||
PolicySet p4out = sp.computeAggregatePolicy();
|
PolicySet p4out = sp.computeAggregatePolicy();
|
||||||
assertNotNull(p4out);
|
assertNotNull(p4out);
|
||||||
assertEquals(20, p4out.mMinPasswordLength);
|
assertEquals(20, p4out.mMinPasswordLength);
|
||||||
|
@ -137,8 +143,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
|
||||||
// wipe required - OR logic - will change here because true
|
// wipe required - OR logic - will change here because true
|
||||||
PolicySet p5in = new PolicySet(4, PolicySet.PASSWORD_MODE_NONE, 5, 6, true);
|
PolicySet p5in = new PolicySet(4, PolicySet.PASSWORD_MODE_NONE, 5, 6, true);
|
||||||
Account a5 = ProviderTestUtils.setupAccount("sec-5", false, mMockContext);
|
Account a5 = ProviderTestUtils.setupAccount("sec-5", false, mMockContext);
|
||||||
p5in.writeAccount(a5, null);
|
p5in.writeAccount(a5, null, true, mMockContext);
|
||||||
a5.save(mMockContext);
|
|
||||||
PolicySet p5out = sp.computeAggregatePolicy();
|
PolicySet p5out = sp.computeAggregatePolicy();
|
||||||
assertNotNull(p5out);
|
assertNotNull(p5out);
|
||||||
assertEquals(20, p5out.mMinPasswordLength);
|
assertEquals(20, p5out.mMinPasswordLength);
|
||||||
|
@ -217,7 +222,7 @@ public class SecurityPolicyTests extends ProviderTestCase2<EmailProvider> {
|
||||||
PolicySet p1 = new PolicySet(1, PolicySet.PASSWORD_MODE_STRONG, 3, 4, true);
|
PolicySet p1 = new PolicySet(1, PolicySet.PASSWORD_MODE_STRONG, 3, 4, true);
|
||||||
Account a = new Account();
|
Account a = new Account();
|
||||||
final String SYNC_KEY = "test_sync_key";
|
final String SYNC_KEY = "test_sync_key";
|
||||||
p1.writeAccount(a, SYNC_KEY);
|
p1.writeAccount(a, SYNC_KEY, false, null);
|
||||||
PolicySet p2 = new PolicySet(a);
|
PolicySet p2 = new PolicySet(a);
|
||||||
assertEquals(p1, p2);
|
assertEquals(p1, p2);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue