/* * Copyright (C) 2012 Neo Visionaries Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.neovisionaries.android.twitter; import twitter4j.Twitter; import twitter4j.TwitterException; import twitter4j.TwitterFactory; import twitter4j.auth.AccessToken; import twitter4j.auth.RequestToken; import android.content.Context; import android.graphics.Bitmap; import android.net.Uri; import android.net.http.SslError; import android.os.AsyncTask; import android.os.Build; import android.util.AttributeSet; import android.util.Log; import android.webkit.SslErrorHandler; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import com.sudosaints.excusepro.util.Logger; /** * A {@link WebView} subclass dedicated to Twitter OAuth on Android, * using <a href="http://twitter4j.org/">twitter4j</a>. * * <p> * As this class is implemented as a subclass of View, it can be * integrated into the Android layout system seamlessly. This fact * makes this class an easily-reusable UI component. * </p> * * <p> * To use this class, it is not necessary to review the flow of * OAuth handshake. Just implement {@link TwitterOAuthView.Listener} * and call {@link #start(String, String, String, boolean, Listener) * start()} method. The result of OAuth handshake is reported via * either of the listener's methods, {@link * Listener#onSuccess(TwitterOAuthView, AccessToken) onSuccess()} or * {@link Listener#onFailure(TwitterOAuthView, TwitterOAuthView.Result) * onFailure()}. * </p> * * <p> * Below is an example Activity implementation using TwitterOAuthView. * </p> * * <pre style="border: 1px solid black; margin: 1em; padding: 0.5em;"> * * package twitteroauthview.sample; * * * import twitter4j.auth.AccessToken; * import com.neovisionaries.android.twitter.{@link TwitterOAuthView}; * import com.neovisionaries.android.twitter.{@link TwitterOAuthView.Result}; * import android.app.Activity; * import android.os.Bundle; * import android.widget.Toast; * * * public class TwitterOAuthActivity extends Activity implements {@link TwitterOAuthView.Listener} * { * <span style="color: darkgreen;">// Replace values of the parameters below with your own.</span> * private static final String CONSUMER_KEY = "YOUR CONSUMER KEY HERE"; * private static final String CONSUMER_SECRET = "YOUR CONSUMER SECRET HERE"; * private static final String CALLBACK_URL = "YOUR CALLBACK URL HERE"; * private static final boolean DUMMY_CALLBACK_URL = true; * * * private {@link TwitterOAuthView} view; * private boolean oauthStarted; * * * @Override * public void onCreate(Bundle savedInstanceState) * { * super.onCreate(savedInstanceState); * * <span style="color: darkgreen;">// Create an instance of TwitterOAuthView.</span> * view = new {@link TwitterOAuthView#TwitterOAuthView(Context) TwitterOAuthView}(this); * * setContentView(view); * * oauthStarted = false; * } * * * @Override * protected void onResume() * { * super.onResume(); * * if (oauthStarted) * { * return; * } * * oauthStarted = true; * * <span style="color: darkgreen;">// Start Twitter OAuth process. Its result will be notified via * // TwitterOAuthView.Listener interface.</span> * view.{@link #start(String, String, String, boolean, Listener) * start}(CONSUMER_KEY, CONSUMER_SECRET, CALLBACK_URL, DUMMY_CALLBACK_URL, this); * } * * * public void {@link TwitterOAuthView.Listener#onSuccess(TwitterOAuthView, AccessToken) * onSuccess}({@link TwitterOAuthView} view, {@link AccessToken} accessToken) * { * <span style="color: darkgreen;">// The application has been authorized and an access token * // has been obtained successfully. Save the access token * // for later use.</span> * showMessage("Authorized by " + accessToken.{@link AccessToken#getScreenName() getScreenName()}); * } * * * public void {@link TwitterOAuthView.Listener#onFailure(TwitterOAuthView, TwitterOAuthView.Result) * onFailure}({@link TwitterOAuthView} view, {@link TwitterOAuthView.Result Result} result) * { * <span style="color: darkgreen;">// Failed to get an access token.</span> * showMessage("Failed due to " + result); * } * * * private void showMessage(String message) * { * <span style="color: darkgreen;">// Show a popup message.</span> * Toast.makeText(this, message, Toast.LENGTH_LONG).show(); * } * } * </pre> * * @author Takahiko Kawasaki */ public class TwitterOAuthView extends WebView { /** * Tag for logging. */ private static final String TAG = "TwitterOAuthView"; /** * Internal flag for debug logging. Change the value to 'true' * to turn on debug logging. */ private static final boolean DEBUG = false; /** * Result code of Twitter OAuth process. * * @author Takahiko Kawasaki */ public enum Result { /** * The application has been authorized by the user and * got an access token successfully. */ SUCCESS, /** * Twitter OAuth process was cancelled. This result code * is generated when the internal {@link AsyncTask} * subclass was cancelled for some reasons. */ CANCELLATION, /** * Twitter OAuth process was not even started due to * failure of getting a request token. The pair of * consumer key and consumer secret was wrong or some * kind of network error occurred. */ REQUEST_TOKEN_ERROR, /** * The application has not been authorized by the user, * or a network error occurred during the OAuth handshake. */ AUTHORIZATION_ERROR, /** * The application has been authorized by the user but * failed to get an access token. */ ACCESS_TOKEN_ERROR } /** * Listener to be notified of Twitter OAuth process result. * * <p> * The methods of this listener are called on the UI thread. * </p> * * @author Takahiko Kawasaki */ public interface Listener { /** * Called when the application has been authorized by the user * and got an access token successfully. * * @param view * @param accessToken */ void onSuccess(TwitterOAuthView view, AccessToken accessToken); /** * Called when the OAuth process was not completed successfully. * * @param view * @param result */ void onFailure(TwitterOAuthView view, Result result); } /** * A constructor that calls {@link WebView#WebView(Context, AttributeSet, int) * super}(context, attrs, defStyle). * * @param context * @param attrs * @param defStyle */ public TwitterOAuthView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // Additional initialization. init(); } /** * A constructor that calls {@link WebView#WebView(Context, AttributeSet) * super}(context, attrs). * * @param context * @param attrs */ public TwitterOAuthView(Context context, AttributeSet attrs) { super(context, attrs); // Additional initialization. init(); } /** * A constructor that calls {@link WebView#WebView(Context) super}(context). * * @param context */ public TwitterOAuthView(Context context) { super(context); // Additional initialization. init(); } private void init() { WebSettings settings = getSettings(); // Not use cache. settings.setCacheMode(WebSettings.LOAD_NO_CACHE); // Enable JavaScript. settings.setJavaScriptEnabled(true); // Enable zoom control. settings.setBuiltInZoomControls(true); // Scroll bar setScrollBarStyle(WebView.SCROLLBARS_INSIDE_OVERLAY); } /** * Start Twitter OAuth process. * * <p> * This method does the following in the background. * </p> * * <ol> * <li>Get a request token using the given pair of consumer key * and consumer secret. * <li>Load the authorization URL that the obtained request token * points to into this TwitterOAuthView instance. * <li>Wait for the user to finish the authorization process at * Twitter's authorization site. This TwitterOAuthView * instance is redirected to the callback URL as a result. * <li>Detect the redirection to the callback URL and retrieve * the value of the oauth_verifier parameter from the URL. * If and only if dummyCallbackUrl is false, the callback * URL is actually accessed. * <li>Get an access token using the oauth_verifier. * <li>Call {@link Listener#onSuccess(TwitterOAuthView, AccessToken) * onSuccess()} method of the {@link Listener listener} on the * UI thread. * </ol> * * <p> * If an error occurred during the above steps, {@link * Listener#onFailure(TwitterOAuthView, TwitterOAuthView.Result) * onFailure()} of the {@link Listener listener} is called. * </p> * * @param consumerKey * @param consumerSecret * @param callbackUrl * @param dummyCallbackUrl * @param listener * * @throws IllegalArgumentException * At least one of 'consumerKey', 'consumerSecret' or * 'callbackUrl' is null. */ public void start(String consumerKey, String consumerSecret, String callbackUrl, boolean dummyCallbackUrl, Listener listener) { if (consumerKey == null || consumerSecret == null || callbackUrl == null || listener == null) { throw new IllegalArgumentException(); } Boolean dummy = Boolean.valueOf(dummyCallbackUrl); new TwitterOAuthTask().execute(consumerKey, consumerSecret, callbackUrl, dummy, listener); } private class TwitterOAuthTask extends AsyncTask<Object, Void, Result> { private String callbackUrl; private boolean dummyCallbackUrl; private Listener listener; private Twitter twitter; private RequestToken requestToken; private volatile boolean authorizationDone; private volatile String verifier; private AccessToken accessToken; @Override protected void onPreExecute() { // Set up a WebViewClient on the UI thread. TwitterOAuthView.this.setWebViewClient(new LocalWebViewClient()); } @Override protected Result doInBackground(Object... args) { String consumerKey = (String)args[0]; String consumerSecret = (String)args[1]; // Callback URL. callbackUrl = (String)args[2]; dummyCallbackUrl = (Boolean)args[3]; // Listener listener = (Listener)args[4]; if (DEBUG) { Log.d(TAG, "CONSUMER KEY = " + consumerKey); Log.d(TAG, "CONSUMER SECRET = " + consumerSecret); Log.d(TAG, "CALLBACK URL = " + callbackUrl); Log.d(TAG, "DUMMY CALLBACK URL = " + dummyCallbackUrl); } if (DEBUG) System.setProperty("twitter4j.debug", "true"); // Create a Twitter instance with the given pair of // consumer key and consumer secret. twitter = new TwitterFactory().getInstance(); twitter.setOAuthConsumer(consumerKey, consumerSecret); // Get a request token. This triggers network access. requestToken = getRequestToken(); if (requestToken == null) { // Failed to get a request token. return Result.REQUEST_TOKEN_ERROR; } // Access Twitter's authorization page. After the user's // operation, this web view is redirected to the callback // URL, which is caught by shouldOverrideUrlLoading() of // LocalWebViewClient. authorize(); // Wait until the authorization step is done. waitForAuthorization(); // If the authorization has succeeded, 'verifier' is not null. if (verifier == null) { // The authorization failed. return Result.AUTHORIZATION_ERROR; } // The authorization succeeded. The last step is to get // an access token using the verifier. accessToken = getAccessToken(); if (accessToken == null) { // Failed to get an access token. return Result.ACCESS_TOKEN_ERROR; } // All the steps were done successfully. return Result.SUCCESS; } @Override protected void onProgressUpdate(Void... values) { // In this implementation, onProgressUpdate() is called // only from authorize(). // The authorization URL. String url = requestToken.getAuthorizationURL(); if (DEBUG) Log.d(TAG, "Loading the authorization URL: " + url); // Load the authorization URL on the UI thread. TwitterOAuthView.this.loadUrl(url); } @Override protected void onPostExecute(Result result) { if (DEBUG) Log.d(TAG, "onPostExecute: result = " + result); if (result == null) { // Probably cancelled. result = Result.CANCELLATION; } if (result == Result.SUCCESS) { // Call onSuccess() method of the listener. listener.onSuccess(TwitterOAuthView.this, accessToken); } else { // Call onFailure() method of the listener. listener.onFailure(TwitterOAuthView.this, result); } } private RequestToken getRequestToken() { try { // Get a request token. This triggers network access. RequestToken token = twitter.getOAuthRequestToken(); if (DEBUG) Log.d(TAG, "Got a request token."); return token; } catch (TwitterException e) { // Failed to get a request token. e.printStackTrace(); Log.e(TAG, "Failed to get a request token.", e); // No request token. return null; } } private void authorize() { // WebView.loadUrl() needs to be called on the UI thread, // so trigger onProgressUpdate(). publishProgress(); } private void waitForAuthorization() { while (authorizationDone == false) { synchronized (this) { try { if (DEBUG) Log.d(TAG, "Waiting for the authorization step to be done."); this.wait(); } catch (InterruptedException e) { } } } if (DEBUG) Log.d(TAG, "Finished waiting for the authorization step to be done."); } private void notifyAuthorization() { // The authorization step was done. authorizationDone = true; synchronized (this) { if (DEBUG) Log.d(TAG, "Notifying that the authorization step was done."); this.notify(); } } private class LocalWebViewClient extends WebViewClient { @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { // Something wrong happened during the authorization step. Log.e(TAG, "onReceivedError: [" + errorCode + "] " + description); // Stop the authorization step. notifyAuthorization(); } @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { handler.proceed(); } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); // 11 = Build.VERSION_CODES.HONEYCOMB (Android 3.0) if (Build.VERSION.SDK_INT < 11) { // According to this page: // // http://www.catchingtales.com/android-webview-shouldoverrideurlloading-and-redirect/416/ // // shouldOverrideUrlLoading() is not called for redirects on // Android earlier than 3.0, so call the method manually. // // The implementation of shouldOverrideUrlLoading() returns // true only when the URL starts with the callback URL and // dummyCallbackUrl is true. boolean stop = shouldOverrideUrlLoading(view, url); if (stop) { // Stop loading the current page. stopLoading(); } } } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { // Check if the given URL is the callback URL. Log.d(Logger.tag, "Call back url is - " + url); if (url.startsWith(callbackUrl) == false) { // The URL is not the callback URL. return false; } // This web view is about to be redirected to the callback URL. if (DEBUG) Log.d(TAG, "Detected the callback URL: " + url); // Convert String to Uri. Uri uri = Uri.parse(url); // Get the value of the query parameter "oauth_verifier". // A successful response should contain the parameter. verifier = uri.getQueryParameter("oauth_verifier"); if (DEBUG) Log.d(TAG, "oauth_verifier = " + verifier); // Notify that the the authorization step was done. notifyAuthorization(); // Whether the callback URL is actually accessed or not // depends on the value of dummyCallbackUrl. If the // value of dummyCallbackUrl is true, the callback URL // is not accessed. return dummyCallbackUrl; } } private AccessToken getAccessToken() { try { // Get an access token. This triggers network access. AccessToken token = twitter.getOAuthAccessToken(requestToken, verifier); if (DEBUG) Log.d(TAG, "Got an access token for " + token.getScreenName()); return token; } catch (TwitterException e) { // Failed to get an access token. e.printStackTrace(); Log.e(TAG, "Failed to get an access token.", e); // No access token. return null; } } } }