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