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