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