AccountSecurity.java revision aeee10e57ef4d931e7708fde218d590453a82aea
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 17package com.android.email.activity.setup; 18 19import com.android.email.Email; 20import com.android.email.R; 21import com.android.email.SecurityPolicy; 22import com.android.email.activity.ActivityHelper; 23import com.android.emailcommon.provider.EmailContent.Account; 24import com.android.emailcommon.provider.EmailContent.HostAuth; 25import com.android.emailcommon.utility.Utility; 26 27import android.app.Activity; 28import android.app.AlertDialog; 29import android.app.Dialog; 30import android.app.DialogFragment; 31import android.app.FragmentManager; 32import android.app.admin.DevicePolicyManager; 33import android.content.Context; 34import android.content.DialogInterface; 35import android.content.Intent; 36import android.content.res.Resources; 37import android.os.AsyncTask; 38import android.os.Bundle; 39import android.util.Log; 40 41/** 42 * Psuedo-activity (no UI) to bootstrap the user up to a higher desired security level. This 43 * bootstrap requires the following steps. 44 * 45 * 1. Confirm the account of interest has any security policies defined - exit early if not 46 * 2. If not actively administrating the device, ask Device Policy Manager to start that 47 * 3. When we are actively administrating, check current policies and see if they're sufficient 48 * 4. If not, set policies 49 * 5. If necessary, request for user to update device password 50 * 6. If necessary, request for user to activate device encryption 51 */ 52public class AccountSecurity extends Activity { 53 private static final String TAG = "Email/AccountSecurity"; 54 55 private static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID"; 56 private static final String EXTRA_SHOW_DIALOG = "SHOW_DIALOG"; 57 private static final String EXTRA_PASSWORD_EXPIRING = "EXPIRING"; 58 private static final String EXTRA_PASSWORD_EXPIRED = "EXPIRED"; 59 60 private static final int REQUEST_ENABLE = 1; 61 private static final int REQUEST_PASSWORD = 2; 62 private static final int REQUEST_ENCRYPTION = 3; 63 64 private boolean mTriedAddAdministrator = false; 65 private boolean mTriedSetPassword = false; 66 private boolean mTriedSetEncryption = false; 67 private Account mAccount; 68 69 /** 70 * Used for generating intent for this activity (which is intended to be launched 71 * from a notification.) 72 * 73 * @param context Calling context for building the intent 74 * @param accountId The account of interest 75 * @param showDialog If true, a simple warning dialog will be shown before kicking off 76 * the necessary system settings. Should be true anywhere the context of the security settings 77 * is not clear (e.g. any time after the account has been set up). 78 * @return an Intent which can be used to view that account 79 */ 80 public static Intent actionUpdateSecurityIntent(Context context, long accountId, 81 boolean showDialog) { 82 Intent intent = new Intent(context, AccountSecurity.class); 83 intent.putExtra(EXTRA_ACCOUNT_ID, accountId); 84 intent.putExtra(EXTRA_SHOW_DIALOG, showDialog); 85 return intent; 86 } 87 88 /** 89 * Used for generating intent for this activity (which is intended to be launched 90 * from a notification.) This is a special mode of this activity which exists only 91 * to give the user a dialog (for context) about a device pin/password expiration event. 92 */ 93 public static Intent actionDevicePasswordExpirationIntent(Context context, long accountId, 94 boolean expired) { 95 Intent intent = new Intent(context, AccountSecurity.class); 96 intent.putExtra(EXTRA_ACCOUNT_ID, accountId); 97 intent.putExtra(expired ? EXTRA_PASSWORD_EXPIRED : EXTRA_PASSWORD_EXPIRING, true); 98 return intent; 99 } 100 101 @Override 102 public void onCreate(Bundle savedInstanceState) { 103 super.onCreate(savedInstanceState); 104 ActivityHelper.debugSetWindowFlags(this); 105 106 Intent i = getIntent(); 107 final long accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1); 108 final boolean showDialog = i.getBooleanExtra(EXTRA_SHOW_DIALOG, false); 109 final boolean passwordExpiring = i.getBooleanExtra(EXTRA_PASSWORD_EXPIRING, false); 110 final boolean passwordExpired = i.getBooleanExtra(EXTRA_PASSWORD_EXPIRED, false); 111 SecurityPolicy security = SecurityPolicy.getInstance(this); 112 security.clearNotification(accountId); 113 if (accountId == -1) { 114 finish(); 115 return; 116 } 117 118 // Let onCreate exit, while background thread retrieves account. 119 // Then start the security check/bootstrap process. 120 new AsyncTask<Void, Void, Account>() { 121 @Override 122 protected Account doInBackground(Void... params) { 123 return Account.restoreAccountWithId(AccountSecurity.this, accountId); 124 } 125 126 @Override 127 protected void onPostExecute(Account result) { 128 mAccount = result; 129 if (mAccount == null) { 130 finish(); 131 return; 132 } 133 // Special handling for password expiration events 134 if (passwordExpiring || passwordExpired) { 135 FragmentManager fm = getFragmentManager(); 136 if (fm.findFragmentByTag("password_expiration") == null) { 137 PasswordExpirationDialog dialog = 138 PasswordExpirationDialog.newInstance(mAccount.getDisplayName(), 139 passwordExpired); 140 dialog.show(fm, "password_expiration"); 141 } 142 return; 143 } 144 // Otherwise, handle normal security settings flow 145 if (mAccount.mPolicyKey != 0) { 146 // This account wants to control security 147 if (showDialog) { 148 // Show dialog first, unless already showing (e.g. after rotation) 149 FragmentManager fm = getFragmentManager(); 150 if (fm.findFragmentByTag("security_needed") == null) { 151 SecurityNeededDialog dialog = 152 SecurityNeededDialog.newInstance(mAccount.getDisplayName()); 153 dialog.show(fm, "security_needed"); 154 } 155 } else { 156 // Go directly to security settings 157 tryAdvanceSecurity(mAccount); 158 } 159 return; 160 } 161 finish(); 162 } 163 }.execute(); 164 } 165 166 /** 167 * After any of the activities return, try to advance to the "next step" 168 */ 169 @Override 170 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 171 tryAdvanceSecurity(mAccount); 172 super.onActivityResult(requestCode, resultCode, data); 173 } 174 175 /** 176 * Walk the user through the required steps to become an active administrator and with 177 * the requisite security settings for the given account. 178 * 179 * These steps will be repeated each time we return from a given attempt (e.g. asking the 180 * user to choose a device pin/password). In a typical activation, we may repeat these 181 * steps a few times. It may go as far as step 5 (password) or step 6 (encryption), but it 182 * will terminate when step 2 (isActive()) succeeds. 183 * 184 * If at any point we do not advance beyond a given user step, (e.g. the user cancels 185 * instead of setting a password) we simply repost the security notification, and exit. 186 * We never want to loop here. 187 */ 188 private void tryAdvanceSecurity(Account account) { 189 SecurityPolicy security = SecurityPolicy.getInstance(this); 190 // Step 1. Check if we are an active device administrator, and stop here to activate 191 if (!security.isActiveAdmin()) { 192 if (mTriedAddAdministrator) { 193 if (Email.DEBUG) { 194 Log.d(TAG, "Not active admin: repost notification"); 195 } 196 repostNotification(account, security); 197 finish(); 198 } else { 199 mTriedAddAdministrator = true; 200 // retrieve name of server for the format string 201 HostAuth hostAuth = HostAuth.restoreHostAuthWithId(this, account.mHostAuthKeyRecv); 202 if (hostAuth == null) { 203 if (Email.DEBUG) { 204 Log.d(TAG, "No HostAuth: repost notification"); 205 } 206 repostNotification(account, security); 207 finish(); 208 } else { 209 if (Email.DEBUG) { 210 Log.d(TAG, "Not active admin: post initial notification"); 211 } 212 // try to become active - must happen here in activity, to get result 213 Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN); 214 intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, 215 security.getAdminComponent()); 216 intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION, 217 this.getString(R.string.account_security_policy_explanation_fmt, 218 hostAuth.mAddress)); 219 startActivityForResult(intent, REQUEST_ENABLE); 220 } 221 } 222 return; 223 } 224 225 // Step 2. Check if the current aggregate security policy is being satisfied by the 226 // DevicePolicyManager (the current system security level). 227 if (security.isActive(null)) { 228 if (Email.DEBUG) { 229 Log.d(TAG, "Security active; clear holds"); 230 } 231 Account.clearSecurityHoldOnAllAccounts(this); 232 finish(); 233 return; 234 } 235 236 // Step 3. Try to assert the current aggregate security requirements with the system. 237 security.setActivePolicies(); 238 239 // Step 4. Recheck the security policy, and determine what changes are needed (if any) 240 // to satisfy the requirements. 241 int inactiveReasons = security.getInactiveReasons(null); 242 243 // Step 5. If password is needed, try to have the user set it 244 if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_PASSWORD) != 0) { 245 if (mTriedSetPassword) { 246 if (Email.DEBUG) { 247 Log.d(TAG, "Password needed; repost notification"); 248 } 249 repostNotification(account, security); 250 finish(); 251 } else { 252 if (Email.DEBUG) { 253 Log.d(TAG, "Password needed; request it via DPM"); 254 } 255 mTriedSetPassword = true; 256 // launch the activity to have the user set a new password. 257 Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); 258 startActivityForResult(intent, REQUEST_PASSWORD); 259 } 260 return; 261 } 262 263 // Step 6. If encryption is needed, try to have the user set it 264 if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_ENCRYPTION) != 0) { 265 if (mTriedSetEncryption) { 266 if (Email.DEBUG) { 267 Log.d(TAG, "Encryption needed; repost notification"); 268 } 269 repostNotification(account, security); 270 finish(); 271 } else { 272 if (Email.DEBUG) { 273 Log.d(TAG, "Encryption needed; request it via DPM"); 274 } 275 mTriedSetEncryption = true; 276 // launch the activity to start up encryption. 277 Intent intent = new Intent(DevicePolicyManager.ACTION_START_ENCRYPTION); 278 startActivityForResult(intent, REQUEST_ENCRYPTION); 279 } 280 return; 281 } 282 283 // Step 7. No problems were found, so clear holds and exit 284 if (Email.DEBUG) { 285 Log.d(TAG, "Policies enforced; clear holds"); 286 } 287 Account.clearSecurityHoldOnAllAccounts(this); 288 finish(); 289 } 290 291 /** 292 * Mark an account as not-ready-for-sync and post a notification to bring the user back here 293 * eventually. 294 */ 295 private void repostNotification(final Account account, final SecurityPolicy security) { 296 Utility.runAsync(new Runnable() { 297 @Override 298 public void run() { 299 security.policiesRequired(account.mId); 300 } 301 }); 302 } 303 304 /** 305 * Dialog briefly shown in some cases, to indicate the user that a security update is needed. 306 * If the user clicks OK, we proceed into the "tryAdvanceSecurity" flow. If the user cancels, 307 * we repost the notification and finish() the activity. 308 */ 309 public static class SecurityNeededDialog extends DialogFragment 310 implements DialogInterface.OnClickListener { 311 private static final String BUNDLE_KEY_ACCOUNT_NAME = "account_name"; 312 313 /** 314 * Create a new dialog. 315 */ 316 public static SecurityNeededDialog newInstance(String accountName) { 317 final SecurityNeededDialog dialog = new SecurityNeededDialog(); 318 Bundle b = new Bundle(); 319 b.putString(BUNDLE_KEY_ACCOUNT_NAME, accountName); 320 dialog.setArguments(b); 321 return dialog; 322 } 323 324 @Override 325 public Dialog onCreateDialog(Bundle savedInstanceState) { 326 final String accountName = getArguments().getString(BUNDLE_KEY_ACCOUNT_NAME); 327 328 final Context context = getActivity(); 329 final Resources res = context.getResources(); 330 final AlertDialog.Builder b = new AlertDialog.Builder(context); 331 b.setTitle(R.string.account_security_dialog_title); 332 b.setIconAttribute(android.R.attr.alertDialogIcon); 333 b.setMessage(res.getString(R.string.account_security_dialog_content_fmt, accountName)); 334 b.setPositiveButton(R.string.okay_action, this); 335 b.setNegativeButton(R.string.cancel_action, this); 336 if (Email.DEBUG) { 337 Log.d(TAG, "Posting security needed dialog"); 338 } 339 return b.create(); 340 } 341 342 @Override 343 public void onClick(DialogInterface dialog, int which) { 344 dismiss(); 345 AccountSecurity activity = (AccountSecurity) getActivity(); 346 if (activity.mAccount == null) { 347 // Clicked before activity fully restored - probably just monkey - exit quickly 348 activity.finish(); 349 return; 350 } 351 switch (which) { 352 case DialogInterface.BUTTON_POSITIVE: 353 if (Email.DEBUG) { 354 Log.d(TAG, "User accepts; advance to next step"); 355 } 356 activity.tryAdvanceSecurity(activity.mAccount); 357 break; 358 case DialogInterface.BUTTON_NEGATIVE: 359 if (Email.DEBUG) { 360 Log.d(TAG, "User declines; repost notification"); 361 } 362 activity.repostNotification( 363 activity.mAccount, SecurityPolicy.getInstance(activity)); 364 activity.finish(); 365 break; 366 } 367 } 368 } 369 370 /** 371 * Dialog briefly shown in some cases, to indicate the user that the PIN/Password is expiring 372 * or has expired. If the user clicks OK, we launch the password settings screen. 373 */ 374 public static class PasswordExpirationDialog extends DialogFragment 375 implements DialogInterface.OnClickListener { 376 private static final String BUNDLE_KEY_ACCOUNT_NAME = "account_name"; 377 private static final String BUNDLE_KEY_EXPIRED = "expired"; 378 379 /** 380 * Create a new dialog. 381 */ 382 public static PasswordExpirationDialog newInstance(String accountName, boolean expired) { 383 final PasswordExpirationDialog dialog = new PasswordExpirationDialog(); 384 Bundle b = new Bundle(); 385 b.putString(BUNDLE_KEY_ACCOUNT_NAME, accountName); 386 b.putBoolean(BUNDLE_KEY_EXPIRED, expired); 387 dialog.setArguments(b); 388 return dialog; 389 } 390 391 /** 392 * Note, this actually creates two slightly different dialogs (for expiring vs. expired) 393 */ 394 @Override 395 public Dialog onCreateDialog(Bundle savedInstanceState) { 396 final String accountName = getArguments().getString(BUNDLE_KEY_ACCOUNT_NAME); 397 final boolean expired = getArguments().getBoolean(BUNDLE_KEY_EXPIRED); 398 final int titleId = expired 399 ? R.string.password_expired_dialog_title 400 : R.string.password_expire_warning_dialog_title; 401 final int contentId = expired 402 ? R.string.password_expired_dialog_content_fmt 403 : R.string.password_expire_warning_dialog_content_fmt; 404 405 final Context context = getActivity(); 406 final Resources res = context.getResources(); 407 final AlertDialog.Builder b = new AlertDialog.Builder(context); 408 b.setTitle(titleId); 409 b.setIconAttribute(android.R.attr.alertDialogIcon); 410 b.setMessage(res.getString(contentId, accountName)); 411 b.setPositiveButton(R.string.okay_action, this); 412 b.setNegativeButton(R.string.cancel_action, this); 413 return b.create(); 414 } 415 416 @Override 417 public void onClick(DialogInterface dialog, int which) { 418 dismiss(); 419 AccountSecurity activity = (AccountSecurity) getActivity(); 420 if (which == DialogInterface.BUTTON_POSITIVE) { 421 Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); 422 activity.startActivity(intent); 423 } 424 activity.finish(); 425 } 426 } 427} 428