Saturday, October 15, 2011

Share on Twitter Integration in Android application

 The last few months were so busy for me learning more about android development and best practice. I can say i learned a lot while developing two android application that covered almost many aspect from handling Intents, Services, BroadcastReciever, Activities and many other android components.

In this post, I am sharing how I made a pretty friendly share on twitter interface that can be used to send tweets via your android application.
For that purpose, we are going to use Twitter4J which is one of the 3 Twitter API libraries ( Scribe, Twitter API ME).
But before we start with the android coding, we need to create a new application on twitter. Really easy to do,  go to https://dev.twitter.com/apps/new and fill in the form. Don't forget to put in the 'Callback URL', for instant put anything like http://myAndroidApp.dev . It is really important to put a Callback URL so twitter can identify your application as a Browser Application which we are going to need in our android project. The Callback URL that the application is going to use will be defined in the callback URL that we’ll pass on when authenticating the user.

After setting up the Twitter application, we move to our android application.
We are going to need the following libraries :
 Now lets start some coding! First we make the XML for the tweet screen like the following :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="fill_parent"
    android:layout_height="fill_parent" android:background="@color/cian">
    <android.webkit.WebView android:id="@+id/web_view"
        android:layout_width="fill_parent" android:layout_height="fill_parent"
        android:visibility="gone" />
    <LinearLayout android:layout_centerInParent="true" android:id="@+id/progress_spinner"
        android:layout_width="wrap_content" android:layout_height="wrap_content"
        android:background="@drawable/popup_frame" android:visibility="gone"
        android:orientation="horizontal">
        <ProgressBar android:layout_width="50.0dip"
            android:layout_height="50.0dip" android:indeterminate="true"
            style="\?android:attr/progressBarStyleInverse" />
        <TextView android:text="@string/progress_loading"
            android:layout_width="wrap_content" android:layout_height="wrap_content" 
            android:layout_gravity="center" />
    </LinearLayout>
    <ImageView android:layout_width="wrap_content" android:id="@+id/twitter_logo"
        android:layout_height="wrap_content" android:src="@drawable/twitter_dialog_title" />
    <EditText android:id="@+id/textStatus" android:typeface="serif"
        android:maxLength="160" android:layout_width="fill_parent"
        android:layout_margin="5px" android:layout_height="wrap_content"
        android:padding="5px" android:minLines="5" android:layout_below="@+id/twitter_logo"/>
    <ImageButton android:layout_height="wrap_content"
        android:id="@+id/btn_sendTweet" android:background="@drawable/btn_tweet"
        android:text="Tweet" android:layout_width="wrap_content"
        android:layout_centerHorizontal="true" android:layout_below="@+id/textStatus"/>
</RelativeLayout>

the layout will look like this, you can customize that layout as you like.
The webView is set to visibility "gone" as we will need it in our activity to get the authorization from the user to post tweets.
So, when we want to send a tweet, that screen will appear as a Dilog, then we can fill in the tweet text manually or grammatically. when clicking the Tweet button, we need to check if the user is authenticated. We can use a code like this :
public static boolean isAuthenticated(SharedPreferences prefs) {

        ACCESS_KEY = prefs.getString("TWITTER_ACCESS_KEY", "none");
        ACCESS_SECRET = prefs.getString("TWITTER_ACCESS_SECRET", "none");

        try {
            AccessToken accessToken = new AccessToken(ACCESS_KEY, ACCESS_SECRET);
            Twitter twitter = new TwitterFactory().getInstance();
            twitter.setOAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET);
            twitter.setOAuthAccessToken(accessToken);

            twitter.getAccountSettings();
            Log.d(TAG, twitter.getScreenName());
            return true;
        } catch (TwitterException e) {
            return false;
        } catch (Exception e) {
            Log.e(TAG, e.getMessage(), e.getCause());
            return false;
        }
    }
As access token provided by twitter never expire, we can store it in the SharedPreferences and reuse it with same user again and again. When the method returns true, that means that the user is authenticated and we can begin the sending the tweet process, we use this function for that :
public static void sendTweet(SharedPreferences prefs, String msg)
            throws TwitterException, Exception {

        ACCESS_KEY = prefs.getString("TWITTER_ACCESS_KEY", "");
        ACCESS_SECRET = prefs.getString("TWITTER_ACCESS_SECRET", "");
        AccessToken accessToken = new AccessToken(ACCESS_KEY, ACCESS_SECRET);
        Twitter twitter = new TwitterFactory().getInstance();
        twitter.setOAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET);
        twitter.setOAuthAccessToken(accessToken);
        twitter.updateStatus(msg);
}

If the user is not authenticated or didn't grant access to our application, we gotta acquire an authentication url as first step, something like this :
    private String getAuthUrl() {
        String authUrl = null;
        try {
            authUrl = httpOauthprovider.retrieveRequestToken(httpOauthConsumer, CALLBACK_URL);
        } catch (OAuthMessageSignerException e) {
            Log.d(TAG, e.getMessage(), e.fillInStackTrace());
        } catch (OAuthNotAuthorizedException e) {
            Log.d(TAG, e.getMessage(), e.fillInStackTrace());
        } catch (OAuthExpectationFailedException e) {
            Log.d(TAG, e.getMessage(), e.fillInStackTrace());
        } catch (OAuthCommunicationException e) {
            Log.d(TAG, e.getMessage(), e.fillInStackTrace());
        }
        return authUrl;
    }
now, here is the tricky part. We are going to use the webView that we made in our layout file. Nothing better than the code to explain the next step. :
private void doOauth() { //init the web view
        webView.getSettings().setJavaScriptEnabled(true);
        webView.setVisibility(View.VISIBLE);
        webView.bringToFront();
        webView.requestFocus(View.FOCUS_DOWN);
        String authUrl = getAuthUrl();
        if (authUrl != null) { //if the auth url is valid, we proceed
            webView.setWebViewClient(new WebViewClient() {
                @Override
                public void onPageStarted(WebView view, String url,
                        Bitmap bitmap) { 
                    mSpinner.bringToFront();
                    mSpinner.setVisibility(View.VISIBLE);
                }
                @Override
                public void onReceivedSslError(WebView view,
                        SslErrorHandler handler, SslError error) { // bypass the SSl check
                    handler.proceed(); // the default behavior of the WebView is to block the access                    
                }
                @Override
                public void onPageFinished(WebView view, String url) {
                    mSpinner.setVisibility(View.GONE);
                    webView.bringToFront();
                    if (url.startsWith(CALLBACK_URL)) { // the user is authenticated and we are being redirected 
                     
                        if (url.indexOf("oauth_token=") != -1) {
                            
                            String verifier = extractParamFromUrl(url, OAuth.OAUTH_VERIFIER);
                            try {

                                httpOauthprovider.retrieveAccessToken(httpOauthConsumer, verifier);
                                ACCESS_KEY = httpOauthConsumer.getToken();
                                ACCESS_SECRET = httpOauthConsumer.getTokenSecret();
                                SharedPreferences.Editor ed = mPrefs.edit();
                                ed.putString("TWITTER_ACCESS_KEY",ACCESS_KEY);
                                ed.putString("TWITTER_ACCESS_SECRET", ACCESS_SECRET);
                                ed.commit();
                                
                                view.setVisibility(View.INVISIBLE);
                                updateStatus();

                            } catch (OAuthMessageSignerException e) {
                                Log.d(TAG, e.getMessage(), e.fillInStackTrace());
                            } catch (OAuthNotAuthorizedException e) {
                                Log.d(TAG, e.getMessage(), e.fillInStackTrace());
                            } catch (OAuthExpectationFailedException e) {
                                Log.d(TAG, e.getMessage(), e.fillInStackTrace());
                            } catch (OAuthCommunicationException e) {
                                Log.d(TAG, e.getMessage(), e.fillInStackTrace());
                            }
                        } else if (url.indexOf("error=") != -1) {
                            view.setVisibility(View.GONE);
                            Toast.makeText(getBaseContext(), R.string.toast_twitter_error,Toast.LENGTH_LONG).show();
                        }
                    }
                }

                private String extractParamFromUrl(String url, String paramName) {
                    String queryString = url.substring(url.indexOf("?", 0) + 1,
                            url.length());
                    QueryStringParser queryStringParser = new QueryStringParser(
                            queryString);
                    return queryStringParser.getQueryParamValue(paramName);
                }
            });
            webView.loadUrl(authUrl);
        } else {
            webView.setVisibility(View.GONE);
            Toast.makeText(getBaseContext(), R.string.toast_twitter_error,Toast.LENGTH_LONG).show();
        }
    }

Ok,As you can see, we are using 0Auth to get all the needed tokens, httpOauthConsumer and httpOauthprovider are declared on our activity like the following:
private static CommonsHttpOAuthConsumer httpOauthConsumer = new CommonsHttpOAuthConsumer(
            TwitterUtils.CONSUMER_KEY, TwitterUtils.CONSUMER_SECRET);
    private static CommonsHttpOAuthProvider httpOauthprovider = new CommonsHttpOAuthProvider(
            REQUEST_URL, ACCESS_TOKEN_URL, AUTH_URL);
all the tokens are handled withing the web view, when the user authorize the application, it will follow him to the provided Callback URL, the one we set when acquiring the authentification Url
authUrl = httpOauthprovider.retrieveRequestToken(httpOauthConsumer, CALLBACK_URL);
the CALLBACK_URL will be something like this "myAndroidApp://twitterOauth"

When the user is not authenticated or didn't authorize the application, the AuthURL will direct us to the Twitter authentication page which look like the following :

When the user name and password are accepted, the Twitter will redirect the user to the CALLBACK_URL that we set earlier, this screen will be shown then :
When we detect the Callback url in the webView, we store the OAuth tokens (ACCESS_KEY & ACCESS_SECRET) that we are using with the Twitter API.
Now we are ready to send the tweet and close the activity to get back to our android application. That will be it, not so hard !!
Here are the full TwitterConnector Activity and the TwitterUtils Class, enjoy.

TwitterUtils.java
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.auth.AccessToken;
import android.content.SharedPreferences;
import android.util.Log;

public class TwitterUtils {

    public final static String CONSUMER_KEY = "YOUR_TWITTER_APP_CONSUMER_KEY";
    public final static String CONSUMER_SECRET = "YOUR_TWITTER_APP_CONSUMER_SECRET";
    private static final String TAG = "Twitter";

    private static String ACCESS_KEY;
    private static String ACCESS_SECRET;

    public static boolean isAuthenticated(SharedPreferences prefs) {

        ACCESS_KEY = prefs.getString("TWITTER_ACCESS_KEY", "none");
        ACCESS_SECRET = prefs.getString("TWITTER_ACCESS_SECRET", "none");

        try {
            AccessToken accessToken = new AccessToken(ACCESS_KEY, ACCESS_SECRET);
            Twitter twitter = new TwitterFactory().getInstance();
            twitter.setOAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET);
            twitter.setOAuthAccessToken(accessToken);

            twitter.getAccountSettings();
            Log.d(TAG, twitter.getScreenName());
            return true;
        } catch (TwitterException e) {
            return false;
        } catch (Exception e) {
            Log.e(TAG, e.getMessage(), e.getCause());
            return false;
        }
    }

    public static void sendTweet(SharedPreferences prefs, String msg)
            throws TwitterException, Exception {

        ACCESS_KEY = prefs.getString("TWITTER_ACCESS_KEY", "");
        ACCESS_SECRET = prefs.getString("TWITTER_ACCESS_SECRET", "");
        AccessToken accessToken = new AccessToken(ACCESS_KEY, ACCESS_SECRET);
        Twitter twitter = new TwitterFactory().getInstance();
        twitter.setOAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET);
        twitter.setOAuthAccessToken(accessToken);
        twitter.updateStatus(msg);
    }
}

TwitterConnector.java
import java.io.IOException;

import oauth.signpost.OAuth;
import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer;
import oauth.signpost.commonshttp.CommonsHttpOAuthProvider;
import oauth.signpost.exception.OAuthCommunicationException;
import oauth.signpost.exception.OAuthExpectationFailedException;
import oauth.signpost.exception.OAuthMessageSignerException;
import oauth.signpost.exception.OAuthNotAuthorizedException;
import twitter4j.TwitterException;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.net.http.SslError;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.webkit.SslErrorHandler;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.Toast;


public class TwitterConnector extends Activity {
    
    private static final String REQUEST_URL = "http://twitter.com/oauth/request_token";
    private static final String ACCESS_TOKEN_URL = "http://twitter.com/oauth/access_token";
    private static final String AUTH_URL = "http://twitter.com/oauth/authorize";
    private static final String CALLBACK_URL = "myAndroidApp://twitterOauth";
    private static final String TAG = "Twitter";

    private static CommonsHttpOAuthConsumer httpOauthConsumer = new CommonsHttpOAuthConsumer(
            TwitterUtils.CONSUMER_KEY, TwitterUtils.CONSUMER_SECRET);
    private static CommonsHttpOAuthProvider httpOauthprovider = new CommonsHttpOAuthProvider(
            REQUEST_URL, ACCESS_TOKEN_URL, AUTH_URL);
    
    private SharedPreferences mPrefs;

    private String tweet;
    private String url;
    private String title;
    private static String ACCESS_KEY;
    private static String ACCESS_SECRET;

    private EditText tweetEditText;
    private ImageButton sendTweetButton;
    private WebView webView;
    private LinearLayout mSpinner;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        
        setContentView(R.layout.twitter_share);
        mSpinner = (LinearLayout)findViewById(R.id.progress_spinner);
        webView = (WebView)findViewById(R.id.web_view);
        url = getIntent().getStringExtra("URL");
        title = getIntent().getStringExtra("TITLE");
        
        int tweetLength = url.length() + title.length() + 3;

        try {
            tweet = title + " : " + TinyURL.shorter(url);
        } catch (IOException e) {
            tweet = title
                    .substring(0, title.length() - (tweetLength - 160 - 1))
                    + " : " + url ;
        }

        tweetEditText = (EditText) findViewById(R.id.textStatus);
        tweetEditText.setText(tweet);

        mPrefs = getSharedPreferences("MA", Context.MODE_PRIVATE);

        sendTweetButton = (ImageButton) findViewById(R.id.btn_sendTweet);
        sendTweetButton.setOnClickListener(sendTweetClickListener);
    }

    @Override
    protected void onResume() {
        super.onResume();
    }
    
    private OnClickListener sendTweetClickListener = new OnClickListener() {

        @Override
        public void onClick(View v) {
            
            if (TwitterUtils.isAuthenticated(getSharedPreferences("MA", Context.MODE_PRIVATE))) {
                updateStatus();
            } else {
                doOauth();
            }
        }
    };

    private void doOauth() {
        webView.getSettings().setJavaScriptEnabled(true);
        webView.setVisibility(View.VISIBLE);
        webView.bringToFront();
        webView.requestFocus(View.FOCUS_DOWN);
        String authUrl = getAuthUrl();
        if (authUrl != null) {
            webView.setWebViewClient(new WebViewClient() {
                @Override
                public void onPageStarted(WebView view, String url,
                        Bitmap bitmap) {
                    mSpinner.bringToFront();
                    mSpinner.setVisibility(View.VISIBLE);
                }
                @Override
                public void onReceivedSslError(WebView view,
                        SslErrorHandler handler, SslError error) {
                    handler.proceed();                    
                }
                @Override
                public void onPageFinished(WebView view, String url) {
                    mSpinner.setVisibility(View.GONE);
                    webView.bringToFront();
                    if (url.startsWith(CALLBACK_URL)) {

                        if (url.indexOf("oauth_token=") != -1) {
                            
                            String verifier = extractParamFromUrl(url, OAuth.OAUTH_VERIFIER);
                            try {

                                httpOauthprovider.retrieveAccessToken(httpOauthConsumer, verifier);
                                ACCESS_KEY = httpOauthConsumer.getToken();
                                ACCESS_SECRET = httpOauthConsumer.getTokenSecret();
                                SharedPreferences.Editor ed = mPrefs.edit();
                                ed.putString("TWITTER_ACCESS_KEY",ACCESS_KEY);
                                ed.putString("TWITTER_ACCESS_SECRET", ACCESS_SECRET);
                                ed.commit();
                                
                                view.setVisibility(View.INVISIBLE);
                                updateStatus();

                            } catch (OAuthMessageSignerException e) {
                                Log.d(TAG, e.getMessage(), e.fillInStackTrace());
                            } catch (OAuthNotAuthorizedException e) {
                                Log.d(TAG, e.getMessage(), e.fillInStackTrace());
                            } catch (OAuthExpectationFailedException e) {
                                Log.d(TAG, e.getMessage(), e.fillInStackTrace());
                            } catch (OAuthCommunicationException e) {
                                Log.d(TAG, e.getMessage(), e.fillInStackTrace());
                            }
                        } else if (url.indexOf("error=") != -1) {
                            view.setVisibility(View.GONE);
                            Toast.makeText(getBaseContext(), R.string.toast_twitter_error,Toast.LENGTH_LONG).show();
                        }
                    }
                }

                private String extractParamFromUrl(String url, String paramName) {
                    String queryString = url.substring(url.indexOf("?", 0) + 1,
                            url.length());
                    QueryStringParser queryStringParser = new QueryStringParser(
                            queryString);
                    return queryStringParser.getQueryParamValue(paramName);
                }
            });
            webView.loadUrl(authUrl);
        } else {
            webView.setVisibility(View.GONE);
            Toast.makeText(getBaseContext(), R.string.toast_twitter_error,Toast.LENGTH_LONG).show();
        }
    }

    protected void updateStatus(){
        mSpinner.bringToFront();
        mSpinner.setVisibility(View.VISIBLE);
        try {
            TwitterUtils.sendTweet(getSharedPreferences("MA", Context.MODE_PRIVATE), tweetEditText.getText()
                    .toString());
            Toast.makeText(getBaseContext(), R.string.toast_tweeted,Toast.LENGTH_LONG).show();
            finish();
        } catch (TwitterException e) {
            Log.d(TAG, e.getMessage(), e.fillInStackTrace());
            Toast.makeText(getBaseContext(), R.string.toast_twitter_error,Toast.LENGTH_LONG).show();
        } catch (Exception e) {
            Log.d(TAG, e.getMessage(), e.fillInStackTrace());
            Toast.makeText(getBaseContext(), R.string.toast_twitter_error,Toast.LENGTH_LONG).show();
        }
        mSpinner.setVisibility(View.GONE);
    }
    
    private String getAuthUrl() {
        String authUrl = null;
        try {
            authUrl = httpOauthprovider.retrieveRequestToken(httpOauthConsumer, CALLBACK_URL);
        } catch (OAuthMessageSignerException e) {
            Log.d(TAG, e.getMessage(), e.fillInStackTrace());
        } catch (OAuthNotAuthorizedException e) {
            Log.d(TAG, e.getMessage(), e.fillInStackTrace());
        } catch (OAuthExpectationFailedException e) {
            Log.d(TAG, e.getMessage(), e.fillInStackTrace());
        } catch (OAuthCommunicationException e) {
            Log.d(TAG, e.getMessage(), e.fillInStackTrace());
        }
        return authUrl;
    }
}