/*
* Copyright 2009 Andrew Shu
*
* This file is part of "reddit is fun".
*
* "reddit is fun" 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.
*
* "reddit is fun" 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 "reddit is fun". If not, see <http://www.gnu.org/licenses/>.
*/
package com.andrewshu.android.reddit.common;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ArrayNode;
import android.app.Activity;
import android.app.ListActivity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.provider.Browser;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.webkit.CookieSyncManager;
import android.widget.Button;
import android.widget.ListView;
import android.widget.RemoteViews;
import android.widget.TextView;
import android.widget.Toast;
import com.andrewshu.android.reddit.R;
import com.andrewshu.android.reddit.browser.BrowserActivity;
import com.andrewshu.android.reddit.captcha.CaptchaException;
import com.andrewshu.android.reddit.comments.CommentsListActivity;
import com.andrewshu.android.reddit.common.util.StringUtils;
import com.andrewshu.android.reddit.common.util.Util;
import com.andrewshu.android.reddit.mail.InboxActivity;
import com.andrewshu.android.reddit.settings.RedditSettings;
import com.andrewshu.android.reddit.threads.ThreadsListActivity;
import com.andrewshu.android.reddit.user.ProfileActivity;
public class Common {
private static final String TAG = "Common";
// 1:subreddit 2:threadId 3:commentId
private static final Pattern COMMENT_LINK = Pattern.compile(Constants.COMMENT_PATH_PATTERN_STRING);
private static final Pattern REDDIT_LINK = Pattern.compile(Constants.REDDIT_PATH_PATTERN_STRING);
private static final Pattern USER_LINK = Pattern.compile(Constants.USER_PATH_PATTERN_STRING);
private static final ObjectMapper mObjectMapper = new ObjectMapper();
public static void showErrorToast(String error, int duration, Context context) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Toast t = new Toast(context);
t.setDuration(duration);
View v = inflater.inflate(R.layout.error_toast, null);
TextView errorMessage = (TextView) v.findViewById(R.id.errorMessage);
errorMessage.setText(error);
t.setView(v);
t.show();
}
public static boolean shouldLoadThumbnails(Activity activity, RedditSettings settings) {
//check for wifi connection and wifi thumbnail setting
boolean thumbOkay = true;
if (settings.isLoadThumbnailsOnlyWifi())
{
thumbOkay = false;
ConnectivityManager connMan = (ConnectivityManager) activity.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo netInfo = connMan.getActiveNetworkInfo();
if (netInfo != null && netInfo.getType() == ConnectivityManager.TYPE_WIFI && netInfo.isConnected()) {
thumbOkay = true;
}
}
return settings.isLoadThumbnails() && thumbOkay;
}
/**
* Set the Drawable for the list selector etc. based on the current theme.
*/
public static void updateListDrawables(ListActivity la, int theme) {
ListView lv = la.getListView();
if (Util.isLightTheme(theme)) {
lv.setBackgroundResource(android.R.color.background_light);
lv.setSelector(R.drawable.list_selector_blue);
} else /* if (Common.isDarkTheme(theme)) */ {
lv.setSelector(android.R.drawable.list_selector_background);
}
}
public static void updateNextPreviousButtons(ListActivity act, View nextPreviousView,
String after, String before, int count, RedditSettings settings,
OnClickListener downloadAfterOnClickListener, OnClickListener downloadBeforeOnClickListener) {
boolean shouldShow = after != null || before != null;
Button nextButton = null;
Button previousButton = null;
// If alwaysShowNextPrevious, use the navbar
if (settings.isAlwaysShowNextPrevious()) {
nextPreviousView = act.findViewById(R.id.next_previous_layout);
if (nextPreviousView == null)
return;
View nextPreviousBorder = act.findViewById(R.id.next_previous_border_top);
if (shouldShow) {
if (nextPreviousView != null && nextPreviousBorder != null) {
if (Util.isLightTheme(settings.getTheme())) {
nextPreviousView.setBackgroundResource(android.R.color.background_light);
nextPreviousBorder.setBackgroundResource(R.color.black);
} else {
nextPreviousBorder.setBackgroundResource(R.color.white);
}
nextPreviousView.setVisibility(View.VISIBLE);
}
// update the "next 25" and "prev 25" buttons
nextButton = (Button) act.findViewById(R.id.next_button);
previousButton = (Button) act.findViewById(R.id.previous_button);
} else {
nextPreviousView.setVisibility(View.GONE);
}
}
// Otherwise we are using the ListView footer
else {
if (nextPreviousView == null)
return;
if (shouldShow && nextPreviousView.getVisibility() != View.VISIBLE) {
nextPreviousView.setVisibility(View.VISIBLE);
} else if (!shouldShow && nextPreviousView.getVisibility() == View.VISIBLE) {
nextPreviousView.setVisibility(View.GONE);
}
// update the "next 25" and "prev 25" buttons
nextButton = (Button) nextPreviousView.findViewById(R.id.next_button);
previousButton = (Button) nextPreviousView.findViewById(R.id.previous_button);
}
if (nextButton != null) {
if (after != null) {
nextButton.setVisibility(View.VISIBLE);
nextButton.setOnClickListener(downloadAfterOnClickListener);
} else {
nextButton.setVisibility(View.INVISIBLE);
}
}
if (previousButton != null) {
if (before != null && count != Constants.DEFAULT_THREAD_DOWNLOAD_LIMIT) {
previousButton.setVisibility(View.VISIBLE);
previousButton.setOnClickListener(downloadBeforeOnClickListener);
} else {
previousButton.setVisibility(View.INVISIBLE);
}
}
}
public static void setTextColorFromTheme(int theme, Resources resources, TextView... textViews) {
int color;
if (Util.isLightTheme(theme))
color = resources.getColor(R.color.reddit_light_dialog_text_color);
else
color = resources.getColor(R.color.reddit_dark_dialog_text_color);
for (TextView textView : textViews)
textView.setTextColor(color);
}
static void clearCookies(RedditSettings settings, HttpClient client, Context context) {
settings.setRedditSessionCookie(null);
RedditIsFunHttpClientFactory.getCookieStore().clear();
CookieSyncManager.getInstance().sync();
SharedPreferences sessionPrefs = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sessionPrefs.edit();
editor.remove("reddit_sessionValue");
editor.remove("reddit_sessionDomain");
editor.remove("reddit_sessionPath");
editor.remove("reddit_sessionExpiryDate");
editor.commit();
}
public static void doLogout(RedditSettings settings, HttpClient client, Context context) {
clearCookies(settings, client, context);
CacheInfo.invalidateAllCaches(context);
settings.setUsername(null);
}
/**
* Get a new modhash by scraping and return it
*
* @param client
* @return
*/
public static String doUpdateModhash(HttpClient client) {
final Pattern MODHASH_PATTERN = Pattern.compile("modhash: '(.*?)'");
String modhash;
HttpEntity entity = null;
// The pattern to find modhash from HTML javascript area
try {
HttpGet httpget = new HttpGet(Constants.MODHASH_URL);
HttpResponse response = client.execute(httpget);
// For modhash, we don't care about the status, since the 404 page has the info we want.
// status = response.getStatusLine().toString();
// if (!status.contains("OK"))
// throw new HttpException(status);
entity = response.getEntity();
BufferedReader in = new BufferedReader(new InputStreamReader(entity.getContent()));
// modhash should appear within first 1200 chars
char[] buffer = new char[1200];
in.read(buffer, 0, 1200);
in.close();
String line = String.valueOf(buffer);
entity.consumeContent();
if (StringUtils.isEmpty(line)) {
throw new HttpException("No content returned from doUpdateModhash GET to "+Constants.MODHASH_URL);
}
if (line.contains("USER_REQUIRED")) {
throw new Exception("User session error: USER_REQUIRED");
}
Matcher modhashMatcher = MODHASH_PATTERN.matcher(line);
if (modhashMatcher.find()) {
modhash = modhashMatcher.group(1);
if (StringUtils.isEmpty(modhash)) {
// Means user is not actually logged in.
return null;
}
} else {
throw new Exception("No modhash found at URL "+Constants.MODHASH_URL);
}
if (Constants.LOGGING) Common.logDLong(TAG, line);
if (Constants.LOGGING) Log.d(TAG, "modhash: "+modhash);
return modhash;
} catch (Exception e) {
if (entity != null) {
try {
entity.consumeContent();
} catch (Exception e2) {
if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e);
}
}
if (Constants.LOGGING) Log.e(TAG, "doUpdateModhash()", e);
return null;
}
}
public static String checkResponseErrors(HttpResponse response, HttpEntity entity) {
String status = response.getStatusLine().toString();
String line;
if (!status.contains("OK")) {
return "HTTP error. Status = "+status;
}
try {
BufferedReader in = new BufferedReader(new InputStreamReader(entity.getContent()));
line = in.readLine();
if (Constants.LOGGING) Common.logDLong(TAG, line);
in.close();
} catch (IOException e) {
if (Constants.LOGGING) Log.e(TAG, "IOException", e);
return "Error reading retrieved data.";
}
if (StringUtils.isEmpty(line)) {
return "API returned empty data.";
}
if (line.contains("WRONG_PASSWORD")) {
return "Wrong password.";
}
if (line.contains("USER_REQUIRED")) {
// The modhash probably expired
return "Login expired.";
}
if (line.contains("SUBREDDIT_NOEXIST")) {
return "That subreddit does not exist.";
}
if (line.contains("SUBREDDIT_NOTALLOWED")) {
return "You are not allowed to post to that subreddit.";
}
return null;
}
public static String checkIDResponse(HttpResponse response, HttpEntity entity) throws CaptchaException, Exception {
// Group 1: fullname. Group 2: kind. Group 3: id36.
final Pattern NEW_ID_PATTERN = Pattern.compile("\"id\": \"((.+?)_(.+?))\"");
// Group 1: whole error. Group 2: the time part
final Pattern RATELIMIT_RETRY_PATTERN = Pattern.compile("(you are trying to submit too fast. try again in (.+?)\\.)");
String status = response.getStatusLine().toString();
String line;
if (!status.contains("OK")) {
throw new Exception("HTTP error. Status = "+status);
}
try {
BufferedReader in = new BufferedReader(new InputStreamReader(entity.getContent()));
line = in.readLine();
if (Constants.LOGGING) Common.logDLong(TAG, line);
in.close();
} catch (IOException e) {
if (Constants.LOGGING) Log.e(TAG, "IOException", e);
throw new Exception("Error reading retrieved data.");
}
if (StringUtils.isEmpty(line)) {
throw new Exception("API returned empty data.");
}
if (line.contains("WRONG_PASSWORD")) {
throw new Exception("Wrong password.");
}
if (line.contains("USER_REQUIRED")) {
// The modhash probably expired
throw new Exception("Login expired.");
}
if (line.contains("SUBREDDIT_NOEXIST")) {
throw new Exception("That subreddit does not exist.");
}
if (line.contains("SUBREDDIT_NOTALLOWED")) {
throw new Exception("You are not allowed to post to that subreddit.");
}
String newId;
Matcher idMatcher = NEW_ID_PATTERN.matcher(line);
if (idMatcher.find()) {
newId = idMatcher.group(3);
} else {
if (line.contains("RATELIMIT")) {
// Try to find the # of minutes using regex
Matcher rateMatcher = RATELIMIT_RETRY_PATTERN.matcher(line);
if (rateMatcher.find())
throw new Exception(rateMatcher.group(1));
else
throw new Exception("you are trying to submit too fast. try again in a few minutes.");
}
if (line.contains("DELETED_LINK")) {
throw new Exception("the link you are commenting on has been deleted");
}
if (line.contains("BAD_CAPTCHA")) {
throw new CaptchaException("Bad CAPTCHA. Try again.");
}
// No id returned by reply POST.
return null;
}
// Getting here means success.
return newId;
}
public static void newMailNotification(Context context, String mailNotificationStyle, int count) {
Intent nIntent = new Intent(context, InboxActivity.class);
PendingIntent contentIntent = PendingIntent.getActivity(context, 0, nIntent, 0);
Notification notification = new Notification(R.drawable.mail, Constants.HAVE_MAIL_TICKER, System.currentTimeMillis());
if (Constants.PREF_MAIL_NOTIFICATION_STYLE_BIG_ENVELOPE.equals(mailNotificationStyle)) {
RemoteViews contentView = new RemoteViews(context.getPackageName(), R.layout.big_envelope_notification);
notification.contentView = contentView;
} else {
notification.setLatestEventInfo(context, Constants.HAVE_MAIL_TITLE,
count + (count == 1 ? " unread message" : " unread messages"), contentIntent);
}
notification.defaults |= Notification.DEFAULT_SOUND;
notification.flags |= Notification.FLAG_ONLY_ALERT_ONCE | Notification.FLAG_AUTO_CANCEL;
notification.contentIntent = contentIntent;
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(Constants.NOTIFICATION_HAVE_MAIL, notification);
}
public static void cancelMailNotification(Context context) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(Constants.NOTIFICATION_HAVE_MAIL);
}
/**
*
* @param url
* @param context
* @param requireNewTask set this to true if context is not an Activity
* @param bypassParser
* @param useExternalBrowser
*/
public static void launchBrowser(Context context, String url, String threadUrl,
boolean requireNewTask, boolean bypassParser, boolean useExternalBrowser,
boolean saveHistory) {
try {
if (saveHistory) {
Browser.updateVisitedHistory(context.getContentResolver(), url, true);
}
} catch (Exception ex) {
if (Constants.LOGGING) Log.i(TAG, "Browser.updateVisitedHistory error", ex);
}
Uri uri = Uri.parse(url);
if (!bypassParser) {
if (Util.isRedditUri(uri)) {
String path = uri.getPath();
Matcher matcher = COMMENT_LINK.matcher(path);
if (matcher.matches()) {
if (matcher.group(3) != null || matcher.group(2) != null) {
CacheInfo.invalidateCachedThread(context);
Intent intent = new Intent(context, CommentsListActivity.class);
intent.setData(uri);
intent.putExtra(Constants.EXTRA_NUM_COMMENTS, Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
if (requireNewTask)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
return;
}
}
matcher = REDDIT_LINK.matcher(path);
if (matcher.matches()) {
CacheInfo.invalidateCachedSubreddit(context);
Intent intent = new Intent(context, ThreadsListActivity.class);
intent.setData(uri);
if (requireNewTask)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
return;
}
matcher = USER_LINK.matcher(path);
if (matcher.matches()) {
Intent intent = new Intent(context, ProfileActivity.class);
intent.setData(uri);
if (requireNewTask)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
return;
}
} else if (Util.isRedditShortenedUri(uri)) {
String path = uri.getPath();
if (path.equals("") || path.equals("/")) {
CacheInfo.invalidateCachedSubreddit(context);
Intent intent = new Intent(context, ThreadsListActivity.class);
intent.setData(uri);
if (requireNewTask)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
} else {
// Assume it points to a thread aka CommentsList
CacheInfo.invalidateCachedThread(context);
Intent intent = new Intent(context, CommentsListActivity.class);
intent.setData(uri);
intent.putExtra(Constants.EXTRA_NUM_COMMENTS, Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
if (requireNewTask)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
return;
}
}
uri = Util.optimizeMobileUri(uri);
// Some URLs should always be opened externally, if BrowserActivity doesn't support their content.
if (Util.isYoutubeUri(uri) || Util.isAndroidMarketUri(uri))
useExternalBrowser = true;
if (useExternalBrowser) {
Intent browser = new Intent(Intent.ACTION_VIEW, uri);
browser.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
if (requireNewTask)
browser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(browser);
} else {
Intent browser = new Intent(context, BrowserActivity.class);
browser.setData(uri);
if (threadUrl != null)
browser.putExtra(Constants.EXTRA_THREAD_URL, threadUrl);
if (requireNewTask)
browser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(browser);
}
}
public static boolean isClicked(Context context, String url) {
Cursor cursor;
try {
cursor = context.getContentResolver().query(
Browser.BOOKMARKS_URI,
Browser.HISTORY_PROJECTION,
Browser.HISTORY_PROJECTION[Browser.HISTORY_PROJECTION_URL_INDEX] + "=?",
new String[]{ url },
null
);
} catch (Exception ex) {
if (Constants.LOGGING) Log.w(TAG, "Error querying Android Browser for history; manually revoked permission?", ex);
return false;
}
if (cursor != null) {
boolean isClicked = cursor.moveToFirst(); // returns true if cursor is not empty
cursor.close();
return isClicked;
} else {
return false;
}
}
public static ObjectMapper getObjectMapper() {
return mObjectMapper;
}
public static void logDLong(String tag, String msg) {
int c;
boolean done = false;
StringBuilder sb = new StringBuilder();
for (int k = 0; k < msg.length(); k += 80) {
for (int i = 0; i < 80; i++) {
if (k + i >= msg.length()) {
done = true;
break;
}
c = msg.charAt(k + i);
sb.append((char) c);
}
if (Constants.LOGGING) Log.d(tag, "multipart log: " + sb.toString());
sb = new StringBuilder();
if (done)
break;
}
}
public static String getSubredditId(String mSubreddit){
String subreddit_id = null;
JsonNode subredditInfo =
RestJsonClient.connect(Constants.REDDIT_BASE_URL + "/r/" + mSubreddit + "/.json?count=1");
if(subredditInfo != null){
ArrayNode children = (ArrayNode) subredditInfo.path("data").path("children");
subreddit_id = children.get(0).get("data").get("subreddit_id").getTextValue();
}
return subreddit_id;
}
/** http://developer.android.com/guide/topics/ui/actionbar.html#Home */
public static void goHome(Activity activity) {
// app icon in action bar clicked; go home
Intent intent = new Intent(activity, ThreadsListActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
activity.startActivity(intent);
}
}