LoginActivity.kt 9.9 KB

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