LoginActivity.kt 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. package io.github.zadam.triliumsender
  2. import android.os.Bundle
  3. import android.text.TextUtils
  4. import android.util.Log
  5. import android.view.View
  6. import android.view.inputmethod.EditorInfo
  7. import android.widget.TextView
  8. import android.widget.Toast
  9. import androidx.appcompat.app.AppCompatActivity
  10. import io.github.zadam.triliumsender.services.TriliumSettings
  11. import io.github.zadam.triliumsender.services.Utils
  12. import kotlinx.android.synthetic.main.activity_login.*
  13. import kotlinx.coroutines.CoroutineScope
  14. import kotlinx.coroutines.Dispatchers
  15. import kotlinx.coroutines.launch
  16. import kotlinx.coroutines.withContext
  17. import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
  18. import okhttp3.OkHttpClient
  19. import okhttp3.Request
  20. import okhttp3.RequestBody.Companion.toRequestBody
  21. import okhttp3.Response
  22. import org.json.JSONObject
  23. class LoginActivity : AppCompatActivity() {
  24. override fun onCreate(savedInstanceState: Bundle?) {
  25. super.onCreate(savedInstanceState)
  26. setContentView(R.layout.activity_login)
  27. passwordEditText.setOnEditorActionListener(TextView.OnEditorActionListener { _, id, _ ->
  28. if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) {
  29. attemptLogin()
  30. return@OnEditorActionListener true
  31. }
  32. false
  33. })
  34. loginButton.setOnClickListener { attemptLogin() }
  35. // Check if we're already set-up.
  36. setSetupStatus()
  37. }
  38. override fun onStop() {
  39. // Save edited label or address. Use apitoken from existing settings.
  40. val settings = TriliumSettings(this)
  41. TriliumSettings(this@LoginActivity).save(triliumAddressEditText.text.toString(), settings.apiToken, labelEditText.text.toString())
  42. super.onStop()
  43. }
  44. /**
  45. * Attempts to sign in or register the account specified by the login form.
  46. *
  47. * If there are form errors (invalid email, missing fields, etc.), the
  48. * errors are presented and no actual login attempt is made.
  49. *
  50. * If the login attempt errors out, some common errors are presented on the form.
  51. *
  52. * If the login attempt succeeds, the LoginActivity finishes.
  53. */
  54. private fun attemptLogin() {
  55. // Reset errors.
  56. triliumAddressEditText.error = null
  57. usernameEditText.error = null
  58. passwordEditText.error = null
  59. // Store values at the time of the login attempt.
  60. val triliumAddress = triliumAddressEditText.text.toString()
  61. val username = usernameEditText.text.toString()
  62. val password = passwordEditText.text.toString()
  63. val noteLabel = labelEditText.text.toString()
  64. // Check for an empty URL. Flag and abort if so.
  65. if (TextUtils.isEmpty(triliumAddress)) {
  66. triliumAddressEditText.error = getString(R.string.error_field_required)
  67. triliumAddressEditText.requestFocus()
  68. return
  69. }
  70. // Check for a valid URL. Flag and abort if not.
  71. // Use the full address to the login API, for full coverage of the URL's validity.
  72. val fullTriliumAddress = "$triliumAddress/api/sender/login"
  73. val url = fullTriliumAddress.toHttpUrlOrNull()
  74. if (url == null) {
  75. triliumAddressEditText.error = getString(R.string.url_invalid)
  76. triliumAddressEditText.requestFocus()
  77. return
  78. }
  79. // Check for an empty username. Flag and abort if so.
  80. if (TextUtils.isEmpty(username)) {
  81. usernameEditText.error = getString(R.string.error_field_required)
  82. usernameEditText.requestFocus()
  83. return
  84. }
  85. // Check for an empty password. Flag and abort if so.
  86. if (TextUtils.isEmpty(password)) {
  87. passwordEditText.error = getString(R.string.error_field_required)
  88. passwordEditText.requestFocus()
  89. return
  90. }
  91. // Kick off a coroutine to handle the actual login attempt without blocking the UI.
  92. // Since we want to be able to fire Toasts, we should use the Main (UI) scope.
  93. val uiScope = CoroutineScope(Dispatchers.Main)
  94. uiScope.launch {
  95. val loginResult = doLogin(triliumAddress, username, password)
  96. if (loginResult.success) {
  97. // Store the address and api token.
  98. TriliumSettings(this@LoginActivity).save(triliumAddress, loginResult.token!!, noteLabel)
  99. // Announce our success.
  100. Toast.makeText(this@LoginActivity, getString(R.string.connection_configured_correctly), Toast.LENGTH_LONG).show()
  101. // End the activity.
  102. finish()
  103. } else {
  104. if (loginResult.errorCode == R.string.error_network_error
  105. || loginResult.errorCode == R.string.error_unexpected_response) {
  106. triliumAddressEditText.error = getString(loginResult.errorCode)
  107. triliumAddressEditText.requestFocus()
  108. } else if (loginResult.errorCode == R.string.error_incorrect_credentials) {
  109. passwordEditText.error = getString(loginResult.errorCode)
  110. passwordEditText.requestFocus()
  111. } else {
  112. throw RuntimeException("Unknown code: " + loginResult.errorCode)
  113. }
  114. }
  115. }
  116. }
  117. /**
  118. * A result from a login attempt.
  119. */
  120. inner class LoginResult(val success: Boolean, val errorCode: Int?,
  121. val token: String? = null)
  122. /**
  123. * Makes the actual login http request in the IO thread, to avoid blocking the UI thread.
  124. *
  125. * @param triliumAddress, the base address of a Trilium server
  126. * @param username, the username to log into the server
  127. * @param password, the password to log into the server
  128. *
  129. * @return A loginResult object.
  130. */
  131. private suspend fun doLogin(triliumAddress: String, username: String, password: String): LoginResult {
  132. return withContext(Dispatchers.IO) {
  133. val tag = "UserLoginCoroutine"
  134. val client = OkHttpClient()
  135. val json = JSONObject()
  136. json.put("username", username)
  137. json.put("password", password)
  138. val body = json.toString().toRequestBody(Utils.JSON)
  139. val request = Request.Builder()
  140. .url("$triliumAddress/api/sender/login")
  141. .post(body)
  142. .build()
  143. val response: Response
  144. try {
  145. // In the Dispatchers.IO context, blocking http requests are allowed.
  146. @Suppress("BlockingMethodInNonBlockingContext")
  147. response = client.newCall(request).execute()
  148. } catch (e: Exception) {
  149. Log.e(tag, "Can't connect to Trilium server", e)
  150. return@withContext LoginResult(false, R.string.error_network_error)
  151. }
  152. Log.i(tag, "Response code: " + response.code)
  153. if (response.code == 401) {
  154. return@withContext LoginResult(false, R.string.error_incorrect_credentials)
  155. } else if (response.code != 200) {
  156. return@withContext LoginResult(false, R.string.error_unexpected_response)
  157. }
  158. // In the Dispatchers.IO context, blocking tasks are allowed.
  159. @Suppress("BlockingMethodInNonBlockingContext")
  160. val responseText = response.body?.string()
  161. Log.i(tag, "Response text: $responseText")
  162. val resp = JSONObject(responseText!!)
  163. val token: String = resp.get("token") as String
  164. Log.i(tag, "Token: $token")
  165. return@withContext LoginResult(true, null, token)
  166. }
  167. }
  168. /**
  169. * Checks the settings object and updates the UI to match the current state.
  170. */
  171. private fun setSetupStatus() {
  172. val settings = TriliumSettings(this)
  173. // Always attempt to restore the note label.
  174. labelEditText.setText(settings.noteLabel)
  175. if (!settings.isConfigured()) {
  176. // Hide the logged-in indicator.
  177. loggedInIndicator.visibility = View.INVISIBLE
  178. } else {
  179. // Populate the text editors for URL.
  180. triliumAddressEditText.setText(settings.triliumAddress)
  181. // Indicate successful login.
  182. loggedInIndicator.visibility = View.VISIBLE
  183. // TODO: This does not actually validate the current API Token.
  184. // In the future we should update this to only show after a quick validation of the token.
  185. }
  186. }
  187. }