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