1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.email.activity.setup; 18 19 import android.app.Activity; 20 import android.app.AlertDialog; 21 import android.app.Dialog; 22 import android.app.DialogFragment; 23 import android.app.FragmentManager; 24 import android.app.LoaderManager; 25 import android.app.admin.DevicePolicyManager; 26 import android.content.Context; 27 import android.content.DialogInterface; 28 import android.content.Intent; 29 import android.content.Loader; 30 import android.content.res.Resources; 31 import android.os.AsyncTask; 32 import android.os.Bundle; 33 import android.os.Handler; 34 35 import com.android.email.R; 36 import com.android.email.SecurityPolicy; 37 import com.android.email2.ui.MailActivityEmail; 38 import com.android.emailcommon.provider.Account; 39 import com.android.emailcommon.provider.HostAuth; 40 import com.android.emailcommon.provider.Policy; 41 import com.android.mail.ui.MailAsyncTaskLoader; 42 import com.android.mail.utils.LogUtils; 43 44 /** 45 * Psuedo-activity (no UI) to bootstrap the user up to a higher desired security level. This 46 * bootstrap requires the following steps. 47 * 48 * 1. Confirm the account of interest has any security policies defined - exit early if not 49 * 2. If not actively administrating the device, ask Device Policy Manager to start that 50 * 3. When we are actively administrating, check current policies and see if they're sufficient 51 * 4. If not, set policies 52 * 5. If necessary, request for user to update device password 53 * 6. If necessary, request for user to activate device encryption 54 */ 55 public class AccountSecurity extends Activity { 56 private static final String TAG = "Email/AccountSecurity"; 57 58 private static final boolean DEBUG = false; // Don't ship with this set to true 59 60 private static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID"; 61 private static final String EXTRA_SHOW_DIALOG = "SHOW_DIALOG"; 62 private static final String EXTRA_PASSWORD_EXPIRING = "EXPIRING"; 63 private static final String EXTRA_PASSWORD_EXPIRED = "EXPIRED"; 64 65 private static final String SAVESTATE_INITIALIZED_TAG = "initialized"; 66 private static final String SAVESTATE_TRIED_ADD_ADMINISTRATOR_TAG = "triedAddAdministrator"; 67 private static final String SAVESTATE_TRIED_SET_PASSWORD_TAG = "triedSetpassword"; 68 private static final String SAVESTATE_TRIED_SET_ENCRYPTION_TAG = "triedSetEncryption"; 69 private static final String SAVESTATE_ACCOUNT_TAG = "account"; 70 71 private static final int REQUEST_ENABLE = 1; 72 private static final int REQUEST_PASSWORD = 2; 73 private static final int REQUEST_ENCRYPTION = 3; 74 75 private boolean mTriedAddAdministrator; 76 private boolean mTriedSetPassword; 77 private boolean mTriedSetEncryption; 78 79 private Account mAccount; 80 81 protected boolean mInitialized; 82 83 private Handler mHandler; 84 private boolean mActivityResumed; 85 86 private static final int ACCOUNT_POLICY_LOADER_ID = 0; 87 private AccountAndPolicyLoaderCallbacks mAPLoaderCallbacks; 88 private Bundle mAPLoaderArgs; 89 90 /** 91 * Used for generating intent for this activity (which is intended to be launched 92 * from a notification.) 93 * 94 * @param context Calling context for building the intent 95 * @param accountId The account of interest 96 * @param showDialog If true, a simple warning dialog will be shown before kicking off 97 * the necessary system settings. Should be true anywhere the context of the security settings 98 * is not clear (e.g. any time after the account has been set up). 99 * @return an Intent which can be used to view that account 100 */ 101 public static Intent actionUpdateSecurityIntent(Context context, long accountId, 102 boolean showDialog) { 103 Intent intent = new Intent(context, AccountSecurity.class); 104 intent.putExtra(EXTRA_ACCOUNT_ID, accountId); 105 intent.putExtra(EXTRA_SHOW_DIALOG, showDialog); 106 return intent; 107 } 108 109 /** 110 * Used for generating intent for this activity (which is intended to be launched 111 * from a notification.) This is a special mode of this activity which exists only 112 * to give the user a dialog (for context) about a device pin/password expiration event. 113 */ 114 public static Intent actionDevicePasswordExpirationIntent(Context context, long accountId, 115 boolean expired) { 116 Intent intent = new ForwardingIntent(context, AccountSecurity.class); 117 intent.putExtra(EXTRA_ACCOUNT_ID, accountId); 118 intent.putExtra(expired ? EXTRA_PASSWORD_EXPIRED : EXTRA_PASSWORD_EXPIRING, true); 119 return intent; 120 } 121 122 @Override 123 public void onCreate(Bundle savedInstanceState) { 124 super.onCreate(savedInstanceState); 125 126 mHandler = new Handler(); 127 128 final Intent i = getIntent(); 129 final long accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1); 130 final SecurityPolicy security = SecurityPolicy.getInstance(this); 131 security.clearNotification(); 132 if (accountId == -1) { 133 finish(); 134 return; 135 } 136 137 if (savedInstanceState != null) { 138 mInitialized = savedInstanceState.getBoolean(SAVESTATE_INITIALIZED_TAG, false); 139 140 mTriedAddAdministrator = 141 savedInstanceState.getBoolean(SAVESTATE_TRIED_ADD_ADMINISTRATOR_TAG, false); 142 mTriedSetPassword = 143 savedInstanceState.getBoolean(SAVESTATE_TRIED_SET_PASSWORD_TAG, false); 144 mTriedSetEncryption = 145 savedInstanceState.getBoolean(SAVESTATE_TRIED_SET_ENCRYPTION_TAG, false); 146 147 mAccount = savedInstanceState.getParcelable(SAVESTATE_ACCOUNT_TAG); 148 } 149 150 if (!mInitialized) { 151 startAccountAndPolicyLoader(i.getExtras()); 152 } 153 } 154 155 @Override 156 protected void onSaveInstanceState(final Bundle outState) { 157 super.onSaveInstanceState(outState); 158 outState.putBoolean(SAVESTATE_INITIALIZED_TAG, mInitialized); 159 160 outState.putBoolean(SAVESTATE_TRIED_ADD_ADMINISTRATOR_TAG, mTriedAddAdministrator); 161 outState.putBoolean(SAVESTATE_TRIED_SET_PASSWORD_TAG, mTriedSetPassword); 162 outState.putBoolean(SAVESTATE_TRIED_SET_ENCRYPTION_TAG, mTriedSetEncryption); 163 164 outState.putParcelable(SAVESTATE_ACCOUNT_TAG, mAccount); 165 } 166 167 @Override 168 protected void onPause() { 169 super.onPause(); 170 mActivityResumed = false; 171 } 172 173 @Override 174 protected void onResume() { 175 super.onResume(); 176 mActivityResumed = true; 177 tickleAccountAndPolicyLoader(); 178 } 179 180 protected boolean isActivityResumed() { 181 return mActivityResumed; 182 } 183 184 private void tickleAccountAndPolicyLoader() { 185 // If we're already initialized we don't need to tickle. 186 if (!mInitialized) { 187 getLoaderManager().initLoader(ACCOUNT_POLICY_LOADER_ID, mAPLoaderArgs, 188 mAPLoaderCallbacks); 189 } 190 } 191 192 private void startAccountAndPolicyLoader(final Bundle args) { 193 mAPLoaderArgs = args; 194 mAPLoaderCallbacks = new AccountAndPolicyLoaderCallbacks(); 195 tickleAccountAndPolicyLoader(); 196 } 197 198 private class AccountAndPolicyLoaderCallbacks 199 implements LoaderManager.LoaderCallbacks<Account> { 200 @Override 201 public Loader<Account> onCreateLoader(final int id, final Bundle args) { 202 final long accountId = args.getLong(EXTRA_ACCOUNT_ID, -1); 203 final boolean showDialog = args.getBoolean(EXTRA_SHOW_DIALOG, false); 204 final boolean passwordExpiring = 205 args.getBoolean(EXTRA_PASSWORD_EXPIRING, false); 206 final boolean passwordExpired = 207 args.getBoolean(EXTRA_PASSWORD_EXPIRED, false); 208 209 return new AccountAndPolicyLoader(getApplicationContext(), accountId, 210 showDialog, passwordExpiring, passwordExpired); 211 } 212 213 @Override 214 public void onLoadFinished(final Loader<Account> loader, final Account account) { 215 mHandler.post(new Runnable() { 216 @Override 217 public void run() { 218 final AccountSecurity activity = AccountSecurity.this; 219 if (!activity.isActivityResumed()) { 220 return; 221 } 222 223 if (account == null || (account.mPolicyKey != 0 && account.mPolicy == null)) { 224 activity.finish(); 225 LogUtils.d(TAG, "could not load account or policy in AccountSecurity"); 226 return; 227 } 228 229 if (!activity.mInitialized) { 230 activity.mInitialized = true; 231 232 final AccountAndPolicyLoader apLoader = (AccountAndPolicyLoader) loader; 233 activity.completeCreate(account, apLoader.mShowDialog, 234 apLoader.mPasswordExpiring, apLoader.mPasswordExpired); 235 } 236 } 237 }); 238 } 239 240 @Override 241 public void onLoaderReset(Loader<Account> loader) {} 242 } 243 244 private static class AccountAndPolicyLoader extends MailAsyncTaskLoader<Account> { 245 private final long mAccountId; 246 public final boolean mShowDialog; 247 public final boolean mPasswordExpiring; 248 public final boolean mPasswordExpired; 249 250 private final Context mContext; 251 252 AccountAndPolicyLoader(final Context context, final long accountId, 253 final boolean showDialog, final boolean passwordExpiring, 254 final boolean passwordExpired) { 255 super(context); 256 mContext = context; 257 mAccountId = accountId; 258 mShowDialog = showDialog; 259 mPasswordExpiring = passwordExpiring; 260 mPasswordExpired = passwordExpired; 261 } 262 263 @Override 264 public Account loadInBackground() { 265 final Account account = Account.restoreAccountWithId(mContext, mAccountId); 266 if (account == null) { 267 return null; 268 } 269 270 final long policyId = account.mPolicyKey; 271 if (policyId != 0) { 272 account.mPolicy = Policy.restorePolicyWithId(mContext, policyId); 273 } 274 275 account.getOrCreateHostAuthRecv(mContext); 276 277 return account; 278 } 279 280 @Override 281 protected void onDiscardResult(Account result) {} 282 } 283 284 protected void completeCreate(final Account account, final boolean showDialog, 285 final boolean passwordExpiring, final boolean passwordExpired) { 286 mAccount = account; 287 288 // Special handling for password expiration events 289 if (passwordExpiring || passwordExpired) { 290 FragmentManager fm = getFragmentManager(); 291 if (fm.findFragmentByTag("password_expiration") == null) { 292 PasswordExpirationDialog dialog = 293 PasswordExpirationDialog.newInstance(mAccount.getDisplayName(), 294 passwordExpired); 295 if (MailActivityEmail.DEBUG || DEBUG) { 296 LogUtils.d(TAG, "Showing password expiration dialog"); 297 } 298 dialog.show(fm, "password_expiration"); 299 } 300 return; 301 } 302 // Otherwise, handle normal security settings flow 303 if (mAccount.mPolicyKey != 0) { 304 // This account wants to control security 305 if (showDialog) { 306 // Show dialog first, unless already showing (e.g. after rotation) 307 FragmentManager fm = getFragmentManager(); 308 if (fm.findFragmentByTag("security_needed") == null) { 309 SecurityNeededDialog dialog = 310 SecurityNeededDialog.newInstance(mAccount.getDisplayName()); 311 if (MailActivityEmail.DEBUG || DEBUG) { 312 LogUtils.d(TAG, "Showing security needed dialog"); 313 } 314 dialog.show(fm, "security_needed"); 315 } 316 } else { 317 // Go directly to security settings 318 tryAdvanceSecurity(mAccount); 319 } 320 return; 321 } 322 finish(); 323 } 324 325 /** 326 * After any of the activities return, try to advance to the "next step" 327 */ 328 @Override 329 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 330 tryAdvanceSecurity(mAccount); 331 super.onActivityResult(requestCode, resultCode, data); 332 } 333 334 /** 335 * Walk the user through the required steps to become an active administrator and with 336 * the requisite security settings for the given account. 337 * 338 * These steps will be repeated each time we return from a given attempt (e.g. asking the 339 * user to choose a device pin/password). In a typical activation, we may repeat these 340 * steps a few times. It may go as far as step 5 (password) or step 6 (encryption), but it 341 * will terminate when step 2 (isActive()) succeeds. 342 * 343 * If at any point we do not advance beyond a given user step, (e.g. the user cancels 344 * instead of setting a password) we simply repost the security notification, and exit. 345 * We never want to loop here. 346 */ 347 private void tryAdvanceSecurity(Account account) { 348 SecurityPolicy security = SecurityPolicy.getInstance(this); 349 // Step 1. Check if we are an active device administrator, and stop here to activate 350 if (!security.isActiveAdmin()) { 351 if (mTriedAddAdministrator) { 352 if (MailActivityEmail.DEBUG || DEBUG) { 353 LogUtils.d(TAG, "Not active admin: repost notification"); 354 } 355 repostNotification(account, security); 356 finish(); 357 } else { 358 mTriedAddAdministrator = true; 359 // retrieve name of server for the format string 360 final HostAuth hostAuth = account.mHostAuthRecv; 361 if (hostAuth == null) { 362 if (MailActivityEmail.DEBUG || DEBUG) { 363 LogUtils.d(TAG, "No HostAuth: repost notification"); 364 } 365 repostNotification(account, security); 366 finish(); 367 } else { 368 if (MailActivityEmail.DEBUG || DEBUG) { 369 LogUtils.d(TAG, "Not active admin: post initial notification"); 370 } 371 // try to become active - must happen here in activity, to get result 372 Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN); 373 intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, 374 security.getAdminComponent()); 375 intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION, 376 this.getString(R.string.account_security_policy_explanation_fmt, 377 hostAuth.mAddress)); 378 startActivityForResult(intent, REQUEST_ENABLE); 379 } 380 } 381 return; 382 } 383 384 // Step 2. Check if the current aggregate security policy is being satisfied by the 385 // DevicePolicyManager (the current system security level). 386 if (security.isActive(null)) { 387 if (MailActivityEmail.DEBUG || DEBUG) { 388 LogUtils.d(TAG, "Security active; clear holds"); 389 } 390 Account.clearSecurityHoldOnAllAccounts(this); 391 security.syncAccount(account); 392 security.clearNotification(); 393 finish(); 394 return; 395 } 396 397 // Step 3. Try to assert the current aggregate security requirements with the system. 398 security.setActivePolicies(); 399 400 // Step 4. Recheck the security policy, and determine what changes are needed (if any) 401 // to satisfy the requirements. 402 int inactiveReasons = security.getInactiveReasons(null); 403 404 // Step 5. If password is needed, try to have the user set it 405 if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_PASSWORD) != 0) { 406 if (mTriedSetPassword) { 407 if (MailActivityEmail.DEBUG || DEBUG) { 408 LogUtils.d(TAG, "Password needed; repost notification"); 409 } 410 repostNotification(account, security); 411 finish(); 412 } else { 413 if (MailActivityEmail.DEBUG || DEBUG) { 414 LogUtils.d(TAG, "Password needed; request it via DPM"); 415 } 416 mTriedSetPassword = true; 417 // launch the activity to have the user set a new password. 418 Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); 419 startActivityForResult(intent, REQUEST_PASSWORD); 420 } 421 return; 422 } 423 424 // Step 6. If encryption is needed, try to have the user set it 425 if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_ENCRYPTION) != 0) { 426 if (mTriedSetEncryption) { 427 if (MailActivityEmail.DEBUG || DEBUG) { 428 LogUtils.d(TAG, "Encryption needed; repost notification"); 429 } 430 repostNotification(account, security); 431 finish(); 432 } else { 433 if (MailActivityEmail.DEBUG || DEBUG) { 434 LogUtils.d(TAG, "Encryption needed; request it via DPM"); 435 } 436 mTriedSetEncryption = true; 437 // launch the activity to start up encryption. 438 Intent intent = new Intent(DevicePolicyManager.ACTION_START_ENCRYPTION); 439 startActivityForResult(intent, REQUEST_ENCRYPTION); 440 } 441 return; 442 } 443 444 // Step 7. No problems were found, so clear holds and exit 445 if (MailActivityEmail.DEBUG || DEBUG) { 446 LogUtils.d(TAG, "Policies enforced; clear holds"); 447 } 448 Account.clearSecurityHoldOnAllAccounts(this); 449 security.syncAccount(account); 450 security.clearNotification(); 451 finish(); 452 } 453 454 /** 455 * Mark an account as not-ready-for-sync and post a notification to bring the user back here 456 * eventually. 457 */ 458 private static void repostNotification(final Account account, final SecurityPolicy security) { 459 if (account == null) return; 460 new AsyncTask<Void, Void, Void>() { 461 @Override 462 protected Void doInBackground(Void... params) { 463 security.policiesRequired(account.mId); 464 return null; 465 } 466 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 467 } 468 469 /** 470 * Dialog briefly shown in some cases, to indicate the user that a security update is needed. 471 * If the user clicks OK, we proceed into the "tryAdvanceSecurity" flow. If the user cancels, 472 * we repost the notification and finish() the activity. 473 */ 474 public static class SecurityNeededDialog extends DialogFragment 475 implements DialogInterface.OnClickListener { 476 private static final String BUNDLE_KEY_ACCOUNT_NAME = "account_name"; 477 478 // Public no-args constructor needed for fragment re-instantiation 479 public SecurityNeededDialog() {} 480 481 /** 482 * Create a new dialog. 483 */ 484 public static SecurityNeededDialog newInstance(String accountName) { 485 final SecurityNeededDialog dialog = new SecurityNeededDialog(); 486 Bundle b = new Bundle(); 487 b.putString(BUNDLE_KEY_ACCOUNT_NAME, accountName); 488 dialog.setArguments(b); 489 return dialog; 490 } 491 492 @Override 493 public Dialog onCreateDialog(Bundle savedInstanceState) { 494 final String accountName = getArguments().getString(BUNDLE_KEY_ACCOUNT_NAME); 495 496 final Context context = getActivity(); 497 final Resources res = context.getResources(); 498 final AlertDialog.Builder b = new AlertDialog.Builder(context); 499 b.setTitle(R.string.account_security_dialog_title); 500 b.setIconAttribute(android.R.attr.alertDialogIcon); 501 b.setMessage(res.getString(R.string.account_security_dialog_content_fmt, accountName)); 502 b.setPositiveButton(android.R.string.ok, this); 503 b.setNegativeButton(android.R.string.cancel, this); 504 if (MailActivityEmail.DEBUG || DEBUG) { 505 LogUtils.d(TAG, "Posting security needed dialog"); 506 } 507 return b.create(); 508 } 509 510 @Override 511 public void onClick(DialogInterface dialog, int which) { 512 dismiss(); 513 AccountSecurity activity = (AccountSecurity) getActivity(); 514 if (activity.mAccount == null) { 515 // Clicked before activity fully restored - probably just monkey - exit quickly 516 activity.finish(); 517 return; 518 } 519 switch (which) { 520 case DialogInterface.BUTTON_POSITIVE: 521 if (MailActivityEmail.DEBUG || DEBUG) { 522 LogUtils.d(TAG, "User accepts; advance to next step"); 523 } 524 activity.tryAdvanceSecurity(activity.mAccount); 525 break; 526 case DialogInterface.BUTTON_NEGATIVE: 527 if (MailActivityEmail.DEBUG || DEBUG) { 528 LogUtils.d(TAG, "User declines; repost notification"); 529 } 530 AccountSecurity.repostNotification( 531 activity.mAccount, SecurityPolicy.getInstance(activity)); 532 activity.finish(); 533 break; 534 } 535 } 536 } 537 538 /** 539 * Dialog briefly shown in some cases, to indicate the user that the PIN/Password is expiring 540 * or has expired. If the user clicks OK, we launch the password settings screen. 541 */ 542 public static class PasswordExpirationDialog extends DialogFragment 543 implements DialogInterface.OnClickListener { 544 private static final String BUNDLE_KEY_ACCOUNT_NAME = "account_name"; 545 private static final String BUNDLE_KEY_EXPIRED = "expired"; 546 547 /** 548 * Create a new dialog. 549 */ 550 public static PasswordExpirationDialog newInstance(String accountName, boolean expired) { 551 final PasswordExpirationDialog dialog = new PasswordExpirationDialog(); 552 Bundle b = new Bundle(); 553 b.putString(BUNDLE_KEY_ACCOUNT_NAME, accountName); 554 b.putBoolean(BUNDLE_KEY_EXPIRED, expired); 555 dialog.setArguments(b); 556 return dialog; 557 } 558 559 // Public no-args constructor needed for fragment re-instantiation 560 public PasswordExpirationDialog() {} 561 562 /** 563 * Note, this actually creates two slightly different dialogs (for expiring vs. expired) 564 */ 565 @Override 566 public Dialog onCreateDialog(Bundle savedInstanceState) { 567 final String accountName = getArguments().getString(BUNDLE_KEY_ACCOUNT_NAME); 568 final boolean expired = getArguments().getBoolean(BUNDLE_KEY_EXPIRED); 569 final int titleId = expired 570 ? R.string.password_expired_dialog_title 571 : R.string.password_expire_warning_dialog_title; 572 final int contentId = expired 573 ? R.string.password_expired_dialog_content_fmt 574 : R.string.password_expire_warning_dialog_content_fmt; 575 576 final Context context = getActivity(); 577 final Resources res = context.getResources(); 578 return new AlertDialog.Builder(context) 579 .setTitle(titleId) 580 .setIconAttribute(android.R.attr.alertDialogIcon) 581 .setMessage(res.getString(contentId, accountName)) 582 .setPositiveButton(android.R.string.ok, this) 583 .setNegativeButton(android.R.string.cancel, this) 584 .create(); 585 } 586 587 @Override 588 public void onClick(DialogInterface dialog, int which) { 589 dismiss(); 590 AccountSecurity activity = (AccountSecurity) getActivity(); 591 if (which == DialogInterface.BUTTON_POSITIVE) { 592 Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); 593 activity.startActivity(intent); 594 } 595 activity.finish(); 596 } 597 } 598 } 599