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.LoaderManager;
25import android.app.admin.DevicePolicyManager;
26import android.content.Context;
27import android.content.DialogInterface;
28import android.content.Intent;
29import android.content.Loader;
30import android.content.res.Resources;
31import android.os.AsyncTask;
32import android.os.Bundle;
33import android.os.Handler;
34
35import com.android.email.R;
36import com.android.email.SecurityPolicy;
37import com.android.email2.ui.MailActivityEmail;
38import com.android.emailcommon.provider.Account;
39import com.android.emailcommon.provider.HostAuth;
40import com.android.emailcommon.provider.Policy;
41import com.android.mail.ui.MailAsyncTaskLoader;
42import com.android.mail.utils.LogUtils;
43
44/**
45 * Psuedo-activity (no UI) to bootstrap the user up to a higher desired security level.  This
46 * bootstrap requires the following steps.
47 *
48 * 1.  Confirm the account of interest has any security policies defined - exit early if not
49 * 2.  If not actively administrating the device, ask Device Policy Manager to start that
50 * 3.  When we are actively administrating, check current policies and see if they're sufficient
51 * 4.  If not, set policies
52 * 5.  If necessary, request for user to update device password
53 * 6.  If necessary, request for user to activate device encryption
54 */
55public class AccountSecurity extends Activity {
56    private static final String TAG = "Email/AccountSecurity";
57
58    private static final boolean DEBUG = false;  // Don't ship with this set to true
59
60    private static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID";
61    private static final String EXTRA_SHOW_DIALOG = "SHOW_DIALOG";
62    private static final String EXTRA_PASSWORD_EXPIRING = "EXPIRING";
63    private static final String EXTRA_PASSWORD_EXPIRED = "EXPIRED";
64
65    private static final String SAVESTATE_INITIALIZED_TAG = "initialized";
66    private static final String SAVESTATE_TRIED_ADD_ADMINISTRATOR_TAG = "triedAddAdministrator";
67    private static final String SAVESTATE_TRIED_SET_PASSWORD_TAG = "triedSetpassword";
68    private static final String SAVESTATE_TRIED_SET_ENCRYPTION_TAG = "triedSetEncryption";
69    private static final String SAVESTATE_ACCOUNT_TAG = "account";
70
71    private static final int REQUEST_ENABLE = 1;
72    private static final int REQUEST_PASSWORD = 2;
73    private static final int REQUEST_ENCRYPTION = 3;
74
75    private boolean mTriedAddAdministrator;
76    private boolean mTriedSetPassword;
77    private boolean mTriedSetEncryption;
78
79    private Account mAccount;
80
81    protected boolean mInitialized;
82
83    private Handler mHandler;
84    private boolean mActivityResumed;
85
86    private static final int ACCOUNT_POLICY_LOADER_ID = 0;
87    private AccountAndPolicyLoaderCallbacks mAPLoaderCallbacks;
88    private Bundle mAPLoaderArgs;
89
90    /**
91     * Used for generating intent for this activity (which is intended to be launched
92     * from a notification.)
93     *
94     * @param context Calling context for building the intent
95     * @param accountId The account of interest
96     * @param showDialog If true, a simple warning dialog will be shown before kicking off
97     * the necessary system settings.  Should be true anywhere the context of the security settings
98     * is not clear (e.g. any time after the account has been set up).
99     * @return an Intent which can be used to view that account
100     */
101    public static Intent actionUpdateSecurityIntent(Context context, long accountId,
102            boolean showDialog) {
103        Intent intent = new Intent(context, AccountSecurity.class);
104        intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
105        intent.putExtra(EXTRA_SHOW_DIALOG, showDialog);
106        return intent;
107    }
108
109    /**
110     * Used for generating intent for this activity (which is intended to be launched
111     * from a notification.)  This is a special mode of this activity which exists only
112     * to give the user a dialog (for context) about a device pin/password expiration event.
113     */
114    public static Intent actionDevicePasswordExpirationIntent(Context context, long accountId,
115            boolean expired) {
116        Intent intent = new ForwardingIntent(context, AccountSecurity.class);
117        intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
118        intent.putExtra(expired ? EXTRA_PASSWORD_EXPIRED : EXTRA_PASSWORD_EXPIRING, true);
119        return intent;
120    }
121
122    @Override
123    public void onCreate(Bundle savedInstanceState) {
124        super.onCreate(savedInstanceState);
125
126        mHandler = new Handler();
127
128        final Intent i = getIntent();
129        final long accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1);
130        final SecurityPolicy security = SecurityPolicy.getInstance(this);
131        security.clearNotification();
132        if (accountId == -1) {
133            finish();
134            return;
135        }
136
137        if (savedInstanceState != null) {
138            mInitialized = savedInstanceState.getBoolean(SAVESTATE_INITIALIZED_TAG, false);
139
140            mTriedAddAdministrator =
141                    savedInstanceState.getBoolean(SAVESTATE_TRIED_ADD_ADMINISTRATOR_TAG, false);
142            mTriedSetPassword =
143                    savedInstanceState.getBoolean(SAVESTATE_TRIED_SET_PASSWORD_TAG, false);
144            mTriedSetEncryption =
145                    savedInstanceState.getBoolean(SAVESTATE_TRIED_SET_ENCRYPTION_TAG, false);
146
147            mAccount = savedInstanceState.getParcelable(SAVESTATE_ACCOUNT_TAG);
148        }
149
150        if (!mInitialized) {
151            startAccountAndPolicyLoader(i.getExtras());
152        }
153    }
154
155    @Override
156    protected void onSaveInstanceState(final Bundle outState) {
157        super.onSaveInstanceState(outState);
158        outState.putBoolean(SAVESTATE_INITIALIZED_TAG, mInitialized);
159
160        outState.putBoolean(SAVESTATE_TRIED_ADD_ADMINISTRATOR_TAG, mTriedAddAdministrator);
161        outState.putBoolean(SAVESTATE_TRIED_SET_PASSWORD_TAG, mTriedSetPassword);
162        outState.putBoolean(SAVESTATE_TRIED_SET_ENCRYPTION_TAG, mTriedSetEncryption);
163
164        outState.putParcelable(SAVESTATE_ACCOUNT_TAG, mAccount);
165    }
166
167    @Override
168    protected void onPause() {
169        super.onPause();
170        mActivityResumed = false;
171    }
172
173    @Override
174    protected void onResume() {
175        super.onResume();
176        mActivityResumed = true;
177        tickleAccountAndPolicyLoader();
178    }
179
180    protected boolean isActivityResumed() {
181        return mActivityResumed;
182    }
183
184    private void tickleAccountAndPolicyLoader() {
185        // If we're already initialized we don't need to tickle.
186        if (!mInitialized) {
187            getLoaderManager().initLoader(ACCOUNT_POLICY_LOADER_ID, mAPLoaderArgs,
188                    mAPLoaderCallbacks);
189        }
190    }
191
192    private void startAccountAndPolicyLoader(final Bundle args) {
193        mAPLoaderArgs = args;
194        mAPLoaderCallbacks = new AccountAndPolicyLoaderCallbacks();
195        tickleAccountAndPolicyLoader();
196    }
197
198    private class AccountAndPolicyLoaderCallbacks
199            implements LoaderManager.LoaderCallbacks<Account> {
200        @Override
201        public Loader<Account> onCreateLoader(final int id, final Bundle args) {
202            final long accountId = args.getLong(EXTRA_ACCOUNT_ID, -1);
203            final boolean showDialog = args.getBoolean(EXTRA_SHOW_DIALOG, false);
204            final boolean passwordExpiring =
205                    args.getBoolean(EXTRA_PASSWORD_EXPIRING, false);
206            final boolean passwordExpired =
207                    args.getBoolean(EXTRA_PASSWORD_EXPIRED, false);
208
209            return new AccountAndPolicyLoader(getApplicationContext(), accountId,
210                    showDialog, passwordExpiring, passwordExpired);
211        }
212
213        @Override
214        public void onLoadFinished(final Loader<Account> loader, final Account account) {
215            mHandler.post(new Runnable() {
216                @Override
217                public void run() {
218                    final AccountSecurity activity = AccountSecurity.this;
219                    if (!activity.isActivityResumed()) {
220                        return;
221                    }
222
223                    if (account == null || (account.mPolicyKey != 0 && account.mPolicy == null)) {
224                        activity.finish();
225                        LogUtils.d(TAG, "could not load account or policy in AccountSecurity");
226                        return;
227                    }
228
229                    if (!activity.mInitialized) {
230                        activity.mInitialized = true;
231
232                        final AccountAndPolicyLoader apLoader = (AccountAndPolicyLoader) loader;
233                        activity.completeCreate(account, apLoader.mShowDialog,
234                                apLoader.mPasswordExpiring, apLoader.mPasswordExpired);
235                    }
236                }
237            });
238        }
239
240        @Override
241        public void onLoaderReset(Loader<Account> loader) {}
242    }
243
244    private static class AccountAndPolicyLoader extends MailAsyncTaskLoader<Account> {
245        private final long mAccountId;
246        public final boolean mShowDialog;
247        public final boolean mPasswordExpiring;
248        public final boolean mPasswordExpired;
249
250        private final Context mContext;
251
252        AccountAndPolicyLoader(final Context context, final long accountId,
253                final boolean showDialog, final boolean passwordExpiring,
254                final boolean passwordExpired) {
255            super(context);
256            mContext = context;
257            mAccountId = accountId;
258            mShowDialog = showDialog;
259            mPasswordExpiring = passwordExpiring;
260            mPasswordExpired = passwordExpired;
261        }
262
263        @Override
264        public Account loadInBackground() {
265            final Account account = Account.restoreAccountWithId(mContext, mAccountId);
266            if (account == null) {
267                return null;
268            }
269
270            final long policyId = account.mPolicyKey;
271            if (policyId != 0) {
272                account.mPolicy = Policy.restorePolicyWithId(mContext, policyId);
273            }
274
275            account.getOrCreateHostAuthRecv(mContext);
276
277            return account;
278        }
279
280        @Override
281        protected void onDiscardResult(Account result) {}
282    }
283
284    protected void completeCreate(final Account account, final boolean showDialog,
285            final boolean passwordExpiring, final boolean passwordExpired) {
286        mAccount = account;
287
288        // Special handling for password expiration events
289        if (passwordExpiring || passwordExpired) {
290            FragmentManager fm = getFragmentManager();
291            if (fm.findFragmentByTag("password_expiration") == null) {
292                PasswordExpirationDialog dialog =
293                    PasswordExpirationDialog.newInstance(mAccount.getDisplayName(),
294                            passwordExpired);
295                if (MailActivityEmail.DEBUG || DEBUG) {
296                    LogUtils.d(TAG, "Showing password expiration dialog");
297                }
298                dialog.show(fm, "password_expiration");
299            }
300            return;
301        }
302        // Otherwise, handle normal security settings flow
303        if (mAccount.mPolicyKey != 0) {
304            // This account wants to control security
305            if (showDialog) {
306                // Show dialog first, unless already showing (e.g. after rotation)
307                FragmentManager fm = getFragmentManager();
308                if (fm.findFragmentByTag("security_needed") == null) {
309                    SecurityNeededDialog dialog =
310                        SecurityNeededDialog.newInstance(mAccount.getDisplayName());
311                    if (MailActivityEmail.DEBUG || DEBUG) {
312                        LogUtils.d(TAG, "Showing security needed dialog");
313                    }
314                    dialog.show(fm, "security_needed");
315                }
316            } else {
317                // Go directly to security settings
318                tryAdvanceSecurity(mAccount);
319            }
320            return;
321        }
322        finish();
323    }
324
325    /**
326     * After any of the activities return, try to advance to the "next step"
327     */
328    @Override
329    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
330        tryAdvanceSecurity(mAccount);
331        super.onActivityResult(requestCode, resultCode, data);
332    }
333
334    /**
335     * Walk the user through the required steps to become an active administrator and with
336     * the requisite security settings for the given account.
337     *
338     * These steps will be repeated each time we return from a given attempt (e.g. asking the
339     * user to choose a device pin/password).  In a typical activation, we may repeat these
340     * steps a few times.  It may go as far as step 5 (password) or step 6 (encryption), but it
341     * will terminate when step 2 (isActive()) succeeds.
342     *
343     * If at any point we do not advance beyond a given user step, (e.g. the user cancels
344     * instead of setting a password) we simply repost the security notification, and exit.
345     * We never want to loop here.
346     */
347    private void tryAdvanceSecurity(Account account) {
348        SecurityPolicy security = SecurityPolicy.getInstance(this);
349        // Step 1.  Check if we are an active device administrator, and stop here to activate
350        if (!security.isActiveAdmin()) {
351            if (mTriedAddAdministrator) {
352                if (MailActivityEmail.DEBUG || DEBUG) {
353                    LogUtils.d(TAG, "Not active admin: repost notification");
354                }
355                repostNotification(account, security);
356                finish();
357            } else {
358                mTriedAddAdministrator = true;
359                // retrieve name of server for the format string
360                final HostAuth hostAuth = account.mHostAuthRecv;
361                if (hostAuth == null) {
362                    if (MailActivityEmail.DEBUG || DEBUG) {
363                        LogUtils.d(TAG, "No HostAuth: repost notification");
364                    }
365                    repostNotification(account, security);
366                    finish();
367                } else {
368                    if (MailActivityEmail.DEBUG || DEBUG) {
369                        LogUtils.d(TAG, "Not active admin: post initial notification");
370                    }
371                    // try to become active - must happen here in activity, to get result
372                    Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
373                    intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN,
374                            security.getAdminComponent());
375                    intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION,
376                            this.getString(R.string.account_security_policy_explanation_fmt,
377                                    hostAuth.mAddress));
378                    startActivityForResult(intent, REQUEST_ENABLE);
379                }
380            }
381            return;
382        }
383
384        // Step 2.  Check if the current aggregate security policy is being satisfied by the
385        // DevicePolicyManager (the current system security level).
386        if (security.isActive(null)) {
387            if (MailActivityEmail.DEBUG || DEBUG) {
388                LogUtils.d(TAG, "Security active; clear holds");
389            }
390            Account.clearSecurityHoldOnAllAccounts(this);
391            security.syncAccount(account);
392            security.clearNotification();
393            finish();
394            return;
395        }
396
397        // Step 3.  Try to assert the current aggregate security requirements with the system.
398        security.setActivePolicies();
399
400        // Step 4.  Recheck the security policy, and determine what changes are needed (if any)
401        // to satisfy the requirements.
402        int inactiveReasons = security.getInactiveReasons(null);
403
404        // Step 5.  If password is needed, try to have the user set it
405        if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_PASSWORD) != 0) {
406            if (mTriedSetPassword) {
407                if (MailActivityEmail.DEBUG || DEBUG) {
408                    LogUtils.d(TAG, "Password needed; repost notification");
409                }
410                repostNotification(account, security);
411                finish();
412            } else {
413                if (MailActivityEmail.DEBUG || DEBUG) {
414                    LogUtils.d(TAG, "Password needed; request it via DPM");
415                }
416                mTriedSetPassword = true;
417                // launch the activity to have the user set a new password.
418                Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
419                startActivityForResult(intent, REQUEST_PASSWORD);
420            }
421            return;
422        }
423
424        // Step 6.  If encryption is needed, try to have the user set it
425        if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_ENCRYPTION) != 0) {
426            if (mTriedSetEncryption) {
427                if (MailActivityEmail.DEBUG || DEBUG) {
428                    LogUtils.d(TAG, "Encryption needed; repost notification");
429                }
430                repostNotification(account, security);
431                finish();
432            } else {
433                if (MailActivityEmail.DEBUG || DEBUG) {
434                    LogUtils.d(TAG, "Encryption needed; request it via DPM");
435                }
436                mTriedSetEncryption = true;
437                // launch the activity to start up encryption.
438                Intent intent = new Intent(DevicePolicyManager.ACTION_START_ENCRYPTION);
439                startActivityForResult(intent, REQUEST_ENCRYPTION);
440            }
441            return;
442        }
443
444        // Step 7.  No problems were found, so clear holds and exit
445        if (MailActivityEmail.DEBUG || DEBUG) {
446            LogUtils.d(TAG, "Policies enforced; clear holds");
447        }
448        Account.clearSecurityHoldOnAllAccounts(this);
449        security.syncAccount(account);
450        security.clearNotification();
451        finish();
452    }
453
454    /**
455     * Mark an account as not-ready-for-sync and post a notification to bring the user back here
456     * eventually.
457     */
458    private static void repostNotification(final Account account, final SecurityPolicy security) {
459        if (account == null) return;
460        new AsyncTask<Void, Void, Void>() {
461            @Override
462            protected Void doInBackground(Void... params) {
463                security.policiesRequired(account.mId);
464                return null;
465            }
466        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
467    }
468
469    /**
470     * Dialog briefly shown in some cases, to indicate the user that a security update is needed.
471     * If the user clicks OK, we proceed into the "tryAdvanceSecurity" flow.  If the user cancels,
472     * we repost the notification and finish() the activity.
473     */
474    public static class SecurityNeededDialog extends DialogFragment
475            implements DialogInterface.OnClickListener {
476        private static final String BUNDLE_KEY_ACCOUNT_NAME = "account_name";
477
478        // Public no-args constructor needed for fragment re-instantiation
479        public SecurityNeededDialog() {}
480
481        /**
482         * Create a new dialog.
483         */
484        public static SecurityNeededDialog newInstance(String accountName) {
485            final SecurityNeededDialog dialog = new SecurityNeededDialog();
486            Bundle b = new Bundle();
487            b.putString(BUNDLE_KEY_ACCOUNT_NAME, accountName);
488            dialog.setArguments(b);
489            return dialog;
490        }
491
492        @Override
493        public Dialog onCreateDialog(Bundle savedInstanceState) {
494            final String accountName = getArguments().getString(BUNDLE_KEY_ACCOUNT_NAME);
495
496            final Context context = getActivity();
497            final Resources res = context.getResources();
498            final AlertDialog.Builder b = new AlertDialog.Builder(context);
499            b.setTitle(R.string.account_security_dialog_title);
500            b.setIconAttribute(android.R.attr.alertDialogIcon);
501            b.setMessage(res.getString(R.string.account_security_dialog_content_fmt, accountName));
502            b.setPositiveButton(android.R.string.ok, this);
503            b.setNegativeButton(android.R.string.cancel, this);
504            if (MailActivityEmail.DEBUG || DEBUG) {
505                LogUtils.d(TAG, "Posting security needed dialog");
506            }
507            return b.create();
508        }
509
510        @Override
511        public void onClick(DialogInterface dialog, int which) {
512            dismiss();
513            AccountSecurity activity = (AccountSecurity) getActivity();
514            if (activity.mAccount == null) {
515                // Clicked before activity fully restored - probably just monkey - exit quickly
516                activity.finish();
517                return;
518            }
519            switch (which) {
520                case DialogInterface.BUTTON_POSITIVE:
521                    if (MailActivityEmail.DEBUG || DEBUG) {
522                        LogUtils.d(TAG, "User accepts; advance to next step");
523                    }
524                    activity.tryAdvanceSecurity(activity.mAccount);
525                    break;
526                case DialogInterface.BUTTON_NEGATIVE:
527                    if (MailActivityEmail.DEBUG || DEBUG) {
528                        LogUtils.d(TAG, "User declines; repost notification");
529                    }
530                    AccountSecurity.repostNotification(
531                            activity.mAccount, SecurityPolicy.getInstance(activity));
532                    activity.finish();
533                    break;
534            }
535        }
536    }
537
538    /**
539     * Dialog briefly shown in some cases, to indicate the user that the PIN/Password is expiring
540     * or has expired.  If the user clicks OK, we launch the password settings screen.
541     */
542    public static class PasswordExpirationDialog extends DialogFragment
543            implements DialogInterface.OnClickListener {
544        private static final String BUNDLE_KEY_ACCOUNT_NAME = "account_name";
545        private static final String BUNDLE_KEY_EXPIRED = "expired";
546
547        /**
548         * Create a new dialog.
549         */
550        public static PasswordExpirationDialog newInstance(String accountName, boolean expired) {
551            final PasswordExpirationDialog dialog = new PasswordExpirationDialog();
552            Bundle b = new Bundle();
553            b.putString(BUNDLE_KEY_ACCOUNT_NAME, accountName);
554            b.putBoolean(BUNDLE_KEY_EXPIRED, expired);
555            dialog.setArguments(b);
556            return dialog;
557        }
558
559        // Public no-args constructor needed for fragment re-instantiation
560        public PasswordExpirationDialog() {}
561
562        /**
563         * Note, this actually creates two slightly different dialogs (for expiring vs. expired)
564         */
565        @Override
566        public Dialog onCreateDialog(Bundle savedInstanceState) {
567            final String accountName = getArguments().getString(BUNDLE_KEY_ACCOUNT_NAME);
568            final boolean expired = getArguments().getBoolean(BUNDLE_KEY_EXPIRED);
569            final int titleId = expired
570                    ? R.string.password_expired_dialog_title
571                    : R.string.password_expire_warning_dialog_title;
572            final int contentId = expired
573                    ? R.string.password_expired_dialog_content_fmt
574                    : R.string.password_expire_warning_dialog_content_fmt;
575
576            final Context context = getActivity();
577            final Resources res = context.getResources();
578            return new AlertDialog.Builder(context)
579                    .setTitle(titleId)
580                    .setIconAttribute(android.R.attr.alertDialogIcon)
581                    .setMessage(res.getString(contentId, accountName))
582                    .setPositiveButton(android.R.string.ok, this)
583                    .setNegativeButton(android.R.string.cancel, this)
584                    .create();
585        }
586
587        @Override
588        public void onClick(DialogInterface dialog, int which) {
589            dismiss();
590            AccountSecurity activity = (AccountSecurity) getActivity();
591            if (which == DialogInterface.BUTTON_POSITIVE) {
592                Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
593                activity.startActivity(intent);
594            }
595            activity.finish();
596        }
597    }
598}
599