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 android.widget.Toast 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 { 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 { 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) { val emails = ArrayList() cursor.moveToFirst() while (!cursor.isAfterLast) { emails.add(cursor.getString(ProfileQuery.ADDRESS)) cursor.moveToNext() } addEmailsToAutoComplete(emails) } override fun onLoaderReset(cursorLoader: Loader) { } private fun addEmailsToAutoComplete(emailAddressCollection: List) { //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() { 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/sender/login") .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) { TriliumSettings(this@LoginActivity).save(mTriliumAddress, loginResult.token!!) Toast.makeText(this@LoginActivity, "Trilium connection settings have been successfully configured.", Toast.LENGTH_LONG).show() 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) } } }