• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
actionUpdateSecurityIntent(Context context, long accountId, boolean showDialog)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      */
actionDevicePasswordExpirationIntent(Context context, long accountId, boolean expired)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
onCreate(Bundle savedInstanceState)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
onActivityResult(int requestCode, int resultCode, Intent data)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      */
tryAdvanceSecurity(Account account)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      */
repostNotification(final Account account, final SecurityPolicy security)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          */
newInstance(String accountName)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
onCreateDialog(Bundle savedInstanceState)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
onClick(DialogInterface dialog, int which)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          */
newInstance(String accountName, boolean expired)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
onCreateDialog(Bundle savedInstanceState)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
onClick(DialogInterface dialog, int which)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