diff --git a/res/values/strings.xml b/res/values/strings.xml index 265e25463..1dee8e703 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -650,6 +650,11 @@ save attachment. being supported --> This server requires security features your phone does not support. + + WARNING: Deactivating the Email application\'s authority + to administer your device will delete all Email accounts that require it, along with their + email, contacts, calendar events, and other data. diff --git a/src/com/android/email/Controller.java b/src/com/android/email/Controller.java index a9ea48718..4d49df434 100644 --- a/src/com/android/email/Controller.java +++ b/src/com/android/email/Controller.java @@ -942,7 +942,7 @@ public class Controller { public void deleteAccount(final long accountId) { Utility.runAsync(new Runnable() { public void run() { - deleteAccountSync(accountId); + deleteAccountSync(accountId, mContext); } }); } @@ -950,19 +950,19 @@ public class Controller { /** * Delete an account synchronously. Intended to be used only by unit tests. */ - public void deleteAccountSync(long accountId) { + public void deleteAccountSync(long accountId, Context context) { try { mLegacyControllerMap.remove(accountId); // Get the account URI. - final Account account = Account.restoreAccountWithId(mContext, accountId); + final Account account = Account.restoreAccountWithId(context, accountId); if (account == null) { return; // Already deleted? } - final String accountUri = account.getStoreUri(mContext); + final String accountUri = account.getStoreUri(context); // Delete Remote store at first. if (!TextUtils.isEmpty(accountUri)) { - Store.getInstance(accountUri, mContext, null).delete(); + Store.getInstance(accountUri, context, null).delete(); // Remove the Store instance from cache. Store.removeInstance(accountUri); } @@ -972,12 +972,12 @@ public class Controller { mContext.getContentResolver().delete(uri, null, null); // Update the backup (side copy) of the accounts - AccountBackupRestore.backupAccounts(mContext); + AccountBackupRestore.backupAccounts(context); // Release or relax device administration, if relevant - SecurityPolicy.getInstance(mContext).reducePolicies(); + SecurityPolicy.getInstance(context).reducePolicies(); - Email.setServicesEnabled(mContext); + Email.setServicesEnabled(context); } catch (Exception e) { Log.w(Email.LOG_TAG, "Exception while deleting account", e); } finally { diff --git a/src/com/android/email/SecurityPolicy.java b/src/com/android/email/SecurityPolicy.java index 5f9250016..e30c637b8 100644 --- a/src/com/android/email/SecurityPolicy.java +++ b/src/com/android/email/SecurityPolicy.java @@ -20,7 +20,6 @@ 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.AccountColumns; -import com.android.email.service.MailService; import android.app.Notification; import android.app.NotificationManager; @@ -28,6 +27,7 @@ import android.app.PendingIntent; import android.app.admin.DeviceAdminReceiver; import android.app.admin.DevicePolicyManager; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -43,7 +43,7 @@ import android.util.Log; * into and out of various security states. */ public class SecurityPolicy { - + private static final String TAG = "SecurityPolicy"; private static SecurityPolicy sInstance = null; private Context mContext; private DevicePolicyManager mDPM; @@ -703,19 +703,40 @@ public class SecurityPolicy { } /** - * Internal handler for enabled->disabled transitions. Resets all security keys - * forcing EAS to resync security state. + * Delete all accounts whose security flags aren't zero (i.e. they have security enabled). + * This method is synchronous, so it should normally be called within a worker thread (the + * exception being for unit tests) + * + * @param context the caller's context */ - /* package */ void onAdminEnabled(boolean isEnabled) { + /*package*/ void deleteSecuredAccounts(Context context) { + ContentResolver cr = context.getContentResolver(); + // Find all accounts with security and delete them + Cursor c = cr.query(Account.CONTENT_URI, EmailContent.ID_PROJECTION, + AccountColumns.SECURITY_FLAGS + "!=0", null, null); + try { + Log.w(TAG, "Email administration disabled; deleting " + c.getCount() + + " secured account(s)"); + while (c.moveToNext()) { + Controller.getInstance(context).deleteAccountSync( + c.getLong(EmailContent.ID_PROJECTION_COLUMN), context); + } + } finally { + c.close(); + } + updatePolicies(-1); + } + + /** + * Internal handler for enabled->disabled transitions. Deletes all secured accounts. + */ + /*package*/ void onAdminEnabled(boolean isEnabled) { if (!isEnabled) { - // transition to disabled state - // Response: clear *all* security state information from the accounts, forcing - // them back to the initial configurations requiring policy administration - ContentValues cv = new ContentValues(); - cv.put(AccountColumns.SECURITY_FLAGS, 0); - cv.putNull(AccountColumns.SECURITY_SYNC_KEY); - mContext.getContentResolver().update(Account.CONTENT_URI, cv, null, null); - updatePolicies(-1); + Utility.runAsync(new Runnable() { + @Override + public void run() { + deleteSecuredAccounts(mContext); + }}); } } @@ -743,6 +764,15 @@ public class SecurityPolicy { SecurityPolicy.getInstance(context).onAdminEnabled(false); } + /** + * Called when the user asks to disable administration; we return a warning string that + * will be presented to the user + */ + @Override + public CharSequence onDisableRequested(Context context, Intent intent) { + return context.getString(R.string.disable_admin_warning); + } + /** * Called after the user has changed their password. */ diff --git a/tests/src/com/android/email/SecurityPolicyTests.java b/tests/src/com/android/email/SecurityPolicyTests.java index 2ecd91c89..f21a682e3 100644 --- a/tests/src/com/android/email/SecurityPolicyTests.java +++ b/tests/src/com/android/email/SecurityPolicyTests.java @@ -379,6 +379,12 @@ public class SecurityPolicyTests extends ProviderTestCase2 { assertEquals(Account.FLAGS_VIBRATE_ALWAYS, a2b.mFlags); } + private static class MockController extends Controller { + protected MockController(Context context) { + super(context); + } + } + /** * Test the response to disabling DeviceAdmin status */ @@ -410,15 +416,22 @@ public class SecurityPolicyTests extends ProviderTestCase2 { Account a3a = Account.restoreAccountWithId(mMockContext, a3.mId); assertNull(a3a.mSecuritySyncKey); - // Revoke device admin status. In the accounts we set up, security values should be reset - sp.onAdminEnabled(false); // "disabled" should clear policies - PolicySet after2 = sp.getAggregatePolicy(); - assertEquals(SecurityPolicy.NO_POLICY_SET, after2); - Account a1b = Account.restoreAccountWithId(mMockContext, a1.mId); - assertNull(a1b.mSecuritySyncKey); - Account a2b = Account.restoreAccountWithId(mMockContext, a2.mId); - assertNull(a2b.mSecuritySyncKey); - Account a3b = Account.restoreAccountWithId(mMockContext, a3.mId); - assertNull(a3b.mSecuritySyncKey); + // Simulate revoke of device admin; directly call deleteSecuredAccounts, which is normally + // called from a background thread + MockController mockController = new MockController(mMockContext); + Controller.injectMockControllerForTest(mockController); + try { + sp.deleteSecuredAccounts(mMockContext); + PolicySet after2 = sp.getAggregatePolicy(); + assertEquals(SecurityPolicy.NO_POLICY_SET, after2); + Account a1b = Account.restoreAccountWithId(mMockContext, a1.mId); + assertNull(a1b); + Account a2b = Account.restoreAccountWithId(mMockContext, a2.mId); + assertNull(a2b); + Account a3b = Account.restoreAccountWithId(mMockContext, a3.mId); + assertNull(a3b.mSecuritySyncKey); + } finally { + Controller.injectMockControllerForTest(null); + } } }