Reinstate optional login via custom browser tab (#3165)
* Reinstate optional login via custom browser tab * Clarify the buttons for the different login options * Add informative labels for the different login options * Move "Login with Browser" to the options menu
This commit is contained in:
parent
56451f029e
commit
412a28e9a9
4 changed files with 92 additions and 39 deletions
|
@ -38,7 +38,18 @@
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".components.login.LoginActivity"
|
android:name=".components.login.LoginActivity"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="${applicationId}"
|
||||||
|
android:scheme="@string/oauth_scheme" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".components.login.LoginWebViewActivity" />
|
<activity android:name=".components.login.LoginWebViewActivity" />
|
||||||
<activity
|
<activity
|
||||||
|
|
|
@ -21,6 +21,7 @@ import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.Menu
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
@ -37,6 +38,7 @@ import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.entity.AccessToken
|
import com.keylesspalace.tusky.entity.AccessToken
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.getNonNullString
|
import com.keylesspalace.tusky.util.getNonNullString
|
||||||
|
import com.keylesspalace.tusky.util.openLinkInCustomTab
|
||||||
import com.keylesspalace.tusky.util.rickRoll
|
import com.keylesspalace.tusky.util.rickRoll
|
||||||
import com.keylesspalace.tusky.util.shouldRickRoll
|
import com.keylesspalace.tusky.util.shouldRickRoll
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
@ -66,24 +68,8 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
is LoginResult.Ok -> lifecycleScope.launch {
|
is LoginResult.Ok -> lifecycleScope.launch {
|
||||||
fetchOauthToken(result.code)
|
fetchOauthToken(result.code)
|
||||||
}
|
}
|
||||||
is LoginResult.Err -> {
|
is LoginResult.Err -> displayError(result.errorMessage)
|
||||||
// Authorization failed. Put the error response where the user can read it and they
|
is LoginResult.Cancel -> setLoading(false)
|
||||||
// can try again.
|
|
||||||
setLoading(false)
|
|
||||||
// Use error returned by the server or fall back to the generic message
|
|
||||||
binding.domainTextInputLayout.error =
|
|
||||||
result.errorMessage.ifBlank { getString(R.string.error_authorization_denied) }
|
|
||||||
Log.e(
|
|
||||||
TAG,
|
|
||||||
"%s %s".format(
|
|
||||||
getString(R.string.error_authorization_denied),
|
|
||||||
result.errorMessage
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is LoginResult.Cancel -> {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +102,7 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE
|
getString(R.string.preferences_file_key), Context.MODE_PRIVATE
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.loginButton.setOnClickListener { onButtonClick() }
|
binding.loginButton.setOnClickListener { onLoginClick(true) }
|
||||||
|
|
||||||
binding.whatsAnInstanceTextView.setOnClickListener {
|
binding.whatsAnInstanceTextView.setOnClickListener {
|
||||||
val dialog = AlertDialog.Builder(this)
|
val dialog = AlertDialog.Builder(this)
|
||||||
|
@ -127,13 +113,9 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
textView?.movementMethod = LinkMovementMethod.getInstance()
|
textView?.movementMethod = LinkMovementMethod.getInstance()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAdditionalLogin() || isAccountMigration()) {
|
setSupportActionBar(binding.toolbar)
|
||||||
setSupportActionBar(binding.toolbar)
|
supportActionBar?.setDisplayHomeAsUpEnabled(isAdditionalLogin() || isAccountMigration())
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
|
||||||
} else {
|
|
||||||
binding.toolbar.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requiresLogin(): Boolean {
|
override fun requiresLogin(): Boolean {
|
||||||
|
@ -147,12 +129,23 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
menu?.add(R.string.action_browser_login)?.apply {
|
||||||
|
setOnMenuItemClickListener {
|
||||||
|
onLoginClick(false)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onCreateOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtain the oauth client credentials for this app. This is only necessary the first time the
|
* 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
|
* 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.
|
* saved in SharedPreferences and every subsequent run they are simply fetched from there.
|
||||||
*/
|
*/
|
||||||
private fun onButtonClick() {
|
private fun onLoginClick(openInWebView: Boolean) {
|
||||||
binding.loginButton.isEnabled = false
|
binding.loginButton.isEnabled = false
|
||||||
binding.domainTextInputLayout.error = null
|
binding.domainTextInputLayout.error = null
|
||||||
|
|
||||||
|
@ -190,7 +183,7 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
.putString(CLIENT_SECRET, credentials.clientSecret)
|
.putString(CLIENT_SECRET, credentials.clientSecret)
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
|
redirectUserToAuthorizeAndLogin(domain, credentials.clientId, openInWebView)
|
||||||
},
|
},
|
||||||
{ e ->
|
{ e ->
|
||||||
binding.loginButton.isEnabled = true
|
binding.loginButton.isEnabled = true
|
||||||
|
@ -204,10 +197,10 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) {
|
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String, openInWebView: Boolean) {
|
||||||
// To authorize this app and log in it's necessary to redirect to the domain given,
|
// To authorize this app and log in it's necessary to redirect to the domain given,
|
||||||
// login there, and the server will redirect back to the app with its response.
|
// login there, and the server will redirect back to the app with its response.
|
||||||
val url = HttpUrl.Builder()
|
val uri = HttpUrl.Builder()
|
||||||
.scheme("https")
|
.scheme("https")
|
||||||
.host(domain)
|
.host(domain)
|
||||||
.addPathSegments(MastodonApi.ENDPOINT_AUTHORIZE)
|
.addPathSegments(MastodonApi.ENDPOINT_AUTHORIZE)
|
||||||
|
@ -216,13 +209,59 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
.addQueryParameter("response_type", "code")
|
.addQueryParameter("response_type", "code")
|
||||||
.addQueryParameter("scope", OAUTH_SCOPES)
|
.addQueryParameter("scope", OAUTH_SCOPES)
|
||||||
.build()
|
.build()
|
||||||
doWebViewAuth.launch(LoginData(domain, url.toString().toUri(), oauthRedirectUri.toUri()))
|
.toString()
|
||||||
|
.toUri()
|
||||||
|
|
||||||
|
if (openInWebView) {
|
||||||
|
doWebViewAuth.launch(LoginData(domain, uri, oauthRedirectUri.toUri()))
|
||||||
|
} else {
|
||||||
|
openLinkInCustomTab(uri, this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
// first show or user cancelled login
|
|
||||||
|
/* Check if we are resuming during authorization by seeing if the intent contains the
|
||||||
|
* redirect that was given to the server. If so, its response is here! */
|
||||||
|
val uri = intent.data
|
||||||
|
|
||||||
|
if (uri?.toString()?.startsWith(oauthRedirectUri) == true) {
|
||||||
|
// This should either have returned an authorization code or an error.
|
||||||
|
val code = uri.getQueryParameter("code")
|
||||||
|
val error = uri.getQueryParameter("error")
|
||||||
|
|
||||||
|
/* restore variables from SharedPreferences */
|
||||||
|
val domain = preferences.getNonNullString(DOMAIN, "")
|
||||||
|
val clientId = preferences.getNonNullString(CLIENT_ID, "")
|
||||||
|
val clientSecret = preferences.getNonNullString(CLIENT_SECRET, "")
|
||||||
|
|
||||||
|
if (code != null && domain.isNotEmpty() && clientId.isNotEmpty() && clientSecret.isNotEmpty()) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
fetchOauthToken(code)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
displayError(error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// first show or user cancelled login
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayError(error: String?) {
|
||||||
|
// Authorization failed. Put the error response where the user can read it and they
|
||||||
|
// can try again.
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
||||||
|
binding.domainTextInputLayout.error = if (error == null) {
|
||||||
|
// This case means a junk response was received somehow.
|
||||||
|
getString(R.string.error_authorization_unknown)
|
||||||
|
} else {
|
||||||
|
// Use error returned by the server or fall back to the generic message
|
||||||
|
Log.e(TAG, "%s %s".format(getString(R.string.error_authorization_denied), error))
|
||||||
|
error.ifBlank { getString(R.string.error_authorization_denied) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchOauthToken(code: String) {
|
private suspend fun fetchOauthToken(code: String) {
|
||||||
|
|
|
@ -252,7 +252,7 @@ private fun openLinkInBrowser(uri: Uri?, context: Context) {
|
||||||
* @param uri the uri to open
|
* @param uri the uri to open
|
||||||
* @param context context
|
* @param context context
|
||||||
*/
|
*/
|
||||||
private fun openLinkInCustomTab(uri: Uri, context: Context) {
|
fun openLinkInCustomTab(uri: Uri, context: Context) {
|
||||||
val toolbarColor = MaterialColors.getColor(context, R.attr.colorSurface, Color.BLACK)
|
val toolbarColor = MaterialColors.getColor(context, R.attr.colorSurface, Color.BLACK)
|
||||||
val navigationbarColor = MaterialColors.getColor(context, android.R.attr.navigationBarColor, Color.BLACK)
|
val navigationbarColor = MaterialColors.getColor(context, android.R.attr.navigationBarColor, Color.BLACK)
|
||||||
val navigationbarDividerColor = MaterialColors.getColor(context, R.attr.dividerColor, Color.BLACK)
|
val navigationbarDividerColor = MaterialColors.getColor(context, R.attr.dividerColor, Color.BLACK)
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
<string name="error_network">A network error occurred! Please check your connection and try again!</string>
|
<string name="error_network">A network error occurred! Please check your connection and try again!</string>
|
||||||
<string name="error_empty">This cannot be empty.</string>
|
<string name="error_empty">This cannot be empty.</string>
|
||||||
<string name="error_invalid_domain">Invalid domain entered</string>
|
<string name="error_invalid_domain">Invalid domain entered</string>
|
||||||
<string name="error_failed_app_registration">Failed authenticating with that instance.</string>
|
<string name="error_failed_app_registration">Failed authenticating with that instance. If this persists, try "Login in Browser" from the menu.</string>
|
||||||
<string name="error_no_web_browser_found">Couldn\'t find a web browser to use.</string>
|
<string name="error_no_web_browser_found">Couldn\'t find a web browser to use.</string>
|
||||||
<string name="error_authorization_unknown">An unidentified authorization error occurred.</string>
|
<string name="error_authorization_unknown">An unidentified authorization error occurred. If this persists, try "Login in Browser" from the menu.</string>
|
||||||
<string name="error_authorization_denied">Authorization was denied.</string>
|
<string name="error_authorization_denied">Authorization was denied. If you\'re sure that you supplied the correct credentials, try "Login in Browser" from the menu.</string>
|
||||||
<string name="error_retrieving_oauth_token">Failed getting a login token.</string>
|
<string name="error_retrieving_oauth_token">Failed getting a login token. If this persists, try "Login in Browser" from the menu.</string>
|
||||||
<string name="error_loading_account_details">Failed loading account details</string>
|
<string name="error_loading_account_details">Failed loading account details</string>
|
||||||
<string name="error_could_not_load_login_page">Could not load the login page.</string>
|
<string name="error_could_not_load_login_page">Could not load the login page.</string>
|
||||||
<string name="error_compose_character_limit">The post is too long!</string>
|
<string name="error_compose_character_limit">The post is too long!</string>
|
||||||
|
@ -96,7 +96,8 @@
|
||||||
<string name="action_unbookmark">Remove bookmark</string>
|
<string name="action_unbookmark">Remove bookmark</string>
|
||||||
<string name="action_more">More</string>
|
<string name="action_more">More</string>
|
||||||
<string name="action_compose">Compose</string>
|
<string name="action_compose">Compose</string>
|
||||||
<string name="action_login">Log in with Mastodon</string>
|
<string name="action_login">Login with Tusky</string>
|
||||||
|
<string name="action_browser_login">Login with Browser</string>
|
||||||
<string name="action_logout">Log out</string>
|
<string name="action_logout">Log out</string>
|
||||||
<string name="action_logout_confirm">Are you sure you want to log out of the account %1$s?</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_follow">Follow</string>
|
||||||
|
@ -561,6 +562,8 @@
|
||||||
Poll with choices: %1$s, %2$s, %3$s, %4$s; %5$s
|
Poll with choices: %1$s, %2$s, %3$s, %4$s; %5$s
|
||||||
</string>
|
</string>
|
||||||
<string name="description_post_language">Post language</string>
|
<string name="description_post_language">Post language</string>
|
||||||
|
<string name="description_login">Works in most cases. No data is leaked to other apps.</string>
|
||||||
|
<string name="description_browser_login">May support additional authentication methods, but requires a supported browser.</string>
|
||||||
|
|
||||||
<string name="hint_list_name">List name</string>
|
<string name="hint_list_name">List name</string>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue