Wednesday, July 18, 2012

Tweeting from Android Apps using twitter4j


S. Gökhan Topçu
gtopcu@gmail.com


Twitter has a great Rest API for accessing its functions outside it's standard web page. But if you're planning on tweeting from inside your Java/Android applications, you might find it a bit uninspiring to get an oAuth authorization and use standard functions such as tweeting, getting timeline, searching for tweets etc. using the Rest API, since you will need to handle HTTP post/gets and JSON parsing yourself, along with building the authorization flow logic. You can use the readily available Java libraries such as Apache HTTP Client and the org.json.* package, both available with the standard Android SDK if you want to go this way, and I've provided some working examples in another blog. Another option is to use twitter4j, which is an unofficial Java API for Twitter since Twitter doesn't provide one itself. It provides several jars based on what you will need to do; apart from the core jar, you can also include media, streaming and async libs if you need them.

I have developed two sample activities - first one getting a message to be tweeted from the activity arguments, checking authorization and sending async tweets. Second activity is a WebView activity which displays the standard Twitter oAuth user login page which is launched by the first activity if the app needs authorization. The code also takes care of the scenario where the user revokes the app's rights after they were granted inside the application, and the user is not allowed to navigate anywhere else. TwitterHandler is designed to be called using startActivityForResult(), and it returns a string explanation along with the resultCode. You can also set a boolean "forget" argument if you want TwitterHandler to 'forget' any previous authentications saved in app preferences and ask for it again. I have used twitter4j-core-2.2.6.jar and twitter4j-async-2.2.6.jar libs from twitter4j in this project. Feel free to play around with the code to suit your needs.

For all of this to work, you first need to go to http://dev.twitter.com/apps and create an app. Note down the consumer key and secret, as you will be providing those as parameters to the TwitterFactory. Also, set a unique callback URL for your app from the app settings in Twitter so that your WebView will catch it when Twitter is calling back your app. This could also be done dynamically while initializing a RequestToken object.

TwitterHandler.java
import twitter4j.AsyncTwitter;
import twitter4j.AsyncTwitterFactory;
import twitter4j.Status;
import twitter4j.TwitterAdapter;
import twitter4j.TwitterException;
import twitter4j.TwitterListener;
import twitter4j.TwitterMethod;
import twitter4j.auth.AccessToken;
import twitter4j.auth.RequestToken;
import twitter4j.conf.ConfigurationBuilder;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;

public class TwitterHandler extends Activity {

 private static final int REQUEST_AUTHORIZATION = 851;

 private SharedPreferences mPrefs;
 private String message;
 
 private AsyncTwitter twitter;
 private RequestToken requestToken;
 
 private TwitterListener listener = new TwitterAdapter() {
        @Override 
        public void updatedStatus(Status status) {
         handlePostResponse(RESULT_OK);
        }
        @Override
        public void onException(TwitterException e, TwitterMethod method) {
         Log.e(TwitterHandler.class.getName(), "Method: " + method.name() + " Error: " + e.getErrorMessage());
         //If we get an authorization error, that means the user revoked the app rights from Twitter
         if(e.getErrorMessage().contains(getString(R.string.twitter_listenerAuthError))) {
          authorizeAndTweet();
         }
         else {
          handlePostResponse(RESULT_FIRST_USER + 1);
         }
        }
    };

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  
  message = getIntent().getStringExtra("message");
  if(message == null) {
   throw new RuntimeException("Message cannot be null");
  }
  
  mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
  String access_token = mPrefs.getString("twitter_access_token", null);
  String access_token_secret = mPrefs.getString("twitter_access_token_secret", null);
  
  ConfigurationBuilder cb = new ConfigurationBuilder()
          .setOAuthConsumerKey(getString(R.string.twitter_consumerKey))
          .setOAuthConsumerSecret(getString(R.string.twitter_consumerSecret));
  AsyncTwitterFactory factory = new AsyncTwitterFactory(cb.build());
     twitter = factory.getInstance();
     
     twitter.addListener(listener);
  
     try {
      requestToken = twitter.getOAuthRequestToken();
   if(access_token == null || access_token_secret == null || getIntent().getBooleanExtra("forget", false) == true) {
    authorizeAndTweet();
   }
   else {
    AccessToken accessToken = new AccessToken(access_token, access_token_secret);
    twitter.setOAuthAccessToken(accessToken);
    twitter.updateStatus(message);
   }
     }
     catch(TwitterException e) {
      Log.e(TwitterHandler.class.getName(), "TwitterError: " + e.getErrorMessage());
      handlePostResponse(RESULT_FIRST_USER + 1);
     }
 }
 
 private void authorizeAndTweet() {
  //Reset token
  Editor editor = mPrefs.edit();
  editor.putString("twitter_access_token", null);
  editor.putString("twitter_access_token_secret", null);
  editor.commit();
  //Start WebView activity
  Intent i = new Intent(this, TwitterWebView.class);
  i.putExtra(TwitterWebView.URL, requestToken.getAuthorizationURL());
  startActivityForResult(i, REQUEST_AUTHORIZATION);
 }
 
 @Override
 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  super.onActivityResult(requestCode, resultCode, data);
  if(requestCode == REQUEST_AUTHORIZATION && resultCode == RESULT_OK) {
   try {
    final String verifier = data.getStringExtra("oauth_verifier");
    if(verifier == null) {
     Log.e(TwitterHandler.class.getName(), "VerifierError");
     handlePostResponse(RESULT_FIRST_USER + 1);
    }
    AccessToken accessToken = twitter.getOAuthAccessToken(requestToken, verifier);
    Editor editor = mPrefs.edit();
    editor.putString("twitter_access_token", accessToken.getToken());
    editor.putString("twitter_access_token_secret", accessToken.getTokenSecret());
    editor.commit();
    twitter.setOAuthAccessToken(accessToken);
    twitter.updateStatus(message);
   }
   catch(TwitterException e) {
    Log.e(TwitterHandler.class.getName(), e.getErrorMessage());
    handlePostResponse(RESULT_FIRST_USER + 1);
   }
  }
  else {
   handlePostResponse(RESULT_FIRST_USER);
  }
 }
 
 private void handlePostResponse(int resultCode) {
  Intent i = new Intent();
  switch(resultCode) {
   case RESULT_OK:     i.putExtra("message", getString(R.string.twitter_okMessage));
           break;
   case RESULT_FIRST_USER:   i.putExtra("message", getString(R.string.twitter_unauthorizedMessage));
           break;
   case RESULT_FIRST_USER + 1:  i.putExtra("message", getString(R.string.twitter_errorMessage));
           break;
   case RESULT_CANCELED:   break;
  }
  setResult(resultCode, i);
  finish();
 }
}

TwitterWebView.java
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.ViewGroup.LayoutParams;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class TwitterWebView extends Activity {
 
 final static String URL = "twitter_authorization_url";

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  WebView webView = new WebView(this);
  
  webView.getSettings().setJavaScriptEnabled(true);
  webView.getSettings().setAppCacheEnabled(false);
  webView.getSettings().setSaveFormData(false);
  webView.getSettings().setSavePassword(false);
  webView.setWebViewClient(new WebViewClient() {
   @Override
   public void onLoadResource(WebView view, String url) {
    checkURL(url);
   }
   
  });
  Log.d(TwitterWebView.class.getName(), "Loading Url: " + getIntent().getStringExtra(URL));
  webView.loadUrl(getIntent().getStringExtra(URL));
  setContentView(webView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
 }
 
 private void checkURL(String url) {
  if(url.contains(getString(R.string.twitter_callbackGranted))) {
   Uri uri = Uri.parse(url);
            String oauthVerifier = uri.getQueryParameter("oauth_verifier");
            Intent i = new Intent();
            i.putExtra("oauth_verifier", oauthVerifier);
            Log.d(TwitterWebView.class.getName(), "OK, Url: " + url);
            setResult(RESULT_OK, i);
            finish();
        }
  else if(url.contains(getString(R.string.twitter_callbackDenied))) {
   Log.d(TwitterWebView.class.getName(), "Unauthorized, Url: " + url);
            setResult(RESULT_FIRST_USER);
            finish();
  }
  //Banned URLs
  else if(url.contains(getString(R.string.twitter_home))    ||
    url.contains(getString(R.string.twitter_appSettings))  ||
    url.contains(getString(R.string.twitter_tos))    ||
    url.contains(getString(R.string.twitter_privacy))   ||
    url.contains(getString(R.string.website)))     {
   Log.d(TwitterWebView.class.getName(), "Unauthorized, Url: " + url);
            setResult(RESULT_FIRST_USER);
            finish();
  }
  else {
   Log.d(TwitterWebView.class.getName(), "Passed Url: " + url);
  }
  
 }
 
 @Override
 public void onBackPressed() {
  super.onBackPressed();
  setResult(RESULT_FIRST_USER);
        finish();
 }

}

The strings_twitterhandler resource file includes consumer key & secret, along with user messages, blocked URLs and parts of the Twitter URL responses to detect authorization & denial.

strings_twitterhandler.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="twitter_consumerKey">r3dmV3mkbacY4CKdtAaNQ</string>
    <string name="twitter_consumerSecret">gomuK2bcO7KPfUyk1PHtx91AAIEQCumvrYxLUwCiHlg</string>
 <string name="twitter_callbackGranted">oauth_verifier=</string>
 <string name="twitter_callbackDenied">callback?denied=</string>
 <string name="twitter_listenerAuthError">Could not authenticate with OAuth</string>
 <!-- Forbidden Urls -->
 <string name="twitter_home">twitter.com/home</string>
 <string name="twitter_appSettings">twitter.com/settings/applications</string>
 <string name="twitter_tos">twitter.com/tos</string>
 <string name="twitter_privacy">twitter.com/privacy</string>
 
 <string name="twitter_unauthorizedMessage">Unauthorized</string>
    <string name="twitter_errorMessage">Error</string>
    <string name="twitter_okMessage">Tweet Successful</string>
</resources>


Further Reading:
twitter4j
Twitter Rest API
Twitter Apps
Apache HTTP Client
Using Twitter's Rest API in Android



7 comments:

  1. Do u ve a downloadable source or zip file to help me understand it more
    Hari

    ReplyDelete
    Replies
    1. yess even i want the source code

      Delete
  2. Wow, this post deserves 10 out of 10 and a koala stamp. It solved all my problems having to handle callbacks from the android web browser. It's a much neater solution. I just changed a few things you may be interested in:

    1 - I'm using a Twitter object instead of AsyncTwitter. The Twitter object seems to make it easier to get GET data from twitter, such as from the User search API, and it doesn't need a TwitterListener.

    2 - My virtual Device crashes when calling twitter.getOAuthRequestToken() and twitter.getOAuthAccessToken(), even with AsyncTwitter objects, so I encapsulated these in AsyncTask threads.

    Would you mind if I use parts of this code (and virtually all of the TwitterWebView class) in my code?

    cheers,

    Dirk Bertels

    ReplyDelete
  3. Hi Dirk, thanks for your kind comments. I wouldn't mind at all, feel free to copy & use any code you need. Happy new year, regards.

    ReplyDelete
  4. Thank you very much, this made my day!

    ReplyDelete
  5. I'm using twitter4j v3.0.3 and Android SDK API 8 and it doesn't seem to work. The app crashes without catching an exception when trying to instantiate the ConfigurationBuilder.
    Looking into twitter4j source I see that there is no constructor for "new ConfigurationBuilder()".

    Could you help me please?

    ReplyDelete

Please leave your feedback below if you found this blog useful, thanks.