package org.ovirt.mobile.movirt.ui.auth;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountManager;
import android.app.DialogFragment;
import android.content.ContentResolver;
import android.content.Intent;
import android.graphics.Color;
import android.text.InputType;
import android.text.TextUtils;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.MultiAutoCompleteTextView;
import android.widget.ProgressBar;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Background;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.InstanceState;
import org.androidannotations.annotations.Receiver;
import org.androidannotations.annotations.UiThread;
import org.androidannotations.annotations.ViewById;
import org.ovirt.mobile.movirt.Broadcasts;
import org.ovirt.mobile.movirt.R;
import org.ovirt.mobile.movirt.auth.MovirtAuthenticator;
import org.ovirt.mobile.movirt.auth.properties.AccountProperty;
import org.ovirt.mobile.movirt.auth.properties.PropertyChangedListener;
import org.ovirt.mobile.movirt.auth.properties.manager.AccountPropertiesManager;
import org.ovirt.mobile.movirt.auth.properties.manager.OnThread;
import org.ovirt.mobile.movirt.provider.OVirtContract;
import org.ovirt.mobile.movirt.provider.ProviderFacade;
import org.ovirt.mobile.movirt.rest.client.LoginClient;
import org.ovirt.mobile.movirt.sync.EventsHandler;
import org.ovirt.mobile.movirt.sync.SyncUtils;
import org.ovirt.mobile.movirt.ui.UiUtils;
import org.ovirt.mobile.movirt.ui.dialogs.ApiPathDialogFragment;
import org.ovirt.mobile.movirt.util.message.CreateDialogBroadcastReceiver;
import org.ovirt.mobile.movirt.util.message.CreateDialogBroadcastReceiverHelper;
import org.ovirt.mobile.movirt.util.message.ErrorType;
import org.ovirt.mobile.movirt.util.message.MessageHelper;
import org.springframework.http.HttpStatus;
import org.springframework.web.client.HttpClientErrorException;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Arrays;
import javax.net.ssl.SSLHandshakeException;
@EActivity(R.layout.activity_authenticator)
public class AuthenticatorActivity extends AccountAuthenticatorActivity implements CreateDialogBroadcastReceiver {
private static final String TAG = AuthenticatorActivity.class.getSimpleName();
private static final String[] URL_COMPLETE = {"http://", "https://", "ovirt-engine/api", "80",
"443", "api"};
private static final String[] USERNAME_COMPLETE = {"admin@", "internal", "admin@internal"};
private static volatile boolean inProgress;
public static final String SHOW_ADVANCED_AUTHENTICATOR = "SHOW_ADVANCED_AUTHENTICATOR";
@Bean
AccountPropertiesManager propertiesManager;
@Bean
LoginClient loginClient;
@ViewById
MultiAutoCompleteTextView txtEndpoint;
@ViewById
MultiAutoCompleteTextView txtUsername;
@ViewById
EditText txtPassword;
@ViewById
ImageView passwordVisibility;
@ViewById
CheckBox chkAdminPriv;
@ViewById
ProgressBar authProgress;
@ViewById
Button btnCreate;
@ViewById
Button btnAdvanced;
@Bean
MovirtAuthenticator authenticator;
@Bean
SyncUtils syncUtils;
@Bean
EventsHandler eventsHandler;
@Bean
ProviderFacade providerFacade;
@InstanceState
URL endpointUrl;
@InstanceState
String username;
@InstanceState
String password;
@InstanceState
Boolean adminPriv;
@InstanceState
String endpoint;
@Bean
MessageHelper messageHelper;
private PropertyChangedListener[] listeners;
@AfterViews
void init() {
if (!propertiesManager.accountConfigured()) {
if (authenticator.initAccount("")) {
messageHelper.showToast("Added new account.");
}
}
txtEndpoint.setText(propertiesManager.getApiUrl());
ArrayAdapter<String> urlAdapter = new ArrayAdapter<>(this,
android.R.layout.simple_dropdown_item_1line, URL_COMPLETE);
txtEndpoint.setAdapter(urlAdapter);
txtEndpoint.setTokenizer(UiUtils.getUrlTokenizer());
txtUsername.setText(propertiesManager.getUsername());
ArrayAdapter<String> usernameAdapter = new ArrayAdapter<>(this,
android.R.layout.simple_dropdown_item_1line, USERNAME_COMPLETE);
txtUsername.setAdapter(usernameAdapter);
txtUsername.setTokenizer(UiUtils.getUsernameTokenizer());
txtPassword.setText(propertiesManager.getPassword());
chkAdminPriv.setChecked(true);
loginProgress(inProgress);
if (getIntent().getBooleanExtra(SHOW_ADVANCED_AUTHENTICATOR, false)) {
btnAdvancedClicked();
}
initViewListeners();
initPropertyListeners();
}
private void initPropertyListeners() {
final AccountProperty.PasswordVisibilityListener passVisibilityListener = new AccountProperty.PasswordVisibilityListener() {
@Override
public void onPropertyChange(Boolean passwordVisibility) {
setPasswordVisibility(passwordVisibility);
}
};
listeners = new PropertyChangedListener[]{passVisibilityListener};
propertiesManager.notifyAndRegisterListener(passVisibilityListener);
}
@Override
protected void onDestroy() {
for (PropertyChangedListener listener : listeners) {
propertiesManager.removeListener(listener);
}
super.onDestroy();
}
@Click(R.id.passwordVisibility)
void togglePasswordVisibility() {
propertiesManager.setPasswordVisibility(!propertiesManager.getPasswordVisibility(), OnThread.BACKGROUND);
}
private void initViewListeners() {
passwordVisibility.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
ImageView view = (ImageView) v;
view.setBackgroundColor(UiUtils.addAlphaToColor(Color.WHITE, 0.25f));
view.invalidate();
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
ImageView view = (ImageView) v;
view.setBackground(null);
view.invalidate();
break;
}
}
return false;
}
});
}
@UiThread(propagation = UiThread.Propagation.REUSE)
public void setPasswordVisibility(Boolean visible) {
passwordVisibility.setImageResource(visible ? R.drawable.ic_visibility_white_24dp :
R.drawable.ic_visibility_off_white_24dp);
txtPassword.setInputType(visible ? InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD :
InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
@Click(R.id.btnAdvanced)
public void btnAdvancedClicked() {
Intent intent = new Intent(this, AdvancedAuthenticatorActivity_.class);
intent.putExtra(AdvancedAuthenticatorActivity.LOAD_CA_FROM, txtEndpoint.getText().toString());
startActivity(intent);
}
@Click(R.id.btnCreate)
void addNew() {
endpoint = txtEndpoint.getText().toString();
try {
endpointUrl = new URL(endpoint);
} catch (MalformedURLException e) {
messageHelper.showError(ErrorType.USER, getString(R.string.login_error_invalid_api_url,
e.getMessage(), getString(R.string.default_endpoint)));
return;
}
endpoint = endpointUrl.toString();
username = txtUsername.getText().toString();
if (!username.matches(".+@.+")) {
messageHelper.showError(ErrorType.USER, getString(R.string.login_error_invalid_username,
getString(R.string.account_username), getString(R.string.default_username)));
return;
}
password = txtPassword.getText().toString();
if (password.length() == 0) {
messageHelper.showToast(getString(R.string.login_error_empty_password));
return;
}
adminPriv = chkAdminPriv.isChecked();
String apiPath = endpointUrl.getPath();
if (apiPath.isEmpty() || !apiPath.contains("api")) {
DialogFragment apiPathDialog = new ApiPathDialogFragment();
apiPathDialog.show(getFragmentManager(), "apiPathDialog");
} else {
finishLogin();
}
}
public void fixUrlAndLogin() {
URL endpointUrlFixed = null;
try {
endpointUrlFixed = new URL(endpointUrl, getString(R.string.default_api_path));
} catch (MalformedURLException ignored) {
}
assert endpointUrlFixed != null;
endpoint = endpointUrlFixed.toString();
txtEndpoint.setText(endpoint);
finishLogin();
}
@Background
public void finishLogin() {
if (endpoint == null || username == null || password == null) {
return;
}
try {
setLoginInProgress(true); // disables syncs because setUserData() may trigger sync
// without option SYNC_EXTRAS_EXPEDITED which may be interrupted by our future sync with option SYNC_EXTRAS_EXPEDITED
setUserData(endpoint, username, password, adminPriv);
String token = loginClient.login(username, password);
onLoginResultReceived(token);
} catch (HttpClientErrorException e) {
setLoginInProgress(false);
HttpStatus statusCode = e.getStatusCode();
switch (statusCode.series()) {
case REDIRECTION:
messageHelper.showError(ErrorType.USER, e.getMessage());
break;
default:
switch (statusCode) {
case NOT_FOUND:
messageHelper.showError(ErrorType.LOGIN, e, getString(R.string.login_error_bad_address_suffix));
break;
case UNAUTHORIZED:
messageHelper.showError(ErrorType.LOGIN, e, getString(R.string.login_error_incorrect_username_password));
break;
default:
messageHelper.showError(ErrorType.LOGIN, messageHelper.createMessage(e));
break;
}
}
} catch (Exception e) {
setLoginInProgress(false);
Throwable cause = e.getCause();
if (cause != null && cause instanceof SSLHandshakeException) {
fireCertificateError(cause);
} else if (cause != null && (cause instanceof ConnectException || cause instanceof UnknownHostException)) {
messageHelper.showError(ErrorType.LOGIN, e, getString(R.string.login_error_incorrect_ip_port));
} else if (cause != null && cause instanceof SocketTimeoutException) {
messageHelper.showError(ErrorType.LOGIN, e, getString(R.string.login_error_timeout));
} else {
messageHelper.showError(ErrorType.LOGIN, getString(R.string.login_error, e.getMessage()));
}
}
}
void onLoginResultReceived(String token) {
if (TextUtils.isEmpty(token)) {
setLoginInProgress(false);
messageHelper.showError(ErrorType.LOGIN,
getString(R.string.login_error_empty_token, getString(R.string.certificate_management)));
return;
}
if (propertiesManager.isFirstLogin()) {
// there is a different set of events and since we are counting only the increments,
// this ones are not needed anymore
eventsHandler.deleteEvents();
propertiesManager.setFirstLogin(false);
}
propertiesManager.setAuthToken(token);
setLoginInProgress(false);
messageHelper.showToast(getString(R.string.login_success));
syncUtils.triggerRefresh();
Account account = authenticator.getAccount();
final Intent intent = new Intent();
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, account.name);
intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, account.type);
intent.putExtra(AccountManager.KEY_AUTHTOKEN, token);
// setAccountAuthenticatorResult(intent.getExtras());
setResult(RESULT_OK, intent);
finish();
}
void setLoginInProgress(boolean loginInProgress) {
inProgress = loginInProgress;
ContentResolver.setIsSyncable(authenticator.getAccount(),
OVirtContract.CONTENT_AUTHORITY, loginInProgress ? 0 : 1);
Intent intent = new Intent(Broadcasts.IN_USER_LOGIN);
intent.putExtra(Broadcasts.Extras.MESSAGE, loginInProgress);
getApplicationContext().sendBroadcast(intent);
}
public static boolean isInUserLogin() {
return inProgress;
}
@Receiver(actions = {Broadcasts.IN_USER_LOGIN},
registerAt = Receiver.RegisterAt.OnResumeOnPause)
@UiThread(propagation = UiThread.Propagation.REUSE)
void loginProgress(
@Receiver.Extra(Broadcasts.Extras.MESSAGE) boolean loginInProgress) {
if (btnCreate != null) {
btnCreate.setEnabled(!loginInProgress);
}
if (btnAdvanced != null) {
btnAdvanced.setEnabled(!loginInProgress);
}
if (authProgress != null) {
authProgress.setVisibility(loginInProgress ? View.VISIBLE : View.GONE);
}
}
public void fireCertificateError(Throwable cause) {
int ignoreIndex = Arrays
.asList(getResources().getStringArray(R.array.cert_option_keys))
.indexOf("ignore");
String certIgnore = getResources()
.getStringArray(R.array.certificate_handling_strategy)[ignoreIndex];
String message = getString(R.string.login_error_bad_cert, certIgnore, cause.getMessage());
Intent intent = new Intent(Broadcasts.REST_CA_FAILURE);
intent.putExtra(Broadcasts.Extras.ERROR_REASON, message);
getApplicationContext().sendBroadcast(intent);
}
private void setUserData(String apiUrl, String name, String password, Boolean hasAdminPermissions) {
// mark First Login
boolean usernameChanged = propertiesManager.propertyDiffers(AccountProperty.USERNAME, username);
boolean urlChanged = propertiesManager.propertyDiffers(AccountProperty.API_URL, endpoint);
if (urlChanged || usernameChanged) { // there can be more attempts to login so set it only the first time
propertiesManager.setFirstLogin(true);
}
propertiesManager.setApiUrl(apiUrl);
propertiesManager.setUsername(name);
propertiesManager.setAdminPermissions(hasAdminPermissions);
propertiesManager.setPassword(password); // triggers sync in later APIs (Android 6)
}
@Receiver(actions = {Broadcasts.ERROR_MESSAGE},
registerAt = Receiver.RegisterAt.OnResumeOnPause)
public void showErrorDialog(
@Receiver.Extra(Broadcasts.Extras.ERROR_REASON) String reason,
@Receiver.Extra(Broadcasts.Extras.REPEATED_MINOR_ERROR) boolean repeatedMinorError) {
CreateDialogBroadcastReceiverHelper.showErrorDialog(getFragmentManager(), reason, repeatedMinorError);
}
@Receiver(actions = {Broadcasts.REST_CA_FAILURE},
registerAt = Receiver.RegisterAt.OnResumeOnPause)
public void showCertificateDialog(
@Receiver.Extra(Broadcasts.Extras.ERROR_REASON) String reason) {
CreateDialogBroadcastReceiverHelper.showCertificateDialog(getFragmentManager(), reason, false);
}
}