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 | ||||
|             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 android:name=".components.login.LoginWebViewActivity" /> | ||||
|         <activity | ||||
|  |  | |||
|  | @ -21,6 +21,7 @@ import android.content.SharedPreferences | |||
| import android.os.Bundle | ||||
| import android.text.method.LinkMovementMethod | ||||
| import android.util.Log | ||||
| import android.view.Menu | ||||
| import android.view.View | ||||
| import android.widget.TextView | ||||
| 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.network.MastodonApi | ||||
| import com.keylesspalace.tusky.util.getNonNullString | ||||
| import com.keylesspalace.tusky.util.openLinkInCustomTab | ||||
| import com.keylesspalace.tusky.util.rickRoll | ||||
| import com.keylesspalace.tusky.util.shouldRickRoll | ||||
| import com.keylesspalace.tusky.util.viewBinding | ||||
|  | @ -66,24 +68,8 @@ class LoginActivity : BaseActivity(), Injectable { | |||
|             is LoginResult.Ok -> 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) { | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -4,11 +4,11 @@ | |||
|     <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_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_authorization_unknown">An unidentified authorization error occurred.</string> | ||||
|     <string name="error_authorization_denied">Authorization was denied.</string> | ||||
|     <string name="error_retrieving_oauth_token">Failed getting a login token.</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. 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. 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_could_not_load_login_page">Could not load the login page.</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_more">More</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_confirm">Are you sure you want to log out of the account %1$s?</string> | ||||
|     <string name="action_follow">Follow</string> | ||||
|  | @ -561,6 +562,8 @@ | |||
|         Poll with choices: %1$s, %2$s, %3$s, %4$s; %5$s | ||||
|     </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> | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue