fm.krui.kruifm.TwitterManager.java Source code

Java tutorial

Introduction

Here is the source code for fm.krui.kruifm.TwitterManager.java

Source

/*
 * fm.krui.kruifm.TwitterManager - TwitterManager.java
 *
 * (C) 2013 - Tony Andrys
 * http://www.tonyandrys.com
 *
 * Created: 11/14/2013
 *
 * ---
 *
 * This file is part of KRUI.FM.
 *
 * KRUI.FM is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or(at your option) any later version.
 *
 * KRUI.FM is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with KRUI.FM.  If not, see <http://www.gnu.org/licenses/>.
 */

package fm.krui.kruifm;

/**
 * fm.krui.kruifm - TwitterManager
 *
 * @author Tony Andrys
 *         Created: 08/14/2013
 *         (C) 2013 - Tony Andrys
 */

import android.content.Context;
import android.net.http.AndroidHttpClient;
import android.util.Base64;
import android.util.Log;
import org.apache.http.NameValuePair;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

/**
 * All Twitter authorization/communication functionality is accessed from this class.
 */
public class TwitterManager {

    final private static String TAG = TwitterManager.class.getName();

    // Condition codes
    final public static String TIMELINE_SUCCESS = "timeline1";
    final public static String TIMELINE_FAILED = "timeline0";
    final public static String AUTHORIZATION_SUCCESS = "auth1";
    final public static String AUTHORIZATION_FAILED = "auth0";

    // API links
    final private String ROOT_URL = "https://api.twitter.com";
    final private String REQUEST_TOKEN_URL = "/oauth2/token?grant_type=client_credentials";
    final private String TIMELINE_URL = "/1.1/statuses/user_timeline.json";

    // JSON Keys
    final public static String KEY_ACCESS_TOKEN = "access_token";
    final public static String KEY_CREATED_AT = "created_at";
    final public static String KEY_TWEET_TEXT = "text";
    final public static String KEY_PROFILE_IMAGE_URL = "profile_image_url_https";
    final public static String OBJ_USER = "user";
    final public static String KEY_SCREEN_NAME = "screen_name";

    // Time hashmap constants (use after extractTimeValue())
    final public static String KEY_EXTRACT_DAY_OF_WEEK = "dayOfWeek";
    final public static String KEY_EXTRACT_MONTH = "month";
    final public static String KEY_EXTRACT_DAY_OF_MONTH = "dayOfMonth";
    final public static String KEY_EXTRACT_24HR_TIME = "24hrTime";
    final public static String KEY_EXTRACT_TIMEZONE_OFFSET = "timeZoneOffset";
    final public static String KEY_EXTRACT_YEAR = "year";

    private Context context;
    private String bearerToken;
    private HTTPConnectionListener callback;
    private HashMap<String, Integer> dateIntMap;

    public TwitterManager(Context context, HTTPConnectionListener listener) {
        this.context = context;
        this.callback = listener;
        dateIntMap = new HashMap<String, Integer>();
    }

    /**
     * Authorizes this device to communicate with Twitter REST API by exchanging private keys and storing token.
     */
    public void authorize() {

        // Retrieve consumer secret and key
        String consumerKey = context.getString(R.string.twitter_consumer_key);
        String consumerSecret = context.getString(R.string.twitter_consumer_secret);

        // Prepare credentials for authorization
        String encodedResult = encodeCredentials(consumerKey, consumerSecret);

        // POST authentication credentials to get a bearer token
        postAuth(encodedResult);
    }

    /**
     * POST authentication credentials to get a bearer token
     */
    private void postAuth(String preparedKey) {

        // Configure HTTP Request
        bearerToken = "";
        String userAgent = "Android"; //FIXME: Use more specific identifier here
        AndroidHttpClient androidHttpClient = AndroidHttpClient.newInstance(userAgent);
        HttpPost httpPost = new HttpPost(ROOT_URL + REQUEST_TOKEN_URL);

        // Apply headers and entity to request
        httpPost.setHeader("Authorization", "Basic " + preparedKey);
        httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");

        // Build callback listener
        HTTPConnectionListener localListener = new HTTPConnectionListener() {
            @Override
            public void onConnectionFinish(String result) {
                JSONObject response;
                String accessToken = "";
                try {
                    // Convert response to JSON and store bearer token.
                    response = new JSONObject(result);

                    // FIXME: Turn off response body logging before release, bearer token cannot leak!
                    bearerToken = response.getString(KEY_ACCESS_TOKEN);

                    if (bearerToken.length() >= 1) {
                        callback.onConnectionFinish(AUTHORIZATION_SUCCESS);
                    } else {
                        callback.onConnectionFinish(AUTHORIZATION_FAILED);
                    }
                } catch (JSONException e) {
                    Log.e(TAG, "Could not convert server response to JSON!");
                    e.printStackTrace();
                }
            }
        };

        // Execute HTTP request
        new HTTPConnection(androidHttpClient, httpPost, localListener).execute();
    }

    /**
     * Returns the (public) timeline of a specific user.
     * @param screenName Username of account to get timeline (with or without leading @)
     * @param tweetCount Number of tweets to pull as an integer
     * @param trimUser true to strip user objects down to only user id, false to return full user object.
     */
    public void getTimeline(String screenName, int tweetCount, boolean trimUser) {

        // URL Parameter constants
        final String SCREEN_NAME = "screen_name";
        final String TWEET_COUNT = "count";
        final String TRIM_USER = "trim_user";

        String url = ROOT_URL + TIMELINE_URL + "?";
        String username = validateUsername(screenName);

        // Encode POST parameters in the URL
        List<NameValuePair> params = new LinkedList<NameValuePair>();
        params.add(new BasicNameValuePair(SCREEN_NAME, username));
        params.add(new BasicNameValuePair(TWEET_COUNT, Integer.toString(tweetCount)));
        if (trimUser) {
            params.add(new BasicNameValuePair(TRIM_USER, Integer.toString(1)));
        }

        String paramString = URLEncodedUtils.format(params, "UTF-8");
        url += paramString;

        // Build connection environment and execute
        String userAgent = "Android"; //FIXME: Use more specific identifier here
        AndroidHttpClient androidHttpClient = AndroidHttpClient.newInstance(userAgent);
        HttpGet httpGet = new HttpGet(url);

        // Build listener
        HTTPConnectionListener localListener = new HTTPConnectionListener() {
            @Override
            public void onConnectionFinish(String result) {
                callback.onConnectionFinish(result);
            }
        };

        // Apply authorization header with bearer token
        httpGet.setHeader("Authorization", "Bearer " + bearerToken);
        Log.v(TAG, "Set Authentication Header using bearerToken " + bearerToken);
        new HTTPConnection(androidHttpClient, httpGet, localListener).execute();
    }

    /**
     * Builds an ArrayList of tweets from a JSON timeline representation
     * @param JSONTimeline Timeline data as String in JSON format
     * @return ArrayList of Tweets
     */
    public ArrayList<Tweet> processJSONTimeline(String JSONTimeline) {

        ArrayList<Tweet> tweetList = new ArrayList<Tweet>();
        try {
            // Convert string to JSON Array
            JSONArray a = new JSONArray(JSONTimeline);

            for (int i = 0; i < a.length(); i++) {
                Tweet tweet = new Tweet();

                // Extract values from JSON Object
                JSONObject o = a.getJSONObject(i);
                Log.v(TAG, "Adding tweet:");
                tweet.setText(o.getString(KEY_TWEET_TEXT));
                Log.v(TAG, "Text: " + o.getString(KEY_TWEET_TEXT));
                JSONObject userObj = o.getJSONObject(OBJ_USER);
                tweet.setProfileImageUrl(userObj.getString(KEY_PROFILE_IMAGE_URL));
                Log.v(TAG, "Profile Image URL: " + userObj.getString(KEY_PROFILE_IMAGE_URL));
                tweet.setScreenName(userObj.getString(KEY_SCREEN_NAME));
                Log.v(TAG, "Screen name: " + userObj.getString(KEY_SCREEN_NAME));

                // Extract time values from string and add relevant values to object
                HashMap<String, String> timeMap = extractTimeValue(o.getString(KEY_CREATED_AT));
                tweet.setTimeOfTweet(timeMap.get(KEY_EXTRACT_24HR_TIME));

                // Change month to integer representation for cleanliness
                String monthStr = timeMap.get(KEY_EXTRACT_MONTH);
                Log.v(TAG, "Trying to convert month: " + monthStr);
                String monthInt = Integer.toString(convertMonthToInt(monthStr));
                tweet.setDateOfTweet(monthInt + "/" + timeMap.get(KEY_EXTRACT_DAY_OF_MONTH));
                tweet.setTimeZoneOffset(timeMap.get(KEY_EXTRACT_TIMEZONE_OFFSET));

                // Add this tweet to list
                tweetList.add(tweet);
                Log.v(TAG, "Tweet added!");
            }

        } catch (JSONException e) {
            Log.e(TAG, "Could not build JSON from input string!");
            e.printStackTrace();
        }

        return tweetList;
    }

    /**
     * Handles time elements from "created_at" field from tweet
     * @return String array of each component
     */
    private HashMap<String, String> extractTimeValue(String createdAt) {
        String[] split = createdAt.split("\\s+");
        HashMap<String, String> timeMap = new HashMap<String, String>();
        timeMap.put(KEY_EXTRACT_DAY_OF_WEEK, split[0]);
        Log.v(TAG, "Adding " + split[0] + " to day of week key");
        timeMap.put(KEY_EXTRACT_MONTH, split[1]);
        Log.v(TAG, "Adding " + split[1] + " to month key");
        timeMap.put(KEY_EXTRACT_DAY_OF_MONTH, split[2]);
        Log.v(TAG, "Adding " + split[2] + " to day of month key");
        timeMap.put(KEY_EXTRACT_24HR_TIME, split[3]);
        Log.v(TAG, "Adding " + split[3] + " to 24 hour time key");
        timeMap.put(KEY_EXTRACT_TIMEZONE_OFFSET, split[4]);
        Log.v(TAG, "Adding " + split[4] + " to timezone offset key");
        timeMap.put(KEY_EXTRACT_YEAR, split[5]);
        Log.v(TAG, "Adding " + split[5] + " to year key");

        return timeMap;
    }

    /**
     * Encodes consumer key and consumer secret using RFC 1783 and formats the result according
     * to Twitter's requirements for authorization.
     * @param consumerKey Consumer Key
     * @param consumerSecret Consumer Secret
     * @return Processed string
     */
    private String encodeCredentials(String consumerKey, String consumerSecret) {

        // Encode credentials according to RFC 1739
        String consumerKey1379 = null;
        String consumerSecret1379 = null;
        try {
            consumerKey1379 = URLEncoder.encode(consumerKey, "UTF-8");
            consumerSecret1379 = URLEncoder.encode(consumerSecret, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            Log.e(TAG, "Could not encode auth credentials!");
            e.printStackTrace();
        }

        // Concatenate the encoded consumer key and secret with a colon
        String singleKey = consumerKey1379 + ":" + consumerSecret1379;

        // Encode single key using base64 and return result
        byte[] keyBytes = singleKey.getBytes();
        String finalKey = Base64.encodeToString(keyBytes, Base64.NO_WRAP);
        Log.v(TAG, "Final key created: " + finalKey);
        return finalKey;
    }

    /**
     * Strips junk from twitter username and outputs a validated username.
     * TODO: Turn this into a URL parser as well
     * @return clean username as a String
     */
    private String validateUsername(String screenName) {

        // Check user name for leading @ and strip it if it exists.
        String verifiedUsername = "";
        String subString = screenName.substring(0, 1);
        if (subString.equals("@")) {
            Log.v(TAG, "Stripping leading @ from screen name (" + screenName + ")");
            verifiedUsername = screenName.substring(1);
        } else {
            verifiedUsername = screenName;
        }
        Log.v(TAG, "Verified username is " + verifiedUsername);
        return verifiedUsername;
    }

    /**
     * Get bearer token used for signing API requests
     * @return KRUI bearer token as a String
     */
    private String getBearerToken() {
        return this.bearerToken;
    }

    private int convertMonthToInt(String text) {

        // If map has not been built yet, build it.
        if (dateIntMap.size() == 0) {
            // Build date/int map
            dateIntMap.put("Jan", 1);
            dateIntMap.put("Feb", 2);
            dateIntMap.put("Mar", 3);
            dateIntMap.put("Apr", 4);
            dateIntMap.put("May", 5);
            dateIntMap.put("Jun", 6);
            dateIntMap.put("Jul", 7);
            dateIntMap.put("Aug", 8);
            dateIntMap.put("Sep", 9);
            dateIntMap.put("Oct", 10);
            dateIntMap.put("Nov", 11);
            dateIntMap.put("Dec", 12);
        }

        // Convert the value and return it.
        return dateIntMap.get(text);
    }

}