diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 23e4d562..e9546172 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -38,7 +38,18 @@
+ android:windowSoftInputMode="adjustResize"
+ android:exported="true">
+
+
+
+
+
+
+
+
lifecycleScope.launch {
fetchOauthToken(result.code)
}
- is LoginResult.Err -> {
- // Authorization failed. Put the error response where the user can read it and they
- // 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)
- }
+ is LoginResult.Err -> displayError(result.errorMessage)
+ is LoginResult.Cancel -> setLoading(false)
}
}
@@ -116,7 +102,7 @@ class LoginActivity : BaseActivity(), Injectable {
getString(R.string.preferences_file_key), Context.MODE_PRIVATE
)
- binding.loginButton.setOnClickListener { onButtonClick() }
+ binding.loginButton.setOnClickListener { onLoginClick(true) }
binding.whatsAnInstanceTextView.setOnClickListener {
val dialog = AlertDialog.Builder(this)
@@ -127,13 +113,9 @@ class LoginActivity : BaseActivity(), Injectable {
textView?.movementMethod = LinkMovementMethod.getInstance()
}
- if (isAdditionalLogin() || isAccountMigration()) {
- setSupportActionBar(binding.toolbar)
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- supportActionBar?.setDisplayShowTitleEnabled(false)
- } else {
- binding.toolbar.visibility = View.GONE
- }
+ setSupportActionBar(binding.toolbar)
+ supportActionBar?.setDisplayHomeAsUpEnabled(isAdditionalLogin() || isAccountMigration())
+ supportActionBar?.setDisplayShowTitleEnabled(false)
}
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
* 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 onLoginClick(openInWebView: Boolean) {
binding.loginButton.isEnabled = false
binding.domainTextInputLayout.error = null
@@ -190,7 +183,7 @@ class LoginActivity : BaseActivity(), Injectable {
.putString(CLIENT_SECRET, credentials.clientSecret)
.apply()
- redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
+ redirectUserToAuthorizeAndLogin(domain, credentials.clientId, openInWebView)
},
{ e ->
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,
// login there, and the server will redirect back to the app with its response.
- val url = HttpUrl.Builder()
+ val uri = HttpUrl.Builder()
.scheme("https")
.host(domain)
.addPathSegments(MastodonApi.ENDPOINT_AUTHORIZE)
@@ -216,13 +209,59 @@ class LoginActivity : BaseActivity(), Injectable {
.addQueryParameter("response_type", "code")
.addQueryParameter("scope", OAUTH_SCOPES)
.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() {
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)
+
+ 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) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
index 371825ef..3e994d09 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
@@ -252,7 +252,7 @@ private fun openLinkInBrowser(uri: Uri?, context: Context) {
* @param uri the uri to open
* @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 navigationbarColor = MaterialColors.getColor(context, android.R.attr.navigationBarColor, Color.BLACK)
val navigationbarDividerColor = MaterialColors.getColor(context, R.attr.dividerColor, Color.BLACK)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2d9e2ce5..4f002b94 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -4,11 +4,11 @@
A network error occurred! Please check your connection and try again!
This cannot be empty.
Invalid domain entered
- Failed authenticating with that instance.
+ Failed authenticating with that instance. If this persists, try "Login in Browser" from the menu.
Couldn\'t find a web browser to use.
- An unidentified authorization error occurred.
- Authorization was denied.
- Failed getting a login token.
+ An unidentified authorization error occurred. If this persists, try "Login in Browser" from the menu.
+ Authorization was denied. If you\'re sure that you supplied the correct credentials, try "Login in Browser" from the menu.
+ Failed getting a login token. If this persists, try "Login in Browser" from the menu.
Failed loading account details
Could not load the login page.
The post is too long!
@@ -96,7 +96,8 @@
Remove bookmark
More
Compose
- Log in with Mastodon
+ Login with Tusky
+ Login with Browser
Log out
Are you sure you want to log out of the account %1$s?
Follow
@@ -561,6 +562,8 @@
Poll with choices: %1$s, %2$s, %3$s, %4$s; %5$s
Post language
+ Works in most cases. No data is leaked to other apps.
+ May support additional authentication methods, but requires a supported browser.
List name