Commit f4d4f057 authored by Thomas Markiewicz's avatar Thomas Markiewicz
Browse files

Added native login screen with option to log in with a web browser as well

parents e5d7836c 3b3beccc
Pipeline #50418 passed with stages
in 14 minutes and 4 seconds
......@@ -19,16 +19,17 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import androidx.browser.customtabs.CustomTabsIntent
import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.webkit.WebView
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.browser.customtabs.CustomTabsIntent
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.AccessToken
import com.keylesspalace.tusky.entity.AppCredentials
......@@ -54,6 +55,8 @@ class LoginActivity : BaseActivity(), Injectable {
private var clientId: String? = null
private var clientSecret: String? = null
private val oauthNoRedirectUri: String = "urn:ietf:wg:oauth:2.0:oob"
private val oauthRedirectUri: String
get() {
val scheme = getString(R.string.oauth_scheme)
......@@ -66,10 +69,6 @@ class LoginActivity : BaseActivity(), Injectable {
setContentView(R.layout.activity_login)
val isLibremSocial = getString(R.string.app_name) == "Librem Social"
if(isLibremSocial) {
domain = "social.librem.one"
}
if (savedInstanceState != null) {
domain = savedInstanceState.getString(DOMAIN)!!
......@@ -77,21 +76,12 @@ class LoginActivity : BaseActivity(), Injectable {
clientSecret = savedInstanceState.getString(CLIENT_SECRET)
}
domainEditText.setText(domain)
preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE)
loginButton.setOnClickListener { onButtonClick() }
whatsAnInstanceTextView.setOnClickListener {
val dialog = AlertDialog.Builder(this)
.setMessage(R.string.dialog_whats_an_instance)
.setPositiveButton(R.string.action_close, null)
.show()
val textView = dialog.findViewById<TextView>(android.R.id.message)
textView?.movementMethod = LinkMovementMethod.getInstance()
}
loginButton.setOnClickListener { onLoginButtonClick() }
loginWithBrowserButton.setOnClickListener { onLoginWithBrowserButtonClick() }
forgotPassphraseButton.setOnClickListener { onForgotPassphraseButtonClick() }
if (isAdditionalLogin()) {
setSupportActionBar(toolbar)
......@@ -101,10 +91,6 @@ class LoginActivity : BaseActivity(), Injectable {
toolbar.visibility = View.GONE
}
if(isLibremSocial && isFirstTime()) {
// automatically transition to login screen assuming librem one domain
onButtonClick()
}
}
......@@ -134,12 +120,69 @@ class LoginActivity : BaseActivity(), Injectable {
super.onSaveInstanceState(outState)
}
/**
private fun onForgotPassphraseButtonClick() {
val url = getString(R.string.forgot_passphrase_url)
val wv = WebView(this)
wv.loadUrl(url)
AlertDialog.Builder(this)
.setView(wv)
.setPositiveButton(android.R.string.ok, null)
.show()
}
private fun onLoginButtonClick() {
loginButton.isEnabled = false
domain = canonicalizeDomain(domainEditText.text.toString())
try {
HttpUrl.Builder().host(domain).scheme("https").build()
} catch (e: IllegalArgumentException) {
setLoading(false)
domainTextInputLayout.error = getString(R.string.error_invalid_domain)
return
}
val callback = object : Callback<AppCredentials> {
override fun onResponse(call: Call<AppCredentials>,
response: Response<AppCredentials>) {
if (!response.isSuccessful) {
loginButton.isEnabled = true
domainTextInputLayout.error = getString(R.string.error_failed_app_registration)
setLoading(false)
Log.e(TAG, "App authentication failed. " + response.message())
return
}
val credentials = response.body()
clientId = credentials!!.clientId
clientSecret = credentials.clientSecret
authorizeAndLogin()
}
override fun onFailure(call: Call<AppCredentials>, t: Throwable) {
loginButton.isEnabled = true
domainTextInputLayout.error = getString(R.string.error_failed_app_registration)
setLoading(false)
Log.e(TAG, Log.getStackTraceString(t))
}
}
mastodonApi
.authenticateApp(domain, getString(R.string.app_name), oauthNoRedirectUri,
OAUTH_SCOPES, getString(R.string.app_website))
.enqueue(callback)
setLoading(true)
}
/**
* Obtain the oauth client credentials for this app. This is only necessary the first time the
* app is run on a given server instance. So, after the first authentication, they are
* saved in SharedPreferences and every subsequent run they are simply fetched from there.
*/
private fun onButtonClick() {
private fun onLoginWithBrowserButtonClick() {
loginButton.isEnabled = false
......@@ -191,6 +234,43 @@ class LoginActivity : BaseActivity(), Injectable {
}
private fun authorizeAndLogin() {
Log.d(TAG, "Authorizing user...")
val callback = object : Callback<AccessToken> {
override fun onResponse(call: Call<AccessToken>, response: Response<AccessToken>) {
if (response.isSuccessful) {
onLoginSuccess(response.body()!!.accessToken)
} else {
Log.e(TAG, String.format("%s %s",
getString(R.string.error_retrieving_oauth_token),
response.message()))
setLoading(false)
val parentLayout = findViewById<View>(android.R.id.content)
Snackbar.make(parentLayout, getString(R.string.error_login_failed), Snackbar.LENGTH_LONG)
.setAction("Close", View.OnClickListener { })
.setActionTextColor(Color.WHITE)
.setTextColor(Color.WHITE)
.setBackgroundTint(Color.RED)
.show()
}
}
override fun onFailure(call: Call<AccessToken>, t: Throwable) {
setLoading(false)
domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
Log.e(TAG, String.format("%s %s",
getString(R.string.error_retrieving_oauth_token),
t.message))
}
}
mastodonApi.fetchOAuthTokenWithPassword(domain, clientId!!, clientSecret!!,
"password", usernameEditText.text.toString(), passphraseEditText.text.toString(), OAUTH_SCOPES).enqueue(callback)
}
private fun redirectUserToAuthorizeAndLogin(editText: EditText) {
/* To authorize this app and log in it's necessary to redirect to the domain given,
* activity_login there, and the server will redirect back to the app with its response. */
......@@ -380,10 +460,5 @@ class LoginActivity : BaseActivity(), Injectable {
return true
}
private fun isFirstTime(): Boolean {
val firstTime = _isFirstTime
_isFirstTime = false
return firstTime;
}
}
}
......@@ -379,6 +379,18 @@ interface MastodonApi {
@Field("grant_type") grantType: String
): Call<AccessToken>
@FormUrlEncoded
@POST("oauth/token")
fun fetchOAuthTokenWithPassword(
@Header(DOMAIN_HEADER) domain: String,
@Field("client_id") clientId: String,
@Field("client_secret") clientSecret: String,
@Field("grant_type") grantType: String,
@Field("username") username: String,
@Field("password") password: String,
@Field("scope") scope: String
): Call<AccessToken>
@FormUrlEncoded
@POST("api/v1/lists")
fun createList(
......
......@@ -25,21 +25,20 @@
<ImageView
android:layout_width="192dp"
android:layout_height="192dp"
android:layout_marginBottom="50dp"
android:layout_marginBottom="16dp"
android:contentDescription="@null"
app:srcCompat="@drawable/splash_with_name" />
<LinearLayout
android:id="@+id/loginInputLayout"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/domainTextInputLayout"
style="@style/TuskyTextInput"
android:layout_width="250dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_domain"
app:errorEnabled="true">
......@@ -48,26 +47,65 @@
android:id="@+id/domainEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:text="social.librem.one"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/usernameLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_username"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/usernameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusedByDefault="true"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passphraseLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_passphrase"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/passphraseEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/loginButton"
style="@style/TuskyButton"
android:layout_width="250dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@string/action_login" />
<TextView
android:id="@+id/whatsAnInstanceTextView"
android:layout_width="0dp"
android:layout_height="0dp"
android:paddingTop="8dp"
android:text="@string/link_whats_an_instance"
android:textAlignment="center" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginWithBrowserButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@string/action_login_with_browser" />
<com.google.android.material.button.MaterialButton
android:id="@+id/forgotPassphraseButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@string/action_forgot_passphrase" />
</LinearLayout>
<LinearLayout
......
......@@ -20,6 +20,7 @@
<string name="error_media_upload_sending">The upload failed.</string>
<string name="error_report_too_few_statuses">At least one status must be reported.</string>
<string name="error_sender_account_gone">Error sending post.</string>
<string name="error_login_failed">Login failed</string>
<string name="title_home">Home</string>
<string name="title_notifications">Notifications</string>
......@@ -72,8 +73,10 @@
<string name="action_unfavourite">Remove favorite</string>
<string name="action_more">More</string>
<string name="action_compose">Compose</string>
<string name="action_login">LOG IN</string>
<string name="action_logout">Log Out</string>
<string name="action_login">Login</string>
<string name="action_login_with_browser">Login with browser</string>
<string name="action_forgot_passphrase">Forgot passphrase?</string>
<string name="action_logout">Logout</string>
<string name="action_logout_confirm">Are you sure you want to log out of the account %1$s?</string>
<string name="action_follow">Follow</string>
<string name="action_unfollow">Unfollow</string>
......@@ -156,7 +159,9 @@
<string name="status_sent">Sent!</string>
<string name="status_sent_long">Reply sent successfully.</string>
<string name="hint_domain">Which instance?</string>
<string name="hint_domain">Host</string>
<string name="hint_username">Username</string>
<string name="hint_passphrase">Passphrase</string>
<string name="hint_compose">What\'s happening?</string>
<string name="hint_configure_scheduled_toot">Configure scheduled post.</string>
<string name="hint_content_warning">Content warning</string>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment