diff options
Diffstat (limited to 'provider_src/com/android/email/activity/setup/AccountSecurity.java')
-rw-r--r-- | provider_src/com/android/email/activity/setup/AccountSecurity.java | 628 |
1 files changed, 628 insertions, 0 deletions
diff --git a/provider_src/com/android/email/activity/setup/AccountSecurity.java b/provider_src/com/android/email/activity/setup/AccountSecurity.java new file mode 100644 index 000000000..c2be45928 --- /dev/null +++ b/provider_src/com/android/email/activity/setup/AccountSecurity.java @@ -0,0 +1,628 @@ +/* + * 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 android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.app.LoaderManager; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.Loader; +import android.content.res.Resources; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; + +import com.android.email.DebugUtils; +import com.android.email.R; +import com.android.email.SecurityPolicy; +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.IntentUtilities; +import com.android.mail.ui.MailAsyncTaskLoader; +import com.android.mail.utils.LogUtils; + +/** + * 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 + * 6. If necessary, request for user to activate device encryption + */ +public class AccountSecurity extends Activity { + private static final String TAG = "Email/AccountSecurity"; + + private static final boolean DEBUG = false; // Don't ship with this set to true + + private static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID"; + private static final String EXTRA_SHOW_DIALOG = "SHOW_DIALOG"; + private static final String EXTRA_PASSWORD_EXPIRING = "EXPIRING"; + private static final String EXTRA_PASSWORD_EXPIRED = "EXPIRED"; + + private static final String SAVESTATE_INITIALIZED_TAG = "initialized"; + private static final String SAVESTATE_TRIED_ADD_ADMINISTRATOR_TAG = "triedAddAdministrator"; + private static final String SAVESTATE_TRIED_SET_PASSWORD_TAG = "triedSetpassword"; + private static final String SAVESTATE_TRIED_SET_ENCRYPTION_TAG = "triedSetEncryption"; + private static final String SAVESTATE_ACCOUNT_TAG = "account"; + + private static final int REQUEST_ENABLE = 1; + private static final int REQUEST_PASSWORD = 2; + private static final int REQUEST_ENCRYPTION = 3; + + private boolean mTriedAddAdministrator; + private boolean mTriedSetPassword; + private boolean mTriedSetEncryption; + + private Account mAccount; + + protected boolean mInitialized; + + private Handler mHandler; + private boolean mActivityResumed; + + private static final int ACCOUNT_POLICY_LOADER_ID = 0; + private AccountAndPolicyLoaderCallbacks mAPLoaderCallbacks; + private Bundle mAPLoaderArgs; + + public static Uri getUpdateSecurityUri(final long accountId, final boolean showDialog) { + final Uri.Builder baseUri = Uri.parse("auth://" + EmailContent.EMAIL_PACKAGE_NAME + + ".ACCOUNT_SECURITY/").buildUpon(); + IntentUtilities.setAccountId(baseUri, accountId); + baseUri.appendQueryParameter(EXTRA_SHOW_DIALOG, Boolean.toString(showDialog)); + return baseUri.build(); + } + + /** + * 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 + * @param showDialog If true, a simple warning dialog will be shown before kicking off + * the necessary system settings. Should be true anywhere the context of the security settings + * is not clear (e.g. any time after the account has been set up). + * @return an Intent which can be used to view that account + */ + public static Intent actionUpdateSecurityIntent(Context context, long accountId, + boolean showDialog) { + Intent intent = new Intent(context, AccountSecurity.class); + intent.putExtra(EXTRA_ACCOUNT_ID, accountId); + intent.putExtra(EXTRA_SHOW_DIALOG, showDialog); + return intent; + } + + /** + * Used for generating intent for this activity (which is intended to be launched + * from a notification.) This is a special mode of this activity which exists only + * to give the user a dialog (for context) about a device pin/password expiration event. + */ + public static Intent actionDevicePasswordExpirationIntent(Context context, long accountId, + boolean expired) { + Intent intent = new ForwardingIntent(context, AccountSecurity.class); + intent.putExtra(EXTRA_ACCOUNT_ID, accountId); + intent.putExtra(expired ? EXTRA_PASSWORD_EXPIRED : EXTRA_PASSWORD_EXPIRING, true); + return intent; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mHandler = new Handler(); + + final Intent i = getIntent(); + final long accountId; + Bundle extras = i.getExtras(); + if (extras == null) { + // We have been invoked via a uri. We need to get our parameters from the URI instead + // of looking in the intent extras. + extras = new Bundle(); + accountId = IntentUtilities.getAccountIdFromIntent(i); + extras.putLong(EXTRA_ACCOUNT_ID, accountId); + boolean showDialog = false; + final String value = i.getData().getQueryParameter(EXTRA_SHOW_DIALOG); + if (!TextUtils.isEmpty(value)) { + showDialog = Boolean.getBoolean(value); + } + extras.putBoolean(EXTRA_SHOW_DIALOG, showDialog); + } else { + accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1); + extras = i.getExtras(); + } + + final SecurityPolicy security = SecurityPolicy.getInstance(this); + security.clearNotification(); + if (accountId == -1) { + finish(); + return; + } + + if (savedInstanceState != null) { + mInitialized = savedInstanceState.getBoolean(SAVESTATE_INITIALIZED_TAG, false); + + mTriedAddAdministrator = + savedInstanceState.getBoolean(SAVESTATE_TRIED_ADD_ADMINISTRATOR_TAG, false); + mTriedSetPassword = + savedInstanceState.getBoolean(SAVESTATE_TRIED_SET_PASSWORD_TAG, false); + mTriedSetEncryption = + savedInstanceState.getBoolean(SAVESTATE_TRIED_SET_ENCRYPTION_TAG, false); + + mAccount = savedInstanceState.getParcelable(SAVESTATE_ACCOUNT_TAG); + } + + if (!mInitialized) { + startAccountAndPolicyLoader(extras); + } + } + + @Override + protected void onSaveInstanceState(final Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(SAVESTATE_INITIALIZED_TAG, mInitialized); + + outState.putBoolean(SAVESTATE_TRIED_ADD_ADMINISTRATOR_TAG, mTriedAddAdministrator); + outState.putBoolean(SAVESTATE_TRIED_SET_PASSWORD_TAG, mTriedSetPassword); + outState.putBoolean(SAVESTATE_TRIED_SET_ENCRYPTION_TAG, mTriedSetEncryption); + + outState.putParcelable(SAVESTATE_ACCOUNT_TAG, mAccount); + } + + @Override + protected void onPause() { + super.onPause(); + mActivityResumed = false; + } + + @Override + protected void onResume() { + super.onResume(); + mActivityResumed = true; + tickleAccountAndPolicyLoader(); + } + + protected boolean isActivityResumed() { + return mActivityResumed; + } + + private void tickleAccountAndPolicyLoader() { + // If we're already initialized we don't need to tickle. + if (!mInitialized) { + getLoaderManager().initLoader(ACCOUNT_POLICY_LOADER_ID, mAPLoaderArgs, + mAPLoaderCallbacks); + } + } + + private void startAccountAndPolicyLoader(final Bundle args) { + mAPLoaderArgs = args; + mAPLoaderCallbacks = new AccountAndPolicyLoaderCallbacks(); + tickleAccountAndPolicyLoader(); + } + + private class AccountAndPolicyLoaderCallbacks + implements LoaderManager.LoaderCallbacks<Account> { + @Override + public Loader<Account> onCreateLoader(final int id, final Bundle args) { + final long accountId = args.getLong(EXTRA_ACCOUNT_ID, -1); + final boolean showDialog = args.getBoolean(EXTRA_SHOW_DIALOG, false); + final boolean passwordExpiring = + args.getBoolean(EXTRA_PASSWORD_EXPIRING, false); + final boolean passwordExpired = + args.getBoolean(EXTRA_PASSWORD_EXPIRED, false); + + return new AccountAndPolicyLoader(getApplicationContext(), accountId, + showDialog, passwordExpiring, passwordExpired); + } + + @Override + public void onLoadFinished(final Loader<Account> loader, final Account account) { + mHandler.post(new Runnable() { + @Override + public void run() { + final AccountSecurity activity = AccountSecurity.this; + if (!activity.isActivityResumed()) { + return; + } + + if (account == null || (account.mPolicyKey != 0 && account.mPolicy == null)) { + activity.finish(); + LogUtils.d(TAG, "could not load account or policy in AccountSecurity"); + return; + } + + if (!activity.mInitialized) { + activity.mInitialized = true; + + final AccountAndPolicyLoader apLoader = (AccountAndPolicyLoader) loader; + activity.completeCreate(account, apLoader.mShowDialog, + apLoader.mPasswordExpiring, apLoader.mPasswordExpired); + } + } + }); + } + + @Override + public void onLoaderReset(Loader<Account> loader) {} + } + + private static class AccountAndPolicyLoader extends MailAsyncTaskLoader<Account> { + private final long mAccountId; + public final boolean mShowDialog; + public final boolean mPasswordExpiring; + public final boolean mPasswordExpired; + + private final Context mContext; + + AccountAndPolicyLoader(final Context context, final long accountId, + final boolean showDialog, final boolean passwordExpiring, + final boolean passwordExpired) { + super(context); + mContext = context; + mAccountId = accountId; + mShowDialog = showDialog; + mPasswordExpiring = passwordExpiring; + mPasswordExpired = passwordExpired; + } + + @Override + public Account loadInBackground() { + final Account account = Account.restoreAccountWithId(mContext, mAccountId); + if (account == null) { + return null; + } + + final long policyId = account.mPolicyKey; + if (policyId != 0) { + account.mPolicy = Policy.restorePolicyWithId(mContext, policyId); + } + + account.getOrCreateHostAuthRecv(mContext); + + return account; + } + + @Override + protected void onDiscardResult(Account result) {} + } + + protected void completeCreate(final Account account, final boolean showDialog, + final boolean passwordExpiring, final boolean passwordExpired) { + mAccount = account; + + // Special handling for password expiration events + if (passwordExpiring || passwordExpired) { + FragmentManager fm = getFragmentManager(); + if (fm.findFragmentByTag("password_expiration") == null) { + PasswordExpirationDialog dialog = + PasswordExpirationDialog.newInstance(mAccount.getDisplayName(), + passwordExpired); + if (DebugUtils.DEBUG || DEBUG) { + LogUtils.d(TAG, "Showing password expiration dialog"); + } + dialog.show(fm, "password_expiration"); + } + return; + } + // Otherwise, handle normal security settings flow + if (mAccount.mPolicyKey != 0) { + // This account wants to control security + if (showDialog) { + // Show dialog first, unless already showing (e.g. after rotation) + FragmentManager fm = getFragmentManager(); + if (fm.findFragmentByTag("security_needed") == null) { + SecurityNeededDialog dialog = + SecurityNeededDialog.newInstance(mAccount.getDisplayName()); + if (DebugUtils.DEBUG || DEBUG) { + LogUtils.d(TAG, "Showing security needed dialog"); + } + dialog.show(fm, "security_needed"); + } + } else { + // Go directly to security settings + tryAdvanceSecurity(mAccount); + } + return; + } + finish(); + } + + /** + * After any of the activities return, try to advance to the "next step" + */ + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + tryAdvanceSecurity(mAccount); + super.onActivityResult(requestCode, resultCode, data); + } + + /** + * Walk the user through the required steps to become an active administrator and with + * the requisite security settings for the given account. + * + * These steps will be repeated each time we return from a given attempt (e.g. asking the + * user to choose a device pin/password). In a typical activation, we may repeat these + * steps a few times. It may go as far as step 5 (password) or step 6 (encryption), but it + * will terminate when step 2 (isActive()) succeeds. + * + * If at any point we do not advance beyond a given user step, (e.g. the user cancels + * instead of setting a password) we simply repost the security notification, and exit. + * We never want to loop here. + */ + private void tryAdvanceSecurity(Account account) { + SecurityPolicy security = SecurityPolicy.getInstance(this); + // Step 1. Check if we are an active device administrator, and stop here to activate + if (!security.isActiveAdmin()) { + if (mTriedAddAdministrator) { + if (DebugUtils.DEBUG || DEBUG) { + LogUtils.d(TAG, "Not active admin: repost notification"); + } + repostNotification(account, security); + finish(); + } else { + mTriedAddAdministrator = true; + // retrieve name of server for the format string + final HostAuth hostAuth = account.mHostAuthRecv; + if (hostAuth == null) { + if (DebugUtils.DEBUG || DEBUG) { + LogUtils.d(TAG, "No HostAuth: repost notification"); + } + repostNotification(account, security); + finish(); + } else { + if (DebugUtils.DEBUG || DEBUG) { + LogUtils.d(TAG, "Not active admin: post initial notification"); + } + // try to become active - must happen here in 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, + hostAuth.mAddress)); + startActivityForResult(intent, REQUEST_ENABLE); + } + } + return; + } + + // Step 2. Check if the current aggregate security policy is being satisfied by the + // DevicePolicyManager (the current system security level). + if (security.isActive(null)) { + if (DebugUtils.DEBUG || DEBUG) { + LogUtils.d(TAG, "Security active; clear holds"); + } + Account.clearSecurityHoldOnAllAccounts(this); + security.syncAccount(account); + security.clearNotification(); + finish(); + return; + } + + // Step 3. Try to assert the current aggregate security requirements with the system. + security.setActivePolicies(); + + // Step 4. Recheck the security policy, and determine what changes are needed (if any) + // to satisfy the requirements. + int inactiveReasons = security.getInactiveReasons(null); + + // Step 5. If password is needed, try to have the user set it + if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_PASSWORD) != 0) { + if (mTriedSetPassword) { + if (DebugUtils.DEBUG || DEBUG) { + LogUtils.d(TAG, "Password needed; repost notification"); + } + repostNotification(account, security); + finish(); + } else { + if (DebugUtils.DEBUG || DEBUG) { + LogUtils.d(TAG, "Password needed; request it via DPM"); + } + mTriedSetPassword = true; + // launch the activity to have the user set a new password. + Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); + startActivityForResult(intent, REQUEST_PASSWORD); + } + return; + } + + // Step 6. If encryption is needed, try to have the user set it + if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_ENCRYPTION) != 0) { + if (mTriedSetEncryption) { + if (DebugUtils.DEBUG || DEBUG) { + LogUtils.d(TAG, "Encryption needed; repost notification"); + } + repostNotification(account, security); + finish(); + } else { + if (DebugUtils.DEBUG || DEBUG) { + LogUtils.d(TAG, "Encryption needed; request it via DPM"); + } + mTriedSetEncryption = true; + // launch the activity to start up encryption. + Intent intent = new Intent(DevicePolicyManager.ACTION_START_ENCRYPTION); + startActivityForResult(intent, REQUEST_ENCRYPTION); + } + return; + } + + // Step 7. No problems were found, so clear holds and exit + if (DebugUtils.DEBUG || DEBUG) { + LogUtils.d(TAG, "Policies enforced; clear holds"); + } + Account.clearSecurityHoldOnAllAccounts(this); + security.syncAccount(account); + security.clearNotification(); + finish(); + } + + /** + * Mark an account as not-ready-for-sync and post a notification to bring the user back here + * eventually. + */ + private static void repostNotification(final Account account, final SecurityPolicy security) { + if (account == null) return; + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + security.policiesRequired(account.mId); + return null; + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + /** + * Dialog briefly shown in some cases, to indicate the user that a security update is needed. + * If the user clicks OK, we proceed into the "tryAdvanceSecurity" flow. If the user cancels, + * we repost the notification and finish() the activity. + */ + public static class SecurityNeededDialog extends DialogFragment + implements DialogInterface.OnClickListener { + private static final String BUNDLE_KEY_ACCOUNT_NAME = "account_name"; + + // Public no-args constructor needed for fragment re-instantiation + public SecurityNeededDialog() {} + + /** + * Create a new dialog. + */ + public static SecurityNeededDialog newInstance(String accountName) { + final SecurityNeededDialog dialog = new SecurityNeededDialog(); + Bundle b = new Bundle(); + b.putString(BUNDLE_KEY_ACCOUNT_NAME, accountName); + dialog.setArguments(b); + return dialog; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final String accountName = getArguments().getString(BUNDLE_KEY_ACCOUNT_NAME); + + final Context context = getActivity(); + final Resources res = context.getResources(); + final AlertDialog.Builder b = new AlertDialog.Builder(context); + b.setTitle(R.string.account_security_dialog_title); + b.setIconAttribute(android.R.attr.alertDialogIcon); + b.setMessage(res.getString(R.string.account_security_dialog_content_fmt, accountName)); + b.setPositiveButton(android.R.string.ok, this); + b.setNegativeButton(android.R.string.cancel, this); + if (DebugUtils.DEBUG || DEBUG) { + LogUtils.d(TAG, "Posting security needed dialog"); + } + return b.create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + AccountSecurity activity = (AccountSecurity) getActivity(); + if (activity.mAccount == null) { + // Clicked before activity fully restored - probably just monkey - exit quickly + activity.finish(); + return; + } + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + if (DebugUtils.DEBUG || DEBUG) { + LogUtils.d(TAG, "User accepts; advance to next step"); + } + activity.tryAdvanceSecurity(activity.mAccount); + break; + case DialogInterface.BUTTON_NEGATIVE: + if (DebugUtils.DEBUG || DEBUG) { + LogUtils.d(TAG, "User declines; repost notification"); + } + AccountSecurity.repostNotification( + activity.mAccount, SecurityPolicy.getInstance(activity)); + activity.finish(); + break; + } + } + } + + /** + * Dialog briefly shown in some cases, to indicate the user that the PIN/Password is expiring + * or has expired. If the user clicks OK, we launch the password settings screen. + */ + public static class PasswordExpirationDialog extends DialogFragment + implements DialogInterface.OnClickListener { + private static final String BUNDLE_KEY_ACCOUNT_NAME = "account_name"; + private static final String BUNDLE_KEY_EXPIRED = "expired"; + + /** + * Create a new dialog. + */ + public static PasswordExpirationDialog newInstance(String accountName, boolean expired) { + final PasswordExpirationDialog dialog = new PasswordExpirationDialog(); + Bundle b = new Bundle(); + b.putString(BUNDLE_KEY_ACCOUNT_NAME, accountName); + b.putBoolean(BUNDLE_KEY_EXPIRED, expired); + dialog.setArguments(b); + return dialog; + } + + // Public no-args constructor needed for fragment re-instantiation + public PasswordExpirationDialog() {} + + /** + * Note, this actually creates two slightly different dialogs (for expiring vs. expired) + */ + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final String accountName = getArguments().getString(BUNDLE_KEY_ACCOUNT_NAME); + final boolean expired = getArguments().getBoolean(BUNDLE_KEY_EXPIRED); + final int titleId = expired + ? R.string.password_expired_dialog_title + : R.string.password_expire_warning_dialog_title; + final int contentId = expired + ? R.string.password_expired_dialog_content_fmt + : R.string.password_expire_warning_dialog_content_fmt; + + final Context context = getActivity(); + final Resources res = context.getResources(); + return new AlertDialog.Builder(context) + .setTitle(titleId) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setMessage(res.getString(contentId, accountName)) + .setPositiveButton(android.R.string.ok, this) + .setNegativeButton(android.R.string.cancel, this) + .create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + AccountSecurity activity = (AccountSecurity) getActivity(); + if (which == DialogInterface.BUTTON_POSITIVE) { + Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); + activity.startActivity(intent); + } + activity.finish(); + } + } +} |