|
@@ -0,0 +1,275 @@
|
|
|
+package io.github.zadam.triliumsender
|
|
|
+
|
|
|
+import android.animation.Animator
|
|
|
+import android.animation.AnimatorListenerAdapter
|
|
|
+import android.annotation.TargetApi
|
|
|
+import android.app.LoaderManager.LoaderCallbacks
|
|
|
+import android.content.Context
|
|
|
+import android.content.CursorLoader
|
|
|
+import android.content.Loader
|
|
|
+import android.database.Cursor
|
|
|
+import android.net.Uri
|
|
|
+import android.os.AsyncTask
|
|
|
+import android.os.Build
|
|
|
+import android.os.Bundle
|
|
|
+import android.provider.ContactsContract
|
|
|
+import android.support.v7.app.AppCompatActivity
|
|
|
+import android.text.TextUtils
|
|
|
+import android.util.Log
|
|
|
+import android.view.View
|
|
|
+import android.view.inputmethod.EditorInfo
|
|
|
+import android.widget.ArrayAdapter
|
|
|
+import android.widget.TextView
|
|
|
+import kotlinx.android.synthetic.main.activity_login.*
|
|
|
+import okhttp3.*
|
|
|
+import org.json.JSONObject
|
|
|
+import java.util.*
|
|
|
+
|
|
|
+
|
|
|
+/**
|
|
|
+ * A login screen that offers login via email/password.
|
|
|
+ */
|
|
|
+class LoginActivity : AppCompatActivity(), LoaderCallbacks<Cursor> {
|
|
|
+
|
|
|
+ val JSON = MediaType.parse("application/json; charset=utf-8")
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Keep track of the login task to ensure we can cancel it if requested.
|
|
|
+ */
|
|
|
+ private var mAuthTask: UserLoginTask? = null
|
|
|
+
|
|
|
+ override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
+ super.onCreate(savedInstanceState)
|
|
|
+ setContentView(R.layout.activity_login)
|
|
|
+
|
|
|
+ password.setOnEditorActionListener(TextView.OnEditorActionListener { _, id, _ ->
|
|
|
+ if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) {
|
|
|
+ attemptLogin()
|
|
|
+ return@OnEditorActionListener true
|
|
|
+ }
|
|
|
+ false
|
|
|
+ })
|
|
|
+
|
|
|
+ email_sign_in_button.setOnClickListener { attemptLogin() }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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.
|
|
|
+ */
|
|
|
+ private fun attemptLogin() {
|
|
|
+ if (mAuthTask != null) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Reset errors.
|
|
|
+ username.error = null
|
|
|
+ password.error = null
|
|
|
+
|
|
|
+ // Store values at the time of the login attempt.
|
|
|
+ val triliumAddress = trilium_address.text.toString();
|
|
|
+ val usernameStr = username.text.toString()
|
|
|
+ val passwordStr = password.text.toString()
|
|
|
+
|
|
|
+ var cancel = false
|
|
|
+ var focusView: View? = null
|
|
|
+
|
|
|
+ // Check for a valid username
|
|
|
+ if (TextUtils.isEmpty(usernameStr)) {
|
|
|
+ username.error = getString(R.string.error_field_required)
|
|
|
+ focusView = username
|
|
|
+ cancel = true
|
|
|
+ }
|
|
|
+
|
|
|
+ if (cancel) {
|
|
|
+ // There was an error; don't attempt login and focus the first
|
|
|
+ // form field with an error.
|
|
|
+ focusView?.requestFocus()
|
|
|
+ } else {
|
|
|
+ // Show a progress spinner, and kick off a background task to
|
|
|
+ // perform the user login attempt.
|
|
|
+ showProgress(true)
|
|
|
+ mAuthTask = UserLoginTask(triliumAddress, usernameStr, passwordStr)
|
|
|
+ mAuthTask!!.execute(null as Void?)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Shows the progress UI and hides the login form.
|
|
|
+ */
|
|
|
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
|
|
|
+ private fun showProgress(show: Boolean) {
|
|
|
+ // On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow
|
|
|
+ // for very easy animations. If available, use these APIs to fade-in
|
|
|
+ // the progress spinner.
|
|
|
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
|
|
|
+ val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
|
|
|
+
|
|
|
+ login_form.visibility = if (show) View.GONE else View.VISIBLE
|
|
|
+ login_form.animate()
|
|
|
+ .setDuration(shortAnimTime)
|
|
|
+ .alpha((if (show) 0 else 1).toFloat())
|
|
|
+ .setListener(object : AnimatorListenerAdapter() {
|
|
|
+ override fun onAnimationEnd(animation: Animator) {
|
|
|
+ login_form.visibility = if (show) View.GONE else View.VISIBLE
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ login_progress.visibility = if (show) View.VISIBLE else View.GONE
|
|
|
+ login_progress.animate()
|
|
|
+ .setDuration(shortAnimTime)
|
|
|
+ .alpha((if (show) 1 else 0).toFloat())
|
|
|
+ .setListener(object : AnimatorListenerAdapter() {
|
|
|
+ override fun onAnimationEnd(animation: Animator) {
|
|
|
+ login_progress.visibility = if (show) View.VISIBLE else View.GONE
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // The ViewPropertyAnimator APIs are not available, so simply show
|
|
|
+ // and hide the relevant UI components.
|
|
|
+ login_progress.visibility = if (show) View.VISIBLE else View.GONE
|
|
|
+ login_form.visibility = if (show) View.GONE else View.VISIBLE
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onCreateLoader(i: Int, bundle: Bundle?): Loader<Cursor> {
|
|
|
+ return CursorLoader(this,
|
|
|
+ // Retrieve data rows for the device user's 'profile' contact.
|
|
|
+ Uri.withAppendedPath(ContactsContract.Profile.CONTENT_URI,
|
|
|
+ ContactsContract.Contacts.Data.CONTENT_DIRECTORY), ProfileQuery.PROJECTION,
|
|
|
+
|
|
|
+ // Select only email addresses.
|
|
|
+ ContactsContract.Contacts.Data.MIMETYPE + " = ?", arrayOf(ContactsContract.CommonDataKinds.Email
|
|
|
+ .CONTENT_ITEM_TYPE),
|
|
|
+
|
|
|
+ // Show primary email addresses first. Note that there won't be
|
|
|
+ // a primary email address if the user hasn't specified one.
|
|
|
+ ContactsContract.Contacts.Data.IS_PRIMARY + " DESC")
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onLoadFinished(cursorLoader: Loader<Cursor>, cursor: Cursor) {
|
|
|
+ val emails = ArrayList<String>()
|
|
|
+ cursor.moveToFirst()
|
|
|
+ while (!cursor.isAfterLast) {
|
|
|
+ emails.add(cursor.getString(ProfileQuery.ADDRESS))
|
|
|
+ cursor.moveToNext()
|
|
|
+ }
|
|
|
+
|
|
|
+ addEmailsToAutoComplete(emails)
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onLoaderReset(cursorLoader: Loader<Cursor>) {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun addEmailsToAutoComplete(emailAddressCollection: List<String>) {
|
|
|
+ //Create adapter to tell the AutoCompleteTextView what to show in its dropdown list.
|
|
|
+ val adapter = ArrayAdapter(this@LoginActivity,
|
|
|
+ android.R.layout.simple_dropdown_item_1line, emailAddressCollection)
|
|
|
+
|
|
|
+ username.setAdapter(adapter)
|
|
|
+ }
|
|
|
+
|
|
|
+ object ProfileQuery {
|
|
|
+ val PROJECTION = arrayOf(
|
|
|
+ ContactsContract.CommonDataKinds.Email.ADDRESS,
|
|
|
+ ContactsContract.CommonDataKinds.Email.IS_PRIMARY)
|
|
|
+ val ADDRESS = 0
|
|
|
+ val IS_PRIMARY = 1
|
|
|
+ }
|
|
|
+
|
|
|
+ inner class LoginResult (val success: Boolean, val errorCode : Int?,
|
|
|
+ val token : String? = null);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Represents an asynchronous login/registration task used to authenticate
|
|
|
+ * the user.
|
|
|
+ */
|
|
|
+ inner class UserLoginTask internal constructor(private val mTriliumAddress: String, private val mUsername: String, private val mPassword: String) : AsyncTask<Void, Void, LoginResult>() {
|
|
|
+
|
|
|
+ val TAG : String = "UserLoginTask"
|
|
|
+
|
|
|
+ override fun doInBackground(vararg params: Void): LoginResult {
|
|
|
+
|
|
|
+ val client = OkHttpClient()
|
|
|
+
|
|
|
+ val json = JSONObject()
|
|
|
+ json.put("username", mUsername)
|
|
|
+ json.put("password", mPassword)
|
|
|
+
|
|
|
+ val body = RequestBody.create(JSON, json.toString())
|
|
|
+ val request = Request.Builder()
|
|
|
+ .url(mTriliumAddress + "/api/login/token")
|
|
|
+ .post(body)
|
|
|
+ .build()
|
|
|
+
|
|
|
+ val response: Response;
|
|
|
+
|
|
|
+ try {
|
|
|
+ response = client.newCall(request).execute()
|
|
|
+ }
|
|
|
+ catch (e: Exception) {
|
|
|
+ Log.e(TAG, "Can't connect to Trilium server", e);
|
|
|
+
|
|
|
+ return LoginResult(false, R.string.error_network_error)
|
|
|
+ }
|
|
|
+
|
|
|
+ Log.i(TAG,"Response code: " + response.code())
|
|
|
+
|
|
|
+ if (response.code() == 401) {
|
|
|
+ return LoginResult(false, R.string.error_incorrect_credentials)
|
|
|
+ }
|
|
|
+ else if (response.code() != 200) {
|
|
|
+ return LoginResult(false, R.string.error_unexpected_response)
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 LoginResult(true, null, token);
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onPostExecute(loginResult: LoginResult) {
|
|
|
+ mAuthTask = null
|
|
|
+ showProgress(false)
|
|
|
+
|
|
|
+ if (loginResult.success) {
|
|
|
+ val prefs = this@LoginActivity.getSharedPreferences(MainActivity.PREFRENCES_NAME, Context.MODE_PRIVATE);
|
|
|
+
|
|
|
+ val editor = prefs.edit()
|
|
|
+ editor.putString(MainActivity.PREF_TRILIUM_ADDRESS, mTriliumAddress)
|
|
|
+ editor.putString(MainActivity.PREF_TOKEN, loginResult.token);
|
|
|
+ editor.apply()
|
|
|
+
|
|
|
+ finish()
|
|
|
+ } else {
|
|
|
+ if (loginResult.errorCode == R.string.error_network_error
|
|
|
+ || loginResult.errorCode == R.string.error_unexpected_response) {
|
|
|
+
|
|
|
+ trilium_address.error = getString(loginResult.errorCode)
|
|
|
+ trilium_address.requestFocus()
|
|
|
+ }
|
|
|
+ else if (loginResult.errorCode == R.string.error_incorrect_credentials) {
|
|
|
+ password.error = getString(loginResult.errorCode)
|
|
|
+ password.requestFocus()
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ throw RuntimeException("Unknown code: " + loginResult.errorCode);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onCancelled() {
|
|
|
+ mAuthTask = null
|
|
|
+ showProgress(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|