From 3ee0cad5f5e21a24dbe43d21afaac1dd76a2059b Mon Sep 17 00:00:00 2001 From: Andrew Stadler Date: Fri, 4 Jun 2010 11:10:03 -0700 Subject: [PATCH] DO NOT MERGE Workaround for KeyguardLock problem * The device policies that enforce the use of a device PIN or password can be sidestepped by apps that implement KeyguardManager.KeyguardLock * This renders the policies unuseable * To prevent this, the email app now scans for any packages holding the DISABLE_KEYGUARD permission. The existence of any non-system app with this permission will put all security-enabled EAS accounts into a security hold, and post a dialog describing the problem. * The user must uninstall any such app(s) in order to sync their EAS data. Bug: 2737842 Change-Id: I4c96d76b12d9242b5c755dd60d7578a825fae597 --- AndroidManifest.xml | 7 ++ res/values/strings.xml | 6 ++ src/com/android/email/Email.java | 6 ++ src/com/android/email/SecurityPolicy.java | 66 ++++++++++++++ .../email/activity/setup/AccountSecurity.java | 86 +++++++++++++++++-- .../activity/setup/AccountSetupNames.java | 23 ++++- .../android/email/service/BootReceiver.java | 14 ++- 7 files changed, 193 insertions(+), 15 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index a21a23d45..19dea82de 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -235,6 +235,13 @@ + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 95b4e9ee5..0d651d49b 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -681,6 +681,12 @@ Email Enables server-specified security policies + + Screen lock disabled + + This account cannot be used while the + application \"%s\" is installed, because it + interferes with the operation of the screen lock. diff --git a/src/com/android/email/Email.java b/src/com/android/email/Email.java index 3c5bcb963..b43cf94ea 100644 --- a/src/com/android/email/Email.java +++ b/src/com/android/email/Email.java @@ -250,6 +250,12 @@ public class Email extends Application { */ MailService.actionReschedule(context); } + // If enabling, clear the cached test for keyguard-disabling apps; This is because + // we may have missed package install/replace/remove broadcasts while we had no accounts + // and the BootReceiver was disabled. + if (enabled) { + SecurityPolicy.getInstance(context.getApplicationContext()).invalidateKeyguardCache(); + } } @Override diff --git a/src/com/android/email/SecurityPolicy.java b/src/com/android/email/SecurityPolicy.java index 5dc9c14e5..335aa75f7 100644 --- a/src/com/android/email/SecurityPolicy.java +++ b/src/com/android/email/SecurityPolicy.java @@ -33,11 +33,16 @@ import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.database.Cursor; import android.media.AudioManager; import android.net.Uri; import android.util.Log; +import java.util.List; + /** * Utility functions to support reading and writing security policies, and handshaking the device * into and out of various security states. @@ -49,6 +54,10 @@ public class SecurityPolicy { private DevicePolicyManager mDPM; private ComponentName mAdminName; private PolicySet mAggregatePolicy; + // false = unknown, true = mKeyguardDisablePackageName is valid (or null) + private boolean mKeyguardDisableChecked; + // null = no apps disabling, non-null = one or more apps disabling + private String mKeyguardDisablePackageName; /* package */ static final PolicySet NO_POLICY_SET = new PolicySet(0, PolicySet.PASSWORD_MODE_NONE, 0, 0, false); @@ -101,6 +110,7 @@ public class SecurityPolicy { mDPM = null; mAdminName = new ComponentName(context, PolicyAdmin.class); mAggregatePolicy = null; + mKeyguardDisableChecked = false; } /** @@ -265,6 +275,13 @@ public class SecurityPolicy { } } if (policies.mPasswordMode > 0) { + // If the server requests a password policy of any kind (PIN, password, etc) + // and the user has a keyguard-disable package, then we cannot support the + // policy. In a rare case of a server that doesn't enforce PIN/Password, we + // won't do this test. + if (getKeyguardDisablePackage() != null) { + return false; + } if (dpm.getPasswordQuality(mAdminName) < policies.getDPManagerPasswordQuality()) { return false; } @@ -610,6 +627,55 @@ public class SecurityPolicy { return dpm.isAdminActive(mAdminName); } + /** + * Invalidate the cached result of checking for lock screen disablers. Called by package + * add/replace/delete broadcasts. + */ + public synchronized void invalidateKeyguardCache() { + mKeyguardDisableChecked = false; + mKeyguardDisablePackageName = null; + } + + /** + * Return the name of any app that is disabling the keyguard (lock screen) - in which case, + * we must treat the device as if required lock screen policies are not met. + * + * @return non-null if any app is disabling the keyguard + */ + public synchronized String getKeyguardDisablePackage() { + if (!mKeyguardDisableChecked) { + mKeyguardDisablePackageName = findKeyguardPermission(); + mKeyguardDisableChecked = true; + } + return mKeyguardDisablePackageName; + } + + /** + * Scan installed packages and look for DISABLE_KEYGUARD permission. Return the first + * installed packages (if any are found). This won't be cheap, so do it as little as possible. + * + * @return the display name of the package, or null if all clear + */ + private String findKeyguardPermission() { + PackageManager pm = mContext.getPackageManager(); + List packages = pm.getInstalledPackages(0); + for (PackageInfo info : packages) { + // skip over system packages; they are allowed to have the permission + if (0 != (info.applicationInfo.flags & + (ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP))) { + continue; + } + // all other packages, check for specific permission + int permissionResult = pm.checkPermission(android.Manifest.permission.DISABLE_KEYGUARD, + info.packageName); + if (permissionResult == PackageManager.PERMISSION_GRANTED) { + Log.d(Email.LOG_TAG, "DISABLE_KEYGUARD found in " + info.packageName); + return info.applicationInfo.loadLabel(pm).toString(); + } + } + return null; + } + /** * Report admin component name - for making calls into device policy manager */ diff --git a/src/com/android/email/activity/setup/AccountSecurity.java b/src/com/android/email/activity/setup/AccountSecurity.java index 68bdbc498..d2b66fd5d 100644 --- a/src/com/android/email/activity/setup/AccountSecurity.java +++ b/src/com/android/email/activity/setup/AccountSecurity.java @@ -21,8 +21,11 @@ import com.android.email.SecurityPolicy; import com.android.email.provider.EmailContent.Account; import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; import android.app.admin.DevicePolicyManager; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; @@ -40,8 +43,12 @@ public class AccountSecurity extends Activity { private static final String EXTRA_ACCOUNT_ID = "com.android.email.activity.setup.ACCOUNT_ID"; + private static final int DIALOG_KEYGUARD_DISABLED = 1; + private static final int REQUEST_ENABLE = 1; + private String mKeyguardBlockerName; + /** * Used for generating intent for this activity (which is intended to be launched * from a notification.) @@ -59,6 +66,11 @@ public class AccountSecurity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + boolean finishNow = true; + // Return RESULT_OK in all cases unless explicitly changed (see below). This is ignored + // when we're launched from notification, but it's checked when we're launched from + // (and return to) AccountSetupNames. + setResult(Activity.RESULT_OK); Intent i = getIntent(); long accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1); @@ -68,7 +80,8 @@ public class AccountSecurity extends Activity { // 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) { + if ((account.mSecurityFlags != 0) || + (0 != (account.mFlags & Account.FLAGS_SECURITY_HOLD))) { // This account wants to control security if (!security.isActiveAdmin()) { // try to become active - must happen here in this activity, to get result @@ -83,12 +96,14 @@ public class AccountSecurity extends Activity { return; } else { // already active - try to set actual policies, finish, and return - setActivePolicies(); + finishNow = setActivePolicies(); } } } } - finish(); + if (finishNow) { + finish(); + } } /** @@ -96,11 +111,12 @@ public class AccountSecurity extends Activity { */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { + boolean finishNow = true; switch (requestCode) { case REQUEST_ENABLE: if (resultCode == Activity.RESULT_OK) { // now active - try to set actual policies - setActivePolicies(); + finishNow = setActivePolicies(); } else { // failed - repost notification, and exit final long accountId = getIntent().getLongExtra(EXTRA_ACCOUNT_ID, -1); @@ -115,31 +131,85 @@ public class AccountSecurity extends Activity { } } } - finish(); + // This activity has no layout and in most cases serves only as a dispatcher for + // checking and adjusting device security policies. However, in a few cases, it + // presents an error dialog. In those cases, setActivePolicies() will return false, and + // we don't call finish() so the dialog is able to display. + if (finishNow) { + 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. + * + * @return true if OK to finish the activity (false if we posted a dialog) */ - private void setActivePolicies() { + private boolean setActivePolicies() { SecurityPolicy sp = SecurityPolicy.getInstance(this); // check current security level - if sufficient, we're done! if (sp.isActive(null)) { sp.clearAccountHoldFlags(); - return; + return true; } // set current security level sp.setActivePolicies(); // check current security level - if sufficient, we're done! if (sp.isActive(null)) { sp.clearAccountHoldFlags(); - return; + return true; + } + // if the problem is an app blocking the lock screen, notify the user + String keyguardBlocker = sp.getKeyguardDisablePackage(); + if (keyguardBlocker != null) { + mKeyguardBlockerName = keyguardBlocker; + showDialog(DIALOG_KEYGUARD_DISABLED); + // Set activity to return RESULT_CANCELED (after dialog) to notify caller that we + // didn't configure policies completely. + setResult(Activity.RESULT_CANCELED); + return false; } // 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); + // TODO: use startActivityForResult, and when we see the result, recheck to see + // if the user actually entered a sufficient password. If not, repost the notification. + return true; } + @Override + public Dialog onCreateDialog(int id) { + switch (id) { + case DIALOG_KEYGUARD_DISABLED: + return new AlertDialog.Builder(this) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(R.string.keyguard_disabled_dlg_title) + .setMessage(getString(R.string.keyguard_disabled_dlg_message_fmt, + mKeyguardBlockerName)) + .setNeutralButton(R.string.okay_action, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + dismissDialog(DIALOG_KEYGUARD_DISABLED); + finish(); + } + }) + .setCancelable(false) + .create(); + } + return super.onCreateDialog(id); + } + + /** + * Update a cached dialog with current values (e.g. account name) + */ + @Override + public void onPrepareDialog(int id, Dialog dialog) { + switch (id) { + case DIALOG_KEYGUARD_DISABLED: + AlertDialog alert = (AlertDialog) dialog; + alert.setMessage(getString(R.string.keyguard_disabled_dlg_message_fmt, + mKeyguardBlockerName)); + } + } } diff --git a/src/com/android/email/activity/setup/AccountSetupNames.java b/src/com/android/email/activity/setup/AccountSetupNames.java index 1b148c5c6..c8885543d 100644 --- a/src/com/android/email/activity/setup/AccountSetupNames.java +++ b/src/com/android/email/activity/setup/AccountSetupNames.java @@ -19,6 +19,7 @@ package com.android.email.activity.setup; import com.android.email.AccountBackupRestore; import com.android.email.R; import com.android.email.Utility; +import com.android.email.activity.AccountFolderList; import com.android.email.activity.Welcome; import com.android.email.provider.EmailContent; import com.android.email.provider.EmailContent.Account; @@ -146,12 +147,25 @@ public class AccountSetupNames extends Activity implements OnClickListener { @Override public void onBackPressed() { + onBackPressed(true); // OK to "proceed" to next step (the account has been created) + } + + private void onBackPressed(boolean okToProceed) { boolean easFlowMode = getIntent().getBooleanExtra(EXTRA_EAS_FLOW, false); if (easFlowMode) { AccountSetupBasics.actionAccountCreateFinishedEas(this); } else { if (mAccount != null) { - AccountSetupBasics.actionAccountCreateFinished(this, mAccount.mId); + if (okToProceed) { + AccountSetupBasics.actionAccountCreateFinished(this, mAccount.mId); + } else { + // This is what we do if the account was created, but somebody called + // onBackPressed(false), indicating that the new account's inbox should not be + // entered. In this case, we'll just go back to the accounts list. + // We don't use the typical "Welcome" activity because if there is only one + // account, it will also try to visit the inbox of that account. + AccountFolderList.actionShowAccounts(this); + } } else { // Safety check here; If mAccount is null (due to external issues or bugs) // just rewind back to Welcome, which can handle any configuration of accounts @@ -249,14 +263,15 @@ public class AccountSetupNames extends Activity implements OnClickListener { /** * Handle the eventual result from the security update activity - * - * TODO: If the user doesn't update the security, don't go to the MessageList. */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_SECURITY: - onBackPressed(); + // If we get RESULT_CANCEL, we tell onBackPressed that there was an error, + // and it won't try to take us to the MessageList of the new account. + onBackPressed(resultCode == Activity.RESULT_OK); + break; } super.onActivityResult(requestCode, resultCode, data); } diff --git a/src/com/android/email/service/BootReceiver.java b/src/com/android/email/service/BootReceiver.java index 880773c2b..8793e1b1a 100644 --- a/src/com/android/email/service/BootReceiver.java +++ b/src/com/android/email/service/BootReceiver.java @@ -18,6 +18,7 @@ package com.android.email.service; import com.android.email.AccountBackupRestore; import com.android.email.Email; +import com.android.email.SecurityPolicy; import android.content.BroadcastReceiver; import android.content.Context; @@ -29,17 +30,24 @@ public class BootReceiver extends BroadcastReceiver { // Restore accounts, if it has not happened already AccountBackupRestore.restoreAccountsIfNeeded(context); - if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { + String intentAction = intent.getAction(); + + if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) { // Returns true if there are any accounts if (Email.setServicesEnabled(context)) { MailService.actionReschedule(context); } } - else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) { + else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intentAction)) { MailService.actionCancel(context); } - else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) { + else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intentAction)) { MailService.actionReschedule(context); } + else if (Intent.ACTION_PACKAGE_ADDED.equals(intentAction) || + Intent.ACTION_PACKAGE_REPLACED.equals(intentAction) || + Intent.ACTION_PACKAGE_REMOVED.equals(intentAction)) { + SecurityPolicy.getInstance(context.getApplicationContext()).invalidateKeyguardCache(); + } } }