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