/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.email.activity.setup;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.app.LoaderManager;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.Loader;
import android.content.res.Resources;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;

import com.android.email.DebugUtils;
import com.android.email.R;
import com.android.email.SecurityPolicy;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Policy;
import com.android.emailcommon.utility.IntentUtilities;
import com.android.mail.ui.MailAsyncTaskLoader;
import com.android.mail.utils.LogUtils;

/**
 * Psuedo-activity (no UI) to bootstrap the user up to a higher desired security level.  This
 * bootstrap requires the following steps.
 *
 * 1.  Confirm the account of interest has any security policies defined - exit early if not
 * 2.  If not actively administrating the device, ask Device Policy Manager to start that
 * 3.  When we are actively administrating, check current policies and see if they're sufficient
 * 4.  If not, set policies
 * 5.  If necessary, request for user to update device password
 * 6.  If necessary, request for user to activate device encryption
 */
public class AccountSecurity extends Activity {
    private static final String TAG = "Email/AccountSecurity";

    private static final boolean DEBUG = false;  // Don't ship with this set to true

    private static final String EXTRA_ACCOUNT_ID = "ACCOUNT_ID";
    private static final String EXTRA_SHOW_DIALOG = "SHOW_DIALOG";
    private static final String EXTRA_PASSWORD_EXPIRING = "EXPIRING";
    private static final String EXTRA_PASSWORD_EXPIRED = "EXPIRED";

    private static final String SAVESTATE_INITIALIZED_TAG = "initialized";
    private static final String SAVESTATE_TRIED_ADD_ADMINISTRATOR_TAG = "triedAddAdministrator";
    private static final String SAVESTATE_TRIED_SET_PASSWORD_TAG = "triedSetpassword";
    private static final String SAVESTATE_TRIED_SET_ENCRYPTION_TAG = "triedSetEncryption";
    private static final String SAVESTATE_ACCOUNT_TAG = "account";

    private static final int REQUEST_ENABLE = 1;
    private static final int REQUEST_PASSWORD = 2;
    private static final int REQUEST_ENCRYPTION = 3;

    private boolean mTriedAddAdministrator;
    private boolean mTriedSetPassword;
    private boolean mTriedSetEncryption;

    private Account mAccount;

    protected boolean mInitialized;

    private Handler mHandler;
    private boolean mActivityResumed;

    private static final int ACCOUNT_POLICY_LOADER_ID = 0;
    private AccountAndPolicyLoaderCallbacks mAPLoaderCallbacks;
    private Bundle mAPLoaderArgs;

    public static Uri getUpdateSecurityUri(final long accountId, final boolean showDialog) {
        final Uri.Builder baseUri = Uri.parse("auth://" + EmailContent.EMAIL_PACKAGE_NAME +
                ".ACCOUNT_SECURITY/").buildUpon();
        IntentUtilities.setAccountId(baseUri, accountId);
        baseUri.appendQueryParameter(EXTRA_SHOW_DIALOG, Boolean.toString(showDialog));
        return baseUri.build();
    }

    /**
     * Used for generating intent for this activity (which is intended to be launched
     * from a notification.)
     *
     * @param context Calling context for building the intent
     * @param accountId The account of interest
     * @param showDialog If true, a simple warning dialog will be shown before kicking off
     * the necessary system settings.  Should be true anywhere the context of the security settings
     * is not clear (e.g. any time after the account has been set up).
     * @return an Intent which can be used to view that account
     */
    public static Intent actionUpdateSecurityIntent(Context context, long accountId,
            boolean showDialog) {
        Intent intent = new Intent(context, AccountSecurity.class);
        intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
        intent.putExtra(EXTRA_SHOW_DIALOG, showDialog);
        return intent;
    }

    /**
     * Used for generating intent for this activity (which is intended to be launched
     * from a notification.)  This is a special mode of this activity which exists only
     * to give the user a dialog (for context) about a device pin/password expiration event.
     */
    public static Intent actionDevicePasswordExpirationIntent(Context context, long accountId,
            boolean expired) {
        Intent intent = new ForwardingIntent(context, AccountSecurity.class);
        intent.putExtra(EXTRA_ACCOUNT_ID, accountId);
        intent.putExtra(expired ? EXTRA_PASSWORD_EXPIRED : EXTRA_PASSWORD_EXPIRING, true);
        return intent;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mHandler = new Handler();

        final Intent i = getIntent();
        final long accountId;
        Bundle extras = i.getExtras();
        if (extras == null) {
            // We have been invoked via a uri. We need to get our parameters from the URI instead
            // of looking in the intent extras.
            extras = new Bundle();
            accountId = IntentUtilities.getAccountIdFromIntent(i);
            extras.putLong(EXTRA_ACCOUNT_ID, accountId);
            boolean showDialog = false;
            final String value = i.getData().getQueryParameter(EXTRA_SHOW_DIALOG);
            if (!TextUtils.isEmpty(value)) {
                showDialog = Boolean.getBoolean(value);
            }
            extras.putBoolean(EXTRA_SHOW_DIALOG, showDialog);
        } else {
            accountId = i.getLongExtra(EXTRA_ACCOUNT_ID, -1);
            extras = i.getExtras();
        }

        final SecurityPolicy security = SecurityPolicy.getInstance(this);
        security.clearNotification();
        if (accountId == -1) {
            finish();
            return;
        }

        if (savedInstanceState != null) {
            mInitialized = savedInstanceState.getBoolean(SAVESTATE_INITIALIZED_TAG, false);

            mTriedAddAdministrator =
                    savedInstanceState.getBoolean(SAVESTATE_TRIED_ADD_ADMINISTRATOR_TAG, false);
            mTriedSetPassword =
                    savedInstanceState.getBoolean(SAVESTATE_TRIED_SET_PASSWORD_TAG, false);
            mTriedSetEncryption =
                    savedInstanceState.getBoolean(SAVESTATE_TRIED_SET_ENCRYPTION_TAG, false);

            mAccount = savedInstanceState.getParcelable(SAVESTATE_ACCOUNT_TAG);
        }

        if (!mInitialized) {
            startAccountAndPolicyLoader(extras);
        }
    }

    @Override
    protected void onSaveInstanceState(final Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean(SAVESTATE_INITIALIZED_TAG, mInitialized);

        outState.putBoolean(SAVESTATE_TRIED_ADD_ADMINISTRATOR_TAG, mTriedAddAdministrator);
        outState.putBoolean(SAVESTATE_TRIED_SET_PASSWORD_TAG, mTriedSetPassword);
        outState.putBoolean(SAVESTATE_TRIED_SET_ENCRYPTION_TAG, mTriedSetEncryption);

        outState.putParcelable(SAVESTATE_ACCOUNT_TAG, mAccount);
    }

    @Override
    protected void onPause() {
        super.onPause();
        mActivityResumed = false;
    }

    @Override
    protected void onResume() {
        super.onResume();
        mActivityResumed = true;
        tickleAccountAndPolicyLoader();
    }

    protected boolean isActivityResumed() {
        return mActivityResumed;
    }

    private void tickleAccountAndPolicyLoader() {
        // If we're already initialized we don't need to tickle.
        if (!mInitialized) {
            getLoaderManager().initLoader(ACCOUNT_POLICY_LOADER_ID, mAPLoaderArgs,
                    mAPLoaderCallbacks);
        }
    }

    private void startAccountAndPolicyLoader(final Bundle args) {
        mAPLoaderArgs = args;
        mAPLoaderCallbacks = new AccountAndPolicyLoaderCallbacks();
        tickleAccountAndPolicyLoader();
    }

    private class AccountAndPolicyLoaderCallbacks
            implements LoaderManager.LoaderCallbacks<Account> {
        @Override
        public Loader<Account> onCreateLoader(final int id, final Bundle args) {
            final long accountId = args.getLong(EXTRA_ACCOUNT_ID, -1);
            final boolean showDialog = args.getBoolean(EXTRA_SHOW_DIALOG, false);
            final boolean passwordExpiring =
                    args.getBoolean(EXTRA_PASSWORD_EXPIRING, false);
            final boolean passwordExpired =
                    args.getBoolean(EXTRA_PASSWORD_EXPIRED, false);

            return new AccountAndPolicyLoader(getApplicationContext(), accountId,
                    showDialog, passwordExpiring, passwordExpired);
        }

        @Override
        public void onLoadFinished(final Loader<Account> loader, final Account account) {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    final AccountSecurity activity = AccountSecurity.this;
                    if (!activity.isActivityResumed()) {
                        return;
                    }

                    if (account == null || (account.mPolicyKey != 0 && account.mPolicy == null)) {
                        activity.finish();
                        LogUtils.d(TAG, "could not load account or policy in AccountSecurity");
                        return;
                    }

                    if (!activity.mInitialized) {
                        activity.mInitialized = true;

                        final AccountAndPolicyLoader apLoader = (AccountAndPolicyLoader) loader;
                        activity.completeCreate(account, apLoader.mShowDialog,
                                apLoader.mPasswordExpiring, apLoader.mPasswordExpired);
                    }
                }
            });
        }

        @Override
        public void onLoaderReset(Loader<Account> loader) {}
    }

    private static class AccountAndPolicyLoader extends MailAsyncTaskLoader<Account> {
        private final long mAccountId;
        public final boolean mShowDialog;
        public final boolean mPasswordExpiring;
        public final boolean mPasswordExpired;

        private final Context mContext;

        AccountAndPolicyLoader(final Context context, final long accountId,
                final boolean showDialog, final boolean passwordExpiring,
                final boolean passwordExpired) {
            super(context);
            mContext = context;
            mAccountId = accountId;
            mShowDialog = showDialog;
            mPasswordExpiring = passwordExpiring;
            mPasswordExpired = passwordExpired;
        }

        @Override
        public Account loadInBackground() {
            final Account account = Account.restoreAccountWithId(mContext, mAccountId);
            if (account == null) {
                return null;
            }

            final long policyId = account.mPolicyKey;
            if (policyId != 0) {
                account.mPolicy = Policy.restorePolicyWithId(mContext, policyId);
            }

            account.getOrCreateHostAuthRecv(mContext);

            return account;
        }

        @Override
        protected void onDiscardResult(Account result) {}
    }

    protected void completeCreate(final Account account, final boolean showDialog,
            final boolean passwordExpiring, final boolean passwordExpired) {
        mAccount = account;

        // Special handling for password expiration events
        if (passwordExpiring || passwordExpired) {
            FragmentManager fm = getFragmentManager();
            if (fm.findFragmentByTag("password_expiration") == null) {
                PasswordExpirationDialog dialog =
                    PasswordExpirationDialog.newInstance(mAccount.getDisplayName(),
                            passwordExpired);
                if (DebugUtils.DEBUG || DEBUG) {
                    LogUtils.d(TAG, "Showing password expiration dialog");
                }
                dialog.show(fm, "password_expiration");
            }
            return;
        }
        // Otherwise, handle normal security settings flow
        if (mAccount.mPolicyKey != 0) {
            // This account wants to control security
            if (showDialog) {
                // Show dialog first, unless already showing (e.g. after rotation)
                FragmentManager fm = getFragmentManager();
                if (fm.findFragmentByTag("security_needed") == null) {
                    SecurityNeededDialog dialog =
                        SecurityNeededDialog.newInstance(mAccount.getDisplayName());
                    if (DebugUtils.DEBUG || DEBUG) {
                        LogUtils.d(TAG, "Showing security needed dialog");
                    }
                    dialog.show(fm, "security_needed");
                }
            } else {
                // Go directly to security settings
                tryAdvanceSecurity(mAccount);
            }
            return;
        }
        finish();
    }

    /**
     * After any of the activities return, try to advance to the "next step"
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        tryAdvanceSecurity(mAccount);
        super.onActivityResult(requestCode, resultCode, data);
    }

    /**
     * Walk the user through the required steps to become an active administrator and with
     * the requisite security settings for the given account.
     *
     * These steps will be repeated each time we return from a given attempt (e.g. asking the
     * user to choose a device pin/password).  In a typical activation, we may repeat these
     * steps a few times.  It may go as far as step 5 (password) or step 6 (encryption), but it
     * will terminate when step 2 (isActive()) succeeds.
     *
     * If at any point we do not advance beyond a given user step, (e.g. the user cancels
     * instead of setting a password) we simply repost the security notification, and exit.
     * We never want to loop here.
     */
    private void tryAdvanceSecurity(Account account) {
        SecurityPolicy security = SecurityPolicy.getInstance(this);
        // Step 1.  Check if we are an active device administrator, and stop here to activate
        if (!security.isActiveAdmin()) {
            if (mTriedAddAdministrator) {
                if (DebugUtils.DEBUG || DEBUG) {
                    LogUtils.d(TAG, "Not active admin: repost notification");
                }
                repostNotification(account, security);
                finish();
            } else {
                mTriedAddAdministrator = true;
                // retrieve name of server for the format string
                final HostAuth hostAuth = account.mHostAuthRecv;
                if (hostAuth == null) {
                    if (DebugUtils.DEBUG || DEBUG) {
                        LogUtils.d(TAG, "No HostAuth: repost notification");
                    }
                    repostNotification(account, security);
                    finish();
                } else {
                    if (DebugUtils.DEBUG || DEBUG) {
                        LogUtils.d(TAG, "Not active admin: post initial notification");
                    }
                    // try to become active - must happen here in activity, to get result
                    Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
                    intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN,
                            security.getAdminComponent());
                    intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION,
                            this.getString(R.string.account_security_policy_explanation_fmt,
                                    hostAuth.mAddress));
                    startActivityForResult(intent, REQUEST_ENABLE);
                }
            }
            return;
        }

        // Step 2.  Check if the current aggregate security policy is being satisfied by the
        // DevicePolicyManager (the current system security level).
        if (security.isActive(null)) {
            if (DebugUtils.DEBUG || DEBUG) {
                LogUtils.d(TAG, "Security active; clear holds");
            }
            Account.clearSecurityHoldOnAllAccounts(this);
            security.syncAccount(account);
            security.clearNotification();
            finish();
            return;
        }

        // Step 3.  Try to assert the current aggregate security requirements with the system.
        security.setActivePolicies();

        // Step 4.  Recheck the security policy, and determine what changes are needed (if any)
        // to satisfy the requirements.
        int inactiveReasons = security.getInactiveReasons(null);

        // Step 5.  If password is needed, try to have the user set it
        if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_PASSWORD) != 0) {
            if (mTriedSetPassword) {
                if (DebugUtils.DEBUG || DEBUG) {
                    LogUtils.d(TAG, "Password needed; repost notification");
                }
                repostNotification(account, security);
                finish();
            } else {
                if (DebugUtils.DEBUG || DEBUG) {
                    LogUtils.d(TAG, "Password needed; request it via DPM");
                }
                mTriedSetPassword = true;
                // launch the activity to have the user set a new password.
                Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
                startActivityForResult(intent, REQUEST_PASSWORD);
            }
            return;
        }

        // Step 6.  If encryption is needed, try to have the user set it
        if ((inactiveReasons & SecurityPolicy.INACTIVE_NEED_ENCRYPTION) != 0) {
            if (mTriedSetEncryption) {
                if (DebugUtils.DEBUG || DEBUG) {
                    LogUtils.d(TAG, "Encryption needed; repost notification");
                }
                repostNotification(account, security);
                finish();
            } else {
                if (DebugUtils.DEBUG || DEBUG) {
                    LogUtils.d(TAG, "Encryption needed; request it via DPM");
                }
                mTriedSetEncryption = true;
                // launch the activity to start up encryption.
                Intent intent = new Intent(DevicePolicyManager.ACTION_START_ENCRYPTION);
                startActivityForResult(intent, REQUEST_ENCRYPTION);
            }
            return;
        }

        // Step 7.  No problems were found, so clear holds and exit
        if (DebugUtils.DEBUG || DEBUG) {
            LogUtils.d(TAG, "Policies enforced; clear holds");
        }
        Account.clearSecurityHoldOnAllAccounts(this);
        security.syncAccount(account);
        security.clearNotification();
        finish();
    }

    /**
     * Mark an account as not-ready-for-sync and post a notification to bring the user back here
     * eventually.
     */
    private static void repostNotification(final Account account, final SecurityPolicy security) {
        if (account == null) return;
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                security.policiesRequired(account.mId);
                return null;
            }
        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    /**
     * Dialog briefly shown in some cases, to indicate the user that a security update is needed.
     * If the user clicks OK, we proceed into the "tryAdvanceSecurity" flow.  If the user cancels,
     * we repost the notification and finish() the activity.
     */
    public static class SecurityNeededDialog extends DialogFragment
            implements DialogInterface.OnClickListener {
        private static final String BUNDLE_KEY_ACCOUNT_NAME = "account_name";

        // Public no-args constructor needed for fragment re-instantiation
        public SecurityNeededDialog() {}

        /**
         * Create a new dialog.
         */
        public static SecurityNeededDialog newInstance(String accountName) {
            final SecurityNeededDialog dialog = new SecurityNeededDialog();
            Bundle b = new Bundle();
            b.putString(BUNDLE_KEY_ACCOUNT_NAME, accountName);
            dialog.setArguments(b);
            return dialog;
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            final String accountName = getArguments().getString(BUNDLE_KEY_ACCOUNT_NAME);

            final Context context = getActivity();
            final Resources res = context.getResources();
            final AlertDialog.Builder b = new AlertDialog.Builder(context);
            b.setTitle(R.string.account_security_dialog_title);
            b.setIconAttribute(android.R.attr.alertDialogIcon);
            b.setMessage(res.getString(R.string.account_security_dialog_content_fmt, accountName));
            b.setPositiveButton(android.R.string.ok, this);
            b.setNegativeButton(android.R.string.cancel, this);
            if (DebugUtils.DEBUG || DEBUG) {
                LogUtils.d(TAG, "Posting security needed dialog");
            }
            return b.create();
        }

        @Override
        public void onClick(DialogInterface dialog, int which) {
            dismiss();
            AccountSecurity activity = (AccountSecurity) getActivity();
            if (activity.mAccount == null) {
                // Clicked before activity fully restored - probably just monkey - exit quickly
                activity.finish();
                return;
            }
            switch (which) {
                case DialogInterface.BUTTON_POSITIVE:
                    if (DebugUtils.DEBUG || DEBUG) {
                        LogUtils.d(TAG, "User accepts; advance to next step");
                    }
                    activity.tryAdvanceSecurity(activity.mAccount);
                    break;
                case DialogInterface.BUTTON_NEGATIVE:
                    if (DebugUtils.DEBUG || DEBUG) {
                        LogUtils.d(TAG, "User declines; repost notification");
                    }
                    AccountSecurity.repostNotification(
                            activity.mAccount, SecurityPolicy.getInstance(activity));
                    activity.finish();
                    break;
            }
        }
    }

    /**
     * Dialog briefly shown in some cases, to indicate the user that the PIN/Password is expiring
     * or has expired.  If the user clicks OK, we launch the password settings screen.
     */
    public static class PasswordExpirationDialog extends DialogFragment
            implements DialogInterface.OnClickListener {
        private static final String BUNDLE_KEY_ACCOUNT_NAME = "account_name";
        private static final String BUNDLE_KEY_EXPIRED = "expired";

        /**
         * Create a new dialog.
         */
        public static PasswordExpirationDialog newInstance(String accountName, boolean expired) {
            final PasswordExpirationDialog dialog = new PasswordExpirationDialog();
            Bundle b = new Bundle();
            b.putString(BUNDLE_KEY_ACCOUNT_NAME, accountName);
            b.putBoolean(BUNDLE_KEY_EXPIRED, expired);
            dialog.setArguments(b);
            return dialog;
        }

        // Public no-args constructor needed for fragment re-instantiation
        public PasswordExpirationDialog() {}

        /**
         * Note, this actually creates two slightly different dialogs (for expiring vs. expired)
         */
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            final String accountName = getArguments().getString(BUNDLE_KEY_ACCOUNT_NAME);
            final boolean expired = getArguments().getBoolean(BUNDLE_KEY_EXPIRED);
            final int titleId = expired
                    ? R.string.password_expired_dialog_title
                    : R.string.password_expire_warning_dialog_title;
            final int contentId = expired
                    ? R.string.password_expired_dialog_content_fmt
                    : R.string.password_expire_warning_dialog_content_fmt;

            final Context context = getActivity();
            final Resources res = context.getResources();
            return new AlertDialog.Builder(context)
                    .setTitle(titleId)
                    .setIconAttribute(android.R.attr.alertDialogIcon)
                    .setMessage(res.getString(contentId, accountName))
                    .setPositiveButton(android.R.string.ok, this)
                    .setNegativeButton(android.R.string.cancel, this)
                    .create();
        }

        @Override
        public void onClick(DialogInterface dialog, int which) {
            dismiss();
            AccountSecurity activity = (AccountSecurity) getActivity();
            if (which == DialogInterface.BUTTON_POSITIVE) {
                Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
                activity.startActivity(intent);
            }
            activity.finish();
        }
    }
}
