/*
 * Copyright (C) 2009 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.development;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorDescription;
import android.accounts.AuthenticatorException;
import android.accounts.OnAccountsUpdateListener;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.Spinner;
import android.widget.TextView;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class AccountsTester extends Activity implements OnAccountsUpdateListener {
    private static final String TAG = "AccountsTester";
    private Spinner mAccountTypesSpinner;
    private ListView mAccountsListView;
    private AccountManager mAccountManager;
    private Account mLongPressedAccount = null;
    private AuthenticatorDescription[] mAuthenticatorDescs;

    private static final int GET_AUTH_TOKEN_DIALOG_ID = 1;
    private static final int UPDATE_CREDENTIALS_DIALOG_ID = 2;
    private static final int INVALIDATE_AUTH_TOKEN_DIALOG_ID = 3;
    private static final int TEST_HAS_FEATURES_DIALOG_ID = 4;
    private static final int MESSAGE_DIALOG_ID = 5;
    private static final int GET_AUTH_TOKEN_BY_TYPE_AND_FEATURE_DIALOG_ID = 6;

    private EditText mDesiredAuthTokenTypeEditText;
    private EditText mDesiredFeaturesEditText;
    private volatile CharSequence mDialogMessage;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAccountManager = AccountManager.get(this);
        setContentView(R.layout.accounts_tester);
        ButtonClickListener buttonClickListener = new ButtonClickListener();

        mAccountTypesSpinner = (Spinner) findViewById(R.id.accounts_tester_account_types_spinner);
        mAccountsListView = (ListView) findViewById(R.id.accounts_tester_accounts_list);
        registerForContextMenu(mAccountsListView);
        initializeAuthenticatorsSpinner();
        findViewById(R.id.accounts_tester_get_all_accounts).setOnClickListener(buttonClickListener);
        findViewById(R.id.accounts_tester_get_accounts_by_type).setOnClickListener(
                buttonClickListener);
        findViewById(R.id.accounts_tester_add_account).setOnClickListener(buttonClickListener);
        findViewById(R.id.accounts_tester_edit_properties).setOnClickListener(buttonClickListener);
        findViewById(R.id.accounts_tester_get_auth_token_by_type_and_feature).setOnClickListener(
                buttonClickListener);
        mDesiredAuthTokenTypeEditText =
                (EditText) findViewById(R.id.accounts_tester_desired_authtokentype);
        mDesiredFeaturesEditText = (EditText) findViewById(R.id.accounts_tester_desired_features);
    }

    private class AccountArrayAdapter extends ArrayAdapter<Account> {
        protected LayoutInflater mInflater;

        public AccountArrayAdapter(Context context, Account[] accounts) {
            super(context, R.layout.account_list_item, accounts);
            mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }

        class ViewHolder {
            TextView name;
            ImageView icon;
            Account account;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            // A ViewHolder keeps references to children views to avoid unneccessary calls
            // to findViewById() on each row.
            ViewHolder holder;

            // When convertView is not null, we can reuse it directly, there is no need
            // to reinflate it. We only inflate a new View when the convertView supplied
            // by ListView is null.
            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.account_list_item, null);

                // Creates a ViewHolder and store references to the two children views
                // we want to bind data to.
                holder = new ViewHolder();
                holder.name = (TextView) convertView.findViewById(
                        R.id.accounts_tester_account_name);
                holder.icon = (ImageView) convertView.findViewById(
                        R.id.accounts_tester_account_type_icon);

                convertView.setTag(holder);
            } else {
                // Get the ViewHolder back to get fast access to the TextView
                // and the ImageView.
                holder = (ViewHolder) convertView.getTag();
            }

            final Account account = getItem(position);
            holder.account = account;
            holder.icon.setVisibility(View.INVISIBLE);
            for (AuthenticatorDescription desc : mAuthenticatorDescs) {
                if (desc.type.equals(account.type)) {
                    final String packageName = desc.packageName;
                    try {
                        final Context authContext = getContext().createPackageContext(packageName,
                                0);
                        holder.icon.setImageDrawable(authContext.getResources().getDrawable(
                                desc.iconId));
                        holder.icon.setVisibility(View.VISIBLE);
                    } catch (PackageManager.NameNotFoundException e) {
                        Log.d(TAG, "error getting the Package Context for " + packageName, e);
                    }
                }
            }

            // Set text field
            holder.name.setText(account.name);
            return convertView;
        }
    }

    private void initializeAuthenticatorsSpinner() {
        mAuthenticatorDescs = mAccountManager.getAuthenticatorTypes();
        List<String> names = new ArrayList(mAuthenticatorDescs.length);
        for (int i = 0; i < mAuthenticatorDescs.length; i++) {
            Context authContext;
            try {
                authContext = createPackageContext(mAuthenticatorDescs[i].packageName, 0);
            } catch (PackageManager.NameNotFoundException e) {
                continue;
            }
            try  {
                names.add(authContext.getString(mAuthenticatorDescs[i].labelId));
            } catch (Resources.NotFoundException e) {
                continue;
            }
        }

        String[] namesArray = names.toArray(new String[names.size()]);
        ArrayAdapter<String> adapter =
                new ArrayAdapter<String>(AccountsTester.this,
                android.R.layout.simple_spinner_item, namesArray);
        mAccountTypesSpinner.setAdapter(adapter);
    }

    public void onAccountsUpdated(Account[] accounts) {
        Log.d(TAG, "onAccountsUpdated: \n  " + TextUtils.join("\n  ", accounts));
        mAccountsListView.setAdapter(new AccountArrayAdapter(this, accounts));
    }

    protected void onStart() {
        super.onStart();
        final Handler mainHandler = new Handler(getMainLooper());
        mAccountManager.addOnAccountsUpdatedListener(this, mainHandler,
                true /* updateImmediately */);
    }

    protected void onStop() {
        super.onStop();
        mAccountManager.removeOnAccountsUpdatedListener(this);
    }

    class ButtonClickListener implements View.OnClickListener {
        public void onClick(View v) {
            if (R.id.accounts_tester_get_all_accounts == v.getId()) {
                onAccountsUpdated(mAccountManager.getAccounts());
            } else if (R.id.accounts_tester_get_accounts_by_type == v.getId()) {
                String type = getSelectedAuthenticator().type;
                onAccountsUpdated(mAccountManager.getAccountsByType(type));
            } else if (R.id.accounts_tester_add_account == v.getId()) {
                String authTokenType = mDesiredAuthTokenTypeEditText.getText().toString();
                if (TextUtils.isEmpty(authTokenType)) {
                    authTokenType = null;
                }
                String featureString = mDesiredFeaturesEditText.getText().toString();
                String[] requiredFeatures = TextUtils.split(featureString, " ");
                if (requiredFeatures.length == 0) {
                    requiredFeatures = null;
                }
                mAccountManager.addAccount(getSelectedAuthenticator().type,
                        authTokenType, requiredFeatures, null /* options */,
                        AccountsTester.this,
                        new CallbackToDialog(AccountsTester.this, "add account"),
                        null /* handler */);
            } else if (R.id.accounts_tester_edit_properties == v.getId()) {
                mAccountManager.editProperties(getSelectedAuthenticator().type,
                        AccountsTester.this,
                        new CallbackToDialog(AccountsTester.this, "edit properties"),
                        null /* handler */);
            } else if (R.id.accounts_tester_get_auth_token_by_type_and_feature == v.getId()) {
                showDialog(GET_AUTH_TOKEN_BY_TYPE_AND_FEATURE_DIALOG_ID);
            } else {
                // unknown button
            }
        }
    }

    private AuthenticatorDescription getSelectedAuthenticator() {
        return mAuthenticatorDescs[mAccountTypesSpinner.getSelectedItemPosition()];
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v,
            ContextMenu.ContextMenuInfo menuInfo) {
        menu.setHeaderTitle(R.string.accounts_tester_account_context_menu_title);

        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)menuInfo;

        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.layout.account_list_context_menu, menu);
        AccountArrayAdapter.ViewHolder holder =
                (AccountArrayAdapter.ViewHolder)info.targetView.getTag();
        mLongPressedAccount = holder.account;
    }

    protected void onSaveInstanceState(Bundle outState) {
        outState.putParcelable("account", mLongPressedAccount);
    }

    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        mLongPressedAccount = savedInstanceState.getParcelable("account");
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.accounts_tester_remove_account) {
            final Account account = mLongPressedAccount;
            mAccountManager.removeAccount(account, new AccountManagerCallback<Boolean>() {
                public void run(AccountManagerFuture<Boolean> future) {
                    try {
                        Log.d(TAG, "removeAccount(" + account + ") = " + future.getResult());
                    } catch (OperationCanceledException e) {
                    } catch (IOException e) {
                    } catch (AuthenticatorException e) {
                    }
                }
            }, null /* handler */);
        } else if (item.getItemId() == R.id.accounts_tester_clear_password) {
            final Account account = mLongPressedAccount;
            mAccountManager.clearPassword(account);
            showMessageDialog("cleared");
        } else if (item.getItemId() == R.id.accounts_tester_get_auth_token) {
            showDialog(GET_AUTH_TOKEN_DIALOG_ID);
        } else if (item.getItemId() == R.id.accounts_tester_test_has_features) {
            showDialog(TEST_HAS_FEATURES_DIALOG_ID);
        } else if (item.getItemId() == R.id.accounts_tester_invalidate_auth_token) {
            showDialog(INVALIDATE_AUTH_TOKEN_DIALOG_ID);
        } else if (item.getItemId() == R.id.accounts_tester_update_credentials) {
            showDialog(UPDATE_CREDENTIALS_DIALOG_ID);
        } else if (item.getItemId() == R.id.accounts_tester_confirm_credentials) {
            mAccountManager.confirmCredentials(mLongPressedAccount, null,
                    AccountsTester.this, new CallbackToDialog(this, "confirm credentials"),
                    null /* handler */);
        }
        return true;
    }

    @Override
    protected Dialog onCreateDialog(final int id) {
        switch (id) {
            case GET_AUTH_TOKEN_DIALOG_ID:
            case INVALIDATE_AUTH_TOKEN_DIALOG_ID:
            case UPDATE_CREDENTIALS_DIALOG_ID:
            case TEST_HAS_FEATURES_DIALOG_ID: {
                final View view = LayoutInflater.from(this).inflate(R.layout.get_auth_token_view,
                        null);
                AlertDialog.Builder builder = new AlertDialog.Builder(this);
                builder.setPositiveButton(R.string.accounts_tester_ok_button,
                        new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int which) {
                                EditText value = (EditText) view.findViewById(
                                        R.id.accounts_tester_auth_token_type);

                                String authTokenType = value.getText().toString();
                                final Account account = mLongPressedAccount;
                                if (id == GET_AUTH_TOKEN_DIALOG_ID) {
                                    mAccountManager.getAuthToken(account,
                                            authTokenType,
                                            null /* loginOptions */,
                                            AccountsTester.this,
                                            new CallbackToDialog(AccountsTester.this,
                                                    "get auth token"),
                                            null /* handler */);
                                } else if (id == INVALIDATE_AUTH_TOKEN_DIALOG_ID) {
                                    mAccountManager.getAuthToken(account, authTokenType, false,
                                            new GetAndInvalidateAuthTokenCallback(account), null);
                                } else if (id == TEST_HAS_FEATURES_DIALOG_ID) {
                                    String[] features = TextUtils.split(authTokenType, ",");
                                    mAccountManager.hasFeatures(account, features,
                                            new TestHasFeaturesCallback(), null);
                                } else {
                                    mAccountManager.updateCredentials(
                                            account,
                                            authTokenType, null /* loginOptions */,
                                            AccountsTester.this,
                                            new CallbackToDialog(AccountsTester.this, "update"),
                                            null /* handler */);
                                }
                            }
                });
                builder.setView(view);
                return builder.create();
            }

            case GET_AUTH_TOKEN_BY_TYPE_AND_FEATURE_DIALOG_ID: {
                final View view = LayoutInflater.from(this).inflate(R.layout.get_features_view,
                        null);
                AlertDialog.Builder builder = new AlertDialog.Builder(this);
                builder.setPositiveButton(R.string.accounts_tester_ok_button,
                        new DialogInterface.OnClickListener() {
                            public void onClick(DialogInterface dialog, int which) {
                                EditText value = (EditText) view.findViewById(
                                        R.id.accounts_tester_auth_token_type);

                                String authTokenType = value.getText().toString();

                                value = (EditText) view.findViewById(
                                        R.id.accounts_tester_features);

                                String features = value.getText().toString();

                                final Account account = mLongPressedAccount;
                                mAccountManager.getAuthTokenByFeatures(
                                        getSelectedAuthenticator().type,
                                        authTokenType,
                                        TextUtils.isEmpty(features) ? null : features.split(" "),
                                        AccountsTester.this,
                                        null /* addAccountOptions */,
                                        null /* getAuthTokenOptions */,
                                        new CallbackToDialog(AccountsTester.this,
                                                "get auth token by features"),
                                        null /* handler */);
                            }
                });
                builder.setView(view);
                return builder.create();
            }

            case MESSAGE_DIALOG_ID: {
                AlertDialog.Builder builder = new AlertDialog.Builder(this);
                builder.setMessage(mDialogMessage);
                return builder.create();
            }
        }

        return super.onCreateDialog(id);
    }

    AccountManagerCallback<Bundle> newAccountsCallback(String type, String[] features) {
        return new GetAccountsCallback(type, features);
    }

    class GetAccountsCallback implements AccountManagerCallback<Bundle> {
        final String[] mFeatures;
        final String mAccountType;

        public GetAccountsCallback(String type, String[] features) {
            mFeatures = features;
            mAccountType = type;
        }

        public void run(AccountManagerFuture<Bundle> future) {
            Log.d(TAG, "GetAccountsCallback: type " + mAccountType
                    + ", features "
                    + (mFeatures == null ? "none" : TextUtils.join(",", mFeatures)));
            try {
                Bundle result = future.getResult();
                Parcelable[] accounts = result.getParcelableArray(AccountManager.KEY_ACCOUNTS);
                Log.d(TAG, "found " + accounts.length + " accounts");
                for (Parcelable account : accounts) {
                    Log.d(TAG, "  " + account);
                }
            } catch (OperationCanceledException e) {
                Log.d(TAG, "failure", e);
            } catch (IOException e) {
                Log.d(TAG, "failure", e);
            } catch (AuthenticatorException e) {
                Log.d(TAG, "failure", e);
            }
        }
    }

    AccountManagerCallback<Bundle> newAuthTokensCallback(String type, String authTokenType,
            String[] features) {
        return new GetAuthTokenCallback(type, authTokenType, features);
    }

    class GetAuthTokenCallback implements AccountManagerCallback<Bundle> {
        final String[] mFeatures;
        final String mAccountType;
        final String mAuthTokenType;

        public GetAuthTokenCallback(String type, String authTokenType, String[] features) {
            mFeatures = features;
            mAccountType = type;
            mAuthTokenType = authTokenType;
        }

        public void run(AccountManagerFuture<Bundle> future) {
            Log.d(TAG, "GetAuthTokenCallback: type " + mAccountType
                    + ", features "
                    + (mFeatures == null ? "none" : TextUtils.join(",", mFeatures)));
            getAndLogResult(future, "get auth token");
        }
    }

    private class GetAndInvalidateAuthTokenCallback implements AccountManagerCallback<Bundle> {
        private final Account mAccount;

        private GetAndInvalidateAuthTokenCallback(Account account) {
            mAccount = account;
        }

        public void run(AccountManagerFuture<Bundle> future) {
            Bundle result = getAndLogResult(future, "get and invalidate");
            if (result != null) {
                String authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
                mAccountManager.invalidateAuthToken(mAccount.type, authToken);
            }
        }
    }

    private void showMessageDialog(String message) {
        mDialogMessage = message;
        removeDialog(MESSAGE_DIALOG_ID);
        showDialog(MESSAGE_DIALOG_ID);
    }

    private class TestHasFeaturesCallback implements AccountManagerCallback<Boolean> {
        public void run(AccountManagerFuture<Boolean> future) {
            try {
                Boolean hasFeatures = future.getResult();
                Log.d(TAG, "hasFeatures: " + hasFeatures);
                showMessageDialog("hasFeatures: " + hasFeatures);
            } catch (OperationCanceledException e) {
                Log.d(TAG, "interrupted");
                showMessageDialog("operation was canceled");
            } catch (IOException e) {
                Log.d(TAG, "error", e);
                showMessageDialog("operation got an IOException");
            } catch (AuthenticatorException e) {
                Log.d(TAG, "error", e);
                showMessageDialog("operation got an AuthenticationException");
            }
        }
    }

    private static class CallbackToDialog implements AccountManagerCallback<Bundle> {
        private final AccountsTester mActivity;
        private final String mLabel;

        private CallbackToDialog(AccountsTester activity, String label) {
            mActivity = activity;
            mLabel = label;
        }

        public void run(AccountManagerFuture<Bundle> future) {
            mActivity.getAndLogResult(future, mLabel);
        }
    }

    private Bundle getAndLogResult(AccountManagerFuture<Bundle> future, String label) {
        try {
            Bundle result = future.getResult();
            result.keySet();
            Log.d(TAG, label + ": " + result);
            StringBuffer sb = new StringBuffer();
            sb.append(label).append(" result:");
            for (String key : result.keySet()) {
                Object value = result.get(key);
                if (AccountManager.KEY_AUTHTOKEN.equals(key)) {
                    value = "<redacted>";
                }
                sb.append("\n  ").append(key).append(" -> ").append(value);
            }
            showMessageDialog(sb.toString());
            return result;
        } catch (OperationCanceledException e) {
            Log.d(TAG, label + " failed", e);
            showMessageDialog(label + " was canceled");
            return null;
        } catch (IOException e) {
            Log.d(TAG, label + " failed", e);
            showMessageDialog(label + " failed with IOException: " + e.getMessage());
            return null;
        } catch (AuthenticatorException e) {
            Log.d(TAG, label + " failed", e);
            showMessageDialog(label + " failed with an AuthenticatorException: " + e.getMessage());
            return null;
        }
    }
}
