edu.mit.mobile.android.locast.accounts.AbsLocastAuthenticatorActivity.java Source code

Java tutorial

Introduction

Here is the source code for edu.mit.mobile.android.locast.accounts.AbsLocastAuthenticatorActivity.java

Source

/*
 * Copyright (C) 2010-2012 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 edu.mit.mobile.android.locast.accounts;

import java.io.IOException;

import org.json.JSONException;

import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import edu.mit.mobile.android.locast.BuildConfig;
import edu.mit.mobile.android.locast.R;
import edu.mit.mobile.android.locast.net.LocastApplicationCallbacks;
import edu.mit.mobile.android.locast.net.NetworkClient;
import edu.mit.mobile.android.locast.net.NetworkProtocolException;

/**
 * Activity which displays login screen to the user.
 */
public abstract class AbsLocastAuthenticatorActivity extends AccountAuthenticatorActivity
        implements OnClickListener, OnEditorActionListener {
    private static final String TAG = AbsLocastAuthenticatorActivity.class.getSimpleName();

    public static final String EXTRA_CONFIRMCREDENTIALS = "confirmCredentials";
    public static final String EXTRA_PASSWORD = "password";
    public static final String EXTRA_USERNAME = "username";
    public static final String EXTRA_AUTHTOKEN_TYPE = "authtokenType";

    private AccountManager mAccountManager;
    private String mAuthtoken;
    private String mAuthtokenType;

    private static final int DIALOG_PROGRESS = 100;

    private static final int REQUEST_REGISTER = 200;

    /**
     * If set we are just checking that the user knows their credentials; this doesn't cause the
     * user's password to be changed on the device.
     */
    private Boolean mConfirmCredentials = false;

    private TextView mMessage;
    private String mPassword;
    private EditText mPasswordEdit;

    /** Was the original caller asking for an entirely new account? */
    protected boolean mRequestNewAccount = false;

    private String mUsername;
    private EditText mUsernameEdit;

    private Button mRegisterButton;

    private Intent mRegistrationComplete;

    /**
     * {@inheritDoc}
     */
    @Override
    public void onCreate(Bundle icicle) {
        if (BuildConfig.DEBUG) {
            Log.i(TAG, "onCreate(" + icicle + ")");
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            requestWindowFeature(Window.FEATURE_ACTION_BAR);
        }
        super.onCreate(icicle);

        mAccountManager = AccountManager.get(this);
        if (BuildConfig.DEBUG) {
            Log.i(TAG, "loading data from Intent");
        }

        final Intent intent = getIntent();
        mUsername = intent.getStringExtra(EXTRA_USERNAME);
        mAuthtokenType = intent.getStringExtra(EXTRA_AUTHTOKEN_TYPE);
        mRequestNewAccount = mUsername == null;
        mConfirmCredentials = intent.getBooleanExtra(EXTRA_CONFIRMCREDENTIALS, false);

        if (BuildConfig.DEBUG) {
            Log.i(TAG, "    request new: " + mRequestNewAccount);
        }
        requestWindowFeature(Window.FEATURE_LEFT_ICON);

        final CharSequence appName = getAppName();

        // make the title based on the app name.
        setTitle(getString(R.string.login_title, appName));

        // TODO make this changeable. Maybe use fragments?
        setContentView(R.layout.login);
        // this is done this way, so the associated icon is managed in XML.
        try {
            getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON,
                    getPackageManager().getActivityIcon(getComponentName()));
        } catch (final NameNotFoundException e) {
            e.printStackTrace();
        }

        mMessage = (TextView) findViewById(R.id.message);
        mUsernameEdit = (EditText) findViewById(R.id.username);
        mUsernameEdit.setHint(isEmailAddressLogin() ? R.string.auth_email_login_hint : R.string.auth_username_hint);
        mPasswordEdit = (EditText) findViewById(R.id.password);
        mPasswordEdit.setOnEditorActionListener(this);
        findViewById(R.id.login).setOnClickListener(this);
        findViewById(R.id.cancel).setOnClickListener(this);
        mRegisterButton = (Button) findViewById(R.id.register);
        mRegisterButton.setOnClickListener(this);
        final String regButton = getString(R.string.signup_text, appName);
        if (regButton.length() < 24) {
            mRegisterButton.setText(regButton);
        }

        ((TextView) findViewById(R.id.username_label)).setText(getString(R.string.username_label, appName));

        mUsernameEdit.setText(mUsername);

        // this will be unnecessary with fragments
        mAuthenticationTask = (AuthenticationTask) getLastNonConfigurationInstance();
        if (mAuthenticationTask != null) {
            mAuthenticationTask.attach(this);
        }
    }

    /**
     * @return the app's name
     */
    protected abstract CharSequence getAppName();

    /**
     * @return true if the user's email address is their login.
     */
    protected abstract boolean isEmailAddressLogin();

    /*
     * {@inheritDoc}
     */
    @Override
    protected Dialog onCreateDialog(int id) {
        switch (id) {
        case DIALOG_PROGRESS:

            final ProgressDialog dialog = new ProgressDialog(this);
            dialog.setMessage(getText(R.string.login_message_authenticating));
            dialog.setIndeterminate(true);
            dialog.setCancelable(true);
            dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
                public void onCancel(DialogInterface dialog) {
                    if (BuildConfig.DEBUG) {
                        Log.i(TAG, "dialog cancel has been invoked");
                    }
                    if (mAuthenticationTask != null) {
                        mAuthenticationTask.cancel(true);
                        mAuthenticationTask = null;
                        finish();
                    }
                }
            });
            return dialog;

        default:
            return null;
        }
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.login) {
            handleLogin();
        } else if (v.getId() == R.id.cancel) {
            finish();
        } else if (v.getId() == R.id.register) {
            startActivityForResult(getSignupIntent(), REQUEST_REGISTER);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {

        switch (requestCode) {
        case REQUEST_REGISTER:
            if (resultCode == RESULT_OK) {
                mRegistrationComplete = data;
            }
            break;
        }
    }

    @Override
    protected void onResume() {
        super.onResume();

        if (mRegistrationComplete != null) {
            onRegistrationComplete(mRegistrationComplete);
            mRegistrationComplete = null;
        }
    }

    /**
     * Called when registration returns successfully. Intent results are straight from the server.
     *
     * @param data
     *            this is an echo of the data sent to the server
     */
    protected void onRegistrationComplete(Intent data) {
        final String username = data.getStringExtra(EXTRA_USERNAME);
        final String password = data.getStringExtra(EXTRA_PASSWORD);
        mUsernameEdit.setText(username);
        mPasswordEdit.setText(password);

        handleLogin();
    }

    /**
     * Generate a new account.
     *
     * @return
     */
    protected abstract Account createAccount(String username);

    /**
     * @return the authority that this authenticator handles.
     */
    protected abstract String getAuthority();

    /**
     * @return an intent which will take the user to the sign up page. Started with startActivity().
     */
    protected abstract Intent getSignupIntent();

    /**
     * Handles onClick event on the Submit button. Sends username/password to the server for
     * authentication.
     */
    private void handleLogin() {
        if (mRequestNewAccount) {
            mUsername = mUsernameEdit.getText().toString();
        }
        mPassword = mPasswordEdit.getText().toString();
        if (validateEntry()) {
            mAuthenticationTask = new AuthenticationTask(this);
            mAuthenticationTask.execute(mUsername, mPassword);
        }
    }

    /**
     * @deprecated this will be retrieved using {@code LocastApplicationCallbacks}
     * @return the base URL for the Locast API
     */
    @Deprecated
    public String getApiUrl() {
        final NetworkClient nc = ((LocastApplicationCallbacks) getApplication()).getNetworkClientForAccount(this,
                null);
        return nc.getBaseUrl();
    }

    /**
     * Called when response is received from the server for confirm credentials request. See
     * onAuthenticationResult(). Sets the AccountAuthenticatorResult which is sent back to the
     * caller.
     *
     * @param the
     *            confirmCredentials result.
     */
    protected void finishConfirmCredentials(boolean result) {
        if (BuildConfig.DEBUG) {
            Log.i(TAG, "finishConfirmCredentials()");
        }
        final Account account = createAccount(mUsername);
        mAccountManager.setPassword(account, mPassword);
        final Intent intent = new Intent();
        intent.putExtra(AccountManager.KEY_BOOLEAN_RESULT, result);
        setAccountAuthenticatorResult(intent.getExtras());
        setResult(RESULT_OK, intent);
        finish();
    }

    /**
     *
     * Called when response is received from the server for authentication request. See
     * onAuthenticationResult(). Sets the AccountAuthenticatorResult which is sent back to the
     * caller. Also sets the authToken in AccountManager for this account.
     *
     * @param userData
     *            TODO
     * @param the
     *            confirmCredentials result.
     */

    protected void finishLogin(Bundle userData) {
        if (BuildConfig.DEBUG) {
            Log.i(TAG, "finishLogin()");
        }

        final Account account = createAccount(mUsername);

        if (mRequestNewAccount) {
            mAccountManager.addAccountExplicitly(account, mPassword, userData);
            // Automatically enable sync for this account
            ContentResolver.setSyncAutomatically(account, getAuthority(), true);
        } else {
            mAccountManager.setPassword(account, mPassword);
        }
        final Intent intent = new Intent();
        mAuthtoken = mPassword;
        intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, mUsername);
        intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, getAccountType());
        if (mAuthtokenType != null && mAuthtokenType.equals(getAuthtokenType())) {
            intent.putExtra(AccountManager.KEY_AUTHTOKEN, mAuthtoken);
        }
        setAccountAuthenticatorResult(intent.getExtras());
        setResult(RESULT_OK, intent);
        finish();
    }

    protected abstract String getAccountType();

    protected abstract String getAuthtokenType();

    private void setLoginNoticeError(int textResID) {
        mMessage.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_warning, 0, 0, 0);
        mMessage.setText(textResID);
        mMessage.setVisibility(View.VISIBLE);

    }

    private void setLoginNoticeError(String text) {
        mMessage.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_warning, 0, 0, 0);
        mMessage.setText(text);
        mMessage.setVisibility(View.VISIBLE);

    }

    /**
     * Called when the authentication process completes.
     */
    public void onAuthenticationResult(Bundle userData, String reason) {
        if (BuildConfig.DEBUG) {
            Log.i(TAG, "onAuthenticationResult(" + userData + ")");
        }

        if (userData != null) {
            if (!mConfirmCredentials) {
                finishLogin(userData);
            } else {
                finishConfirmCredentials(true);
            }
        } else {
            if (reason == null) {
                Log.e(TAG, "onAuthenticationResult: failed to authenticate");
                setLoginNoticeError(R.string.login_message_loginfail);
            } else {
                setLoginNoticeError(reason);
            }
        }
    }

    /**
     * Validates the login form.
     *
     * @return true if the form is valid.
     */
    private boolean validateEntry() {
        if (TextUtils.isEmpty(mUsername)) {
            // If no username, then we ask the user to log in using an
            // appropriate service.

            mUsernameEdit.setError(getString(R.string.login_message_login_empty_username, getAppName()));
            mUsernameEdit.requestFocus();
            return false;
        } else {
            mUsernameEdit.setError(null);
        }

        if (TextUtils.isEmpty(mPassword)) {
            mPasswordEdit.setError(getText(R.string.login_message_login_empty_password));
            mPasswordEdit.requestFocus();
            return false;
        } else {
            mPasswordEdit.setError(null);
        }
        return true;
    }

    private AuthenticationTask mAuthenticationTask = null;

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (mAuthenticationTask != null) {
            mAuthenticationTask.detach();
        }
        return mAuthenticationTask;
    }

    private class AuthenticationTask extends AsyncTask<String, Long, Bundle> {
        private AbsLocastAuthenticatorActivity mActivity;

        private String reason;

        public AuthenticationTask(AbsLocastAuthenticatorActivity activity) {
            mActivity = activity;
        }

        @SuppressWarnings("deprecation")
        @Override
        protected void onPreExecute() {
            mActivity.showDialog(DIALOG_PROGRESS);
        }

        @Override
        protected Bundle doInBackground(String... userPass) {

            try {
                final NetworkClient nc = ((LocastApplicationCallbacks) getApplication())
                        .getNetworkClientForAccount(AbsLocastAuthenticatorActivity.this, null);
                return nc.authenticate(userPass[0], userPass[1]);

            } catch (final IOException e) {
                reason = mActivity.getString(R.string.auth_error_could_not_contact_server);
                e.printStackTrace();
            } catch (final JSONException e) {
                reason = mActivity.getString(R.string.auth_error_server_returned_invalid_data);
                e.printStackTrace();
            } catch (final NetworkProtocolException e) {
                reason = mActivity.getString(R.string.auth_error_network_protocol_error,
                        e.getHttpResponseMessage());
                e.printStackTrace();
            }
            return null;
        }

        @SuppressWarnings("deprecation")
        @Override
        protected void onPostExecute(Bundle userData) {
            mActivity.dismissDialog(DIALOG_PROGRESS);
            mActivity.onAuthenticationResult(userData, reason);
        }

        public void detach() {
            mActivity = null;
        }

        public void attach(AbsLocastAuthenticatorActivity activity) {
            mActivity = activity;
        }
    }

    @Override
    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
        if (v.getId() == R.id.password) {
            handleLogin();
            return true;
        }
        return false;
    }

    /**
     * Handles the removal of an account when the logout button is pressed. Also provides a callback
     * for when an account is successfully removed.
     *
     * @author <a href="mailto:spomeroy@mit.edu">Steve Pomeroy</a>
     *
     */
    public static abstract class LogoutHandler implements DialogInterface.OnClickListener {

        private final AccountManagerCallback<Boolean> mAccountManagerCallback = new AccountManagerCallback<Boolean>() {

            @Override
            public void run(AccountManagerFuture<Boolean> amf) {
                boolean success = false;
                try {
                    success = amf.getResult();
                } catch (final OperationCanceledException e) {
                    Log.e(TAG, e.getLocalizedMessage(), e);

                } catch (final AuthenticatorException e) {
                    Log.e(TAG, e.getLocalizedMessage(), e);

                } catch (final IOException e) {
                    Log.e(TAG, e.getLocalizedMessage(), e);
                }

                onAccountRemoved(success);
            }
        };

        private final AccountManager mAccountManager;

        private Account mAccount;

        public LogoutHandler(Context context) {
            mAccountManager = AccountManager.get(context);
        }

        public void setAccount(Account account) {
            mAccount = account;
        }

        private Account getAccount() {
            return mAccount;
        }

        private void removeAccount(Account account) {
            if (account == null) {
                Log.e(TAG, "null was passed in, so no accounts were removed");
                onAccountRemoved(false);
                return;
            }

            mAccountManager.removeAccount(account, mAccountManagerCallback, null);
        }

        @Override
        public void onClick(DialogInterface dialog, int which) {
            switch (which) {
            case AlertDialog.BUTTON_POSITIVE:
                removeAccount(getAccount());
                break;
            }
        }

        /**
         * This will be called after the account removal completes. If there is an error, success
         * will be false.
         *
         * @param success
         *            true if the account was successfully removed.
         */
        public abstract void onAccountRemoved(boolean success);

    };

    /**
     * Given a logout handler and information about the app, create a standard logout dialog box
     * that prompts the user if they want to logout.
     *
     * @param context
     * @param appName
     *            the name of your app. This is integrated into the text using the
     *            {@code auth_logout_title} and {@code auth_logout_message} string resources.
     * @param onLogoutHandler
     * @return
     */
    public static Dialog createLogoutDialog(Context context, CharSequence appName, LogoutHandler onLogoutHandler) {

        final AlertDialog.Builder b = new AlertDialog.Builder(context);
        b.setTitle(context.getString(R.string.auth_logout_title, appName));
        b.setMessage(context.getString(R.string.auth_logout_message, appName));
        b.setCancelable(true);
        b.setPositiveButton(R.string.auth_logout, onLogoutHandler);
        b.setNegativeButton(android.R.string.cancel, null);

        return b.create();
    }
}