AccountSecurity.java revision bc47398187c6ffd132435e51d8d61e6ec79a79db
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        // 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
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     */
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     */
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         */
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
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
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         */
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
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
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