292 lines
11 KiB
Kotlin
292 lines
11 KiB
Kotlin
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
|
|
|
|
internal 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)
|
|
}
|
|
|
|
private fun doCheckUrl() {
|
|
val url = binding.gotifyUrl.text.toString().trim().trimEnd('/')
|
|
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
|
|
|
|
try {
|
|
ClientFactory.versionApi(url, tempSslSettings())
|
|
.version
|
|
.enqueue(Callback.callInUI(this, onValidUrl(url), onInvalidUrl(url)))
|
|
} catch (e: Exception) {
|
|
binding.checkurlProgress.visibility = View.GONE
|
|
binding.checkurl.visibility = View.VISIBLE
|
|
val errorMsg = getString(R.string.version_failed, "$url/version", e.message)
|
|
Utils.showSnackBar(this, errorMsg)
|
|
}
|
|
}
|
|
|
|
private 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()
|
|
}
|
|
|
|
private fun openLogs() {
|
|
startActivity(Intent(this, LogsActivity::class.java))
|
|
}
|
|
|
|
private 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) { "result was $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))
|
|
}
|
|
}
|
|
|
|
private 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()
|
|
}
|
|
|
|
private 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, { if (it != null) 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)
|
|
}
|
|
}
|