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