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 import android.util.Log;
     31 
     32 import com.android.email.Email;
     33 import com.android.email.R;
     34 import com.android.email.SecurityPolicy;
     35 import com.android.email.activity.ActivityHelper;
     36 import com.android.emailcommon.provider.Account;
     37 import com.android.emailcommon.provider.HostAuth;
     38 import 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  */
     51 public 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