Java tutorial
/* * 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 org.ohmage.authenticator; import android.accounts.Account; import android.accounts.AccountManager; import android.app.AlertDialog; import android.app.Dialog; import android.app.ProgressDialog; import android.content.ContentResolver; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.webkit.URLUtil; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; import org.ohmage.AccountHelper; import org.ohmage.ConfigHelper; import org.ohmage.NotificationHelper; import org.ohmage.OhmageApi.AuthenticateResponse; import org.ohmage.OhmageApi.CampaignReadResponse; import org.ohmage.OhmageApplication; import org.ohmage.PreferenceStore; import org.ohmage.UserPreferencesHelper; import org.ohmage.Utilities; import org.ohmage.activity.DashboardActivity; import org.ohmage.activity.PasswordChangeActivity; import org.ohmage.async.CampaignReadTask; import org.ohmage.db.DbContract; import org.ohmage.db.Models.Campaign; import org.ohmage.db.utils.Lists; import org.ohmage.library.R; import org.ohmage.logprobe.Analytics; import org.ohmage.logprobe.LogProbe.Status; import java.net.URI; import java.util.ArrayList; /** * Activity which displays login screen to the user. */ public class AuthenticatorActivity extends AccountAuthenticatorFragmentActivity implements LoaderCallbacks<CampaignReadResponse> { private static final String TAG = "AuthenticatorActivity"; private static final int LOGIN_FINISHED = 0; private static final int PASSWORD_CHANGE = 1; private static final int DIALOG_LOGIN_ERROR = 1; private static final int DIALOG_NETWORK_ERROR = 2; private static final int DIALOG_LOGIN_PROGRESS = 3; private static final int DIALOG_INTERNAL_ERROR = 4; private static final int DIALOG_USER_DISABLED = 5; private static final int DIALOG_DOWNLOADING_CAMPAIGNS = 6; private static final int DIALOG_SERVER_LIST = 7; public static final String PARAM_CONFIRMCREDENTIALS = "confirmCredentials"; public static final String PARAM_PASSWORD = "password"; /** * The {@link AuthenticatorActivity} looks for this extra to determine if it * should update the credentials for the user */ public static final String PARAM_USERNAME = "username"; public static final String PARAM_AUTHTOKEN_TYPE = "authtokenType"; private static final String KEY_OHMAGE_SERVER = "key_ohmage_server"; private AccountManager mAccountManager; private Thread mAuthThread; private String mAuthtoken; private String mAuthtokenType; /** * 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; /** for posting authentication attempts back to UI thread */ private final Handler mHandler = new Handler(); 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 EditText mServerEdit; private PreferenceStore mPreferencesHelper; private String mHashedPassword; /** * {@inheritDoc} */ @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); mAccountManager = AccountManager.get(this); final Intent intent = getIntent(); mUsername = intent.getStringExtra(PARAM_USERNAME); mAuthtokenType = intent.getStringExtra(PARAM_AUTHTOKEN_TYPE); // If we are just logging in regularly, we need to set the authtoken // type if (mAuthtokenType == null) { mAuthtokenType = OhmageApplication.AUTHTOKEN_TYPE; } mRequestNewAccount = mUsername == null; mConfirmCredentials = intent.getBooleanExtra(PARAM_CONFIRMCREDENTIALS, false); mPreferencesHelper = new PreferenceStore(this); if (mPreferencesHelper.isUserDisabled()) { ((OhmageApplication) getApplication()).resetAll(); } // if they are, redirect them to the dashboard if (AccountHelper.accountExists() && !mConfirmCredentials) { startActivityForResult(new Intent(this, DashboardActivity.class), LOGIN_FINISHED); return; } setContentView(R.layout.login); mMessage = (TextView) findViewById(R.id.version); mUsernameEdit = (EditText) findViewById(R.id.login_username); mPasswordEdit = (EditText) findViewById(R.id.login_password); mServerEdit = (EditText) findViewById(R.id.login_server_edit); if (mConfirmCredentials) { mUsernameEdit.setEnabled(false); mPasswordEdit.requestFocus(); } mUsernameEdit.setText(mUsername); mMessage.setText(getVersion()); boolean showRegisterLink = getResources().getBoolean(R.bool.allow_user_registration); TextView registerAccountLink = (TextView) findViewById(R.id.login_register_new_account); registerAccountLink.setVisibility(showRegisterLink ? View.VISIBLE : View.GONE); registerAccountLink.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // reads the currently selected server and fires a browser // intent which takes the user to the registration page for that // server if (ensureServerUrl()) { // use the textbox to make a url String url = mServerEdit.getText().toString().split(" ")[0] + "#register"; Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(url)); startActivity(i); } else Toast.makeText(v.getContext(), R.string.login_invalid_server, Toast.LENGTH_SHORT).show(); } }); if (getResources().getBoolean(R.bool.allow_custom_server)) { View serverContainer = findViewById(R.id.login_server_container); serverContainer.setVisibility(View.VISIBLE); } String defaultServer = ConfigHelper.serverUrl(); if (TextUtils.isEmpty(defaultServer)) defaultServer = getResources().getStringArray(R.array.servers)[0]; mServerEdit.setText(defaultServer); ensureServerUrl(); mServerEdit.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (!hasFocus) { ensureServerUrl(); } } }); ImageButton addServer = (ImageButton) findViewById(R.id.login_add_server); addServer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ensureServerUrl(); showDialog(DIALOG_SERVER_LIST); } }); } @Override protected void onPause() { super.onPause(); Analytics.activity(this, Status.OFF); } @Override public void onResume() { super.onResume(); Analytics.activity(this, Status.ON); // Hide any notifications since we started the login activity NotificationHelper.hideAuthNotification(this); NotificationHelper.hideAccountDisabledNotification(this); } private CharSequence getVersion() { try { return "v" + Utilities.getVersion(this); } catch (Exception e) { Log.e(TAG, "unable to retrieve version", e); return null; } } /** * The easiest way to make sure the progress dialog is hidden when it is * supposed to be is to have a static reference to it... */ private static ProgressDialog pDialog; @Override public void onDestroy() { super.onDestroy(); pDialog = null; getSupportLoaderManager().destroyLoader(0); } @Override protected Dialog onCreateDialog(int id) { Dialog dialog = super.onCreateDialog(id); AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); switch (id) { case DIALOG_LOGIN_ERROR: dialogBuilder.setTitle(R.string.login_error).setMessage(R.string.login_auth_error).setCancelable(true) .setPositiveButton(R.string.ok, null) /* * .setNeutralButton("Help", new * DialogInterface.OnClickListener() { * @Override public void onClick(DialogInterface dialog, int * which) { startActivity(new Intent(LoginActivity.this, * HelpActivity.class)); //put extras for specific help on login * error } }) */; // add button for contact dialog = dialogBuilder.create(); break; case DIALOG_USER_DISABLED: dialogBuilder.setTitle(R.string.login_error).setMessage(R.string.login_account_disabled) .setCancelable(true).setPositiveButton(R.string.ok, null) /* * .setNeutralButton("Help", new * DialogInterface.OnClickListener() { * @Override public void onClick(DialogInterface dialog, int * which) { startActivity(new Intent(LoginActivity.this, * HelpActivity.class)); //put extras for specific help on login * error } }) */; // add button for contact dialog = dialogBuilder.create(); break; case DIALOG_NETWORK_ERROR: dialogBuilder.setTitle(R.string.login_error).setMessage(R.string.login_network_error) .setCancelable(true).setPositiveButton(R.string.ok, null) /* * .setNeutralButton("Help", new * DialogInterface.OnClickListener() { * @Override public void onClick(DialogInterface dialog, int * which) { startActivity(new Intent(LoginActivity.this, * HelpActivity.class)); //put extras for specific help on http * error } }) */; // add button for contact dialog = dialogBuilder.create(); break; case DIALOG_INTERNAL_ERROR: dialogBuilder.setTitle(R.string.login_error).setMessage(R.string.login_server_error).setCancelable(true) .setPositiveButton(R.string.ok, null) /* * .setNeutralButton("Help", new * DialogInterface.OnClickListener() { * @Override public void onClick(DialogInterface dialog, int * which) { startActivity(new Intent(LoginActivity.this, * HelpActivity.class)); //put extras for specific help on http * error } }) */; // add button for contact dialog = dialogBuilder.create(); break; case DIALOG_LOGIN_PROGRESS: { pDialog = new ProgressDialog(this); pDialog.setMessage(getString(R.string.login_authenticating, getString(R.string.server_name))); pDialog.setIndeterminate(true); pDialog.setCancelable(false); dialog = pDialog; break; } case DIALOG_DOWNLOADING_CAMPAIGNS: { ProgressDialog pDialog = new ProgressDialog(this); pDialog.setMessage(getString(R.string.login_download_campaign)); pDialog.setCancelable(false); // pDialog.setIndeterminate(true); dialog = pDialog; break; } case DIALOG_SERVER_LIST: { AlertDialog.Builder builder = new AlertDialog.Builder(this); ArrayList<String> servers = Lists.newArrayList(getResources().getStringArray(R.array.servers)); if (OhmageApplication.DEBUG_BUILD) { servers.add("https://test.ohmage.org/"); } final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.simple_list_item_1, servers); builder.setTitle(R.string.login_choose_server); builder.setAdapter(adapter, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { mServerEdit .setText(((AlertDialog) dialog).getListView().getAdapter().getItem(which).toString()); } }); dialog = builder.create(); break; } } return dialog; } /** * Handles onClick event on the Submit button. Sends username/password to * the server for authentication. * * @param view The Submit button for which this method is invoked */ public void handleLogin(View view) { Analytics.widget(view); if (!ensureServerUrl()) { Toast.makeText(this, R.string.login_invalid_server, Toast.LENGTH_SHORT).show(); return; } String server = mServerEdit.getText().toString(); ConfigHelper.setServerUrl(server.split("\\(")[0].trim()); ((OhmageApplication) getApplication()).updateLogLevel(); if (mRequestNewAccount) { mUsername = mUsernameEdit.getText().toString(); } mPassword = mPasswordEdit.getText().toString(); if (!TextUtils.isEmpty(mUsername) && !TextUtils.isEmpty(mPassword)) { showDialog(DIALOG_LOGIN_PROGRESS); // Start authenticating... mAuthThread = AuthenticationUtilities.attemptAuth(mUsername, mPassword, mHandler, AuthenticatorActivity.this); } } /** * 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.v(TAG, "finishConfirmCredentials()"); final Account account = new Account(mUsername, OhmageApplication.ACCOUNT_TYPE); mAccountManager.setPassword(account, mPassword); if (mAuthtokenType != null && mAuthtokenType.equals(OhmageApplication.AUTHTOKEN_TYPE)) { mAccountManager.setAuthToken(account, mAuthtokenType, mHashedPassword); } 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 the confirmCredentials result. */ protected void createAccount() { Log.v(TAG, "finishLogin()"); final Account account = new Account(mUsername, OhmageApplication.ACCOUNT_TYPE); Bundle userData = new Bundle(); userData.putString(KEY_OHMAGE_SERVER, ConfigHelper.serverUrl()); mAuthtoken = mHashedPassword; if (TextUtils.isEmpty(mAuthtoken)) { Log.w(TAG, "Trying to create account with empty password"); return; } if (mRequestNewAccount) { mAccountManager.addAccountExplicitly(account, mPassword, userData); mAccountManager.setAuthToken(account, OhmageApplication.AUTHTOKEN_TYPE, mAuthtoken); // Set sync for this account. ContentResolver.setIsSyncable(account, DbContract.CONTENT_AUTHORITY, 1); ContentResolver.setSyncAutomatically(account, DbContract.CONTENT_AUTHORITY, true); ContentResolver.addPeriodicSync(account, DbContract.CONTENT_AUTHORITY, new Bundle(), 3600); } else { mAccountManager.setPassword(account, mPassword); } final Intent intent = new Intent(); intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, mUsername); intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, OhmageApplication.ACCOUNT_TYPE); if (mAuthtokenType != null && mAuthtokenType.equals(OhmageApplication.AUTHTOKEN_TYPE)) { intent.putExtra(AccountManager.KEY_AUTHTOKEN, mAuthtoken); } setAccountAuthenticatorResult(intent.getExtras()); setResult(RESULT_OK, intent); mPreferencesHelper.edit().putLoginTimestamp(System.currentTimeMillis()).commit(); if (UserPreferencesHelper.isSingleCampaignMode()) { // Download the single campaign showDialog(DIALOG_DOWNLOADING_CAMPAIGNS); getSupportLoaderManager().restartLoader(0, null, this); } else { finishLogin(); } } protected void finishLogin() { if (mConfirmCredentials) finish(); else startActivityForResult(new Intent(this, DashboardActivity.class), LOGIN_FINISHED); } /** * Called when the authentication process completes (see attemptLogin()). */ public void onAuthenticationResult(AuthenticateResponse response) { try { dismissDialog(DIALOG_LOGIN_PROGRESS); } catch (IllegalArgumentException e) { Log.e(TAG, "Attempting to dismiss dialog that had not been shown."); e.printStackTrace(); if (pDialog != null) pDialog.dismiss(); } switch (response.getResult()) { case SUCCESS: Log.v(TAG, "login success"); mHashedPassword = response.getHashedPassword(); if (!mConfirmCredentials) { createAccount(); } else { finishConfirmCredentials(true); } break; case FAILURE: if (response.getErrorCodes().contains("0202")) { Log.v(TAG, "password change required"); Intent intent = new Intent(this, PasswordChangeActivity.class); intent.putExtra(PasswordChangeActivity.ACCOUNT_NAME, mUsername); intent.putExtra(PasswordChangeActivity.OLD_PASSWORD, mPassword); startActivityForResult(intent, PASSWORD_CHANGE); break; } else { Log.e(TAG, "login failure: " + response.getErrorCodes()); } // show error dialog if (response.getErrorCodes().contains("0201")) { mPreferencesHelper.edit().setUserDisabled(true).commit(); showDialog(DIALOG_USER_DISABLED); } else { showDialog(DIALOG_LOGIN_ERROR); } break; case HTTP_ERROR: Log.w(TAG, "login http error"); // show error dialog showDialog(DIALOG_NETWORK_ERROR); break; case INTERNAL_ERROR: Log.e(TAG, "login internal error"); // show error dialog showDialog(DIALOG_INTERNAL_ERROR); break; } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case LOGIN_FINISHED: finish(); break; case PASSWORD_CHANGE: if (resultCode == RESULT_OK && data != null && data.hasExtra(PasswordChangeActivity.NEW_PASSWORD)) { mHashedPassword = data.getStringExtra(PasswordChangeActivity.NEW_PASSWORD); createAccount(); } else { Toast.makeText(this, R.string.change_password_unsuccessful, Toast.LENGTH_SHORT).show(); } break; default: this.onActivityResult(requestCode, resultCode, data); } } /** * Ensures that the server url provided is valid. Once it is made valid, it * is set as the server url. * * @return */ private boolean ensureServerUrl() { String text = mServerEdit.getText().toString(); if (TextUtils.isEmpty(text)) return false; // Assume they want https by default URI uri = URI.create(text.split(" ")[0]); if (uri.getScheme() == null) { text = "https://" + text; } text = URLUtil.guessUrl(text); if (URLUtil.isHttpsUrl(text) || URLUtil.isHttpUrl(text)) { mServerEdit.setText(text); return true; } return false; } @Override public Loader<CampaignReadResponse> onCreateLoader(int id, Bundle args) { return new CampaignReadTask(AuthenticatorActivity.this); } @Override public void onLoadFinished(Loader<CampaignReadResponse> loader, CampaignReadResponse data) { String urn = Campaign.getSingleCampaign(AuthenticatorActivity.this); if (urn == null) { Toast.makeText(AuthenticatorActivity.this, R.string.login_error_downloading_campaign, Toast.LENGTH_LONG) .show(); } else { createAccount(); } dismissDialog(DIALOG_DOWNLOADING_CAMPAIGNS); finishLogin(); } @Override public void onLoaderReset(Loader<CampaignReadResponse> loader) { } }