package org.ovirt.mobile.movirt.ui.auth;
import android.app.DialogFragment;
import android.app.FragmentTransaction;
import android.content.DialogInterface;
import android.content.Intent;
import android.support.v7.app.ActionBarActivity;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.Switch;
import android.widget.TextView;
import com.unnamed.b.atv.model.TreeNode;
import com.unnamed.b.atv.view.AndroidTreeView;
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.OptionsItem;
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.properties.AccountProperty;
import org.ovirt.mobile.movirt.auth.properties.PropertyChangedListener;
import org.ovirt.mobile.movirt.auth.properties.PropertyUtils;
import org.ovirt.mobile.movirt.auth.properties.UiAwareProperty;
import org.ovirt.mobile.movirt.auth.properties.manager.AccountPropertiesManager;
import org.ovirt.mobile.movirt.auth.properties.manager.OnThread;
import org.ovirt.mobile.movirt.auth.properties.property.Cert;
import org.ovirt.mobile.movirt.auth.properties.property.CertHandlingStrategy;
import org.ovirt.mobile.movirt.ui.dialogs.ConfirmDialogFragment;
import org.ovirt.mobile.movirt.ui.dialogs.DialogListener;
import org.ovirt.mobile.movirt.util.CertHelper;
import org.ovirt.mobile.movirt.util.URIUtils;
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 java.net.URL;
import java.security.cert.Certificate;
import static org.ovirt.mobile.movirt.Constants.APP_PACKAGE_DOT;
@EActivity(R.layout.activity_advanced_authenticator)
public class AdvancedAuthenticatorActivity extends ActionBarActivity
implements ConfirmDialogFragment.ConfirmDialogListener, CreateDialogBroadcastReceiver, DialogListener.UrlListener {
private static final String TAG = AdvancedAuthenticatorActivity.class.getSimpleName();
private static final String CUSTOM_DIALOG_TAG = "downloadCaIssuer";
private static final String DELETE_DIALOG_TAG = "deleteCert";
private static final String DELETE_DIALOG_FROM_CUSTOM_LOCATION_LISTENER_TAG = "deleteCertFromCustomLocationListener";
private static final int NORMAL_DELETE_DIALOG = 0;
private static final int CUSTOM_SWITCH_DELETE_DIALOG = 1;
private static final int MAX_VISIBLE_CERTIFICATES = 10;
public final static String LOAD_CA_FROM = APP_PACKAGE_DOT + "ui.LOAD_CA_FROM";
@ViewById
Spinner certHandlingStrategySpinner;
@ViewById
Switch customCertificateLocationSwitch;
@ViewById
TextView txtCertUrl;
@ViewById
EditText txtValidForHostnames;
@ViewById
TextView txtCertDetails;
@ViewById
TextView certUrlLabel;
@ViewById
TextView validHostnamesLabel;
@ViewById
Button btnDelete;
@ViewById
Button btnLoad;
@ViewById
ProgressBar progress;
@ViewById
LinearLayout certTreeContainer;
@InstanceState
boolean inProgress;
private boolean initializedUi;
@Bean
MessageHelper messageHelper;
@Bean
AccountPropertiesManager propertiesManager;
@Bean
CertHelper certHelper;
private PropertyChangedListener[] listeners;
private boolean maxCertsReachedErrorAlreadyShown;
@AfterViews
void init() {
showProgressBar(inProgress);
initViewListeners();
initPropertyListeners();
}
@Override
protected void onDestroy() {
for (PropertyChangedListener listener : listeners) {
propertiesManager.removeListener(listener);
}
super.onDestroy();
}
@UiThread(propagation = UiThread.Propagation.REUSE)
void updateViews(UiAwareProperty<CertHandlingStrategy> certHandlingStrategy, UiAwareProperty<Cert[]> certs,
UiAwareProperty<String> hostnames, UiAwareProperty<Boolean> isCustomCertificateLocation) {
updateCertHandlingStrategy(certHandlingStrategy);
switch (certHandlingStrategy.getProperty()) {
case TRUST_CUSTOM:
seCustomCertVisibility(true);
updateHostnames(hostnames);
updateCertificateLocationType(isCustomCertificateLocation);
Cert[] chain = certs.getProperty();
assertMaxCertsMessage(chain);
showCerts(chain, isCustomCertificateLocation.getProperty());
break;
default:
seCustomCertVisibility(false);
hideCerts();
break;
}
}
@Click(R.id.btnLoad)
void btnDownLoad() {
try {
if (propertiesManager.isCustomCertificateLocation()) {
Cert[] certs = propertiesManager.getCertificateChain();
if (certs.length == 0) {
downloadCustomCa(null, true);
} else {
assertCaIncluded(certs);
}
} else {
downloadAndSaveCert(null, true);
}
} catch (IllegalArgumentException parseError) {
messageHelper.showError(ErrorType.USER, parseError.getMessage(), getString(R.string.wrong_url_in_connection_settings));
}
}
@Override
public void onNewDialogUrl(URL url, boolean startNewChain) {
downloadAndSaveCert(new URL[]{url}, startNewChain);
}
@Background
void downloadAndSaveCert(URL[] urls, boolean startNewChain) {
URL hostUrl = null;
try {
hostUrl = URIUtils.tryToParseUrl(getIntent().getStringExtra(LOAD_CA_FROM));
} catch (Exception x) {
messageHelper.showError(ErrorType.USER, getString(R.string.api_url_not_valid));
return;
}
if (urls == null) {
urls = URIUtils.getEngineCertificateUrls(hostUrl);
}
try {
broadcastProgress(true);
for (int i = 0; i < urls.length; i++) { // try all URLs - used in ENGINE certificate location
try {
certHelper.downloadAndStoreCert(urls[i], hostUrl, startNewChain);
break;
} catch (Exception x) {
if (i == urls.length - 1) {
throw x;
}
}
}
} catch (IllegalArgumentException | IllegalStateException e) {
messageHelper.showError(ErrorType.USER, e.getMessage(), checkedUrlsToString(urls));
} catch (Exception e) {
messageHelper.showError(ErrorType.NORMAL, e, checkedUrlsToString(urls));
} finally {
broadcastProgress(false);
}
}
@Click(R.id.btnDelete)
void btnDelete() {
DialogFragment confirmDialog = ConfirmDialogFragment
.newInstance(NORMAL_DELETE_DIALOG, getString(R.string.dialog_action_delete_certificate));
confirmDialog.show(getFragmentManager(), DELETE_DIALOG_TAG);
}
@Override
public void onDialogResult(int dialogButton, int actionId) {
if (dialogButton == DialogInterface.BUTTON_POSITIVE) {
if (actionId == CUSTOM_SWITCH_DELETE_DIALOG) { // toggle switch
boolean isChecked = customCertificateLocationSwitch.isChecked();
changeCertificateLocation(isChecked);
} else {
deleteAllCertsInBackground();
}
}
}
private void deleteAllCertsInBackground() {
certHelper.deleteAllCertsInBackground();
messageHelper.showToast(getString(R.string.deleted_cert_chain));
}
@Background
void changeCertificateLocation(boolean customCertificateChecked) {
certHelper.deleteAllCerts(); // before cert location
messageHelper.showToast(getString(R.string.deleted_cert_chain));
propertiesManager.setCustomCertificateLocation(!customCertificateChecked, OnThread.BACKGROUND);
}
private void downloadCustomCa(String url, boolean startNewChain) {
if (getFragmentManager().findFragmentByTag(CUSTOM_DIALOG_TAG) == null) { // updateViews can call this multiple times
DownloadCustomCertDialogFragment dialog = new DownloadCustomCertDialogFragment_();
dialog.setUrl(url);
dialog.setStartNewChain(startNewChain);
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.add(dialog, CUSTOM_DIALOG_TAG);
transaction.commitAllowingStateLoss();
}
}
private static String checkedUrlsToString(URL[] urls) {
StringBuilder urlsChecked = new StringBuilder("Urls checked:\n");
for (int u = 0; u < urls.length; u++) {
urlsChecked.append(urls[u]);
if (u != urls.length - 1) {
urlsChecked.append(",\n");
}
}
urlsChecked.append("\n");
return urlsChecked.toString();
}
private boolean assertCaIncluded(Cert[] certs) {
return !(certs == null || certs.length == 0) && assertCaIncluded(certs[certs.length - 1].asCertificate());
}
/**
* check if Ca is included and show dialog to import issuer certs if isn't
*/
private boolean assertCaIncluded(Certificate certificate) {
boolean isCA = CertHelper.isCA(certificate);
if (!isCA) {
downloadCustomCa(CertHelper.getIssuerUrl(certificate), false);
}
return isCA;
}
// update view functions
private void updateCertHandlingStrategy(UiAwareProperty<CertHandlingStrategy> certHandlingStrategy) {
if (certHandlingStrategy.uiNotUpdated()) {
certHandlingStrategySpinner.setSelection((int) certHandlingStrategy.getProperty().id());
}
}
private void assertMaxCertsMessage(Cert[] certs) {
if (certs.length > MAX_VISIBLE_CERTIFICATES) {
maxCertsReachedErrorAlreadyShown = false;
}
}
private void updateHostnames(UiAwareProperty<String> hostnames) {
if (hostnames.uiNotUpdated()) {
txtValidForHostnames.setText(hostnames.getProperty());
}
}
private void updateCertificateLocationType(UiAwareProperty<Boolean> isCustomCertificateLocation) {
if (isCustomCertificateLocation.uiNotUpdated()) {
customCertificateLocationSwitch.setChecked(isCustomCertificateLocation.getProperty());
}
}
private void hideCerts() {
showCerts(null, null);
}
private void showCerts(Cert[] certs, Boolean isCustomCert) {
boolean hasCerts = certs != null && certs.length > 0;
setHasCertsVisibility(hasCerts);
certTreeContainer.removeAllViews(); // remove previous tree
boolean isCA = false;
if (hasCerts) {
try {
isCA = assertCaIncluded(certs);
createTreeView(certTreeContainer, certs);
} catch (Exception x) {
messageHelper.showError(ErrorType.NORMAL, x, "Certificates badly formatted");
deleteAllCertsInBackground();
}
if (!isCustomCert && !isCA) { //engine
messageHelper.showError(ErrorType.USER, "Engine doesn't use self-signed CA, please report this issue.");
}
}
if (isCustomCert != null) { // null -> already hidden
setLoadButton(isCA, isCustomCert, hasCerts);
}
}
private void seCustomCertVisibility(boolean visible) {
btnDelete.setVisibility(visible ? View.VISIBLE : View.GONE);
btnLoad.setVisibility(visible ? View.VISIBLE : View.GONE);
customCertificateLocationSwitch.setVisibility(visible ? View.VISIBLE : View.GONE);
}
private void setLoadButton(boolean isCA, boolean isCustomCert, boolean hasCerts) {
boolean visible = !isCustomCert || !isCA; // show allways for engine and for custom without CA
btnLoad.setVisibility(visible ? View.VISIBLE : View.GONE);
btnLoad.setText(isCustomCert && hasCerts ? R.string.download_issuer : R.string.download);
}
private void setHasCertsVisibility(boolean hasCerts) {
btnDelete.setEnabled(hasCerts);
txtValidForHostnames.setVisibility(hasCerts ? View.VISIBLE : View.GONE);
validHostnamesLabel.setVisibility(hasCerts ? View.VISIBLE : View.GONE);
txtCertUrl.setVisibility(hasCerts ? View.VISIBLE : View.GONE);
certUrlLabel.setVisibility(hasCerts ? View.VISIBLE : View.GONE);
txtCertDetails.setVisibility(hasCerts ? View.VISIBLE : View.GONE);
txtCertDetails.setText(null);
certTreeContainer.setVisibility(hasCerts ? View.VISIBLE : View.GONE);
}
private void createTreeView(LinearLayout container, Cert[] certs) {
TreeNode root = TreeNode.root();
TreeNode intermediateLeaf = root;
CertHolder leafHolder = null;
int visibleCertificates;
if (certs.length > MAX_VISIBLE_CERTIFICATES) {
visibleCertificates = MAX_VISIBLE_CERTIFICATES;
if (!maxCertsReachedErrorAlreadyShown) {
maxCertsReachedErrorAlreadyShown = true;
messageHelper.showError(ErrorType.USER, getString(R.string.advanced_authenticator_error_max_visible_certs_reached, MAX_VISIBLE_CERTIFICATES));
}
} else {
visibleCertificates = certs.length;
}
CertHolder.CertificateSelectedListener listener = new CertHolder.CertificateSelectedListener() {
@Override
public void onSelect(Certificate certificate, String location) {
txtCertDetails.setText(certificate.toString());
txtCertUrl.setText(location);
}
};
for (int i = visibleCertificates - 1; i >= 0; i--) { // create tree hierarchy
CertHolder.TreeItem data = new CertHolder.TreeItem(certs[i], listener);
leafHolder = new CertHolder(this);
TreeNode newNode = new TreeNode(data).setViewHolder(leafHolder);
intermediateLeaf.addChild(newNode);
intermediateLeaf.setExpanded(true);
intermediateLeaf = newNode;
}
AndroidTreeView atv = new AndroidTreeView(this, root);
atv.setUseAutoToggle(false);
atv.setSelectionModeEnabled(true);
atv.setDefaultContainerStyle(R.style.CertTreeNodeStyle);
container.addView(atv.getView());
if (leafHolder != null) {
leafHolder.selectNode(); // select Api certificate
}
}
// Data Flow and listeners
private void initViewListeners() {
certHandlingStrategySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
propertiesManager.setCertHandlingStrategy(CertHandlingStrategy.from(id),
OnThread.BACKGROUND);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
txtValidForHostnames.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
propertiesManager.setValidHostnameList(PropertyUtils.parseHostnames(s.toString()),
OnThread.BACKGROUND);
}
@Override
public void afterTextChanged(Editable s) {
}
});
customCertificateLocationSwitch.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (propertiesManager.getCertificateChain().length > 0) {
customCertificateLocationSwitch.setChecked(propertiesManager.isCustomCertificateLocation());
// delete certs before switching mode
DialogFragment confirmDialog = ConfirmDialogFragment
.newInstance(CUSTOM_SWITCH_DELETE_DIALOG, getString(R.string.dialog_action_delete_certificate),
getString(R.string.dialog_action_delete_certificate_header));
confirmDialog.show(getFragmentManager(), DELETE_DIALOG_FROM_CUSTOM_LOCATION_LISTENER_TAG);
} else {
propertiesManager.setCustomCertificateLocation(!propertiesManager.isCustomCertificateLocation(),
OnThread.BACKGROUND);
}
}
});
}
private void initPropertyListeners() {
AccountProperty.CertHandlingStrategyListener certHandlingStrategyListener = new AccountProperty.CertHandlingStrategyListener() {
@Override
public void onPropertyChange(CertHandlingStrategy certHandlingStrategy) {
prepareDataAndUpdateViews(certHandlingStrategy, null, null, null);
}
};
AccountProperty.CertificateChainListener certChainListener = new AccountProperty.CertificateChainListener() {
@Override
public void onPropertyChange(Cert[] certificates) {
prepareDataAndUpdateViews(null, certificates, null, null);
}
};
AccountProperty.ValidHostnamesListener validHostnamesListener = new AccountProperty.ValidHostnamesListener() {
@Override
public void onPropertyChange(String validHostnames) {
prepareDataAndUpdateViews(null, null, validHostnames, null);
}
};
AccountProperty.CustomCertificateLocationListener customCertificateLocationListener = new AccountProperty.CustomCertificateLocationListener() {
@Override
public void onPropertyChange(Boolean customCertificateLocation) {
prepareDataAndUpdateViews(null, null, null, customCertificateLocation);
}
};
listeners = new PropertyChangedListener[]{
certHandlingStrategyListener,
certChainListener,
validHostnamesListener,
customCertificateLocationListener};
propertiesManager.notifyAndRegisterListener(certHandlingStrategyListener);
propertiesManager.registerListener(certChainListener);
propertiesManager.registerListener(validHostnamesListener);
propertiesManager.registerListener(customCertificateLocationListener);
}
private void prepareDataAndUpdateViews(CertHandlingStrategy certHandlingStrategy, Cert[] certs, String hostnames, Boolean isCustomCertificateLocation) {
boolean initialized = initializedUi; //first time propagate all properties to UI
if (!initializedUi) {
initializedUi = true;
}
// fill empty properties
if (certHandlingStrategy == null) {
certHandlingStrategy = propertiesManager.getCertHandlingStrategy();
}
if (certs == null) {
certs = propertiesManager.getCertificateChain();
}
if (hostnames == null) {
hostnames = propertiesManager.getValidHostnames();
}
if (isCustomCertificateLocation == null) {
isCustomCertificateLocation = propertiesManager.isCustomCertificateLocation();
}
// set which changes were updated from this UI
CertHandlingStrategy oldStrategy = initialized ? CertHandlingStrategy.from(certHandlingStrategySpinner.getSelectedItemId()) : null;
UiAwareProperty<CertHandlingStrategy> uiStrategy = new UiAwareProperty<>(certHandlingStrategy, oldStrategy);
String oldHostnames = initialized ? PropertyUtils.catenateToCsv(PropertyUtils.parseHostnames(txtValidForHostnames.getText().toString())) : null;
UiAwareProperty<String> uiHostnames = new UiAwareProperty<>(hostnames, oldHostnames);
Boolean oldLocation = initialized ? customCertificateLocationSwitch.isChecked() : null;
UiAwareProperty<Boolean> uiLocation = new UiAwareProperty<>(isCustomCertificateLocation, oldLocation);
UiAwareProperty<Cert[]> uiCerts = new UiAwareProperty<>(certs); // doesn't depend on UI
updateViews(uiStrategy, uiCerts, uiHostnames, uiLocation);
}
@OptionsItem(android.R.id.home)
public void homeSelected() {
finish();
}
void broadcastProgress(boolean inProgress) {
Intent intent = new Intent(Broadcasts.DOWNLOADING_CERTIFICATE);
intent.putExtra(Broadcasts.Extras.IN_PROGRESS, inProgress);
getApplicationContext().sendBroadcast(intent);
}
@Receiver(actions = {Broadcasts.DOWNLOADING_CERTIFICATE},
registerAt = Receiver.RegisterAt.OnCreateOnDestroy)
void showProgressBar(@Receiver.Extra(Broadcasts.Extras.IN_PROGRESS) boolean inProgress) {
this.inProgress = inProgress;
if (progress != null) {
progress.setVisibility(inProgress ? View.VISIBLE : View.GONE);
}
}
@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) {
// Do not use CreateDialogBroadcastReceiverHelper implementation, coz we are already setting certificate in this Activity
messageHelper.showError(getString(R.string.dialog_certificate_missing_start));
}
}