package com.battlelancer.seriesguide.ui; import android.annotation.SuppressLint; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.ActionBar; import android.view.MenuItem; import android.view.View; import android.webkit.CookieManager; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Button; import android.widget.TextView; import com.battlelancer.seriesguide.R; import com.battlelancer.seriesguide.util.Utils; import timber.log.Timber; /** * Base class to create an OAuth 2.0 authorization flow using the browser, offering fallback to an * embedded {@link android.webkit.WebView}. */ public abstract class BaseOAuthActivity extends BaseActivity { /** Pass with true to not auto launch the external browser, display default error message. */ public static final String EXTRA_KEY_IS_RETRY = "isRetry"; /** Needs to match with the scheme registered in the manifest. */ private static final String OAUTH_URI_SCHEME = "sgoauth"; public static final String OAUTH_CALLBACK_URL_CUSTOM = OAUTH_URI_SCHEME + "://callback"; private WebView webview; private View buttonContainer; private View progressBar; private TextView textViewMessage; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_oauth); setupActionBar(); setupViews(); if (handleAuthIntent(getIntent())) { return; } boolean isRetry = getIntent().getBooleanExtra(EXTRA_KEY_IS_RETRY, false); if (isRetry) { setMessage(getAuthErrorMessage()); } if (savedInstanceState == null && !isRetry) { // try to launch external browser with OAuth page launchBrowser(); } } @Override protected void onDestroy() { super.onDestroy(); /** * Force the text-to-speech accessibility Javascript plug-in service on Android 4.2.2 to * get shutdown, to avoid leaking its context. * * http://stackoverflow.com/a/18798305/1000543 */ if (webview != null) { webview.getSettings().setJavaScriptEnabled(false); // remove client to avoid callbacks to non-existent views webview.setWebViewClient(null); webview = null; } } @Override protected void setupActionBar() { super.setupActionBar(); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } } private void setupViews() { webview = (WebView) findViewById(R.id.webView); buttonContainer = findViewById(R.id.containerOauthButtons); progressBar = findViewById(R.id.progressBarOauth); textViewMessage = (TextView) buttonContainer.findViewById(R.id.textViewOauthMessage); // setup buttons (can be used if browser launch fails or user comes back without code) Button buttonBrowser = (Button) findViewById(R.id.buttonOauthBrowser); buttonBrowser.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { launchBrowser(); } }); Button buttonWebView = (Button) findViewById(R.id.buttonOauthWebView); buttonWebView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { activateWebView(); } }); activateFallbackButtons(); setMessage(null); } private void launchBrowser() { String authorizationUrl = getAuthorizationUrl(); if (authorizationUrl != null) { Utils.launchWebsite(this, authorizationUrl, "OAuth", "Launch browser for OAuth"); } } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); handleAuthIntent(intent); } private boolean handleAuthIntent(Intent intent) { // handle auth callback from external browser Uri callbackUri = intent.getData(); if (callbackUri == null || !callbackUri.getScheme().equals(OAUTH_URI_SCHEME)) { return false; } fetchTokensAndFinish(callbackUri.getQueryParameter("code"), callbackUri.getQueryParameter("state")); return true; } protected void activateFallbackButtons() { webview.setVisibility(View.GONE); buttonContainer.setVisibility(View.VISIBLE); } @SuppressLint("SetJavaScriptEnabled") protected void activateWebView() { buttonContainer.setVisibility(View.GONE); webview.setVisibility(View.VISIBLE); webview.setWebViewClient(webViewClient); webview.getSettings().setJavaScriptEnabled(true); // make sure we start fresh CookieManager cookieManager = CookieManager.getInstance(); cookieManager.removeAllCookie(); webview.clearCache(true); // load the authorization page Timber.d("Initiating authorization request..."); String authUrl = getAuthorizationUrl(); if (authUrl != null) { webview.loadUrl(authUrl); } } protected void setMessage(String message) { setMessage(message, false); } protected void setMessage(String message, boolean progressVisible) { if (message == null) { textViewMessage.setVisibility(View.GONE); } else { textViewMessage.setVisibility(View.VISIBLE); textViewMessage.setText(message); } progressBar.setVisibility(progressVisible ? View.VISIBLE : View.GONE); } protected WebViewClient webViewClient = new WebViewClient() { @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { Timber.e("WebView error: %s %s", errorCode, description); activateFallbackButtons(); setMessage(getAuthErrorMessage() + "\n\n(" + errorCode + " " + description + ")"); } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url != null && url.startsWith(OAUTH_CALLBACK_URL_CUSTOM)) { Uri uri = Uri.parse(url); fetchTokensAndFinish(uri.getQueryParameter("code"), uri.getQueryParameter("state")); return true; } return false; } }; public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { onBackPressed(); return true; } return super.onOptionsItemSelected(item); } /** * Return the url of the OAuth authorization page. */ @Nullable protected abstract String getAuthorizationUrl(); /** * Return an error message displayed if authorization fails at any point. */ protected abstract String getAuthErrorMessage(); /** * Called with the OAuth auth code and state retrieved from the {@link #getAuthorizationUrl()} * once the user has authorized us. If state was sent, ensure it matches. Then retrieve the * OAuth tokens with the auth code. */ protected abstract void fetchTokensAndFinish(@Nullable String authCode, @Nullable String state); }