blob: ec336d5287324e8b0f4a35c5a004959ba79bd0ce [file] [log] [blame]
Andrew Stadler3d2b3b32010-02-05 11:10:39 -08001/*
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
Andrew Stadler3d2b3b32010-02-05 11:10:39 -080019import android.app.Activity;
Andy Stadlerf4894132011-02-18 18:23:18 -080020import android.app.AlertDialog;
21import android.app.Dialog;
22import android.app.DialogFragment;
23import android.app.FragmentManager;
Dianne Hackborn6d001622010-02-26 17:26:45 -080024import android.app.admin.DevicePolicyManager;
Andrew Stadler3d2b3b32010-02-05 11:10:39 -080025import android.content.Context;
Andy Stadlerf4894132011-02-18 18:23:18 -080026import android.content.DialogInterface;
Andrew Stadler3d2b3b32010-02-05 11:10:39 -080027import android.content.Intent;
Andy Stadlerf4894132011-02-18 18:23:18 -080028import android.content.res.Resources;
Andrew Stadler3d2b3b32010-02-05 11:10:39 -080029import android.os.Bundle;
Marc Blankaeee10e2011-04-27 17:12:06 -070030import android.util.Log;
Andrew Stadler3d2b3b32010-02-05 11:10:39 -080031
Marc Blanke2d28a02011-07-17 15:46:03 -070032import 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
Andrew Stadler3d2b3b32010-02-05 11:10:39 -080040/**
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
Andy Stadler469f2982011-01-13 13:12:55 -080049 * 6. If necessary, request for user to activate device encryption
Andrew Stadler3d2b3b32010-02-05 11:10:39 -080050 */
51public class AccountSecurity extends Activity {
Marc Blankaeee10e2011-04-27 17:12:06 -070052 private static final String TAG = "Email/AccountSecurity";
Andrew Stadler3d2b3b32010-02-05 11:10:39 -080053
Andy Stadlerf4894132011-02-18 18:23:18 -080054 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";
Andrew Stadler3d2b3b32010-02-05 11:10:39 -080058
59 private static final int REQUEST_ENABLE = 1;
Andy Stadler469f2982011-01-13 13:12:55 -080060 private static final int REQUEST_PASSWORD = 2;
61 private static final int REQUEST_ENCRYPTION = 3;
Andrew Stadler3d2b3b32010-02-05 11:10:39 -080062
Andy Stadler45bfac02011-01-24 11:08:31 -080063 private boolean mTriedAddAdministrator = false;
64 private boolean mTriedSetPassword = false;
65 private boolean mTriedSetEncryption = false;
66 private Account mAccount;
67
Andrew Stadler3d2b3b32010-02-05 11:10:39 -080068 /**
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
Andy Stadlerf4894132011-02-18 18:23:18 -080074 * @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).
Andrew Stadler3d2b3b32010-02-05 11:10:39 -080077 * @return an Intent which can be used to view that account
78 */
Andy Stadlerf4894132011-02-18 18:23:18 -080079 public static Intent actionUpdateSecurityIntent(Context context, long accountId,
80 boolean showDialog) {
Andrew Stadler3d2b3b32010-02-05 11:10:39 -080081 Intent intent = new Intent(context, AccountSecurity.class);
82 intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
Andy Stadlerf4894132011-02-18 18:23:18 -080083 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);
Andrew Stadler3d2b3b32010-02-05 11:10:39 -080097 return intent;
98 }
99
100 @Override
101 public void onCreate(Bundle savedInstanceState) {
102 super.onCreate(savedInstanceState);
Andrew Stadlercd095452010-11-01 16:15:15 -0700103 ActivityHelper.debugSetWindowFlags(this);
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800104
105 Intent i = getIntent();
Andy Stadler45bfac02011-01-24 11:08:31 -0800106 final long accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1);
Andy Stadlerf4894132011-02-18 18:23:18 -0800107 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);
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800110 SecurityPolicy security = SecurityPolicy.getInstance(this);
Marc Blankc6df1d62011-07-19 14:09:11 -0700111 security.clearNotification();
Andy Stadler45bfac02011-01-24 11:08:31 -0800112 if (accountId == -1) {
113 finish();
114 return;
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800115 }
Andy Stadler45bfac02011-01-24 11:08:31 -0800116
Marc Blankc6df1d62011-07-19 14:09:11 -0700117 mAccount = Account.restoreAccountWithId(AccountSecurity.this, accountId);
118 if (mAccount == null) {
119 finish();
120 return;
121 }
Marc Blank2736c1a2011-10-20 10:13:02 -0700122
Marc Blankc6df1d62011-07-19 14:09:11 -0700123 // 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");
Andy Stadler45bfac02011-01-24 11:08:31 -0800131 }
Marc Blankc6df1d62011-07-19 14:09:11 -0700132 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");
Andy Stadlerf4894132011-02-18 18:23:18 -0800144 }
Marc Blankc6df1d62011-07-19 14:09:11 -0700145 } else {
146 // Go directly to security settings
147 tryAdvanceSecurity(mAccount);
Andy Stadler45bfac02011-01-24 11:08:31 -0800148 }
Marc Blankc6df1d62011-07-19 14:09:11 -0700149 return;
150 }
151 finish();
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800152 }
153
154 /**
Andy Stadler45bfac02011-01-24 11:08:31 -0800155 * After any of the activities return, try to advance to the "next step"
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800156 */
157 @Override
158 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
Andy Stadler45bfac02011-01-24 11:08:31 -0800159 tryAdvanceSecurity(mAccount);
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800160 super.onActivityResult(requestCode, resultCode, data);
161 }
162
163 /**
Andy Stadler45bfac02011-01-24 11:08:31 -0800164 * 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.
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800175 */
Andy Stadler45bfac02011-01-24 11:08:31 -0800176 private void tryAdvanceSecurity(Account account) {
177 SecurityPolicy security = SecurityPolicy.getInstance(this);
Andy Stadler45bfac02011-01-24 11:08:31 -0800178 // Step 1. Check if we are an active device administrator, and stop here to activate
179 if (!security.isActiveAdmin()) {
180 if (mTriedAddAdministrator) {
Marc Blankaeee10e2011-04-27 17:12:06 -0700181 if (Email.DEBUG) {
182 Log.d(TAG, "Not active admin: repost notification");
183 }
Andy Stadler45bfac02011-01-24 11:08:31 -0800184 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) {
Marc Blankaeee10e2011-04-27 17:12:06 -0700191 if (Email.DEBUG) {
192 Log.d(TAG, "No HostAuth: repost notification");
193 }
Andy Stadler45bfac02011-01-24 11:08:31 -0800194 repostNotification(account, security);
195 finish();
196 } else {
Marc Blankaeee10e2011-04-27 17:12:06 -0700197 if (Email.DEBUG) {
198 Log.d(TAG, "Not active admin: post initial notification");
199 }
Andy Stadler45bfac02011-01-24 11:08:31 -0800200 // 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;
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800211 }
Andy Stadler45bfac02011-01-24 11:08:31 -0800212
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)) {
Marc Blankaeee10e2011-04-27 17:12:06 -0700216 if (Email.DEBUG) {
217 Log.d(TAG, "Security active; clear holds");
218 }
Makoto Onukibcf32322010-07-27 12:52:46 -0700219 Account.clearSecurityHoldOnAllAccounts(this);
Marc Blankc6df1d62011-07-19 14:09:11 -0700220 security.clearNotification();
Andy Stadler45bfac02011-01-24 11:08:31 -0800221 finish();
222 return;
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800223 }
Andy Stadler45bfac02011-01-24 11:08:31 -0800224
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
Andy Stadler469f2982011-01-13 13:12:55 -0800233 if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_PASSWORD) != 0) {
Andy Stadler45bfac02011-01-24 11:08:31 -0800234 if (mTriedSetPassword) {
Marc Blankaeee10e2011-04-27 17:12:06 -0700235 if (Email.DEBUG) {
236 Log.d(TAG, "Password needed; repost notification");
237 }
Andy Stadler45bfac02011-01-24 11:08:31 -0800238 repostNotification(account, security);
239 finish();
240 } else {
Marc Blankaeee10e2011-04-27 17:12:06 -0700241 if (Email.DEBUG) {
242 Log.d(TAG, "Password needed; request it via DPM");
243 }
Andy Stadler45bfac02011-01-24 11:08:31 -0800244 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;
Andy Stadler469f2982011-01-13 13:12:55 -0800250 }
Andy Stadler45bfac02011-01-24 11:08:31 -0800251
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) {
Marc Blankaeee10e2011-04-27 17:12:06 -0700255 if (Email.DEBUG) {
256 Log.d(TAG, "Encryption needed; repost notification");
257 }
Andy Stadler45bfac02011-01-24 11:08:31 -0800258 repostNotification(account, security);
259 finish();
260 } else {
Marc Blankaeee10e2011-04-27 17:12:06 -0700261 if (Email.DEBUG) {
262 Log.d(TAG, "Encryption needed; request it via DPM");
263 }
Ben Komalod09cff02011-05-06 14:57:47 -0700264 mTriedSetEncryption = true;
Andy Stadler45bfac02011-01-24 11:08:31 -0800265 // 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
Marc Blankaeee10e2011-04-27 17:12:06 -0700273 if (Email.DEBUG) {
274 Log.d(TAG, "Policies enforced; clear holds");
275 }
Andy Stadler45bfac02011-01-24 11:08:31 -0800276 Account.clearSecurityHoldOnAllAccounts(this);
Marc Blankc6df1d62011-07-19 14:09:11 -0700277 security.clearNotification();
Andy Stadler45bfac02011-01-24 11:08:31 -0800278 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) {
Marc Blankc6df1d62011-07-19 14:09:11 -0700286 if (account == null) return;
Andy Stadler45bfac02011-01-24 11:08:31 -0800287 Utility.runAsync(new Runnable() {
288 @Override
289 public void run() {
290 security.policiesRequired(account.mId);
291 }
292 });
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800293 }
Andy Stadlerf4894132011-02-18 18:23:18 -0800294
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);
Marc Blankaeee10e2011-04-27 17:12:06 -0700327 if (Email.DEBUG) {
328 Log.d(TAG, "Posting security needed dialog");
329 }
Andy Stadlerf4894132011-02-18 18:23:18 -0800330 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:
Marc Blankaeee10e2011-04-27 17:12:06 -0700344 if (Email.DEBUG) {
345 Log.d(TAG, "User accepts; advance to next step");
346 }
Andy Stadlerf4894132011-02-18 18:23:18 -0800347 activity.tryAdvanceSecurity(activity.mAccount);
348 break;
349 case DialogInterface.BUTTON_NEGATIVE:
Marc Blankaeee10e2011-04-27 17:12:06 -0700350 if (Email.DEBUG) {
351 Log.d(TAG, "User declines; repost notification");
352 }
Andy Stadlerf4894132011-02-18 18:23:18 -0800353 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 }
Andrew Stadler3d2b3b32010-02-05 11:10:39 -0800418}