/*
* 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;
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.BackgroundManager;
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.R;
import org.ohmage.UserPreferencesHelper;
import org.ohmage.activity.DashboardActivity;
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.logprobe.Analytics;
import org.ohmage.logprobe.LogProbe.Status;
import java.util.ArrayList;
import java.util.Arrays;
/**
* Activity which displays login screen to the user.
*/
public class AuthenticatorActivity extends AccountAuthenticatorFragmentActivity {
private static final String TAG = "AuthenticatorActivity";
private static final int LOGIN_FINISHED = 0;
private static final int DIALOG_FIRST_RUN = 1;
private static final int DIALOG_LOGIN_ERROR = 2;
private static final int DIALOG_NETWORK_ERROR = 3;
private static final int DIALOG_LOGIN_PROGRESS = 4;
private static final int DIALOG_INTERNAL_ERROR = 5;
private static final int DIALOG_USER_DISABLED = 6;
private static final int DIALOG_DOWNLOADING_CAMPAIGNS = 7;
private static final int DIALOG_SERVER_LIST = 8;
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 UserPreferencesHelper mPreferencesHelper;
private ConfigHelper mAppPrefs;
private CampaignReadTask mCampaignDownloadTask;
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 UserPreferencesHelper(this);
mAppPrefs = new ConfigHelper(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());
TextView registerAccountLink = (TextView) findViewById(R.id.login_register_new_account);
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);
}
});
mCampaignDownloadTask = (CampaignReadTask) getSupportLoaderManager().initLoader(0, null,
new LoaderManager.LoaderCallbacks<CampaignReadResponse>() {
@Override
public Loader<CampaignReadResponse> onCreateLoader(int id, Bundle args) {
return new CampaignReadTask(AuthenticatorActivity.this, null, null);
}
@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 {
finishLogin();
}
dismissDialog(DIALOG_DOWNLOADING_CAMPAIGNS);
}
@Override
public void onLoaderReset(Loader<CampaignReadResponse> loader) {
}
});
}
@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);
}
private CharSequence getVersion() {
try {
return "v" + getPackageManager().getPackageInfo("org.ohmage", 0).versionName;
} 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_FIRST_RUN:
dialogBuilder
.setTitle(R.string.eula_title)
.setMessage(R.string.eula_text)
.setCancelable(false)
.setPositiveButton(R.string.eula_accept,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mAppPrefs.setFirstRun(false);
}
})
.setNegativeButton(R.string.eula_cancel,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
AuthenticatorActivity.this.finish();
}
});
dialog = dialogBuilder.create();
break;
case DIALOG_LOGIN_ERROR:
dialogBuilder.setTitle(R.string.login_error)
.setMessage(R.string.login_invalid_password).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(true);
pDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
if (mAuthThread != null) {
mAuthThread.interrupt();
finish();
}
}
});
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.isDebugBuild()) {
servers.add("https://test.ohmage.org/");
servers.add("https://dev.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());
configureForDeployment(server);
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 finishLogin() {
Log.v(TAG, "finishLogin()");
final Account account = new Account(mUsername, OhmageApplication.ACCOUNT_TYPE);
Bundle userData = new Bundle();
userData.putString(KEY_OHMAGE_SERVER, mServerEdit.getText().toString());
mAuthtoken = mHashedPassword;
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);
if (mAppPrefs.isFirstRun()) {
Log.v(TAG, "this is the first run");
BackgroundManager.initComponents(this);
// cancel get started notification. this works regardless of how we
// start the app (notification or launcher)
// NotificationHelper.cancel(this,
// NotificationHelper.NOTIFY_GET_STARTED, null);
// show intro dialog
// showDialog(DIALOG_FIRST_RUN);
mAppPrefs.setFirstRun(false);
}
mPreferencesHelper.putLoginTimestamp(System.currentTimeMillis());
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) {
if (ConfigHelper.isSingleCampaignMode()) {
final String hashedPassword = response.getHashedPassword();
// Download the single campaign
showDialog(DIALOG_DOWNLOADING_CAMPAIGNS);
mCampaignDownloadTask.setCredentials(mUsername, mHashedPassword);
mCampaignDownloadTask.forceLoad();
} else {
finishLogin();
}
} else {
finishConfirmCredentials(true);
}
break;
case FAILURE:
Log.e(TAG, "login failure: " + response.getErrorCodes());
// show error dialog
if (Arrays.asList(response.getErrorCodes()).contains("0201")) {
mPreferencesHelper.setUserDisabled(true);
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;
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();
text = URLUtil.guessUrl(text);
if (URLUtil.isHttpsUrl(text) || URLUtil.isHttpUrl(text)) {
mServerEdit.setText(text);
return true;
}
return false;
}
/**
* Configures some settings based on the deployment. Looks at the server url
* and deployment name to figure out what the settings should be
*
* @param server
*/
private void configureForDeployment(String server) {
if (server == null)
return;
server = server.split(" ")[0];
ConfigHelper config = new ConfigHelper(this);
if ("https://lausd.mobilizingcs.org/".equals(server)) {
mPreferencesHelper.setShowFeedback(true);
mPreferencesHelper.setShowMobility(false);
mPreferencesHelper.setUploadResponsesWifiOnly(false);
mPreferencesHelper.setUploadProbesWifiOnly(true);
config.setAdminMode(false);
config.setLogLevel("verbose");
config.setLogAnalytics(true);
((OhmageApplication) getApplication()).updateLogLevel();
} else if ("https://pilots.mobilizelabs.org/".equals(server)) {
mPreferencesHelper.setShowFeedback(true);
mPreferencesHelper.setShowMobility(false);
mPreferencesHelper.setUploadResponsesWifiOnly(false);
mPreferencesHelper.setUploadProbesWifiOnly(true);
config.setAdminMode(false);
config.setLogLevel("error");
config.setLogAnalytics(false);
((OhmageApplication) getApplication()).updateLogLevel();
} else if ("https://dev.ohmage.org/".equals(server)
|| "https://test.ohmage.org/".equals(server)) {
mPreferencesHelper.setShowFeedback(true);
mPreferencesHelper.setShowMobility(true);
mPreferencesHelper.setUploadResponsesWifiOnly(false);
mPreferencesHelper.setUploadProbesWifiOnly(false);
config.setAdminMode(true);
config.setLogLevel("verbose");
config.setLogAnalytics(true);
((OhmageApplication) getApplication()).updateLogLevel();
} else if ("https://play.ohmage.org/".equals(server)) {
mPreferencesHelper.setShowFeedback(true);
mPreferencesHelper.setShowMobility(true);
mPreferencesHelper.setUploadResponsesWifiOnly(false);
mPreferencesHelper.setUploadProbesWifiOnly(true);
config.setAdminMode(true);
config.setLogLevel("error");
config.setLogAnalytics(false);
((OhmageApplication) getApplication()).updateLogLevel();
}
}
}