1
0

LoginActivity.kt 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. package io.github.zadam.triliumsender
  2. import android.animation.Animator
  3. import android.animation.AnimatorListenerAdapter
  4. import android.annotation.TargetApi
  5. import android.app.LoaderManager.LoaderCallbacks
  6. import android.content.Context
  7. import android.content.CursorLoader
  8. import android.content.Loader
  9. import android.database.Cursor
  10. import android.net.Uri
  11. import android.os.AsyncTask
  12. import android.os.Build
  13. import android.os.Bundle
  14. import android.provider.ContactsContract
  15. import android.support.v7.app.AppCompatActivity
  16. import android.text.TextUtils
  17. import android.util.Log
  18. import android.view.View
  19. import android.view.inputmethod.EditorInfo
  20. import android.widget.ArrayAdapter
  21. import android.widget.TextView
  22. import android.widget.Toast
  23. import kotlinx.android.synthetic.main.activity_login.*
  24. import okhttp3.*
  25. import org.json.JSONObject
  26. import java.util.*
  27. /**
  28. * A login screen that offers login via email/password.
  29. */
  30. class LoginActivity : AppCompatActivity(), LoaderCallbacks<Cursor> {
  31. val JSON = MediaType.parse("application/json; charset=utf-8")
  32. /**
  33. * Keep track of the login task to ensure we can cancel it if requested.
  34. */
  35. private var mAuthTask: UserLoginTask? = null
  36. override fun onCreate(savedInstanceState: Bundle?) {
  37. super.onCreate(savedInstanceState)
  38. setContentView(R.layout.activity_login)
  39. password.setOnEditorActionListener(TextView.OnEditorActionListener { _, id, _ ->
  40. if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) {
  41. attemptLogin()
  42. return@OnEditorActionListener true
  43. }
  44. false
  45. })
  46. email_sign_in_button.setOnClickListener { attemptLogin() }
  47. }
  48. /**
  49. * Attempts to sign in or register the account specified by the login form.
  50. * If there are form errors (invalid email, missing fields, etc.), the
  51. * errors are presented and no actual login attempt is made.
  52. */
  53. private fun attemptLogin() {
  54. if (mAuthTask != null) {
  55. return
  56. }
  57. // Reset errors.
  58. username.error = null
  59. password.error = null
  60. // Store values at the time of the login attempt.
  61. val triliumAddress = trilium_address.text.toString();
  62. val usernameStr = username.text.toString()
  63. val passwordStr = password.text.toString()
  64. var cancel = false
  65. var focusView: View? = null
  66. // Check for a valid username
  67. if (TextUtils.isEmpty(usernameStr)) {
  68. username.error = getString(R.string.error_field_required)
  69. focusView = username
  70. cancel = true
  71. }
  72. if (cancel) {
  73. // There was an error; don't attempt login and focus the first
  74. // form field with an error.
  75. focusView?.requestFocus()
  76. } else {
  77. // Show a progress spinner, and kick off a background task to
  78. // perform the user login attempt.
  79. showProgress(true)
  80. mAuthTask = UserLoginTask(triliumAddress, usernameStr, passwordStr)
  81. mAuthTask!!.execute(null as Void?)
  82. }
  83. }
  84. /**
  85. * Shows the progress UI and hides the login form.
  86. */
  87. @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
  88. private fun showProgress(show: Boolean) {
  89. // On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow
  90. // for very easy animations. If available, use these APIs to fade-in
  91. // the progress spinner.
  92. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
  93. val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
  94. login_form.visibility = if (show) View.GONE else View.VISIBLE
  95. login_form.animate()
  96. .setDuration(shortAnimTime)
  97. .alpha((if (show) 0 else 1).toFloat())
  98. .setListener(object : AnimatorListenerAdapter() {
  99. override fun onAnimationEnd(animation: Animator) {
  100. login_form.visibility = if (show) View.GONE else View.VISIBLE
  101. }
  102. })
  103. login_progress.visibility = if (show) View.VISIBLE else View.GONE
  104. login_progress.animate()
  105. .setDuration(shortAnimTime)
  106. .alpha((if (show) 1 else 0).toFloat())
  107. .setListener(object : AnimatorListenerAdapter() {
  108. override fun onAnimationEnd(animation: Animator) {
  109. login_progress.visibility = if (show) View.VISIBLE else View.GONE
  110. }
  111. })
  112. } else {
  113. // The ViewPropertyAnimator APIs are not available, so simply show
  114. // and hide the relevant UI components.
  115. login_progress.visibility = if (show) View.VISIBLE else View.GONE
  116. login_form.visibility = if (show) View.GONE else View.VISIBLE
  117. }
  118. }
  119. override fun onCreateLoader(i: Int, bundle: Bundle?): Loader<Cursor> {
  120. return CursorLoader(this,
  121. // Retrieve data rows for the device user's 'profile' contact.
  122. Uri.withAppendedPath(ContactsContract.Profile.CONTENT_URI,
  123. ContactsContract.Contacts.Data.CONTENT_DIRECTORY), ProfileQuery.PROJECTION,
  124. // Select only email addresses.
  125. ContactsContract.Contacts.Data.MIMETYPE + " = ?", arrayOf(ContactsContract.CommonDataKinds.Email
  126. .CONTENT_ITEM_TYPE),
  127. // Show primary email addresses first. Note that there won't be
  128. // a primary email address if the user hasn't specified one.
  129. ContactsContract.Contacts.Data.IS_PRIMARY + " DESC")
  130. }
  131. override fun onLoadFinished(cursorLoader: Loader<Cursor>, cursor: Cursor) {
  132. val emails = ArrayList<String>()
  133. cursor.moveToFirst()
  134. while (!cursor.isAfterLast) {
  135. emails.add(cursor.getString(ProfileQuery.ADDRESS))
  136. cursor.moveToNext()
  137. }
  138. addEmailsToAutoComplete(emails)
  139. }
  140. override fun onLoaderReset(cursorLoader: Loader<Cursor>) {
  141. }
  142. private fun addEmailsToAutoComplete(emailAddressCollection: List<String>) {
  143. //Create adapter to tell the AutoCompleteTextView what to show in its dropdown list.
  144. val adapter = ArrayAdapter(this@LoginActivity,
  145. android.R.layout.simple_dropdown_item_1line, emailAddressCollection)
  146. username.setAdapter(adapter)
  147. }
  148. object ProfileQuery {
  149. val PROJECTION = arrayOf(
  150. ContactsContract.CommonDataKinds.Email.ADDRESS,
  151. ContactsContract.CommonDataKinds.Email.IS_PRIMARY)
  152. val ADDRESS = 0
  153. val IS_PRIMARY = 1
  154. }
  155. inner class LoginResult (val success: Boolean, val errorCode : Int?,
  156. val token : String? = null);
  157. /**
  158. * Represents an asynchronous login/registration task used to authenticate
  159. * the user.
  160. */
  161. inner class UserLoginTask internal constructor(private val mTriliumAddress: String, private val mUsername: String, private val mPassword: String) : AsyncTask<Void, Void, LoginResult>() {
  162. val TAG : String = "UserLoginTask"
  163. override fun doInBackground(vararg params: Void): LoginResult {
  164. val client = OkHttpClient()
  165. val json = JSONObject()
  166. json.put("username", mUsername)
  167. json.put("password", mPassword)
  168. val body = RequestBody.create(JSON, json.toString())
  169. val request = Request.Builder()
  170. .url(mTriliumAddress + "/api/sender/login")
  171. .post(body)
  172. .build()
  173. val response: Response;
  174. try {
  175. response = client.newCall(request).execute()
  176. }
  177. catch (e: Exception) {
  178. Log.e(TAG, "Can't connect to Trilium server", e);
  179. return LoginResult(false, R.string.error_network_error)
  180. }
  181. Log.i(TAG,"Response code: " + response.code())
  182. if (response.code() == 401) {
  183. return LoginResult(false, R.string.error_incorrect_credentials)
  184. }
  185. else if (response.code() != 200) {
  186. return LoginResult(false, R.string.error_unexpected_response)
  187. }
  188. val responseText = response.body()?.string()
  189. Log.i(TAG,"Response text: " + responseText)
  190. val resp = JSONObject(responseText)
  191. val token : String = resp.get("token") as String
  192. Log.i(TAG,"Token: " + token)
  193. return LoginResult(true, null, token);
  194. }
  195. override fun onPostExecute(loginResult: LoginResult) {
  196. mAuthTask = null
  197. showProgress(false)
  198. if (loginResult.success) {
  199. TriliumSettings(this@LoginActivity).save(mTriliumAddress, loginResult.token!!)
  200. Toast.makeText(this@LoginActivity, "Trilium connection settings have been successfully configured.", Toast.LENGTH_LONG).show()
  201. finish()
  202. } else {
  203. if (loginResult.errorCode == R.string.error_network_error
  204. || loginResult.errorCode == R.string.error_unexpected_response) {
  205. trilium_address.error = getString(loginResult.errorCode)
  206. trilium_address.requestFocus()
  207. }
  208. else if (loginResult.errorCode == R.string.error_incorrect_credentials) {
  209. password.error = getString(loginResult.errorCode)
  210. password.requestFocus()
  211. }
  212. else {
  213. throw RuntimeException("Unknown code: " + loginResult.errorCode);
  214. }
  215. }
  216. }
  217. override fun onCancelled() {
  218. mAuthTask = null
  219. showProgress(false)
  220. }
  221. }
  222. }