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.admin.DevicePolicyManager;
25import android.content.Context;
26import android.content.DialogInterface;
27import android.content.Intent;
28import android.content.res.Resources;
29import android.os.Bundle;
30
31import com.android.email.R;
32import com.android.email.SecurityPolicy;
33import com.android.email.activity.ActivityHelper;
34import com.android.email2.ui.MailActivityEmail;
35import com.android.emailcommon.provider.Account;
36import com.android.emailcommon.provider.HostAuth;
37import com.android.emailcommon.utility.Utility;
38import 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 */
51public 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