123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- package io.github.zadam.triliumsender
- import android.os.Bundle
- import android.text.TextUtils
- import android.util.Log
- import android.view.View
- import android.view.inputmethod.EditorInfo
- import android.widget.TextView
- import android.widget.Toast
- import androidx.appcompat.app.AppCompatActivity
- import io.github.zadam.triliumsender.services.TriliumSettings
- import io.github.zadam.triliumsender.services.Utils
- import kotlinx.android.synthetic.main.activity_login.*
- import kotlinx.coroutines.CoroutineScope
- import kotlinx.coroutines.Dispatchers
- import kotlinx.coroutines.launch
- import kotlinx.coroutines.withContext
- import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
- import okhttp3.OkHttpClient
- import okhttp3.Request
- import okhttp3.RequestBody.Companion.toRequestBody
- import okhttp3.Response
- import org.json.JSONObject
- class LoginActivity : AppCompatActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_login)
- passwordEditText.setOnEditorActionListener(TextView.OnEditorActionListener { _, id, _ ->
- if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) {
- attemptLogin()
- return@OnEditorActionListener true
- }
- false
- })
- loginButton.setOnClickListener { attemptLogin() }
- // Check if we're already set-up.
- setSetupStatus()
- }
- override fun onStop() {
- // Save edited label or address. Use apitoken from existing settings.
- val settings = TriliumSettings(this)
- TriliumSettings(this@LoginActivity).save(triliumAddressEditText.text.toString(), settings.apiToken, labelEditText.text.toString())
- super.onStop()
- }
- /**
- * Attempts to sign in or register the account specified by the login form.
- *
- * If there are form errors (invalid email, missing fields, etc.), the
- * errors are presented and no actual login attempt is made.
- *
- * If the login attempt errors out, some common errors are presented on the form.
- *
- * If the login attempt succeeds, the LoginActivity finishes.
- */
- private fun attemptLogin() {
- // Reset errors.
- triliumAddressEditText.error = null
- usernameEditText.error = null
- passwordEditText.error = null
- // Store values at the time of the login attempt.
- val triliumAddress = triliumAddressEditText.text.toString()
- val username = usernameEditText.text.toString()
- val password = passwordEditText.text.toString()
- val noteLabel = labelEditText.text.toString()
- // Check for an empty URL. Flag and abort if so.
- if (TextUtils.isEmpty(triliumAddress)) {
- triliumAddressEditText.error = getString(R.string.error_field_required)
- triliumAddressEditText.requestFocus()
- return
- }
- // Check for a valid URL. Flag and abort if not.
- // Use the full address to the login API, for full coverage of the URL's validity.
- val fullTriliumAddress = "$triliumAddress/api/sender/login"
- val url = fullTriliumAddress.toHttpUrlOrNull()
- if (url == null) {
- triliumAddressEditText.error = getString(R.string.url_invalid)
- triliumAddressEditText.requestFocus()
- return
- }
- // Check for an empty username. Flag and abort if so.
- if (TextUtils.isEmpty(username)) {
- usernameEditText.error = getString(R.string.error_field_required)
- usernameEditText.requestFocus()
- return
- }
- // Check for an empty password. Flag and abort if so.
- if (TextUtils.isEmpty(password)) {
- passwordEditText.error = getString(R.string.error_field_required)
- passwordEditText.requestFocus()
- return
- }
- // Kick off a coroutine to handle the actual login attempt without blocking the UI.
- // Since we want to be able to fire Toasts, we should use the Main (UI) scope.
- val uiScope = CoroutineScope(Dispatchers.Main)
- uiScope.launch {
- val loginResult = doLogin(triliumAddress, username, password)
- if (loginResult.success) {
- // Store the address and api token.
- TriliumSettings(this@LoginActivity).save(triliumAddress, loginResult.token!!, noteLabel)
- // Announce our success.
- Toast.makeText(this@LoginActivity, getString(R.string.connection_configured_correctly), Toast.LENGTH_LONG).show()
- // End the activity.
- finish()
- } else {
- if (loginResult.errorCode == R.string.error_network_error
- || loginResult.errorCode == R.string.error_unexpected_response) {
- triliumAddressEditText.error = getString(loginResult.errorCode)
- triliumAddressEditText.requestFocus()
- } else if (loginResult.errorCode == R.string.error_incorrect_credentials) {
- passwordEditText.error = getString(loginResult.errorCode)
- passwordEditText.requestFocus()
- } else {
- throw RuntimeException("Unknown code: " + loginResult.errorCode)
- }
- }
- }
- }
- /**
- * A result from a login attempt.
- */
- inner class LoginResult(val success: Boolean, val errorCode: Int?,
- val token: String? = null)
- /**
- * Makes the actual login http request in the IO thread, to avoid blocking the UI thread.
- *
- * @param triliumAddress, the base address of a Trilium server
- * @param username, the username to log into the server
- * @param password, the password to log into the server
- *
- * @return A loginResult object.
- */
- private suspend fun doLogin(triliumAddress: String, username: String, password: String): LoginResult {
- return withContext(Dispatchers.IO) {
- val tag = "UserLoginCoroutine"
- val client = OkHttpClient()
- val json = JSONObject()
- json.put("username", username)
- json.put("password", password)
- val body = json.toString().toRequestBody(Utils.JSON)
- val request = Request.Builder()
- .url("$triliumAddress/api/sender/login")
- .post(body)
- .build()
- val response: Response
- try {
- // In the Dispatchers.IO context, blocking http requests are allowed.
- @Suppress("BlockingMethodInNonBlockingContext")
- response = client.newCall(request).execute()
- } catch (e: Exception) {
- Log.e(tag, "Can't connect to Trilium server", e)
- return@withContext LoginResult(false, R.string.error_network_error)
- }
- Log.i(tag, "Response code: " + response.code)
- if (response.code == 401) {
- return@withContext LoginResult(false, R.string.error_incorrect_credentials)
- } else if (response.code != 200) {
- return@withContext LoginResult(false, R.string.error_unexpected_response)
- }
- // In the Dispatchers.IO context, blocking tasks are allowed.
- @Suppress("BlockingMethodInNonBlockingContext")
- val responseText = response.body?.string()
- Log.i(tag, "Response text: $responseText")
- val resp = JSONObject(responseText!!)
- val token: String = resp.get("token") as String
- Log.i(tag, "Token: $token")
- return@withContext LoginResult(true, null, token)
- }
- }
- /**
- * Checks the settings object and updates the UI to match the current state.
- */
- private fun setSetupStatus() {
- val settings = TriliumSettings(this)
- // Always attempt to restore the note label.
- labelEditText.setText(settings.noteLabel)
- if (!settings.isConfigured()) {
- // Hide the logged-in indicator.
- loggedInIndicator.visibility = View.INVISIBLE
- } else {
- // Populate the text editors for URL.
- triliumAddressEditText.setText(settings.triliumAddress)
- // Indicate successful login.
- loggedInIndicator.visibility = View.VISIBLE
- // TODO: This does not actually validate the current API Token.
- // In the future we should update this to only show after a quick validation of the token.
- }
- }
- }
|