package org.commcare.dalvik.activities;
import java.math.BigInteger;
import java.security.MessageDigest;
import org.commcare.android.database.SqlStorage;
import org.commcare.android.database.app.models.UserKeyRecord;
import org.commcare.android.framework.CommCareActivity;
import org.commcare.android.framework.ManagedUi;
import org.commcare.android.framework.UiElement;
import org.commcare.android.javarosa.AndroidLogger;
import org.commcare.android.models.notifications.NotificationMessage;
import org.commcare.android.models.notifications.NotificationMessageFactory;
import org.commcare.android.models.notifications.NotificationMessageFactory.StockMessages;
import org.commcare.android.tasks.DataPullTask;
import org.commcare.android.tasks.ManageKeyRecordListener;
import org.commcare.android.tasks.ManageKeyRecordTask;
import org.commcare.android.tasks.templates.HttpCalloutTask.HttpCalloutOutcomes;
import org.commcare.android.util.DemoUserUtil;
import org.commcare.android.util.SessionUnavailableException;
import org.commcare.dalvik.R;
import org.commcare.dalvik.application.CommCareApplication;
import org.commcare.dalvik.dialogs.CustomProgressDialog;
import org.commcare.dalvik.preferences.CommCarePreferences;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.locale.Localization;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.text.InputType;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
/**
* @author ctsims
*
*/
@ManagedUi(R.layout.screen_login)
public class LoginActivity extends CommCareActivity<LoginActivity> {
public final static int MENU_DEMO = Menu.FIRST;
public final static String NOTIFICATION_MESSAGE_LOGIN = "login_message";
public static String ALREADY_LOGGED_IN = "la_loggedin";
@UiElement(value=R.id.login_button, locale="login.button")
Button login;
@UiElement(value=R.id.text_username, locale="login.username")
TextView userLabel;
@UiElement(value=R.id.text_password, locale="login.password")
TextView passLabel;
@UiElement(R.id.screen_login_bad_password)
TextView errorBox;
@UiElement(R.id.edit_username)
EditText username;
@UiElement(R.id.edit_password)
EditText password;
@UiElement(R.id.screen_login_banner_pane)
View banner;
@UiElement(R.id.str_version)
TextView versionDisplay;
public static final int TASK_KEY_EXCHANGE = 1;
SqlStorage<UserKeyRecord> storage;
/*
* (non-Javadoc)
* @see org.commcare.android.framework.CommCareActivity#onCreate(android.os.Bundle)
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
username.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
//Only on the initial creation
if(savedInstanceState ==null) {
String lastUser = CommCareApplication._().getCurrentApp().getAppPreferences().getString(CommCarePreferences.LAST_LOGGED_IN_USER, null);
if(lastUser != null) {
username.setText(lastUser);
password.requestFocus();
}
}
login.setOnClickListener(new OnClickListener() {
public void onClick(View arg0) {
errorBox.setVisibility(View.GONE);
//Try logging in locally
if(tryLocalLogin(false)) {
return;
}
startOta();
}
});
versionDisplay.setText(CommCareApplication._().getCurrentVersionString());
final View activityRootView = findViewById(R.id.screen_login_main);
activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
/*
* (non-Javadoc)
* @see android.view.ViewTreeObserver.OnGlobalLayoutListener#onGlobalLayout()
*/
@Override
public void onGlobalLayout() {
int hideAll = LoginActivity.this.getResources().getInteger(R.integer.login_screen_hide_all_cuttoff);
int hideBanner = LoginActivity.this.getResources().getInteger(R.integer.login_screen_hide_banner_cuttoff);
int height = activityRootView.getHeight();
if(height < hideAll) {
versionDisplay.setVisibility(View.GONE);
banner.setVisibility(View.GONE);
} else if(height < hideBanner) {
versionDisplay.setVisibility(View.VISIBLE);
banner.setVisibility(View.GONE);
} else {
versionDisplay.setVisibility(View.VISIBLE);
banner.setVisibility(View.VISIBLE);
}
}
});
}
public String getActivityTitle() {
//TODO: "Login"?
return null;
}
private void startOta() {
//We should go digest auth this user on the server and see whether to pull them
//down.
SharedPreferences prefs = CommCareApplication._().getCurrentApp().getAppPreferences();
// TODO Auto-generated method stub
//TODO: we don't actually always want to do this. We need to have an alternate route where we log in locally and sync
//(with unsent form submissions) more centrally.
DataPullTask<LoginActivity> dataPuller = new DataPullTask<LoginActivity>(getUsername(),
password.getText().toString(),
prefs.getString("ota-restore-url",LoginActivity.this.getString(R.string.ota_restore_url)),
prefs.getString("key_server",LoginActivity.this.getString(R.string.key_server)),
LoginActivity.this) {
/*
* (non-Javadoc)
* @see org.commcare.android.tasks.templates.CommCareTask#deliverResult(java.lang.Object, java.lang.Object)
*/
@Override
protected void deliverResult( LoginActivity receiver, Integer result) {
switch(result) {
case DataPullTask.AUTH_FAILED:
receiver.raiseMessage(NotificationMessageFactory.message(StockMessages.Auth_BadCredentials, new String[3], NOTIFICATION_MESSAGE_LOGIN), false);
break;
case DataPullTask.BAD_DATA:
receiver.raiseMessage(NotificationMessageFactory.message(StockMessages.Remote_BadRestore, new String[3], NOTIFICATION_MESSAGE_LOGIN));
break;
case DataPullTask.DOWNLOAD_SUCCESS:
if(!tryLocalLogin(true)) {
receiver.raiseMessage(NotificationMessageFactory.message(StockMessages.Auth_CredentialMismatch, new String[3], NOTIFICATION_MESSAGE_LOGIN));
} else {
break;
}
case DataPullTask.UNREACHABLE_HOST:
receiver.raiseMessage(NotificationMessageFactory.message(StockMessages.Remote_NoNetwork, new String[3], NOTIFICATION_MESSAGE_LOGIN), true);
break;
case DataPullTask.CONNECTION_TIMEOUT:
receiver.raiseMessage(NotificationMessageFactory.message(StockMessages.Remote_Timeout, new String[3], NOTIFICATION_MESSAGE_LOGIN), true);
break;
case DataPullTask.SERVER_ERROR:
receiver.raiseMessage(NotificationMessageFactory.message(StockMessages.Remote_ServerError, new String[3], NOTIFICATION_MESSAGE_LOGIN), true);
break;
case DataPullTask.UNKNOWN_FAILURE:
receiver.raiseMessage(NotificationMessageFactory.message(StockMessages.Restore_Unknown, new String[3], NOTIFICATION_MESSAGE_LOGIN), true);
break;
}
}
/*
* (non-Javadoc)
* @see org.commcare.android.tasks.templates.CommCareTask#deliverUpdate(java.lang.Object, java.lang.Object[])
*/
@Override
protected void deliverUpdate( LoginActivity receiver, Integer... update) {
if(update[0] == DataPullTask.PROGRESS_STARTED) {
receiver.updateProgress(Localization.get("sync.progress.purge"), DataPullTask.DATA_PULL_TASK_ID);
} else if(update[0] == DataPullTask.PROGRESS_CLEANED) {
receiver.updateProgress(Localization.get("sync.progress.authing"), DataPullTask.DATA_PULL_TASK_ID);
} else if(update[0] == DataPullTask.PROGRESS_AUTHED) {
receiver.updateProgress(Localization.get("sync.progress.downloading"), DataPullTask.DATA_PULL_TASK_ID);
} else if(update[0] == DataPullTask.PROGRESS_RECOVERY_NEEDED) {
receiver.updateProgress(Localization.get("sync.recover.needed"), DataPullTask.DATA_PULL_TASK_ID);
} else if(update[0] == DataPullTask.PROGRESS_RECOVERY_STARTED) {
receiver.updateProgress(Localization.get("sync.recover.started"), DataPullTask.DATA_PULL_TASK_ID);
}
}
/*
* (non-Javadoc)
* @see org.commcare.android.tasks.templates.CommCareTask#deliverError(java.lang.Object, java.lang.Exception)
*/
@Override
protected void deliverError( LoginActivity receiver, Exception e) {
receiver.raiseMessage(NotificationMessageFactory.message(StockMessages.Restore_Unknown, new String[3], NOTIFICATION_MESSAGE_LOGIN), true);
}
};
dataPuller.connect(this);
dataPuller.execute();
}
/*
* (non-Javadoc)
*
* @see android.app.Activity#onResume()
*/
@Override
protected void onResume() {
super.onResume();
try {
//TODO: there is a weird circumstance where we're logging in somewhere else and this gets locked.
if(CommCareApplication._().getSession().isLoggedIn() && CommCareApplication._().getSession().getLoggedInUser() != null) {
Intent i = new Intent();
i.putExtra(ALREADY_LOGGED_IN, true);
setResult(RESULT_OK, i);
CommCareApplication._().clearNotifications(NOTIFICATION_MESSAGE_LOGIN);
finish();
return;
}
}catch(SessionUnavailableException sue) {
//Nothing, we're logging in here anyway
}
refreshView();
}
private void refreshView() {
}
private String getUsername() {
return username.getText().toString().toLowerCase().trim();
}
private boolean tryLocalLogin(final boolean warnMultipleAccounts) {
//TODO: check username/password for emptiness
return tryLocalLogin(getUsername(), password.getText().toString(), warnMultipleAccounts);
}
private boolean tryLocalLogin(final String username, String password, final boolean warnMultipleAccounts) {
try{
//TODO: We don't actually even use this anymore other than for hte local login count, which
//seems super silly.
UserKeyRecord matchingRecord = null;
int count = 0;
for(UserKeyRecord record : storage()) {
if(!record.getUsername().equals(username)) {
continue;
}
count++;
String hash = record.getPasswordHash();
if(hash.contains("$")) {
String alg = "sha1";
String salt = hash.split("\\$")[1];
String check = hash.split("\\$")[2];
MessageDigest md = MessageDigest.getInstance("SHA-1");
BigInteger number = new BigInteger(1, md.digest((salt+password).getBytes()));
String hashed = number.toString(16);
while(hashed.length() < check.length()) {
hashed = "0" + hashed;
}
if(hash.equals(alg + "$" + salt + "$" + hashed)) {
matchingRecord = record;
}
}
}
final boolean triggerTooManyUsers = count > 1 && warnMultipleAccounts;
ManageKeyRecordTask<LoginActivity> task = new ManageKeyRecordTask<LoginActivity>(this, TASK_KEY_EXCHANGE, username, password, CommCareApplication._().getCurrentApp(), new ManageKeyRecordListener<LoginActivity>() {
@Override
public void keysLoginComplete(LoginActivity r) {
if(triggerTooManyUsers) {
//We've successfully pulled down new user data.
//Should see if the user already has a sandbox and let them know that their old data doesn't transition
r.raiseMessage(NotificationMessageFactory.message(StockMessages.Auth_RemoteCredentialsChanged, new String[3]), true);
Logger.log(AndroidLogger.TYPE_USER, "User " + username + " has logged in for the first time with a new password. They may have unsent data in their other sandbox");
}
r.done();
}
@Override
public void keysReadyForSync(LoginActivity r) {
//TODO: we only wanna do this on the _first_ try. Not subsequent ones (IE: On return from startOta)
r.startOta();
}
@Override
public void keysDoneOther(LoginActivity r, HttpCalloutOutcomes outcome) {
switch(outcome) {
case AuthFailed:
Logger.log(AndroidLogger.TYPE_USER, "auth failed");
r.raiseMessage(NotificationMessageFactory.message(StockMessages.Auth_BadCredentials, new String[3], NOTIFICATION_MESSAGE_LOGIN), false);
break;
case BadResponse:
Logger.log(AndroidLogger.TYPE_USER, "bad response");
r.raiseMessage(NotificationMessageFactory.message(StockMessages.Remote_BadRestore, new String[3], NOTIFICATION_MESSAGE_LOGIN), true);
break;
case NetworkFailure:
Logger.log(AndroidLogger.TYPE_USER, "bad network");
r.raiseMessage(NotificationMessageFactory.message(StockMessages.Remote_NoNetwork, new String[3], NOTIFICATION_MESSAGE_LOGIN), false);
break;
case BadCertificate:
Logger.log(AndroidLogger.TYPE_USER, "bad certificate");
r.raiseMessage(NotificationMessageFactory.message(StockMessages.BadSSLCertificate, new String[3], NOTIFICATION_MESSAGE_LOGIN), false);
break;
case UnkownError:
Logger.log(AndroidLogger.TYPE_USER, "unknown");
r.raiseMessage(NotificationMessageFactory.message(StockMessages.Restore_Unknown, new String[3], NOTIFICATION_MESSAGE_LOGIN), true);
break;
}
}
}) {
/*
* (non-Javadoc)
* @see org.commcare.android.tasks.templates.CommCareTask#deliverUpdate(java.lang.Object, java.lang.Object[])
*/
@Override
protected void deliverUpdate(LoginActivity receiver, String... update) {
receiver.updateProgress(update[0], TASK_KEY_EXCHANGE);
}
};
task.connect(this);
task.execute();
return true;
}catch (Exception e) {
e.printStackTrace();
return false;
}
}
private void done() {
Intent i = new Intent();
setResult(RESULT_OK, i);
CommCareApplication._().clearNotifications(NOTIFICATION_MESSAGE_LOGIN);
finish();
}
private SqlStorage<UserKeyRecord> storage() throws SessionUnavailableException{
if(storage == null) {
storage= CommCareApplication._().getAppStorage(UserKeyRecord.class);
}
return storage;
}
public void finished(int status) {
}
/* (non-Javadoc)
* @see android.app.Activity#onCreateOptionsMenu(android.view.Menu)
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
menu.add(0, MENU_DEMO, 0, Localization.get("login.menu.demo")).setIcon(android.R.drawable.ic_menu_preferences);
return true;
}
/* (non-Javadoc)
* @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
boolean otherResult = super.onOptionsItemSelected(item);
switch(item.getItemId()) {
case MENU_DEMO:
//Make sure we have a demo user
DemoUserUtil.checkOrCreateDemoUser(this, CommCareApplication._().getCurrentApp());
//Now try to log in as the demo user
tryLocalLogin(DemoUserUtil.DEMO_USER, DemoUserUtil.DEMO_USER, false);
return true;
default:
return otherResult;
}
}
private void raiseMessage(NotificationMessage message) {
raiseMessage(message, true);
}
private void raiseMessage(NotificationMessage message, boolean showTop) {
String toastText = message.getTitle();
if(showTop) {
CommCareApplication._().reportNotificationMessage(message);
toastText = Localization.get("notification.for.details.wrapper", new String[] {toastText});
}
//either way
errorBox.setVisibility(View.VISIBLE);
errorBox.setText(toastText);
Toast.makeText(this,toastText, Toast.LENGTH_LONG).show();
}
/*
* (non-Javadoc)
* @see org.commcare.android.framework.CommCareActivity#generateProgressDialog(int)
*
* Implementation of generateProgressDialog() for DialogController -- other methods
* handled entirely in CommCareActivity
*/
@Override
public CustomProgressDialog generateProgressDialog(int taskId) {
CustomProgressDialog dialog;
switch (taskId) {
case TASK_KEY_EXCHANGE:
dialog = CustomProgressDialog.newInstance(Localization.get("key.manage.title"),
Localization.get("key.manage.start"), taskId);
break;
case DataPullTask.DATA_PULL_TASK_ID:
dialog = CustomProgressDialog.newInstance(Localization.get("sync.progress.title"),
Localization.get("sync.progress.starting"), taskId);
dialog.addCancelButton();
break;
default:
System.out.println("WARNING: taskId passed to generateProgressDialog does not match "
+ "any valid possibilities in LoginActivity");
return null;
}
return dialog;
}
}