/* * 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); } }