com.google.appinventor.components.runtime.Twitter.java Source code

Java tutorial

Introduction

Here is the source code for com.google.appinventor.components.runtime.Twitter.java

Source

// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

package com.google.appinventor.components.runtime;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.io.File;

import twitter4j.DirectMessage;
import twitter4j.IDs;
import twitter4j.Query;
import twitter4j.Status;
import twitter4j.StatusUpdate;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.User;
import twitter4j.auth.AccessToken;
import twitter4j.auth.RequestToken;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;

import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesLibraries;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.common.PropertyTypeConstants;
import com.google.appinventor.components.common.YaVersion;
import com.google.appinventor.components.runtime.util.AsynchUtil;
import com.google.appinventor.components.runtime.util.ErrorMessages;

/**
 * Component for accessing Twitter.
 *
 * @author sharon@google.com (Sharon Perl) - added OAuth support
 * @author ajcolter@gmail.com (Aubrey Colter) - added the twitter4j 2.2.6 jars
 * @author josmasflores@gmail.com (Jose Dominguez) - added the twitter4j 3.0.3 jars and fixed auth bug 2413
 * @author edwinhzhang@gmail.com (Edwin Zhang) - added twitter4j-media-support-3.03 jar, status + image upload
 */
@DesignerComponent(version = YaVersion.TWITTER_COMPONENT_VERSION, description = "A non-visible component that enables communication "
        + "with <a href=\"http://www.twitter.com\" target=\"_blank\">Twitter</a>. "
        + "Once a user has logged into their Twitter account (and the authorization has been confirmed successful by the "
        + "<code>IsAuthorized</code> event), many more operations are available:<ul>"
        + "<li> Searching Twitter for tweets or labels (<code>SearchTwitter</code>)</li>\n"
        + "<li> Sending a Tweet (<code>Tweet</code>)" + "     </li>\n"
        + "<li> Sending a Tweet with an Image (<code>TweetWithImage</code>)" + "     </li>\n"
        + "<li> Directing a message to a specific user " + "     (<code>DirectMessage</code>)</li>\n "
        + "<li> Receiving the most recent messages directed to the logged-in user "
        + "     (<code>RequestDirectMessages</code>)</li>\n "
        + "<li> Following a specific user (<code>Follow</code>)</li>\n"
        + "<li> Ceasing to follow a specific user (<code>StopFollowing</code>)</li>\n"
        + "<li> Getting a list of users following the logged-in user "
        + "     (<code>RequestFollowers</code>)</li>\n "
        + "<li> Getting the most recent messages of users followed by the "
        + "     logged-in user (<code>RequestFriendTimeline</code>)</li>\n "
        + "<li> Getting the most recent mentions of the logged-in user "
        + "     (<code>RequestMentions</code>)</li></ul></p>\n "
        + "<p>You must obtain a Consumer Key and Consumer Secret for Twitter authorization "
        + " specific to your app from http://twitter.com/oauth_clients/new", category = ComponentCategory.SOCIAL, nonVisible = true, iconName = "images/twitter.png")
@SimpleObject
@UsesPermissions(permissionNames = "android.permission.INTERNET")
@UsesLibraries(libraries = "twitter4j.jar," + "twitter4jmedia.jar")
public final class Twitter extends AndroidNonvisibleComponent implements ActivityResultListener, Component {
    private static final String ACCESS_TOKEN_TAG = "TwitterOauthAccessToken";
    private static final String ACCESS_SECRET_TAG = "TwitterOauthAccessSecret";
    private static final String MAX_CHARACTERS = "160";
    private static final String URL_HOST = "twitter";
    private static final String CALLBACK_URL = Form.APPINVENTOR_URL_SCHEME + "://" + URL_HOST;
    private static final String WEBVIEW_ACTIVITY_CLASS = WebViewActivity.class.getName();

    // the following fields should only be accessed from the UI thread
    private String consumerKey = "";
    private String consumerSecret = "";
    private String TwitPic_API_Key = "";
    private final List<String> mentions;
    private final List<String> followers;
    private final List<List<String>> timeline;
    private final List<String> directMessages;
    private final List<String> searchResults;

    // the following final fields are not synchronized -- twitter4j is thread
    // safe as of 2.2.6
    private twitter4j.Twitter twitter;
    private RequestToken requestToken;
    private AccessToken accessToken;
    private String userName = "";
    private final SharedPreferences sharedPreferences;
    private final int requestCode;
    private final ComponentContainer container;
    private final Handler handler;

    // TODO(sharon): twitter4j apparently has an asynchronous interface
    // (AsynchTwitter).
    // We should consider whether it has any advantages over AsynchUtil.

    /**
     * The maximum number of mentions returned by the following methods:
     *
     * <table>
     * <tr>
     * <td>component</td>
     * <td>twitter4j library</td>
     * <td>twitter API</td>
     * </tr>
     * <tr>
     * <td>RequestMentions</td>
     * <td>getMentions</td>
     * <td>statuses/mentions</td>
     * </tr>
     * <tr>
     * <td>RequestDirectMessages</td>
     * <td>getDirectMessages</td>
     * <td>direct_messages</td>
     * </tr>
     * </table>
     */
    private static final String MAX_MENTIONS_RETURNED = "20";

    public Twitter(ComponentContainer container) {
        super(container.$form());
        this.container = container;
        handler = new Handler();

        mentions = new ArrayList<String>();
        followers = new ArrayList<String>();
        timeline = new ArrayList<List<String>>();
        directMessages = new ArrayList<String>();
        searchResults = new ArrayList<String>();

        sharedPreferences = container.$context().getSharedPreferences("Twitter", Context.MODE_PRIVATE);
        accessToken = retrieveAccessToken();

        requestCode = form.registerForActivityResult(this);
    }

    /**
     * Logs in to Twitter with a username and password.
     */
    @Deprecated
    @SimpleFunction(userVisible = false, description = "Twitter's API no longer supports login via username and "
            + "password. Use the Authorize call instead.")
    public void Login(String username, String password) {
        form.dispatchErrorOccurredEvent(this, "Login", ErrorMessages.ERROR_TWITTER_UNSUPPORTED_LOGIN_FUNCTION);
    }

    @SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "The user name of the authorized user. Empty if "
            + "there is no authorized user.")
    public String Username() {
        return userName;
    }

    /**
     * ConsumerKey property getter method.
     */
    @SimpleProperty(category = PropertyCategory.BEHAVIOR)
    public String ConsumerKey() {
        return consumerKey;
    }

    /**
     * ConsumerKey property setter method: sets the consumer key to be used when
     * authorizing with Twitter via OAuth.
     *
     * @param consumerKey
     *          the key for use in Twitter OAuth
     */
    @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "")
    @SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "The the consumer key to be used when authorizing with Twitter via OAuth.")
    public void ConsumerKey(String consumerKey) {
        this.consumerKey = consumerKey;
    }

    /**
     * ConsumerSecret property getter method.
     */
    @SimpleProperty(category = PropertyCategory.BEHAVIOR)
    public String ConsumerSecret() {
        return consumerSecret;
    }

    /**
     * ConsumerSecret property setter method: sets the consumer secret to be used
     * when authorizing with Twitter via OAuth.
     *
     * @param consumerSecret
     *          the secret for use in Twitter OAuth
     */
    @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "")
    @SimpleProperty(description = "The consumer secret to be used when authorizing with Twitter via OAuth")
    public void ConsumerSecret(String consumerSecret) {
        this.consumerSecret = consumerSecret;
    }

    /**
     * TwitPicAPIkey property getter method.
     */
    @Deprecated
    @SimpleProperty(userVisible = false, category = PropertyCategory.BEHAVIOR)
    public String TwitPic_API_Key() {
        return TwitPic_API_Key;
    }

    /**
     * TwitPicAPIkey property setter method: sets the TwitPicAPIkey to be used
     * for image uploading with twitter.
     *
     * @param TwitPic_API_Key
     *          the API Key for image uploading, given by TwitPic
     */
    @Deprecated
    // Hide the deprecated property from the Designer
    //@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "")
    @SimpleProperty(userVisible = false, category = PropertyCategory.BEHAVIOR, description = "The API Key for image uploading, provided by TwitPic.")
    public void TwitPic_API_Key(String TwitPic_API_Key) {
        this.TwitPic_API_Key = TwitPic_API_Key;
    }

    /**
     * Indicates when the login has been successful.
     */
    @SimpleEvent(description = "This event is raised after the program calls "
            + "<code>Authorize</code> if the authorization was successful.  "
            + "It is also called after a call to <code>CheckAuthorized</code> "
            + "if we already have a valid access token. "
            + "After this event has been raised, any other method for this " + "component can be called.")
    public void IsAuthorized() {
        EventDispatcher.dispatchEvent(this, "IsAuthorized");
    }

    /**
     * Authenticate to Twitter using OAuth
     */
    @SimpleFunction(description = "Redirects user to login to Twitter via the Web browser using "
            + "the OAuth protocol if we don't already have authorization.")
    public void Authorize() {
        if (consumerKey.length() == 0 || consumerSecret.length() == 0) {
            form.dispatchErrorOccurredEvent(this, "Authorize",
                    ErrorMessages.ERROR_TWITTER_BLANK_CONSUMER_KEY_OR_SECRET);
            return;
        }
        if (twitter == null) {
            twitter = new TwitterFactory().getInstance();
        }
        final String myConsumerKey = consumerKey;
        final String myConsumerSecret = consumerSecret;
        AsynchUtil.runAsynchronously(new Runnable() {
            public void run() {
                if (checkAccessToken(myConsumerKey, myConsumerSecret)) {
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            IsAuthorized();
                        }
                    });
                    return;
                }
                try {
                    // potentially time-consuming calls
                    RequestToken newRequestToken;
                    twitter.setOAuthConsumer(myConsumerKey, myConsumerSecret);
                    newRequestToken = twitter.getOAuthRequestToken(CALLBACK_URL);
                    String authURL = newRequestToken.getAuthorizationURL();
                    requestToken = newRequestToken; // request token will be
                    // needed to get access token
                    Intent browserIntent = new Intent(Intent.ACTION_MAIN, Uri.parse(authURL));
                    browserIntent.setClassName(container.$context(), WEBVIEW_ACTIVITY_CLASS);
                    container.$context().startActivityForResult(browserIntent, requestCode);
                } catch (TwitterException e) {
                    Log.i("Twitter", "Got exception: " + e.getMessage());
                    e.printStackTrace();
                    form.dispatchErrorOccurredEvent(Twitter.this, "Authorize",
                            ErrorMessages.ERROR_TWITTER_EXCEPTION, e.getMessage());
                    DeAuthorize(); // clean up
                } catch (IllegalStateException ise) { //This should never happen cause it should return
                    // at the if (checkAccessToken...). We mark as an error but let continue
                    Log.e("Twitter", "OAuthConsumer was already set: launch IsAuthorized()");
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            IsAuthorized();
                        }
                    });
                }
            }
        });
    }

    /**
     * Check whether we already have a valid Twitter access token
     */
    @SimpleFunction(description = "Checks whether we already have access, and if so, causes "
            + "IsAuthorized event handler to be called.")
    public void CheckAuthorized() {
        final String myConsumerKey = consumerKey;
        final String myConsumerSecret = consumerSecret;
        AsynchUtil.runAsynchronously(new Runnable() {
            public void run() {
                if (checkAccessToken(myConsumerKey, myConsumerSecret)) {
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            IsAuthorized();
                        }
                    });
                }
            }
        });
    }

    /*
     * Get result from starting WebView activity to authorize access
     */
    @Override
    public void resultReturned(int requestCode, int resultCode, Intent data) {
        Log.i("Twitter", "Got result " + resultCode);
        if (data != null) {
            Uri uri = data.getData();
            if (uri != null) {
                Log.i("Twitter", "Intent URI: " + uri.toString());
                final String oauthVerifier = uri.getQueryParameter("oauth_verifier");
                if (twitter == null) {
                    Log.e("Twitter", "twitter field is unexpectedly null");
                    form.dispatchErrorOccurredEvent(this, "Authorize",
                            ErrorMessages.ERROR_TWITTER_UNABLE_TO_GET_ACCESS_TOKEN,
                            "internal error: can't access Twitter library");
                    new RuntimeException().printStackTrace();
                }
                if (requestToken != null && oauthVerifier != null && oauthVerifier.length() != 0) {
                    AsynchUtil.runAsynchronously(new Runnable() {
                        public void run() {
                            try {
                                AccessToken resultAccessToken;
                                resultAccessToken = twitter.getOAuthAccessToken(requestToken, oauthVerifier);
                                accessToken = resultAccessToken;
                                userName = accessToken.getScreenName();
                                saveAccessToken(resultAccessToken);
                                handler.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        IsAuthorized();
                                    }
                                });
                            } catch (TwitterException e) {
                                Log.e("Twitter", "Got exception: " + e.getMessage());
                                e.printStackTrace();
                                form.dispatchErrorOccurredEvent(Twitter.this, "Authorize",
                                        ErrorMessages.ERROR_TWITTER_UNABLE_TO_GET_ACCESS_TOKEN, e.getMessage());
                                deAuthorize(); // clean up
                            }
                        }
                    });
                } else {
                    form.dispatchErrorOccurredEvent(this, "Authorize",
                            ErrorMessages.ERROR_TWITTER_AUTHORIZATION_FAILED);
                    deAuthorize(); // clean up
                }
            } else {
                Log.e("Twitter", "uri returned from WebView activity was unexpectedly null");
                deAuthorize(); // clean up so we can call Authorize again
            }
        } else {
            Log.e("Twitter", "intent returned from WebView activity was unexpectedly null");
            deAuthorize(); // clean up so we can call Authorize again
        }
    }

    private void saveAccessToken(AccessToken accessToken) {
        final SharedPreferences.Editor sharedPrefsEditor = sharedPreferences.edit();
        if (accessToken == null) {
            sharedPrefsEditor.remove(ACCESS_TOKEN_TAG);
            sharedPrefsEditor.remove(ACCESS_SECRET_TAG);
        } else {
            sharedPrefsEditor.putString(ACCESS_TOKEN_TAG, accessToken.getToken());
            sharedPrefsEditor.putString(ACCESS_SECRET_TAG, accessToken.getTokenSecret());
        }
        sharedPrefsEditor.commit();
    }

    private AccessToken retrieveAccessToken() {
        String token = sharedPreferences.getString(ACCESS_TOKEN_TAG, "");
        String secret = sharedPreferences.getString(ACCESS_SECRET_TAG, "");
        if (token.length() == 0 || secret.length() == 0) {
            return null;
        }
        return new AccessToken(token, secret);
    }

    /**
     * Remove authentication for this app instance
     */
    @SimpleFunction(description = "Removes Twitter authorization from this running app instance")
    public void DeAuthorize() {
        deAuthorize();
    }

    private void deAuthorize() {
        final twitter4j.Twitter oldTwitter;
        requestToken = null;
        accessToken = null;
        userName = "";
        oldTwitter = twitter;
        twitter = null; // setting twitter to null gives us a quick check
        // that we don't have an authorized version around.
        saveAccessToken(accessToken);

        // clear the access token from the old twitter instance, just in case
        // someone stashed it away.
        if (oldTwitter != null) {
            oldTwitter.setOAuthAccessToken(null);
        }
    }

    /**
     * Sends a Tweet of the currently logged in user.
     */
    @SimpleFunction(description = "This sends a tweet as the logged-in user with the "
            + "specified Text, which will be trimmed if it exceeds " + MAX_CHARACTERS + " characters. "
            + "<p><u>Requirements</u>: This should only be called after the "
            + "<code>IsAuthorized</code> event has been raised, indicating that the "
            + "user has successfully logged in to Twitter.</p>")
    public void Tweet(final String status) {

        if (twitter == null || userName.length() == 0) {
            form.dispatchErrorOccurredEvent(this, "Tweet", ErrorMessages.ERROR_TWITTER_SET_STATUS_FAILED,
                    "Need to login?");
            return;
        }
        // TODO(sharon): note that if the user calls DeAuthorize immediately
        // after
        // Tweet it is possible that the DeAuthorize call can slip in
        // and invalidate the authorization credentials for myTwitter, causing
        // the call below to fail. If we want to prevent this we could consider
        // using an ExecutorService object to serialize calls to Twitter.
        AsynchUtil.runAsynchronously(new Runnable() {
            public void run() {
                try {
                    twitter.updateStatus(status);
                } catch (TwitterException e) {
                    form.dispatchErrorOccurredEvent(Twitter.this, "Tweet",
                            ErrorMessages.ERROR_TWITTER_SET_STATUS_FAILED, e.getMessage());
                }
            }
        });
    }

    /**
     * Tweet with Image, Uploaded to Twitter
     */
    @SimpleFunction(description = "This sends a tweet as the logged-in user with the "
            + "specified Text and a path to the image to be uploaded, which will be trimmed if it " + "exceeds "
            + MAX_CHARACTERS + " characters. "
            + "If an image is not found or invalid, only the text will be tweeted."
            + "<p><u>Requirements</u>: This should only be called after the "
            + "<code>IsAuthorized</code> event has been raised, indicating that the "
            + "user has successfully logged in to Twitter.</p>")
    public void TweetWithImage(final String status, final String imagePath) {
        if (twitter == null || userName.length() == 0) {
            form.dispatchErrorOccurredEvent(this, "TweetWithImage", ErrorMessages.ERROR_TWITTER_SET_STATUS_FAILED,
                    "Need to login?");
            return;
        }

        AsynchUtil.runAsynchronously(new Runnable() {
            public void run() {
                try {
                    String cleanImagePath = imagePath;
                    // Clean up the file path if necessary
                    if (cleanImagePath.startsWith("file://")) {
                        cleanImagePath = imagePath.replace("file://", "");
                    }
                    File imageFilePath = new File(cleanImagePath);
                    if (imageFilePath.exists()) {
                        StatusUpdate theTweet = new StatusUpdate(status);
                        theTweet.setMedia(imageFilePath);
                        twitter.updateStatus(theTweet);
                    } else {
                        form.dispatchErrorOccurredEvent(Twitter.this, "TweetWithImage",
                                ErrorMessages.ERROR_TWITTER_INVALID_IMAGE_PATH);
                    }
                } catch (TwitterException e) {
                    form.dispatchErrorOccurredEvent(Twitter.this, "TweetWithImage",
                            ErrorMessages.ERROR_TWITTER_SET_STATUS_FAILED, e.getMessage());
                }
            }
        });

    }

    /**
     * Gets the most recent messages where your username is mentioned.
     */
    @SimpleFunction(description = "Requests the " + MAX_MENTIONS_RETURNED + " most "
            + "recent mentions of the logged-in user.  When the mentions have been "
            + "retrieved, the system will raise the <code>MentionsReceived</code> "
            + "event and set the <code>Mentions</code> property to the list of " + "mentions."
            + "<p><u>Requirements</u>: This should only be called after the "
            + "<code>IsAuthorized</code> event has been raised, indicating that the "
            + "user has successfully logged in to Twitter.</p>")
    public void RequestMentions() {
        if (twitter == null || userName.length() == 0) {
            form.dispatchErrorOccurredEvent(this, "RequestMentions",
                    ErrorMessages.ERROR_TWITTER_REQUEST_MENTIONS_FAILED, "Need to login?");
            return;
        }
        AsynchUtil.runAsynchronously(new Runnable() {
            List<Status> replies = Collections.emptyList();

            public void run() {
                try {
                    replies = twitter.getMentionsTimeline();
                } catch (TwitterException e) {
                    form.dispatchErrorOccurredEvent(Twitter.this, "RequestMentions",
                            ErrorMessages.ERROR_TWITTER_REQUEST_MENTIONS_FAILED, e.getMessage());
                } finally {
                    handler.post(new Runnable() {
                        public void run() {
                            mentions.clear();
                            for (Status status : replies) {
                                mentions.add(status.getUser().getScreenName() + " " + status.getText());
                            }
                            MentionsReceived(mentions);
                        }
                    });
                }
            }
        });
    }

    /**
     * Indicates when all the mentions requested through
     * {@link #RequestMentions()} have been received.
     */
    @SimpleEvent(description = "This event is raised when the mentions of the logged-in user "
            + "requested through <code>RequestMentions</code> have been retrieved.  "
            + "A list of the mentions can then be found in the <code>mentions</code> "
            + "parameter or the <code>Mentions</code> property.")
    public void MentionsReceived(final List<String> mentions) {
        EventDispatcher.dispatchEvent(this, "MentionsReceived", mentions);
    }

    @SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "This property contains a list of mentions of the "
            + "logged-in user.  Initially, the list is empty.  To set it, the " + "program must: <ol> "
            + "<li> Call the <code>Authorize</code> method.</li> "
            + "<li> Wait for the <code>IsAuthorized</code> event.</li> "
            + "<li> Call the <code>RequestMentions</code> method.</li> "
            + "<li> Wait for the <code>MentionsReceived</code> event.</li></ol>\n"
            + "The value of this property will then be set to the list of mentions "
            + "(and will maintain its value until any subsequent calls to " + "<code>RequestMentions</code>).")
    public List<String> Mentions() {
        return mentions;
    }

    /**
     * Gets who is following you.
     */
    @SimpleFunction
    public void RequestFollowers() {
        if (twitter == null || userName.length() == 0) {
            form.dispatchErrorOccurredEvent(this, "RequestFollowers",
                    ErrorMessages.ERROR_TWITTER_REQUEST_FOLLOWERS_FAILED, "Need to login?");
            return;
        }
        AsynchUtil.runAsynchronously(new Runnable() {
            List<User> friends = new ArrayList<User>();

            public void run() {
                try {
                    IDs followerIDs = twitter.getFollowersIDs(-1);
                    for (long id : followerIDs.getIDs()) {
                        // convert from the IDs returned to the User
                        friends.add(twitter.showUser(id));
                    }
                } catch (TwitterException e) {
                    form.dispatchErrorOccurredEvent(Twitter.this, "RequestFollowers",
                            ErrorMessages.ERROR_TWITTER_REQUEST_FOLLOWERS_FAILED, e.getMessage());
                } finally {
                    handler.post(new Runnable() {
                        public void run() {
                            followers.clear();
                            for (User user : friends) {
                                followers.add(user.getName());
                            }
                            FollowersReceived(followers);
                        }
                    });
                }
            }
        });
    }

    /**
     * Indicates when all of your followers requested through
     * {@link #RequestFollowers()} have been received.
     */
    @SimpleEvent(description = "This event is raised when all of the followers of the "
            + "logged-in user requested through <code>RequestFollowers</code> have "
            + "been retrieved. A list of the followers can then be found in the "
            + "<code>followers</code> parameter or the <code>Followers</code> " + "property.")
    public void FollowersReceived(final List<String> followers2) {
        EventDispatcher.dispatchEvent(this, "FollowersReceived", followers2);
    }

    @SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "This property contains a list of the followers of the "
            + "logged-in user.  Initially, the list is empty.  To set it, the " + "program must: <ol> "
            + "<li> Call the <code>Authorize</code> method.</li> "
            + "<li> Wait for the <code>IsAuthorized</code> event.</li> "
            + "<li> Call the <code>RequestFollowers</code> method.</li> "
            + "<li> Wait for the <code>FollowersReceived</code> event.</li></ol>\n"
            + "The value of this property will then be set to the list of "
            + "followers (and maintain its value until any subsequent call to " + "<code>RequestFollowers</code>).")
    public List<String> Followers() {
        return followers;
    }

    /**
     * Gets the most recent messages sent directly to you.
     */
    @SimpleFunction(description = "Requests the " + MAX_MENTIONS_RETURNED + " most "
            + "recent direct messages sent to the logged-in user.  When the "
            + "messages have been retrieved, the system will raise the "
            + "<code>DirectMessagesReceived</code> event and set the "
            + "<code>DirectMessages</code> property to the list of messages."
            + "<p><u>Requirements</u>: This should only be called after the "
            + "<code>IsAuthorized</code> event has been raised, indicating that the "
            + "user has successfully logged in to Twitter.</p>")
    public void RequestDirectMessages() {
        if (twitter == null || userName.length() == 0) {
            form.dispatchErrorOccurredEvent(this, "RequestDirectMessages",
                    ErrorMessages.ERROR_TWITTER_REQUEST_DIRECT_MESSAGES_FAILED, "Need to login?");
            return;
        }
        AsynchUtil.runAsynchronously(new Runnable() {
            List<DirectMessage> messages = Collections.emptyList();

            @Override
            public void run() {
                try {
                    messages = twitter.getDirectMessages();
                } catch (TwitterException e) {
                    form.dispatchErrorOccurredEvent(Twitter.this, "RequestDirectMessages",
                            ErrorMessages.ERROR_TWITTER_REQUEST_DIRECT_MESSAGES_FAILED, e.getMessage());
                } finally {
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            directMessages.clear();
                            for (DirectMessage message : messages) {
                                directMessages.add(message.getSenderScreenName() + " " + message.getText());
                            }
                            DirectMessagesReceived(directMessages);
                        }
                    });
                }
            }

        });
    }

    /**
     * Indicates when all the direct messages requested through
     * {@link #RequestDirectMessages()} have been received.
     */
    @SimpleEvent(description = "This event is raised when the recent messages "
            + "requested through <code>RequestDirectMessages</code> have "
            + "been retrieved. A list of the messages can then be found in the "
            + "<code>messages</code> parameter or the <code>Messages</code> " + "property.")
    public void DirectMessagesReceived(final List<String> messages) {
        EventDispatcher.dispatchEvent(this, "DirectMessagesReceived", messages);
    }

    @SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "This property contains a list of the most recent "
            + "messages mentioning the logged-in user.  Initially, the list is "
            + "empty.  To set it, the program must: <ol> " + "<li> Call the <code>Authorize</code> method.</li> "
            + "<li> Wait for the <code>Authorized</code> event.</li> "
            + "<li> Call the <code>RequestDirectMessages</code> method.</li> "
            + "<li> Wait for the <code>DirectMessagesReceived</code> event.</li>" + "</ol>\n"
            + "The value of this property will then be set to the list of direct "
            + "messages retrieved (and maintain that value until any subsequent "
            + "call to <code>RequestDirectMessages</code>).")
    public List<String> DirectMessages() {
        return directMessages;
    }

    /**
     * Sends a direct message to a specified username.
     */
    @SimpleFunction(description = "This sends a direct (private) message to the specified "
            + "user.  The message will be trimmed if it exceeds " + MAX_CHARACTERS + "characters. "
            + "<p><u>Requirements</u>: This should only be called after the "
            + "<code>IsAuthorized</code> event has been raised, indicating that the "
            + "user has successfully logged in to Twitter.</p>")
    public void DirectMessage(final String user, final String message) {
        if (twitter == null || userName.length() == 0) {
            form.dispatchErrorOccurredEvent(this, "DirectMessage",
                    ErrorMessages.ERROR_TWITTER_DIRECT_MESSAGE_FAILED, "Need to login?");
            return;
        }
        AsynchUtil.runAsynchronously(new Runnable() {
            public void run() {
                try {
                    twitter.sendDirectMessage(user, message);
                } catch (TwitterException e) {
                    form.dispatchErrorOccurredEvent(Twitter.this, "DirectMessage",
                            ErrorMessages.ERROR_TWITTER_DIRECT_MESSAGE_FAILED, e.getMessage());
                }
            }
        });
    }

    /**
     * Starts following a user.
     */
    @SimpleFunction
    public void Follow(final String user) {
        if (twitter == null || userName.length() == 0) {
            form.dispatchErrorOccurredEvent(this, "Follow", ErrorMessages.ERROR_TWITTER_FOLLOW_FAILED,
                    "Need to login?");
            return;
        }
        AsynchUtil.runAsynchronously(new Runnable() {
            public void run() {
                try {
                    twitter.createFriendship(user);
                } catch (TwitterException e) {
                    form.dispatchErrorOccurredEvent(Twitter.this, "Follow",
                            ErrorMessages.ERROR_TWITTER_FOLLOW_FAILED, e.getMessage());
                }
            }
        });
    }

    /**
     * Stops following a user.
     */
    @SimpleFunction
    public void StopFollowing(final String user) {
        if (twitter == null || userName.length() == 0) {
            form.dispatchErrorOccurredEvent(this, "StopFollowing",
                    ErrorMessages.ERROR_TWITTER_STOP_FOLLOWING_FAILED, "Need to login?");
            return;
        }
        AsynchUtil.runAsynchronously(new Runnable() {
            public void run() {
                try {
                    twitter.destroyFriendship(user);
                } catch (TwitterException e) {
                    form.dispatchErrorOccurredEvent(Twitter.this, "StopFollowing",
                            ErrorMessages.ERROR_TWITTER_STOP_FOLLOWING_FAILED, e.getMessage());
                }
            }
        });
    }

    /**
     * Gets the most recent 20 messages in the user's timeline.
     */
    @SimpleFunction
    public void RequestFriendTimeline() {
        if (twitter == null || userName.length() == 0) {
            form.dispatchErrorOccurredEvent(this, "RequestFriendTimeline",
                    ErrorMessages.ERROR_TWITTER_REQUEST_FRIEND_TIMELINE_FAILED, "Need to login?");
            return;
        }
        AsynchUtil.runAsynchronously(new Runnable() {
            List<Status> messages = Collections.emptyList();

            public void run() {
                try {
                    messages = twitter.getHomeTimeline();
                } catch (TwitterException e) {
                    form.dispatchErrorOccurredEvent(Twitter.this, "RequestFriendTimeline",
                            ErrorMessages.ERROR_TWITTER_REQUEST_FRIEND_TIMELINE_FAILED, e.getMessage());
                } finally {
                    handler.post(new Runnable() {
                        public void run() {
                            timeline.clear();
                            for (Status message : messages) {
                                List<String> status = new ArrayList<String>();
                                status.add(message.getUser().getScreenName());
                                status.add(message.getText());
                                timeline.add(status);
                            }
                            FriendTimelineReceived(timeline);
                        }
                    });
                }
            }
        });
    }

    /**
     * Indicates when the friend timeline requested through
     * {@link #RequestFriendTimeline()} has been received.
     */
    @SimpleEvent(description = "This event is raised when the messages "
            + "requested through <code>RequestFriendTimeline</code> have "
            + "been retrieved. The <code>timeline</code> parameter and the "
            + "<code>Timeline</code> property will contain a list of lists, where "
            + "each sub-list contains a status update of the form (username message)")
    public void FriendTimelineReceived(final List<List<String>> timeline) {
        EventDispatcher.dispatchEvent(this, "FriendTimelineReceived", timeline);
    }

    @SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "This property contains the 20 most recent messages of "
            + "users being followed.  Initially, the list is empty.  To set it, " + "the program must: <ol> "
            + "<li> Call the <code>Authorize</code> method.</li> "
            + "<li> Wait for the <code>IsAuthorized</code> event.</li> "
            + "<li> Specify users to follow with one or more calls to the " + "<code>Follow</code> method.</li> "
            + "<li> Call the <code>RequestFriendTimeline</code> method.</li> "
            + "<li> Wait for the <code>FriendTimelineReceived</code> event.</li> " + "</ol>\n"
            + "The value of this property will then be set to the list of messages "
            + "(and maintain its value until any subsequent call to " + "<code>RequestFriendTimeline</code>.")
    public List<List<String>> FriendTimeline() {
        return timeline;
    }

    /**
     * Search for tweets or labels
     */
    @SimpleFunction(description = "This searches Twitter for the given String query."
            + "<p><u>Requirements</u>: This should only be called after the "
            + "<code>IsAuthorized</code> event has been raised, indicating that the "
            + "user has successfully logged in to Twitter.</p>")
    public void SearchTwitter(final String query) {
        if (twitter == null || userName.length() == 0) {
            form.dispatchErrorOccurredEvent(this, "SearchTwitter", ErrorMessages.ERROR_TWITTER_SEARCH_FAILED,
                    "Need to login?");
            return;
        }
        AsynchUtil.runAsynchronously(new Runnable() {
            List<Status> tweets = Collections.emptyList();

            public void run() {
                try {
                    tweets = twitter.search(new Query(query)).getTweets();
                } catch (TwitterException e) {
                    form.dispatchErrorOccurredEvent(Twitter.this, "SearchTwitter",
                            ErrorMessages.ERROR_TWITTER_SEARCH_FAILED, e.getMessage());
                } finally {
                    handler.post(new Runnable() {
                        public void run() {
                            searchResults.clear();
                            for (Status tweet : tweets) {
                                searchResults.add(tweet.getUser().getName() + " " + tweet.getText());
                            }
                            SearchSuccessful(searchResults);
                        }
                    });
                }
            }
        });
    }

    /**
     * Indicates when the search requested through {@link #SearchTwitter(String)}
     * has completed.
     */
    @SimpleEvent(description = "This event is raised when the results of the search "
            + "requested through <code>SearchSuccessful</code> have "
            + "been retrieved. A list of the results can then be found in the "
            + "<code>results</code> parameter or the <code>Results</code> " + "property.")
    public void SearchSuccessful(final List<String> searchResults) {
        EventDispatcher.dispatchEvent(this, "SearchSuccessful", searchResults);
    }

    @SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "This property, which is initially empty, is set to a "
            + "list of search results after the program: <ol>"
            + "<li>Calls the <code>SearchTwitter</code> method.</li> "
            + "<li>Waits for the <code>SearchSuccessful</code> event.</li></ol>\n"
            + "The value of the property will then be the same as the parameter to "
            + "<code>SearchSuccessful</code>.  Note that it is not necessary to "
            + "call the <code>Authorize</code> method before calling " + "<code>SearchTwitter</code>.")
    public List<String> SearchResults() {
        return searchResults;
    }

    /**
     * Check whether accessToken is stored in preferences. If there is one, set it.
     * If it was already set (for instance calling Authorize twice in a row),
     * it will throw an IllegalStateException that, in this case, can be ignored.
     * @return true if accessToken is valid and set (user authorized), false otherwise.
     */
    private boolean checkAccessToken(String myConsumerKey, String myConsumerSecret) {
        accessToken = retrieveAccessToken();
        if (accessToken == null) {
            return false;
        } else {
            if (twitter == null) {
                twitter = new TwitterFactory().getInstance();
            }
            try {
                twitter.setOAuthConsumer(consumerKey, consumerSecret);
                twitter.setOAuthAccessToken(accessToken);
            } catch (IllegalStateException ies) {
                //ignore: it means that the consumer data was already set
            }
            if (userName.trim().length() == 0) {
                User user;
                try {
                    user = twitter.verifyCredentials();
                    userName = user.getScreenName();
                } catch (TwitterException e) {// something went wrong (networks or bad credentials <-- DeAuthorize
                    deAuthorize();
                    return false;
                }
            }
            return true;
        }
    }
}