7 package com.google.appinventor.components.runtime;
9 import java.util.ArrayList;
10 import java.util.Collections;
11 import java.util.List;
14 import twitter4j.DirectMessage;
16 import twitter4j.Query;
17 import twitter4j.Status;
18 import twitter4j.StatusUpdate;
19 import twitter4j.TwitterException;
20 import twitter4j.TwitterFactory;
21 import twitter4j.User;
22 import twitter4j.auth.AccessToken;
23 import twitter4j.auth.RequestToken;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.SharedPreferences;
27 import android.net.Uri;
28 import android.os.Handler;
29 import android.util.Log;
74 @DesignerComponent(version = YaVersion.TWITTER_COMPONENT_VERSION, description =
"A non-visible component that enables communication "
75 +
"with <a href=\"http://www.twitter.com\" target=\"_blank\">Twitter</a>. "
76 +
"Once a user has logged into their Twitter account (and the authorization has been confirmed successful by the "
77 +
"<code>IsAuthorized</code> event), many more operations are available:<ul>"
78 +
"<li> Searching Twitter for tweets or labels (<code>SearchTwitter</code>)</li>\n"
79 +
"<li> Sending a Tweet (<code>Tweet</code>)"
81 +
"<li> Sending a Tweet with an Image (<code>TweetWithImage</code>)"
83 +
"<li> Directing a message to a specific user "
84 +
" (<code>DirectMessage</code>)</li>\n "
85 +
"<li> Receiving the most recent messages directed to the logged-in user "
86 +
" (<code>RequestDirectMessages</code>)</li>\n "
87 +
"<li> Following a specific user (<code>Follow</code>)</li>\n"
88 +
"<li> Ceasing to follow a specific user (<code>StopFollowing</code>)</li>\n"
89 +
"<li> Getting a list of users following the logged-in user "
90 +
" (<code>RequestFollowers</code>)</li>\n "
91 +
"<li> Getting the most recent messages of users followed by the "
92 +
" logged-in user (<code>RequestFriendTimeline</code>)</li>\n "
93 +
"<li> Getting the most recent mentions of the logged-in user "
94 +
" (<code>RequestMentions</code>)</li></ul></p>\n "
95 +
"<p>You must obtain a Consumer Key and Consumer Secret for Twitter authorization "
96 +
" specific to your app from http://twitter.com/oauth_clients/new",
97 category = ComponentCategory.SOCIAL, nonVisible =
true, iconName =
"images/twitter.png")
99 @UsesPermissions(permissionNames =
"android.permission.INTERNET")
100 @UsesLibraries(libraries =
"twitter4j.jar," +
"twitter4jmedia.jar")
101 @UsesActivities(activities = {
102 @ActivityElement(name =
"com.google.appinventor.components.runtime.WebViewActivity",
103 configChanges =
"orientation|keyboardHidden",
104 screenOrientation =
"behind",
106 @IntentFilterElement(actionElements = {
107 @ActionElement(name =
"android.intent.action.MAIN")
113 private static final String ACCESS_TOKEN_TAG =
"TwitterOauthAccessToken";
114 private static final String ACCESS_SECRET_TAG =
"TwitterOauthAccessSecret";
115 private static final String MAX_CHARACTERS =
"160";
116 private static final String URL_HOST =
"twitter";
119 private static final String WEBVIEW_ACTIVITY_CLASS =
WebViewActivity.class
123 private String consumerKey =
"";
124 private String consumerSecret =
"";
125 private String TwitPic_API_Key =
"";
126 private final List<String> mentions;
127 private final List<String> followers;
128 private final List<List<String>> timeline;
129 private final List<String> directMessages;
130 private final List<String> searchResults;
134 private twitter4j.Twitter twitter;
135 private RequestToken requestToken;
136 private AccessToken accessToken;
137 private String userName =
"";
138 private final SharedPreferences sharedPreferences;
139 private final int requestCode;
141 private final Handler handler;
168 private static final String MAX_MENTIONS_RETURNED =
"20";
171 super(container.
$form());
172 this.container = container;
173 handler =
new Handler();
175 mentions =
new ArrayList<String>();
176 followers =
new ArrayList<String>();
177 timeline =
new ArrayList<List<String>>();
178 directMessages =
new ArrayList<String>();
179 searchResults =
new ArrayList<String>();
181 sharedPreferences = container.$context().getSharedPreferences(
"Twitter",
182 Context.MODE_PRIVATE);
183 accessToken = retrieveAccessToken();
185 requestCode = form.registerForActivityResult(
this);
198 @
SimpleFunction(userVisible =
false, description =
"Twitter's API no longer supports login via username and "
199 +
"password. Use the Authorize call instead.")
200 public
void Login(String username, String password) {
201 form.dispatchErrorOccurredEvent(
this,
"Login",
206 +
"there is no authorized user.")
207 public String Username() {
215 public String ConsumerKey() {
227 public
void ConsumerKey(String consumerKey) {
228 this.consumerKey = consumerKey;
235 public String ConsumerSecret() {
236 return consumerSecret;
246 @
SimpleProperty(description=
"The consumer secret to be used when authorizing with Twitter via OAuth")
247 public
void ConsumerSecret(String consumerSecret) {
248 this.consumerSecret = consumerSecret;
257 public String TwitPic_API_Key() {
258 return TwitPic_API_Key;
273 description=
"The API Key for image uploading, provided by TwitPic.")
274 public
void TwitPic_API_Key(String TwitPic_API_Key) {
275 this.TwitPic_API_Key = TwitPic_API_Key;
284 @
SimpleEvent(description =
"This event is raised after the program calls "
285 +
"<code>Authorize</code> if the authorization was successful. "
286 +
"It is also called after a call to <code>CheckAuthorized</code> "
287 +
"if we already have a valid access token. "
288 +
"After this event has been raised, any other method for this "
289 +
"component can be called.")
290 public
void IsAuthorized() {
298 @
SimpleFunction(description =
"Redirects user to login to Twitter via the Web browser using "
299 +
"the OAuth protocol if we don't already have authorization.")
300 public
void Authorize() {
301 if (consumerKey.length() == 0 || consumerSecret.length() == 0) {
302 form.dispatchErrorOccurredEvent(
this,
"Authorize",
306 if (twitter ==
null) {
307 twitter =
new TwitterFactory().getInstance();
309 final String myConsumerKey = consumerKey;
310 final String myConsumerSecret = consumerSecret;
313 if (checkAccessToken(myConsumerKey, myConsumerSecret)) {
314 handler.post(
new Runnable() {
324 RequestToken newRequestToken;
325 twitter.setOAuthConsumer(myConsumerKey, myConsumerSecret);
326 newRequestToken = twitter.getOAuthRequestToken(CALLBACK_URL);
327 String authURL = newRequestToken.getAuthorizationURL();
328 requestToken = newRequestToken;
330 Intent browserIntent =
new Intent(Intent.ACTION_MAIN, Uri
332 browserIntent.setClassName(container.
$context(),
333 WEBVIEW_ACTIVITY_CLASS);
334 container.
$context().startActivityForResult(browserIntent,
336 }
catch (TwitterException e) {
337 Log.i(
"Twitter",
"Got exception: " + e.getMessage());
339 form.dispatchErrorOccurredEvent(
Twitter.this,
"Authorize",
342 }
catch (IllegalStateException ise){
344 Log.e(
"Twitter",
"OAuthConsumer was already set: launch IsAuthorized()");
345 handler.post(
new Runnable() {
360 @
SimpleFunction(description =
"Checks whether we already have access, and if so, causes "
361 +
"IsAuthorized event handler to be called.")
362 public
void CheckAuthorized() {
363 final String myConsumerKey = consumerKey;
364 final String myConsumerSecret = consumerSecret;
367 if (checkAccessToken(myConsumerKey, myConsumerSecret)) {
368 handler.post(
new Runnable() {
384 Log.i(
"Twitter",
"Got result " + resultCode);
386 Uri uri = data.getData();
388 Log.i(
"Twitter",
"Intent URI: " + uri.toString());
389 final String oauthVerifier = uri.getQueryParameter(
"oauth_verifier");
390 if (twitter ==
null) {
391 Log.e(
"Twitter",
"twitter field is unexpectedly null");
392 form.dispatchErrorOccurredEvent(
this,
"Authorize",
394 "internal error: can't access Twitter library");
395 new RuntimeException().printStackTrace();
397 if (requestToken !=
null && oauthVerifier !=
null
398 && oauthVerifier.length() != 0) {
402 AccessToken resultAccessToken;
403 resultAccessToken = twitter.getOAuthAccessToken(requestToken,
405 accessToken = resultAccessToken;
406 userName = accessToken.getScreenName();
407 saveAccessToken(resultAccessToken);
408 handler.post(
new Runnable() {
414 }
catch (TwitterException e) {
415 Log.e(
"Twitter",
"Got exception: " + e.getMessage());
417 form.dispatchErrorOccurredEvent(
Twitter.this,
"Authorize",
425 form.dispatchErrorOccurredEvent(
this,
"Authorize",
430 Log.e(
"Twitter",
"uri returned from WebView activity was unexpectedly null");
434 Log.e(
"Twitter",
"intent returned from WebView activity was unexpectedly null");
439 private void saveAccessToken(AccessToken accessToken) {
440 final SharedPreferences.Editor sharedPrefsEditor = sharedPreferences.edit();
441 if (accessToken ==
null) {
442 sharedPrefsEditor.remove(ACCESS_TOKEN_TAG);
443 sharedPrefsEditor.remove(ACCESS_SECRET_TAG);
445 sharedPrefsEditor.putString(ACCESS_TOKEN_TAG, accessToken.getToken());
446 sharedPrefsEditor.putString(ACCESS_SECRET_TAG,
447 accessToken.getTokenSecret());
449 sharedPrefsEditor.commit();
452 private AccessToken retrieveAccessToken() {
453 String token = sharedPreferences.getString(ACCESS_TOKEN_TAG,
"");
454 String secret = sharedPreferences.getString(ACCESS_SECRET_TAG,
"");
455 if (token.length() == 0 || secret.length() == 0) {
458 return new AccessToken(token, secret);
464 @SimpleFunction(description =
"Removes Twitter authorization from this running app instance")
465 public
void DeAuthorize() {
469 private void deAuthorize() {
470 final twitter4j.Twitter oldTwitter;
474 oldTwitter = twitter;
477 saveAccessToken(accessToken);
481 if (oldTwitter !=
null) {
482 oldTwitter.setOAuthAccessToken(
null);
493 @SimpleFunction(description =
"This sends a tweet as the logged-in user with the "
494 +
"specified Text, which will be trimmed if it exceeds "
497 +
"<p><u>Requirements</u>: This should only be called after the "
498 +
"<code>IsAuthorized</code> event has been raised, indicating that the "
499 +
"user has successfully logged in to Twitter.</p>")
500 public
void Tweet(final String status) {
502 if (twitter ==
null || userName.length() == 0) {
503 form.dispatchErrorOccurredEvent(
this,
"Tweet",
516 twitter.updateStatus(status);
517 }
catch (TwitterException e) {
518 form.dispatchErrorOccurredEvent(
Twitter.this,
"Tweet",
533 @
SimpleFunction(description =
"This sends a tweet as the logged-in user with the "
534 +
"specified Text and a path to the image to be uploaded, which will be trimmed if it "
535 +
"exceeds " + MAX_CHARACTERS +
" characters. "
536 +
"If an image is not found or invalid, only the text will be tweeted."
537 +
"<p><u>Requirements</u>: This should only be called after the "
538 +
"<code>IsAuthorized</code> event has been raised, indicating that the "
539 +
"user has successfully logged in to Twitter.</p>" )
540 public
void TweetWithImage(final String status, final String imagePath) {
541 if (twitter ==
null || userName.length() == 0) {
542 form.dispatchErrorOccurredEvent(
this,
"TweetWithImage",
550 String cleanImagePath = imagePath;
552 if (cleanImagePath.startsWith(
"file://")) {
553 cleanImagePath = imagePath.replace(
"file://",
"");
555 File imageFilePath =
new File(cleanImagePath);
556 if (imageFilePath.exists()) {
557 StatusUpdate theTweet =
new StatusUpdate(status);
558 theTweet.setMedia(imageFilePath);
559 twitter.updateStatus(theTweet);
562 form.dispatchErrorOccurredEvent(
Twitter.this,
"TweetWithImage",
565 }
catch (TwitterException e) {
566 form.dispatchErrorOccurredEvent(
Twitter.this,
"TweetWithImage",
582 @
SimpleFunction(description =
"Requests the " + MAX_MENTIONS_RETURNED
584 +
"recent mentions of the logged-in user. When the mentions have been "
585 +
"retrieved, the system will raise the <code>MentionsReceived</code> "
586 +
"event and set the <code>Mentions</code> property to the list of "
588 +
"<p><u>Requirements</u>: This should only be called after the "
589 +
"<code>IsAuthorized</code> event has been raised, indicating that the "
590 +
"user has successfully logged in to Twitter.</p>")
591 public
void RequestMentions() {
592 if (twitter ==
null || userName.length() == 0) {
593 form.dispatchErrorOccurredEvent(
this,
"RequestMentions",
598 List<Status> replies = Collections.emptyList();
602 replies = twitter.getMentionsTimeline();
603 }
catch (TwitterException e) {
604 form.dispatchErrorOccurredEvent(
Twitter.this,
"RequestMentions",
608 handler.post(
new Runnable() {
611 for (Status status : replies) {
612 mentions.add(status.getUser().getScreenName() +
" "
615 MentionsReceived(mentions);
628 @
SimpleEvent(description =
"This event is raised when the mentions of the logged-in user "
629 +
"requested through <code>RequestMentions</code> have been retrieved. "
630 +
"A list of the mentions can then be found in the <code>mentions</code> "
631 +
"parameter or the <code>Mentions</code> property.")
632 public
void MentionsReceived(final List<String> mentions) {
649 +
"logged-in user. Initially, the list is empty. To set it, the "
650 +
"program must: <ol> "
651 +
"<li> Call the <code>Authorize</code> method.</li> "
652 +
"<li> Wait for the <code>IsAuthorized</code> event.</li> "
653 +
"<li> Call the <code>RequestMentions</code> method.</li> "
654 +
"<li> Wait for the <code>MentionsReceived</code> event.</li></ol>\n"
655 +
"The value of this property will then be set to the list of mentions "
656 +
"(and will maintain its value until any subsequent calls to "
657 +
"<code>RequestMentions</code>).")
658 public List<String> Mentions() {
667 if (twitter ==
null || userName.length() == 0) {
668 form.dispatchErrorOccurredEvent(
this,
"RequestFollowers",
674 List<User> friends =
new ArrayList<User>();
678 IDs followerIDs = twitter.getFollowersIDs(-1);
679 for (
long id : followerIDs.getIDs()) {
681 friends.add(twitter.showUser(
id));
683 }
catch (TwitterException e) {
684 form.dispatchErrorOccurredEvent(
Twitter.this,
"RequestFollowers",
688 handler.post(
new Runnable() {
691 for (User user : friends) {
692 followers.add(user.getName());
694 FollowersReceived(followers);
707 @
SimpleEvent(description =
"This event is raised when all of the followers of the "
708 +
"logged-in user requested through <code>RequestFollowers</code> have "
709 +
"been retrieved. A list of the followers can then be found in the "
710 +
"<code>followers</code> parameter or the <code>Followers</code> "
712 public
void FollowersReceived(final List<String> followers2) {
729 +
"logged-in user. Initially, the list is empty. To set it, the "
730 +
"program must: <ol> "
731 +
"<li> Call the <code>Authorize</code> method.</li> "
732 +
"<li> Wait for the <code>IsAuthorized</code> event.</li> "
733 +
"<li> Call the <code>RequestFollowers</code> method.</li> "
734 +
"<li> Wait for the <code>FollowersReceived</code> event.</li></ol>\n"
735 +
"The value of this property will then be set to the list of "
736 +
"followers (and maintain its value until any subsequent call to "
737 +
"<code>RequestFollowers</code>).")
738 public List<String> Followers() {
750 @
SimpleFunction(description =
"Requests the " + MAX_MENTIONS_RETURNED
752 +
"recent direct messages sent to the logged-in user. When the "
753 +
"messages have been retrieved, the system will raise the "
754 +
"<code>DirectMessagesReceived</code> event and set the "
755 +
"<code>DirectMessages</code> property to the list of messages."
756 +
"<p><u>Requirements</u>: This should only be called after the "
757 +
"<code>IsAuthorized</code> event has been raised, indicating that the "
758 +
"user has successfully logged in to Twitter.</p>")
759 public
void RequestDirectMessages() {
760 if (twitter ==
null || userName.length() == 0) {
761 form.dispatchErrorOccurredEvent(
this,
"RequestDirectMessages",
767 List<DirectMessage> messages = Collections.emptyList();
772 messages = twitter.getDirectMessages();
773 }
catch (TwitterException e) {
774 form.dispatchErrorOccurredEvent(
Twitter.this,
775 "RequestDirectMessages",
779 handler.post(
new Runnable() {
782 directMessages.clear();
783 for (DirectMessage message : messages) {
784 directMessages.add(message.getSenderScreenName() +
" "
785 + message.getText());
787 DirectMessagesReceived(directMessages);
801 @
SimpleEvent(description =
"This event is raised when the recent messages "
802 +
"requested through <code>RequestDirectMessages</code> have "
803 +
"been retrieved. A list of the messages can then be found in the "
804 +
"<code>messages</code> parameter or the <code>Messages</code> "
806 public
void DirectMessagesReceived(final List<String> messages) {
823 +
"messages mentioning the logged-in user. Initially, the list is "
824 +
"empty. To set it, the program must: <ol> "
825 +
"<li> Call the <code>Authorize</code> method.</li> "
826 +
"<li> Wait for the <code>Authorized</code> event.</li> "
827 +
"<li> Call the <code>RequestDirectMessages</code> method.</li> "
828 +
"<li> Wait for the <code>DirectMessagesReceived</code> event.</li>"
830 +
"The value of this property will then be set to the list of direct "
831 +
"messages retrieved (and maintain that value until any subsequent "
832 +
"call to <code>RequestDirectMessages</code>).")
833 public List<String> DirectMessages() {
834 return directMessages;
844 @
SimpleFunction(description =
"This sends a direct (private) message to the specified "
845 +
"user. The message will be trimmed if it exceeds "
848 +
"<p><u>Requirements</u>: This should only be called after the "
849 +
"<code>IsAuthorized</code> event has been raised, indicating that the "
850 +
"user has successfully logged in to Twitter.</p>")
851 public
void DirectMessage(final String user, final String message) {
852 if (twitter ==
null || userName.length() == 0) {
853 form.dispatchErrorOccurredEvent(
this,
"DirectMessage",
860 twitter.sendDirectMessage(user, message);
861 }
catch (TwitterException e) {
862 form.dispatchErrorOccurredEvent(
Twitter.this,
"DirectMessage",
874 if (twitter ==
null || userName.length() == 0) {
875 form.dispatchErrorOccurredEvent(
this,
"Follow",
882 twitter.createFriendship(user);
883 }
catch (TwitterException e) {
884 form.dispatchErrorOccurredEvent(
Twitter.this,
"Follow",
896 if (twitter ==
null || userName.length() == 0) {
897 form.dispatchErrorOccurredEvent(
this,
"StopFollowing",
904 twitter.destroyFriendship(user);
905 }
catch (TwitterException e) {
906 form.dispatchErrorOccurredEvent(
Twitter.this,
"StopFollowing",
918 if (twitter ==
null || userName.length() == 0) {
919 form.dispatchErrorOccurredEvent(
this,
"RequestFriendTimeline",
925 List<Status> messages = Collections.emptyList();
929 messages = twitter.getHomeTimeline();
930 }
catch (TwitterException e) {
931 form.dispatchErrorOccurredEvent(
Twitter.this,
932 "RequestFriendTimeline",
936 handler.post(
new Runnable() {
939 for (Status message : messages) {
940 List<String> status =
new ArrayList<String>();
941 status.add(message.getUser().getScreenName());
942 status.add(message.getText());
943 timeline.add(status);
945 FriendTimelineReceived(timeline);
959 @
SimpleEvent(description =
"This event is raised when the messages "
960 +
"requested through <code>RequestFriendTimeline</code> have "
961 +
"been retrieved. The <code>timeline</code> parameter and the "
962 +
"<code>Timeline</code> property will contain a list of lists, where "
963 +
"each sub-list contains a status update of the form (username message)")
964 public
void FriendTimelineReceived(final List<List<String>> timeline) {
982 +
"users being followed. Initially, the list is empty. To set it, "
983 +
"the program must: <ol> "
984 +
"<li> Call the <code>Authorize</code> method.</li> "
985 +
"<li> Wait for the <code>IsAuthorized</code> event.</li> "
986 +
"<li> Specify users to follow with one or more calls to the "
987 +
"<code>Follow</code> method.</li> "
988 +
"<li> Call the <code>RequestFriendTimeline</code> method.</li> "
989 +
"<li> Wait for the <code>FriendTimelineReceived</code> event.</li> "
991 +
"The value of this property will then be set to the list of messages "
992 +
"(and maintain its value until any subsequent call to "
993 +
"<code>RequestFriendTimeline</code>.")
994 public List<List<String>> FriendTimeline() {
1004 @
SimpleFunction(description =
"This searches Twitter for the given String query."
1005 +
"<p><u>Requirements</u>: This should only be called after the "
1006 +
"<code>IsAuthorized</code> event has been raised, indicating that the "
1007 +
"user has successfully logged in to Twitter.</p>")
1008 public
void SearchTwitter(final String query) {
1009 if (twitter ==
null || userName.length() == 0) {
1010 form.dispatchErrorOccurredEvent(
this,
"SearchTwitter",
1015 List<Status> tweets = Collections.emptyList();
1019 tweets = twitter.search(
new Query(query)).getTweets();
1020 }
catch (TwitterException e) {
1021 form.dispatchErrorOccurredEvent(
Twitter.this,
"SearchTwitter",
1024 handler.post(
new Runnable() {
1026 searchResults.clear();
1027 for (Status tweet : tweets) {
1028 searchResults.add(tweet.getUser().getName() +
" " + tweet.getText());
1030 SearchSuccessful(searchResults);
1043 @
SimpleEvent(description =
"This event is raised when the results of the search "
1044 +
"requested through <code>SearchSuccessful</code> have "
1045 +
"been retrieved. A list of the results can then be found in the "
1046 +
"<code>results</code> parameter or the <code>Results</code> "
1048 public
void SearchSuccessful(final List<String> searchResults) {
1063 +
"list of search results after the program: <ol>"
1064 +
"<li>Calls the <code>SearchTwitter</code> method.</li> "
1065 +
"<li>Waits for the <code>SearchSuccessful</code> event.</li></ol>\n"
1066 +
"The value of the property will then be the same as the parameter to "
1067 +
"<code>SearchSuccessful</code>. Note that it is not necessary to "
1068 +
"call the <code>Authorize</code> method before calling "
1069 +
"<code>SearchTwitter</code>.")
1070 public List<String> SearchResults() {
1071 return searchResults;
1080 private boolean checkAccessToken(String myConsumerKey, String myConsumerSecret) {
1081 accessToken = retrieveAccessToken();
1082 if (accessToken ==
null) {
1086 if (twitter ==
null) {
1087 twitter =
new TwitterFactory().getInstance();
1090 twitter.setOAuthConsumer(consumerKey, consumerSecret);
1091 twitter.setOAuthAccessToken(accessToken);
1093 catch (IllegalStateException ies) {
1096 if (userName.trim().length() == 0) {
1099 user = twitter.verifyCredentials();
1100 userName = user.getScreenName();
1101 }
catch (TwitterException e) {