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