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