Rewrite 'login' to Kotlin
This commit is contained in:
@@ -1,75 +0,0 @@
|
|||||||
package com.github.gotify.login;
|
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.widget.CompoundButton;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import com.github.gotify.R;
|
|
||||||
import com.github.gotify.databinding.AdvancedSettingsDialogBinding;
|
|
||||||
|
|
||||||
class AdvancedDialog {
|
|
||||||
|
|
||||||
private Context context;
|
|
||||||
private LayoutInflater layoutInflater;
|
|
||||||
private AdvancedSettingsDialogBinding binding;
|
|
||||||
private CompoundButton.OnCheckedChangeListener onCheckedChangeListener;
|
|
||||||
private Runnable onClickSelectCaCertificate;
|
|
||||||
private Runnable onClickRemoveCaCertificate;
|
|
||||||
|
|
||||||
AdvancedDialog(Context context, LayoutInflater layoutInflater) {
|
|
||||||
this.context = context;
|
|
||||||
this.layoutInflater = layoutInflater;
|
|
||||||
}
|
|
||||||
|
|
||||||
AdvancedDialog onDisableSSLChanged(
|
|
||||||
CompoundButton.OnCheckedChangeListener onCheckedChangeListener) {
|
|
||||||
this.onCheckedChangeListener = onCheckedChangeListener;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
AdvancedDialog onClickSelectCaCertificate(Runnable onClickSelectCaCertificate) {
|
|
||||||
this.onClickSelectCaCertificate = onClickSelectCaCertificate;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
AdvancedDialog onClickRemoveCaCertificate(Runnable onClickRemoveCaCertificate) {
|
|
||||||
this.onClickRemoveCaCertificate = onClickRemoveCaCertificate;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
AdvancedDialog show(boolean disableSSL, @Nullable String selectedCertificate) {
|
|
||||||
binding = AdvancedSettingsDialogBinding.inflate(layoutInflater);
|
|
||||||
binding.disableSSL.setChecked(disableSSL);
|
|
||||||
binding.disableSSL.setOnCheckedChangeListener(onCheckedChangeListener);
|
|
||||||
|
|
||||||
if (selectedCertificate == null) {
|
|
||||||
showSelectCACertificate();
|
|
||||||
} else {
|
|
||||||
showRemoveCACertificate(selectedCertificate);
|
|
||||||
}
|
|
||||||
|
|
||||||
new AlertDialog.Builder(context)
|
|
||||||
.setView(binding.getRoot())
|
|
||||||
.setTitle(R.string.advanced_settings)
|
|
||||||
.setPositiveButton(context.getString(R.string.done), (ignored, ignored2) -> {})
|
|
||||||
.show();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showSelectCACertificate() {
|
|
||||||
binding.toggleCaCert.setText(R.string.select_ca_certificate);
|
|
||||||
binding.toggleCaCert.setOnClickListener((a) -> onClickSelectCaCertificate.run());
|
|
||||||
binding.selecetedCaCert.setText(R.string.no_certificate_selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
void showRemoveCACertificate(String certificate) {
|
|
||||||
binding.toggleCaCert.setText(R.string.remove_ca_certificate);
|
|
||||||
binding.toggleCaCert.setOnClickListener(
|
|
||||||
(a) -> {
|
|
||||||
showSelectCACertificate();
|
|
||||||
onClickRemoveCaCertificate.run();
|
|
||||||
});
|
|
||||||
binding.selecetedCaCert.setText(certificate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
67
app/src/main/java/com/github/gotify/login/AdvancedDialog.kt
Normal file
67
app/src/main/java/com/github/gotify/login/AdvancedDialog.kt
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package com.github.gotify.login
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import com.github.gotify.R
|
||||||
|
import com.github.gotify.databinding.AdvancedSettingsDialogBinding
|
||||||
|
|
||||||
|
internal class AdvancedDialog(
|
||||||
|
private val context: Context,
|
||||||
|
private val layoutInflater: LayoutInflater
|
||||||
|
) {
|
||||||
|
private lateinit var binding: AdvancedSettingsDialogBinding
|
||||||
|
private var onCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
||||||
|
private lateinit var onClickSelectCaCertificate: Runnable
|
||||||
|
private lateinit var onClickRemoveCaCertificate: Runnable
|
||||||
|
|
||||||
|
fun onDisableSSLChanged(
|
||||||
|
onCheckedChangeListener: CompoundButton.OnCheckedChangeListener?
|
||||||
|
): AdvancedDialog {
|
||||||
|
this.onCheckedChangeListener = onCheckedChangeListener
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onClickSelectCaCertificate(onClickSelectCaCertificate: Runnable): AdvancedDialog {
|
||||||
|
this.onClickSelectCaCertificate = onClickSelectCaCertificate
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onClickRemoveCaCertificate(onClickRemoveCaCertificate: Runnable): AdvancedDialog {
|
||||||
|
this.onClickRemoveCaCertificate = onClickRemoveCaCertificate
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(disableSSL: Boolean, selectedCertificate: String?): AdvancedDialog {
|
||||||
|
binding = AdvancedSettingsDialogBinding.inflate(layoutInflater)
|
||||||
|
binding.disableSSL.isChecked = disableSSL
|
||||||
|
binding.disableSSL.setOnCheckedChangeListener(onCheckedChangeListener)
|
||||||
|
if (selectedCertificate == null) {
|
||||||
|
showSelectCACertificate()
|
||||||
|
} else {
|
||||||
|
showRemoveCACertificate(selectedCertificate)
|
||||||
|
}
|
||||||
|
AlertDialog.Builder(context)
|
||||||
|
.setView(binding.root)
|
||||||
|
.setTitle(R.string.advanced_settings)
|
||||||
|
.setPositiveButton(context.getString(R.string.done), null)
|
||||||
|
.show()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSelectCACertificate() {
|
||||||
|
binding.toggleCaCert.setText(R.string.select_ca_certificate)
|
||||||
|
binding.toggleCaCert.setOnClickListener { onClickSelectCaCertificate.run() }
|
||||||
|
binding.selecetedCaCert.setText(R.string.no_certificate_selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showRemoveCACertificate(certificate: String) {
|
||||||
|
binding.toggleCaCert.setText(R.string.remove_ca_certificate)
|
||||||
|
binding.toggleCaCert.setOnClickListener {
|
||||||
|
showSelectCACertificate()
|
||||||
|
onClickRemoveCaCertificate.run()
|
||||||
|
}
|
||||||
|
binding.selecetedCaCert.text = certificate
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
package com.github.gotify.login;
|
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.appcompat.view.ContextThemeWrapper;
|
|
||||||
import com.github.gotify.R;
|
|
||||||
import com.github.gotify.SSLSettings;
|
|
||||||
import com.github.gotify.Settings;
|
|
||||||
import com.github.gotify.Utils;
|
|
||||||
import com.github.gotify.api.ApiException;
|
|
||||||
import com.github.gotify.api.Callback;
|
|
||||||
import com.github.gotify.api.CertUtils;
|
|
||||||
import com.github.gotify.api.ClientFactory;
|
|
||||||
import com.github.gotify.client.ApiClient;
|
|
||||||
import com.github.gotify.client.api.ClientApi;
|
|
||||||
import com.github.gotify.client.api.UserApi;
|
|
||||||
import com.github.gotify.client.model.Client;
|
|
||||||
import com.github.gotify.client.model.VersionInfo;
|
|
||||||
import com.github.gotify.databinding.ActivityLoginBinding;
|
|
||||||
import com.github.gotify.init.InitializationActivity;
|
|
||||||
import com.github.gotify.log.Log;
|
|
||||||
import com.github.gotify.log.LogsActivity;
|
|
||||||
import com.github.gotify.log.UncaughtExceptionHandler;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.security.cert.Certificate;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import okhttp3.HttpUrl;
|
|
||||||
|
|
||||||
import static com.github.gotify.api.Callback.callInUI;
|
|
||||||
|
|
||||||
public class LoginActivity extends AppCompatActivity {
|
|
||||||
|
|
||||||
// return value from startActivityForResult when choosing a certificate
|
|
||||||
private final int FILE_SELECT_CODE = 1;
|
|
||||||
|
|
||||||
private ActivityLoginBinding binding;
|
|
||||||
private Settings settings;
|
|
||||||
|
|
||||||
private boolean disableSSLValidation;
|
|
||||||
private String caCertContents;
|
|
||||||
private AdvancedDialog advancedDialog;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
UncaughtExceptionHandler.registerCurrentThread();
|
|
||||||
binding = ActivityLoginBinding.inflate(getLayoutInflater());
|
|
||||||
setContentView(binding.getRoot());
|
|
||||||
Log.i("Entering " + getClass().getSimpleName());
|
|
||||||
settings = new Settings(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
|
|
||||||
super.onPostCreate(savedInstanceState);
|
|
||||||
|
|
||||||
binding.gotifyUrl.addTextChangedListener(
|
|
||||||
new TextWatcher() {
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(
|
|
||||||
CharSequence charSequence, int i, int i1, int i2) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
|
||||||
invalidateUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable editable) {}
|
|
||||||
});
|
|
||||||
|
|
||||||
binding.checkurl.setOnClickListener(ignored -> doCheckUrl());
|
|
||||||
binding.openLogs.setOnClickListener(ignored -> openLogs());
|
|
||||||
binding.advancedSettings.setOnClickListener(ignored -> toggleShowAdvanced());
|
|
||||||
binding.login.setOnClickListener(ignored -> doLogin());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void invalidateUrl() {
|
|
||||||
binding.username.setVisibility(View.GONE);
|
|
||||||
binding.password.setVisibility(View.GONE);
|
|
||||||
binding.login.setVisibility(View.GONE);
|
|
||||||
binding.checkurl.setText(getString(R.string.check_url));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void doCheckUrl() {
|
|
||||||
String url = binding.gotifyUrl.getText().toString();
|
|
||||||
HttpUrl parsedUrl = HttpUrl.parse(url);
|
|
||||||
if (parsedUrl == null) {
|
|
||||||
Utils.showSnackBar(LoginActivity.this, "Invalid URL (include http:// or https://)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("http".equals(parsedUrl.scheme())) {
|
|
||||||
showHttpWarning();
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.checkurlProgress.setVisibility(View.VISIBLE);
|
|
||||||
binding.checkurl.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
final String trimmedUrl = url.trim();
|
|
||||||
final String fixedUrl =
|
|
||||||
trimmedUrl.endsWith("/")
|
|
||||||
? trimmedUrl.substring(0, trimmedUrl.length() - 1)
|
|
||||||
: trimmedUrl;
|
|
||||||
|
|
||||||
try {
|
|
||||||
ClientFactory.versionApi(fixedUrl, tempSSLSettings())
|
|
||||||
.getVersion()
|
|
||||||
.enqueue(callInUI(this, onValidUrl(fixedUrl), onInvalidUrl(fixedUrl)));
|
|
||||||
} catch (Exception e) {
|
|
||||||
binding.checkurlProgress.setVisibility(View.GONE);
|
|
||||||
binding.checkurl.setVisibility(View.VISIBLE);
|
|
||||||
String errorMsg =
|
|
||||||
getString(R.string.version_failed, fixedUrl + "/version", e.getMessage());
|
|
||||||
Utils.showSnackBar(LoginActivity.this, errorMsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showHttpWarning() {
|
|
||||||
new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.AppTheme_Dialog))
|
|
||||||
.setTitle(R.string.warning)
|
|
||||||
.setCancelable(true)
|
|
||||||
.setMessage(R.string.http_warning)
|
|
||||||
.setPositiveButton(R.string.i_understand, (a, b) -> {})
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void openLogs() {
|
|
||||||
startActivity(new Intent(this, LogsActivity.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
void toggleShowAdvanced() {
|
|
||||||
String selectedCertName =
|
|
||||||
caCertContents != null ? getNameOfCertContent(caCertContents) : null;
|
|
||||||
|
|
||||||
advancedDialog =
|
|
||||||
new AdvancedDialog(this, getLayoutInflater())
|
|
||||||
.onDisableSSLChanged(
|
|
||||||
(ignored, disable) -> {
|
|
||||||
invalidateUrl();
|
|
||||||
disableSSLValidation = disable;
|
|
||||||
})
|
|
||||||
.onClickSelectCaCertificate(
|
|
||||||
() -> {
|
|
||||||
invalidateUrl();
|
|
||||||
doSelectCACertificate();
|
|
||||||
})
|
|
||||||
.onClickRemoveCaCertificate(
|
|
||||||
() -> {
|
|
||||||
invalidateUrl();
|
|
||||||
caCertContents = null;
|
|
||||||
})
|
|
||||||
.show(disableSSLValidation, selectedCertName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doSelectCACertificate() {
|
|
||||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
|
||||||
// we don't really care what kind of file it is as long as we can parse it
|
|
||||||
intent.setType("*/*");
|
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
|
||||||
|
|
||||||
try {
|
|
||||||
startActivityForResult(
|
|
||||||
Intent.createChooser(intent, getString(R.string.select_ca_file)),
|
|
||||||
FILE_SELECT_CODE);
|
|
||||||
} catch (ActivityNotFoundException e) {
|
|
||||||
// case for user not having a file browser installed
|
|
||||||
Utils.showSnackBar(this, getString(R.string.please_install_file_browser));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
|
||||||
try {
|
|
||||||
if (requestCode == FILE_SELECT_CODE) {
|
|
||||||
if (resultCode != RESULT_OK) {
|
|
||||||
throw new IllegalArgumentException(String.format("result was %d", resultCode));
|
|
||||||
} else if (data == null) {
|
|
||||||
throw new IllegalArgumentException("file path was null");
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri uri = data.getData();
|
|
||||||
if (uri == null) {
|
|
||||||
throw new IllegalArgumentException("file path was null");
|
|
||||||
}
|
|
||||||
|
|
||||||
InputStream fileStream = getContentResolver().openInputStream(uri);
|
|
||||||
if (fileStream == null) {
|
|
||||||
throw new IllegalArgumentException("file path was invalid");
|
|
||||||
}
|
|
||||||
|
|
||||||
String content = Utils.readFileFromStream(fileStream);
|
|
||||||
String name = getNameOfCertContent(content);
|
|
||||||
|
|
||||||
// temporarily set the contents (don't store to settings until they decide to login)
|
|
||||||
caCertContents = content;
|
|
||||||
advancedDialog.showRemoveCACertificate(name);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Utils.showSnackBar(this, getString(R.string.select_ca_failed, e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getNameOfCertContent(String content) {
|
|
||||||
Certificate ca = CertUtils.parseCertificate(content);
|
|
||||||
return ((X509Certificate) ca).getSubjectDN().getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Callback.SuccessCallback<VersionInfo> onValidUrl(String url) {
|
|
||||||
return (version) -> {
|
|
||||||
settings.url(url);
|
|
||||||
binding.checkurlProgress.setVisibility(View.GONE);
|
|
||||||
binding.checkurl.setVisibility(View.VISIBLE);
|
|
||||||
binding.checkurl.setText(
|
|
||||||
getString(R.string.found_gotify_version, version.getVersion()));
|
|
||||||
binding.username.setVisibility(View.VISIBLE);
|
|
||||||
binding.username.requestFocus();
|
|
||||||
binding.password.setVisibility(View.VISIBLE);
|
|
||||||
binding.login.setVisibility(View.VISIBLE);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Callback.ErrorCallback onInvalidUrl(String url) {
|
|
||||||
return (exception) -> {
|
|
||||||
binding.checkurlProgress.setVisibility(View.GONE);
|
|
||||||
binding.checkurl.setVisibility(View.VISIBLE);
|
|
||||||
Utils.showSnackBar(LoginActivity.this, versionError(url, exception));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void doLogin() {
|
|
||||||
String username = binding.username.getText().toString();
|
|
||||||
String password = binding.password.getText().toString();
|
|
||||||
|
|
||||||
binding.login.setVisibility(View.GONE);
|
|
||||||
binding.loginProgress.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
ApiClient client =
|
|
||||||
ClientFactory.basicAuth(settings.url(), tempSSLSettings(), username, password);
|
|
||||||
client.createService(UserApi.class)
|
|
||||||
.currentUser()
|
|
||||||
.enqueue(callInUI(this, (user) -> newClientDialog(client), this::onInvalidLogin));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onInvalidLogin(ApiException e) {
|
|
||||||
binding.login.setVisibility(View.VISIBLE);
|
|
||||||
binding.loginProgress.setVisibility(View.GONE);
|
|
||||||
Utils.showSnackBar(this, getString(R.string.wronguserpw));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void newClientDialog(ApiClient client) {
|
|
||||||
EditText clientName = new EditText(this);
|
|
||||||
clientName.setText(Build.MODEL);
|
|
||||||
|
|
||||||
new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.AppTheme_Dialog))
|
|
||||||
.setTitle(R.string.create_client_title)
|
|
||||||
.setMessage(R.string.create_client_message)
|
|
||||||
.setView(clientName)
|
|
||||||
.setPositiveButton(R.string.create, doCreateClient(client, clientName))
|
|
||||||
.setNegativeButton(R.string.cancel, this::onCancelClientDialog)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
public DialogInterface.OnClickListener doCreateClient(ApiClient client, EditText nameProvider) {
|
|
||||||
return (a, b) -> {
|
|
||||||
Client newClient = new Client().name(nameProvider.getText().toString());
|
|
||||||
client.createService(ClientApi.class)
|
|
||||||
.createClient(newClient)
|
|
||||||
.enqueue(callInUI(this, this::onCreatedClient, this::onFailedToCreateClient));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onCreatedClient(Client client) {
|
|
||||||
settings.token(client.getToken());
|
|
||||||
settings.validateSSL(!disableSSLValidation);
|
|
||||||
settings.cert(caCertContents);
|
|
||||||
|
|
||||||
Utils.showSnackBar(this, getString(R.string.created_client));
|
|
||||||
startActivity(new Intent(this, InitializationActivity.class));
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onFailedToCreateClient(ApiException e) {
|
|
||||||
Utils.showSnackBar(this, getString(R.string.create_client_failed));
|
|
||||||
binding.loginProgress.setVisibility(View.GONE);
|
|
||||||
binding.login.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onCancelClientDialog(DialogInterface dialog, int which) {
|
|
||||||
binding.loginProgress.setVisibility(View.GONE);
|
|
||||||
binding.login.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String versionError(String url, ApiException exception) {
|
|
||||||
return getString(R.string.version_failed_status_code, url + "/version", exception.code());
|
|
||||||
}
|
|
||||||
|
|
||||||
private SSLSettings tempSSLSettings() {
|
|
||||||
return new SSLSettings(!disableSSLValidation, caCertContents);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
293
app/src/main/java/com/github/gotify/login/LoginActivity.kt
Normal file
293
app/src/main/java/com/github/gotify/login/LoginActivity.kt
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
package com.github.gotify.login
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.EditText
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
import com.github.gotify.R
|
||||||
|
import com.github.gotify.SSLSettings
|
||||||
|
import com.github.gotify.Settings
|
||||||
|
import com.github.gotify.Utils
|
||||||
|
import com.github.gotify.api.ApiException
|
||||||
|
import com.github.gotify.api.Callback
|
||||||
|
import com.github.gotify.api.Callback.SuccessCallback
|
||||||
|
import com.github.gotify.api.CertUtils
|
||||||
|
import com.github.gotify.api.ClientFactory
|
||||||
|
import com.github.gotify.client.ApiClient
|
||||||
|
import com.github.gotify.client.api.ClientApi
|
||||||
|
import com.github.gotify.client.api.UserApi
|
||||||
|
import com.github.gotify.client.model.Client
|
||||||
|
import com.github.gotify.client.model.VersionInfo
|
||||||
|
import com.github.gotify.databinding.ActivityLoginBinding
|
||||||
|
import com.github.gotify.init.InitializationActivity
|
||||||
|
import com.github.gotify.log.Log
|
||||||
|
import com.github.gotify.log.LogsActivity
|
||||||
|
import com.github.gotify.log.UncaughtExceptionHandler
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
|
class LoginActivity : AppCompatActivity() {
|
||||||
|
companion object {
|
||||||
|
// return value from startActivityForResult when choosing a certificate
|
||||||
|
private const val FILE_SELECT_CODE = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityLoginBinding
|
||||||
|
private lateinit var settings: Settings
|
||||||
|
|
||||||
|
private var disableSslValidation = false
|
||||||
|
private var caCertContents: String? = null
|
||||||
|
private lateinit var advancedDialog: AdvancedDialog
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
UncaughtExceptionHandler.registerCurrentThread()
|
||||||
|
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
Log.i("Entering ${javaClass.simpleName}")
|
||||||
|
settings = Settings(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onPostCreate(savedInstanceState)
|
||||||
|
|
||||||
|
binding.gotifyUrl.addTextChangedListener(object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||||
|
|
||||||
|
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
|
||||||
|
invalidateUrl()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun afterTextChanged(editable: Editable) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.checkurl.setOnClickListener { doCheckUrl() }
|
||||||
|
binding.openLogs.setOnClickListener { openLogs() }
|
||||||
|
binding.advancedSettings.setOnClickListener { toggleShowAdvanced() }
|
||||||
|
binding.login.setOnClickListener { doLogin() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invalidateUrl() {
|
||||||
|
binding.username.visibility = View.GONE
|
||||||
|
binding.password.visibility = View.GONE
|
||||||
|
binding.login.visibility = View.GONE
|
||||||
|
binding.checkurl.text = getString(R.string.check_url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doCheckUrl() {
|
||||||
|
val url = binding.gotifyUrl.text.toString()
|
||||||
|
val parsedUrl = HttpUrl.parse(url)
|
||||||
|
if (parsedUrl == null) {
|
||||||
|
Utils.showSnackBar(this, "Invalid URL (include http:// or https://)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("http" == parsedUrl.scheme()) {
|
||||||
|
showHttpWarning()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.checkurlProgress.visibility = View.VISIBLE
|
||||||
|
binding.checkurl.visibility = View.GONE
|
||||||
|
|
||||||
|
val trimmedUrl = url.trim()
|
||||||
|
val fixedUrl = if (trimmedUrl.endsWith("/")) {
|
||||||
|
trimmedUrl.substring(0, trimmedUrl.length - 1)
|
||||||
|
} else trimmedUrl
|
||||||
|
|
||||||
|
try {
|
||||||
|
ClientFactory.versionApi(fixedUrl, tempSslSettings())
|
||||||
|
.version
|
||||||
|
.enqueue(Callback.callInUI(this, onValidUrl(fixedUrl), onInvalidUrl(fixedUrl)))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
binding.checkurlProgress.visibility = View.GONE
|
||||||
|
binding.checkurl.visibility = View.VISIBLE
|
||||||
|
val errorMsg = getString(R.string.version_failed, "$fixedUrl/version", e.message)
|
||||||
|
Utils.showSnackBar(this, errorMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showHttpWarning() {
|
||||||
|
AlertDialog.Builder(ContextThemeWrapper(this, R.style.AppTheme_Dialog))
|
||||||
|
.setTitle(R.string.warning)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setMessage(R.string.http_warning)
|
||||||
|
.setPositiveButton(R.string.i_understand, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openLogs() {
|
||||||
|
startActivity(Intent(this, LogsActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleShowAdvanced() {
|
||||||
|
val selectedCertName = if (caCertContents != null) {
|
||||||
|
getNameOfCertContent(caCertContents!!)
|
||||||
|
} else null
|
||||||
|
|
||||||
|
advancedDialog = AdvancedDialog(this, layoutInflater)
|
||||||
|
.onDisableSSLChanged { _, disable ->
|
||||||
|
invalidateUrl()
|
||||||
|
disableSslValidation = disable
|
||||||
|
}
|
||||||
|
.onClickSelectCaCertificate {
|
||||||
|
invalidateUrl()
|
||||||
|
doSelectCACertificate()
|
||||||
|
}
|
||||||
|
.onClickRemoveCaCertificate {
|
||||||
|
invalidateUrl()
|
||||||
|
caCertContents = null
|
||||||
|
}
|
||||||
|
.show(disableSslValidation, selectedCertName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doSelectCACertificate() {
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
|
// we don't really care what kind of file it is as long as we can parse it
|
||||||
|
intent.type = "*/*"
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivityForResult(
|
||||||
|
Intent.createChooser(intent, getString(R.string.select_ca_file)),
|
||||||
|
FILE_SELECT_CODE
|
||||||
|
)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
// case for user not having a file browser installed
|
||||||
|
Utils.showSnackBar(this, getString(R.string.please_install_file_browser))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
try {
|
||||||
|
if (requestCode == FILE_SELECT_CODE) {
|
||||||
|
require(resultCode == RESULT_OK) { String.format("result was %d", resultCode) }
|
||||||
|
requireNotNull(data) { "file path was null" }
|
||||||
|
|
||||||
|
val uri = data.data ?: throw IllegalArgumentException("file path was null")
|
||||||
|
|
||||||
|
val fileStream = contentResolver.openInputStream(uri)
|
||||||
|
?: throw IllegalArgumentException("file path was invalid")
|
||||||
|
|
||||||
|
val content = Utils.readFileFromStream(fileStream)
|
||||||
|
val name = getNameOfCertContent(content)
|
||||||
|
|
||||||
|
// temporarily set the contents (don't store to settings until they decide to login)
|
||||||
|
caCertContents = content
|
||||||
|
advancedDialog.showRemoveCACertificate(name)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Utils.showSnackBar(this, getString(R.string.select_ca_failed, e.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNameOfCertContent(content: String): String {
|
||||||
|
val ca = CertUtils.parseCertificate(content)
|
||||||
|
return (ca as X509Certificate).subjectDN.name
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onValidUrl(url: String): SuccessCallback<VersionInfo> {
|
||||||
|
return SuccessCallback { version ->
|
||||||
|
settings.url(url)
|
||||||
|
binding.checkurlProgress.visibility = View.GONE
|
||||||
|
binding.checkurl.visibility = View.VISIBLE
|
||||||
|
binding.checkurl.text = getString(R.string.found_gotify_version, version.version)
|
||||||
|
binding.username.visibility = View.VISIBLE
|
||||||
|
binding.username.requestFocus()
|
||||||
|
binding.password.visibility = View.VISIBLE
|
||||||
|
binding.login.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onInvalidUrl(url: String): Callback.ErrorCallback {
|
||||||
|
return Callback.ErrorCallback { exception ->
|
||||||
|
binding.checkurlProgress.visibility = View.GONE
|
||||||
|
binding.checkurl.visibility = View.VISIBLE
|
||||||
|
Utils.showSnackBar(this, versionError(url, exception))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doLogin() {
|
||||||
|
val username = binding.username.text.toString()
|
||||||
|
val password = binding.password.text.toString()
|
||||||
|
|
||||||
|
binding.login.visibility = View.GONE
|
||||||
|
binding.loginProgress.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val client = ClientFactory.basicAuth(settings.url(), tempSslSettings(), username, password)
|
||||||
|
client.createService(UserApi::class.java)
|
||||||
|
.currentUser()
|
||||||
|
.enqueue(Callback.callInUI(this, { newClientDialog(client) }) {
|
||||||
|
onInvalidLogin()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onInvalidLogin() {
|
||||||
|
binding.login.visibility = View.VISIBLE
|
||||||
|
binding.loginProgress.visibility = View.GONE
|
||||||
|
Utils.showSnackBar(this, getString(R.string.wronguserpw))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newClientDialog(client: ApiClient) {
|
||||||
|
val clientName = EditText(this)
|
||||||
|
clientName.setText(Build.MODEL)
|
||||||
|
|
||||||
|
AlertDialog.Builder(ContextThemeWrapper(this, R.style.AppTheme_Dialog))
|
||||||
|
.setTitle(R.string.create_client_title)
|
||||||
|
.setMessage(R.string.create_client_message)
|
||||||
|
.setView(clientName)
|
||||||
|
.setPositiveButton(R.string.create, doCreateClient(client, clientName))
|
||||||
|
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||||
|
onCancelClientDialog()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doCreateClient(client: ApiClient, nameProvider: EditText): DialogInterface.OnClickListener {
|
||||||
|
return DialogInterface.OnClickListener { _, _ ->
|
||||||
|
val newClient = Client().name(nameProvider.text.toString())
|
||||||
|
client.createService(ClientApi::class.java)
|
||||||
|
.createClient(newClient)
|
||||||
|
.enqueue(Callback.callInUI(this, { onCreatedClient(it) }) {
|
||||||
|
onFailedToCreateClient()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCreatedClient(client: Client) {
|
||||||
|
settings.token(client.token)
|
||||||
|
settings.validateSSL(!disableSslValidation)
|
||||||
|
settings.cert(caCertContents)
|
||||||
|
|
||||||
|
Utils.showSnackBar(this, getString(R.string.created_client))
|
||||||
|
startActivity(Intent(this, InitializationActivity::class.java))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onFailedToCreateClient() {
|
||||||
|
Utils.showSnackBar(this, getString(R.string.create_client_failed))
|
||||||
|
binding.loginProgress.visibility = View.GONE
|
||||||
|
binding.login.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCancelClientDialog() {
|
||||||
|
binding.loginProgress.visibility = View.GONE
|
||||||
|
binding.login.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun versionError(url: String, exception: ApiException): String {
|
||||||
|
return getString(R.string.version_failed_status_code, "$url/version", exception.code())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tempSslSettings(): SSLSettings {
|
||||||
|
return SSLSettings(!disableSslValidation, caCertContents)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user