in.shick.diode.common.Common.java Source code

Java tutorial

Introduction

Here is the source code for in.shick.diode.common.Common.java

Source

/*
 * Copyright 2009 Andrew Shu
 *
 * This file is part of "diode".
 *
 * "diode" 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.
 *
 * "diode" 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 "diode".  If not, see <http://www.gnu.org/licenses/>.
 */

package in.shick.diode.common;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.app.*;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.telephony.PhoneNumberUtils;
import android.text.Html;
import android.view.ContextThemeWrapper;
import android.view.ViewGroup;
import android.widget.*;
import in.shick.diode.markdown.MarkdownURL;
import in.shick.diode.things.ThingInfo;
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.map.DeserializationConfig;
import org.codehaus.jackson.node.ArrayNode;

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 in.shick.diode.R;
import in.shick.diode.browser.BrowserActivity;
import in.shick.diode.captcha.CaptchaException;
import in.shick.diode.comments.CommentsListActivity;
import in.shick.diode.common.util.StringUtils;
import in.shick.diode.common.util.Util;
import in.shick.diode.mail.InboxActivity;
import in.shick.diode.settings.RedditSettings;
import in.shick.diode.threads.ThreadsListActivity;
import in.shick.diode.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()
            .configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    private static final Pattern m_imgurRegex = Pattern.compile(
            "^https?:\\/\\/(?:i\\.|m\\.|edge\\.|www\\.)*imgur\\.com\\/(?:r\\/\\w+\\/)*(?!gallery)(?!removalrequest)(?!random)(?!memegen)((?:\\w{5}|\\w{7})(?:[&,](?:\\w{5}|\\w{7}))*)(?:#\\d+)?[a-z]?(\\.(?:jpe?g|gif|png|gifv))?(\\?.*)?$");

    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 (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);
    }

    /**
     * Helper function to display a list of URLs.
     * @param theContext The current application context.
     * @param settings The settings to use regarding the browser component.
     * @param theItem The ThingInfo item to get URLs from.
     */
    public static void showLinksDialog(final Context theContext, final RedditSettings settings,
            final ThingInfo theItem) {
        assert (theContext != null);
        assert (theItem != null);
        assert (settings != null);
        final ArrayList<String> urls = new ArrayList<String>();
        final ArrayList<MarkdownURL> vtUrls = theItem.getUrls();
        for (MarkdownURL vtUrl : vtUrls) {
            urls.add(vtUrl.url);
        }
        ArrayAdapter<MarkdownURL> adapter = new ArrayAdapter<MarkdownURL>(theContext,
                android.R.layout.select_dialog_item, vtUrls) {
            public View getView(int position, View convertView, ViewGroup parent) {
                TextView tv;
                if (convertView == null) {
                    tv = (TextView) ((LayoutInflater) theContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
                            .inflate(android.R.layout.select_dialog_item, null);
                } else {
                    tv = (TextView) convertView;
                }

                String url = getItem(position).url;
                String anchorText = getItem(position).anchorText;
                //                        if (Constants.LOGGING) Log.d(TAG, "links url="+url + " anchorText="+anchorText);

                Drawable d = null;
                try {
                    d = theContext.getPackageManager()
                            .getActivityIcon(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
                } catch (PackageManager.NameNotFoundException ignore) {
                }
                if (d != null) {
                    d.setBounds(0, 0, d.getIntrinsicHeight(), d.getIntrinsicHeight());
                    tv.setCompoundDrawablePadding(10);
                    tv.setCompoundDrawables(d, null, null, null);
                }

                final String telPrefix = "tel:";
                if (url.startsWith(telPrefix)) {
                    url = PhoneNumberUtils.formatNumber(url.substring(telPrefix.length()));
                }

                if (anchorText != null)
                    tv.setText(Html.fromHtml("<span>" + anchorText + "</span><br /><small>" + url + "</small>"));
                else
                    tv.setText(Html.fromHtml(url));

                return tv;
            }
        };

        AlertDialog.Builder b = new AlertDialog.Builder(
                new ContextThemeWrapper(theContext, settings.getDialogTheme()));

        DialogInterface.OnClickListener click = new DialogInterface.OnClickListener() {
            public final void onClick(DialogInterface dialog, int which) {
                if (which >= 0) {
                    Common.launchBrowser(settings, theContext, urls.get(which),
                            Util.createThreadUri(theItem).toString(), false, false, settings.isUseExternalBrowser(),
                            settings.isSaveHistory());
                }
            }
        };

        b.setTitle(R.string.select_link_title);
        b.setCancelable(true);
        b.setAdapter(adapter, click);

        b.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
            public final void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        });

        b.show();
    }

    /**
     * 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 settings The {@link RedditSettings} object used to for preference-retrieving.
     * @param context The {@link Context} object to use, as necessary.
     * @param url The URL to launch in the browser.
     * @param threadUrl The (optional) URL of the comments thread for the 'View comments' menu option in the browser.
     * @param requireNewTask set this to true if context is not an Activity
     * @param bypassParser Should URI parsing be bypassed, usually true in the case an external browser is being launched.
     * @param useExternalBrowser Should the external browser app be launched instead of the internal one.
     * @param saveHistory Should the URL be entered into the browser history?
     */
    public static void launchBrowser(RedditSettings settings, 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);
        }
        boolean forceDesktopUserAgent = false;
        if (!bypassParser && settings != null && settings.isLoadImgurImagesDirectly()) {
            Matcher m = m_imgurRegex.matcher(url);
            if (m.matches() && m.group(1) != null) {
                // We've determined it's an imgur link, no need to parse it further.
                bypassParser = true;
                url = "http://i.imgur.com/" + m.group(1);
                if (!StringUtils.isEmpty(m.group(2))) {
                    String extension = m.group(2);
                    if (".gifv".equalsIgnoreCase(extension)) {
                        extension = ".mp4";
                    }
                    url += extension;
                } else {
                    // Need to give images an extension, or imgur will redirect to the mobile site.
                    url += ".png";
                }
                forceDesktopUserAgent = true;
            }
        }
        if (settings != null && settings.isLoadVredditLinksDirectly()) {
            if (url.contains("v.redd.it")) {
                url += "/DASH_600_K";
            }
        }

        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 (forceDesktopUserAgent) {
                browser.putExtra(Constants.EXTRA_FORCE_UA_STRING, "desktop");
            }
            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);
    }
}