/* * 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 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.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager.NameNotFoundException; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; 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.Constants; import edu.mit.mobile.android.locast.SettingsActivity; import edu.mit.mobile.android.locast.data.Cast; import edu.mit.mobile.android.locast.data.MediaProvider; import edu.mit.mobile.android.locast.net.NetworkClient; import edu.mit.mobile.android.locast.net.NetworkProtocolException; import edu.mit.mobile.android.locast.ver2.R; /** * Activity which displays login screen to the user. */ public class AuthenticatorActivity extends AccountAuthenticatorActivity implements OnClickListener, OnEditorActionListener { private static final String TAG = AuthenticatorActivity.class.getSimpleName(); public static final String EXTRA_CONFIRMCREDENTIALS = "confirmCredentials", EXTRA_PASSWORD = "password", EXTRA_USERNAME = "username", EXTRA_AUTHTOKEN_TYPE = "authtokenType"; private AccountManager mAccountManager; private String mAuthtoken; private String mAuthtokenType; private static final int DIALOG_PROGRESS = 0, DIALOG_SET_BASE_URL = 1; /** * 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; /** * {@inheritDoc} */ @Override public void onCreate(Bundle icicle) { Log.i(TAG, "onCreate(" + icicle + ")"); super.onCreate(icicle); mAccountManager = AccountManager.get(this); 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); Log.i(TAG, " request new: " + mRequestNewAccount); requestWindowFeature(Window.FEATURE_LEFT_ICON); // make the title based on the app name. setTitle(getString(R.string.login_title, getString(R.string.app_name))); 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); mPasswordEdit = (EditText) findViewById(R.id.password); mPasswordEdit.setOnEditorActionListener(this); findViewById(R.id.login).setOnClickListener(this); findViewById(R.id.cancel).setOnClickListener(this); ((Button) findViewById(R.id.register)).setOnClickListener(this); mUsernameEdit.setText(mUsername); mAuthenticationTask = (AuthenticationTask) getLastNonConfigurationInstance(); if (mAuthenticationTask != null) { mAuthenticationTask.attach(this); } } /* * {@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) { Log.i(TAG, "dialog cancel has been invoked"); if (mAuthenticationTask != null) { mAuthenticationTask.cancel(true); mAuthenticationTask = null; finish(); } } }); return dialog; case DIALOG_SET_BASE_URL: final EditText baseUrl = new EditText(this); baseUrl.setText(getString(R.string.default_api_url)); final AlertDialog.Builder db = new AlertDialog.Builder(this); return db.create(); default: return null; } } @Override public void onClick(View v) { switch (v.getId()) { case R.id.login: handleLogin(); break; case R.id.cancel: finish(); break; case R.id.register: startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.signup_url)))); break; } } /** * 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()) { final String baseUrl = NetworkClient.getBaseUrlFromPreferences(this); mAuthenticationTask = new AuthenticationTask(this); mAuthenticationTask.execute(baseUrl, mUsername, mPassword); } } /** * 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) { Log.i(TAG, "finishConfirmCredentials()"); final Account account = new Account(mUsername, AuthenticationService.ACCOUNT_TYPE); 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) { Log.i(TAG, "finishLogin()"); // ensure that there isn't a demo account sticking around. // TODO this is NOT the place where this code belongs. Find it a better home if (Authenticator.isDemoMode(this)) { Log.d(TAG, "cleaning up demo mode account..."); ContentResolver .cancelSync(Authenticator.getFirstAccount(this), MediaProvider.AUTHORITY); mAccountManager.removeAccount(new Account(Authenticator.DEMO_ACCOUNT, AuthenticationService.ACCOUNT_TYPE), new AccountManagerCallback<Boolean>() { @Override public void run(AccountManagerFuture<Boolean> arg0) { try { if (arg0.getResult()) { final ContentValues cv = new ContentValues(); // invalidate all the content to force a sync. // this is to ensure that items which were marked favorite get set as // such. cv.put(Cast._SERVER_MODIFIED_DATE, 0); cv.put(Cast._MODIFIED_DATE, 0); getContentResolver().update(Cast.CONTENT_URI, cv, null, null); if (Constants.DEBUG) { Log.d(TAG, "reset all cast modified dates to force a reload"); } } } catch (final OperationCanceledException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (final AuthenticatorException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (final IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }, null); } final Account account = new Account(mUsername, AuthenticationService.ACCOUNT_TYPE); if (mRequestNewAccount) { mAccountManager.addAccountExplicitly(account, mPassword, userData); // Automatically enable sync for this account ContentResolver.setSyncAutomatically(account, MediaProvider.AUTHORITY, 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, AuthenticationService.ACCOUNT_TYPE); if (mAuthtokenType != null && mAuthtokenType.equals(AuthenticationService.AUTHTOKEN_TYPE)) { intent.putExtra(AccountManager.KEY_AUTHTOKEN, mAuthtoken); } setAccountAuthenticatorResult(intent.getExtras()); setResult(RESULT_OK, intent); finish(); } 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); } private void setLoginNoticeInfo(int textResID) { mMessage.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_info, 0, 0, 0); mMessage.setText(textResID); mMessage.setVisibility(View.VISIBLE); } /** * Called when the authentication process completes (see attemptLogin()). */ public void onAuthenticationResult(Bundle userData, String reason) { 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(getText(R.string.login_message_login_empty_username)); 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 AuthenticatorActivity mActivity; private String reason; public AuthenticationTask(AuthenticatorActivity activity) { mActivity = activity; } @Override protected void onPreExecute() { mActivity.showDialog(DIALOG_PROGRESS); } @Override protected Bundle doInBackground(String... userPass) { try { return NetworkClient.authenticate(AuthenticatorActivity.this, userPass[0], userPass[1], userPass[2]); } 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; } @Override protected void onPostExecute(Bundle userData) { mActivity.dismissDialog(DIALOG_PROGRESS); mActivity.onAuthenticationResult(userData, reason); } public void detach() { mActivity = null; } public void attach(AuthenticatorActivity activity) { mActivity = activity; } } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.login_options, menu); if (Constants.DEBUG) { menu.findItem(R.id.set_base_url).setVisible(true); } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.set_base_url: startActivity(new Intent(this, SettingsActivity.class)); return true; default: return super.onOptionsItemSelected(item); } } @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { switch (v.getId()) { case R.id.password: handleLogin(); return true; } return false; } public static abstract class LogoutHandler implements DialogInterface.OnClickListener { private final Context mContext; 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); } }; public LogoutHandler(Context context) { mContext = context; } @Override public void onClick(DialogInterface dialog, int which) { switch (which) { case AlertDialog.BUTTON_POSITIVE: AccountManager.get(mContext).removeAccount( Authenticator.getFirstAccount(mContext), mAccountManagerCallback, null); 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); }; public static Dialog createLogoutDialog(Context context, LogoutHandler onLogoutHandler) { final AlertDialog.Builder b = new AlertDialog.Builder(context); final String appName = context.getString(R.string.app_name); 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(); } }