public static class DeferredCommentProcessing {
	public int commentIndex;
	public ThingInfo comment;
	public DeferredCommentProcessing(ThingInfo comment, int commentIndex) {
		this.comment = comment;
		this.commentIndex = commentIndex;
	}
} public class CacheInfo implements Serializable {
	static final long serialVersionUID = 39;
	static final String TAG = "CacheInfo";
	
	static final Object CACHE_LOCK = new Object();
	
	// timestamps for each cache
	public long subredditTime = 0;
	public long threadTime = 0;
	public long subredditListTime = 0;
	
	// the ids for the cached JSON objects
	public String subredditUrl = null;
	public String threadUrl = null;
	public ArrayList<String> subredditList = null;

	
	
	/**
	 * Copy the contents of an InputStream to cache file, then close the InputStream,
	 * and return a new FileInputStream to the just-written file.
	 * @param context
	 * @param in
	 * @param filename
	 * @return
	 * @throws IOException
	 */
	public static FileInputStream writeThenRead(Context context, InputStream in, String filename) throws IOException {
		synchronized (CACHE_LOCK) {
			FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE);
	    	byte[] buf = new byte[1024];
	    	int len = 0;
	    	long total = 0;  // for debugging
	    	while ((len = in.read(buf)) > 0) {
	    		fos.write(buf, 0, len);
	    		total += len;
	    	}
	    	if (Constants.LOGGING) Log.d(TAG, total + " bytes written to cache file: " + filename);
	    	fos.close();
	    	in.close();
    	}
    	
    	// return a new InputStream
   		return context.openFileInput(filename);
	}
	
	public static boolean checkFreshSubredditCache(Context context) {
    	long time = System.currentTimeMillis();
    	long subredditTime = getCachedSubredditTime(context);
		return time - subredditTime <= Constants.DEFAULT_FRESH_DURATION;
	}
    
    public static boolean checkFreshThreadCache(Context context) {
    	long time = System.currentTimeMillis();
    	long threadTime = getCachedThreadTime(context);
		return time - threadTime <= Constants.DEFAULT_FRESH_DURATION;
    }
    
    public static boolean checkFreshSubredditListCache(Context context) {
    	long time = System.currentTimeMillis();
    	long subredditListTime = getCachedSubredditListTime(context);
    	return time - subredditListTime <= Constants.DEFAULT_FRESH_SUBREDDIT_LIST_DURATION;
    }
    
    static CacheInfo getCacheInfo(Context context) throws IOException, ClassNotFoundException {
    	CacheInfo ci;
    	synchronized (CACHE_LOCK) {
	    	FileInputStream fis = context.openFileInput(Constants.FILENAME_CACHE_INFO);
	    	ObjectInputStream ois = new ObjectInputStream(fis);
	    	ci = (CacheInfo) ois.readObject();
	    	ois.close();
	    	fis.close();
    	}
    	return ci;
    }
    
    public static String getCachedSubredditUrl(Context context) {
    	try {
    		return getCacheInfo(context).subredditUrl;
    	} catch (Exception e) {
    		if (Constants.LOGGING) Log.e(TAG, "error w/ getCacheInfo", e);
    		return null;
    	}
    }
    
    static long getCachedSubredditTime(Context context) {
    	try {
    		return getCacheInfo(context).subredditTime;
    	} catch (Exception e) {
    		if (Constants.LOGGING) Log.e(TAG, "error w/ getCacheInfo", e);
    		return 0;
    	}
    }

    public static String getCachedThreadUrl(Context context) {
    	try {
    		return getCacheInfo(context).threadUrl;
    	} catch (Exception e) {
    		if (Constants.LOGGING) Log.e(TAG, "error w/ getCacheInfo", e);
    		return null;
    	}
    }
    
    static long getCachedThreadTime(Context context) {
    	try {
    		return getCacheInfo(context).threadTime;
    	} catch (Exception e) {
    		if (Constants.LOGGING) Log.e(TAG, "error w/ getCacheInfo", e);
    		return 0;
    	}
    }
    
    public static ArrayList<String> getCachedSubredditList(Context context) {
    	try {
    		return getCacheInfo(context).subredditList;
    	} catch (Exception e) {
    		if (Constants.LOGGING) Log.e(TAG, "error w/ getCacheInfo", e);
    		return null;
    	}
    }
    
    static long getCachedSubredditListTime(Context context) {
    	try {
    		return getCacheInfo(context).subredditListTime;
    	} catch (Exception e) {
    		if (Constants.LOGGING) Log.e(TAG, "error w/ getCacheInfo", e);
    		return 0;
    	}
    }
    
    @SuppressWarnings("unused")
	public
    static void invalidateAllCaches(Context context) {
    	if (!Constants.USE_COMMENTS_CACHE && !Constants.USE_THREADS_CACHE && !Constants.USE_SUBREDDITS_CACHE)
    		return;
    	
    	try {
    		synchronized (CACHE_LOCK) {
		    	FileOutputStream fos = context.openFileOutput(Constants.FILENAME_CACHE_INFO, Context.MODE_PRIVATE);
		    	ObjectOutputStream oos = new ObjectOutputStream(fos);
		    	oos.writeObject(new CacheInfo());
		    	oos.close();
		    	fos.close();
		    	if (Constants.LOGGING) Log.d(TAG, "invalidateAllCaches: wrote blank CacheInfo");
    		}
    	} catch (IOException e) {
    		if (Constants.LOGGING) Log.e(TAG, "invalidateAllCaches: Error writing CacheInfo", e);
    	}
    }
    
    @SuppressWarnings("unused")
	public
    static void invalidateCachedSubreddit(Context context) {
    	if (!Constants.USE_THREADS_CACHE)
    		return;
    	
    	CacheInfo ci = null;
    	try {
    		ci = getCacheInfo(context);
    	} catch (Exception e) {
    		if (Constants.LOGGING) Log.e(TAG, "error w/ getCacheInfo", e);
    	}
		if (ci == null)
			ci = new CacheInfo();
    	
    	try {
    		synchronized (CACHE_LOCK) {
		    	FileOutputStream fos = context.openFileOutput(Constants.FILENAME_CACHE_INFO, Context.MODE_PRIVATE);
		    	ObjectOutputStream oos = new ObjectOutputStream(fos);
		    	ci.subredditUrl = null;
		    	ci.subredditTime = 0;
		    	oos.writeObject(ci);
		    	oos.close();
		    	fos.close();
    		}
    	} catch (IOException e) {
    		if (Constants.LOGGING) Log.e(TAG, "invalidateCachedSubreddit: Error writing CacheInfo", e);
    	}
    }
    
    @SuppressWarnings("unused")
	public
    static void invalidateCachedThread(Context context) {
    	if (!Constants.USE_COMMENTS_CACHE)
    		return;
    	
    	CacheInfo ci = null;
    	try {
    		ci = getCacheInfo(context);
    	} catch (Exception e) {
    		if (Constants.LOGGING) Log.e(TAG, "error w/ getCacheInfo", e);
    	}
		if (ci == null)
			ci = new CacheInfo();
    	
    	try {
    		synchronized (CACHE_LOCK) {
		    	FileOutputStream fos = context.openFileOutput(Constants.FILENAME_CACHE_INFO, Context.MODE_PRIVATE);
		    	ObjectOutputStream oos = new ObjectOutputStream(fos);
		    	ci.threadUrl = null;
		    	ci.threadTime = 0;
		    	oos.writeObject(ci);
		    	oos.close();
		    	fos.close();
    		}
    	} catch (IOException e) {
    		if (Constants.LOGGING) Log.e(TAG, "invalidateCachedThreadId: Error writing CacheInfo", e);
    	}
    }
    
    @SuppressWarnings("unused")
	public
    static void setCachedSubredditUrl(Context context, String subredditUrl) throws IOException {
    	if (!Constants.USE_THREADS_CACHE)
    		return;
    	
    	CacheInfo ci = null;
    	try {
    		ci = getCacheInfo(context);
    	} catch (Exception e) {
    		if (Constants.LOGGING) Log.e(TAG, "error w/ getCacheInfo", e);
    	}
		if (ci == null)
			ci = new CacheInfo();
    	
		synchronized (CACHE_LOCK) {
			FileOutputStream fos = context.openFileOutput(Constants.FILENAME_CACHE_INFO, Context.MODE_PRIVATE);
	    	ObjectOutputStream oos = new ObjectOutputStream(fos);
	    	ci.subredditUrl = subredditUrl;
	    	ci.subredditTime = System.currentTimeMillis();
	    	oos.writeObject(ci);
	    	oos.close();
	    	fos.close();
		}
    }

    @SuppressWarnings("unused")
	public
    static void setCachedThreadUrl(Context context, String threadUrl) throws IOException {
    	if (!Constants.USE_COMMENTS_CACHE)
    		return;
    	
    	CacheInfo ci = null;
    	try {
    		ci = getCacheInfo(context);
    	} catch (Exception e) {
    		if (Constants.LOGGING) Log.e(TAG, "error w/ getCacheInfo", e);
    	}
		if (ci == null)
			ci = new CacheInfo();

		synchronized (CACHE_LOCK) {
			FileOutputStream fos = context.openFileOutput(Constants.FILENAME_CACHE_INFO, Context.MODE_PRIVATE);
	    	ObjectOutputStream oos = new ObjectOutputStream(fos);
	    	ci.threadUrl = threadUrl;
	    	ci.threadTime = System.currentTimeMillis();
	    	oos.writeObject(ci);
	    	oos.close();
	    	fos.close();
		}
    }
    
    public static void setCachedSubredditList(Context context, ArrayList<String> subredditList) throws IOException {
    	if (!Constants.USE_SUBREDDITS_CACHE)
    		return;
    	
    	CacheInfo ci = null;
    	try {
    		ci = getCacheInfo(context);
    	} catch (Exception e) {
    		if (Constants.LOGGING) Log.e(TAG, "error w/ getCacheInfo", e);
    	}
		if (ci == null)
			ci = new CacheInfo();

		synchronized (CACHE_LOCK) {
			FileOutputStream fos = context.openFileOutput(Constants.FILENAME_CACHE_INFO, Context.MODE_PRIVATE);
	    	ObjectOutputStream oos = new ObjectOutputStream(fos);
	    	ci.subredditList = subredditList;
	    	ci.subredditListTime = System.currentTimeMillis();
	    	oos.writeObject(ci);
	    	oos.close();
	    	fos.close();
		}
    }
} 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);
    }
} public class Constants {
	
	public static final boolean LOGGING = true;
	
	public static final boolean USE_COMMENTS_CACHE = false;
	public static final boolean USE_THREADS_CACHE = false;
	public static final boolean USE_SUBREDDITS_CACHE = true;
	
	// File containing the serialized variables of last subreddit viewed
	public static final String FILENAME_SUBREDDIT_CACHE = "subreddit.dat";
	// File containing the serialized variables of last comments viewed
	public static final String FILENAME_THREAD_CACHE = "thread.dat";
	// File containing a long integer System.currentTimeMillis(). Timestamp is shared among caches.
	public static final String FILENAME_CACHE_INFO = "cacheinfo.dat";
	public static final String[] FILENAMES_CACHE = {
		FILENAME_SUBREDDIT_CACHE, FILENAME_THREAD_CACHE, FILENAME_CACHE_INFO
	};
	
    public static final long MESSAGE_CHECK_MINIMUM_INTERVAL_MILLIS = 5 * 60 * 1000;  // 5 minutes
    public static final String LAST_MAIL_CHECK_TIME_MILLIS_KEY = "LAST_MAIL_CHECK_TIME_MILLIS_KEY";
	
	// 1:subreddit 2:threadId 3:commentId
	// The following commented-out one is good, but tough to get right, e.g.,
	// http://www.reddit.com/eorhm vs. http://www.reddit.com/prefs, mobile, store, etc.
	// So, for now require the captured URLs to have /comments or /tb prefix.
//	public static final String COMMENT_PATH_PATTERN_STRING
//		= "(?:/r/([^/]+)/comments|/comments|/tb)?/([^/]+)(?:/?$|/[^/]+/([a-zA-Z0-9]+)?)?";
	public static final String COMMENT_PATH_PATTERN_STRING
		= "(?:/r/([^/]+)/comments|/comments|/tb)/([^/]+)(?:/?$|/[^/]+/([a-zA-Z0-9]+)?)?";
	public static final String REDDIT_PATH_PATTERN_STRING = "(?:/r/([^/]+))?/?$";
	public static final String USER_PATH_PATTERN_STRING = "/user/([^/]+)/?$";
	
	public static final String COMMENT_KIND = "t1";
	public static final String THREAD_KIND = "t3";
	public static final String MESSAGE_KIND = "t4";
	public static final String SUBREDDIT_KIND = "t5";
	public static final String MORE_KIND = "more";
    
	public static final int DEFAULT_THREAD_DOWNLOAD_LIMIT = 25;
    public static final int DEFAULT_COMMENT_DOWNLOAD_LIMIT = 200;
    public static final long DEFAULT_FRESH_DURATION = 1800000;  // 30 minutes
    public static final long DEFAULT_FRESH_SUBREDDIT_LIST_DURATION = 86400000;  // 24 hours

    // startActivityForResult request codes
    public static final int ACTIVITY_PICK_SUBREDDIT = 0;
    public static final int ACTIVITY_SUBMIT_LINK = 1;
    
    // notifications
    public static final int NOTIFICATION_HAVE_MAIL = 0;
    
    // services
    public static final int SERVICE_ENVELOPE = 0;
    
    // --- Intent extras ---
    // Tell PickSubredditActivity to hide the fake subreddits string
    public static final String EXTRA_HIDE_FAKE_SUBREDDITS_STRING = "hideFakeSubreddits";
    public static final String EXTRA_ID = "id";
    // Tell CommentsListActivity to jump to a comment context (a URL. pattern match)
    public static final String EXTRA_COMMENT_CONTEXT = "jumpToComment";
    // Tell CommentsListActivity to show "more children"
    public static final String EXTRA_MORE_CHILDREN_ID = "moreChildrenId";
    public static final String EXTRA_NUM_COMMENTS = "num_comments";
    public static final String EXTRA_SUBREDDIT = "subreddit";
    public static final String EXTRA_THREAD_URL = "thread_url";
    public static final String EXTRA_TITLE = "title";
    
    // User-defined result codes
    public static final int RESULT_LOGIN_REQUIRED = Activity.RESULT_FIRST_USER;
    
    // Menu and dialog actions
    public static final int DIALOG_LOGIN = 2;
    public static final int DIALOG_LOGOUT = 3;
    public static final int DIALOG_THEME = 12;
    public static final int DIALOG_REPLY = 14;
    public static final int DIALOG_HIDE_COMMENT = 17;
    public static final int DIALOG_SHOW_COMMENT = 18;
    public static final int DIALOG_SORT_BY = 20;
    public static final int DIALOG_SORT_BY_NEW = 21;
    public static final int DIALOG_SORT_BY_CONTROVERSIAL = 22;
    public static final int DIALOG_SORT_BY_TOP = 23;
    public static final int DIALOG_COMMENT_CLICK = 24;
    public static final int DIALOG_MESSAGE_CLICK = 25;
    public static final int DIALOG_GOTO_PARENT = 28;
    public static final int DIALOG_EDIT = 29;
    public static final int DIALOG_DELETE = 30;
    public static final int DIALOG_COMPOSE = 31;
    public static final int DIALOG_FIND = 32;
    public static final int DIALOG_REPORT = 33;
    public static final int DIALOG_THREAD_CLICK = 34;
    public static final int DIALOG_VIEW_PROFILE = 35;

    // progress dialogs
    public static final int DIALOG_LOGGING_IN = 1000;
    public static final int DIALOG_SUBMITTING = 1004;
    public static final int DIALOG_REPLYING = 1005;
    public static final int DIALOG_LOADING_REDDITS_LIST = 1006;
    public static final int DIALOG_DELETING = 1008;
    public static final int DIALOG_EDITING = 1009;
    public static final int DIALOG_COMPOSING = 1012;
    
	public static final int SHARE_CONTEXT_ITEM = 1013;
	public static final int OPEN_IN_BROWSER_CONTEXT_ITEM = 1014;
	public static final int OPEN_COMMENTS_CONTEXT_ITEM = 1015;
	public static final int SAVE_CONTEXT_ITEM = 1016;
	public static final int UNSAVE_CONTEXT_ITEM = 1017;
	public static final int HIDE_CONTEXT_ITEM = 1018;
	public static final int UNHIDE_CONTEXT_ITEM = 1019;
	public static final int VIEW_SUBREDDIT_CONTEXT_ITEM = 1020;

    
    // Special CSS for webviews to match themes
    public static final String CSS_DARK = "<style>body{color:#c0c0c0;background-color:#000000}a:link{color:#ffffff}</style>";

    // Colors for markdown
    public static final int MARKDOWN_LINK_COLOR = 0xff2288cc;
    
    // States for StateListDrawables
    public static final int[] STATE_CHECKED = new int[]{android.R.attr.state_checked};
    public static final int[] STATE_NONE = new int[0];
    
    // Strings
    public static final String NO_STRING = "no";
    
    public static final String FRONTPAGE_STRING = "reddit front page";
    
    public static final String HAVE_MAIL_TICKER = "reddit mail";
    public static final String HAVE_MAIL_TITLE = "reddit is fun";
    public static final String HAVE_MAIL_TEXT = "You have reddit mail.";
    
    // save instance state Bundle keys
    public static final String AFTER_KEY = "after";
    public static final String BEFORE_KEY = "before";
    public static final String DELETE_TARGET_KIND_KEY = "delete_target_kind";
    public static final String EDIT_TARGET_BODY_KEY = "edit_target_body";
    public static final String ID_KEY = "id";
    public static final String JUMP_TO_THREAD_ID_KEY = "jump_to_thread_id";
    public static final String KARMA_KEY = "karma";
    public static final String LAST_AFTER_KEY = "last_after";
    public static final String LAST_BEFORE_KEY = "last_before";
    public static final String REPORT_TARGET_NAME_KEY = "report_target_name";
    public static final String REPLY_TARGET_NAME_KEY = "reply_target_name";
    public static final String SUBREDDIT_KEY = "subreddit";
    public static final String THREAD_COUNT_KEY = "thread_count";
    public static final String THREAD_ID_KEY = "thread_id";
    public static final String THREAD_LAST_COUNT_KEY = "last_thread_count";
    public static final String THREAD_TITLE_KEY = "thread_title";
    public static final String USERNAME_KEY = "username";
    public static final String VOTE_TARGET_THING_INFO_KEY = "vote_target_thing_info";
    public static final String WHICH_INBOX_KEY = "which_inbox";
    
    public static final String SUBMIT_KIND_LINK = "link";
    public static final String SUBMIT_KIND_SELF = "self";
    public static final String SUBMIT_KIND_POLL = "poll";
    
    // Sorting things
    public static final class ThreadsSort {
	    public static final String SORT_BY_KEY = "threads_sort_by";
	    public static final String SORT_BY_HOT = "hot";
	    public static final String SORT_BY_NEW = "new";
	    public static final String SORT_BY_CONTROVERSIAL = "controversial";
	    public static final String SORT_BY_TOP = "top";
	    public static final String SORT_BY_HOT_URL = "";
	    public static final String SORT_BY_NEW_URL = "new/";
	    public static final String SORT_BY_CONTROVERSIAL_URL = "controversial/";
	    public static final String SORT_BY_TOP_URL = "top/";
	    public static final String[] SORT_BY_CHOICES = {SORT_BY_HOT, SORT_BY_NEW, SORT_BY_CONTROVERSIAL, SORT_BY_TOP};
	    public static final String[] SORT_BY_URL_CHOICES = {SORT_BY_HOT_URL, SORT_BY_NEW_URL, SORT_BY_CONTROVERSIAL_URL, SORT_BY_TOP_URL};
	    public static final String SORT_BY_NEW_NEW = "new";
	    public static final String SORT_BY_NEW_RISING = "rising";
	    public static final String SORT_BY_NEW_NEW_URL = "sort=new";
	    public static final String SORT_BY_NEW_RISING_URL = "sort=rising";
	    public static final String[] SORT_BY_NEW_CHOICES = {SORT_BY_NEW_NEW, SORT_BY_NEW_RISING};
	    public static final String[] SORT_BY_NEW_URL_CHOICES = {SORT_BY_NEW_NEW_URL, SORT_BY_NEW_RISING_URL};
	    public static final String SORT_BY_CONTROVERSIAL_HOUR = "this hour";
	    public static final String SORT_BY_CONTROVERSIAL_DAY = "today";
	    public static final String SORT_BY_CONTROVERSIAL_WEEK = "this week";
	    public static final String SORT_BY_CONTROVERSIAL_MONTH = "this month";
	    public static final String SORT_BY_CONTROVERSIAL_YEAR = "this year";
	    public static final String SORT_BY_CONTROVERSIAL_ALL = "all time";
	    public static final String SORT_BY_CONTROVERSIAL_HOUR_URL = "t=hour";
	    public static final String SORT_BY_CONTROVERSIAL_DAY_URL = "t=day";
	    public static final String SORT_BY_CONTROVERSIAL_WEEK_URL = "t=week";
	    public static final String SORT_BY_CONTROVERSIAL_MONTH_URL = "t=month";
	    public static final String SORT_BY_CONTROVERSIAL_YEAR_URL = "t=year";
	    public static final String SORT_BY_CONTROVERSIAL_ALL_URL = "t=all";
	    public static final String[] SORT_BY_CONTROVERSIAL_CHOICES = {SORT_BY_CONTROVERSIAL_HOUR, SORT_BY_CONTROVERSIAL_DAY,
	    	SORT_BY_CONTROVERSIAL_WEEK, SORT_BY_CONTROVERSIAL_MONTH, SORT_BY_CONTROVERSIAL_YEAR, SORT_BY_CONTROVERSIAL_ALL};
	    public static final String[] SORT_BY_CONTROVERSIAL_URL_CHOICES = {SORT_BY_CONTROVERSIAL_HOUR_URL, SORT_BY_CONTROVERSIAL_DAY_URL,
	    	SORT_BY_CONTROVERSIAL_WEEK_URL, SORT_BY_CONTROVERSIAL_MONTH_URL, SORT_BY_CONTROVERSIAL_YEAR_URL, SORT_BY_CONTROVERSIAL_ALL_URL};
	    public static final String SORT_BY_TOP_HOUR = "this hour";
	    public static final String SORT_BY_TOP_DAY = "today";
	    public static final String SORT_BY_TOP_WEEK = "this week";
	    public static final String SORT_BY_TOP_MONTH = "this month";
	    public static final String SORT_BY_TOP_YEAR = "this year";
	    public static final String SORT_BY_TOP_ALL = "all time";
	    public static final String SORT_BY_TOP_HOUR_URL = "t=hour";
	    public static final String SORT_BY_TOP_DAY_URL = "t=day";
	    public static final String SORT_BY_TOP_WEEK_URL = "t=week";
	    public static final String SORT_BY_TOP_MONTH_URL = "t=month";
	    public static final String SORT_BY_TOP_YEAR_URL = "t=year";
	    public static final String SORT_BY_TOP_ALL_URL = "t=all";
	    public static final String[] SORT_BY_TOP_CHOICES = {SORT_BY_TOP_HOUR, SORT_BY_TOP_DAY,
	    	SORT_BY_TOP_WEEK, SORT_BY_TOP_MONTH, SORT_BY_TOP_YEAR, SORT_BY_TOP_ALL};
	    public static final String[] SORT_BY_TOP_URL_CHOICES = {SORT_BY_TOP_HOUR_URL, SORT_BY_TOP_DAY_URL,
	    	SORT_BY_TOP_WEEK_URL, SORT_BY_TOP_MONTH_URL, SORT_BY_TOP_YEAR_URL, SORT_BY_TOP_ALL_URL};
    }
    public static final class CommentsSort {
	    public static final String SORT_BY_KEY = "comments_sort_by";
	    public static final String SORT_BY_BEST = "best";
	    public static final String SORT_BY_HOT = "hot";
	    public static final String SORT_BY_NEW = "new";
	    public static final String SORT_BY_CONTROVERSIAL = "controversial";
	    public static final String SORT_BY_TOP = "top";
	    public static final String SORT_BY_OLD = "old";
	    public static final String SORT_BY_BEST_URL = "sort=confidence";
	    public static final String SORT_BY_HOT_URL = "sort=hot";
	    public static final String SORT_BY_NEW_URL = "sort=new";
	    public static final String SORT_BY_CONTROVERSIAL_URL = "sort=controversial";
	    public static final String SORT_BY_TOP_URL = "sort=top";
	    public static final String SORT_BY_OLD_URL = "sort=old";
	    public static final String[] SORT_BY_CHOICES =
	    	{SORT_BY_BEST, SORT_BY_HOT, SORT_BY_NEW,
	    	SORT_BY_CONTROVERSIAL, SORT_BY_TOP, SORT_BY_OLD};
	    public static final String[] SORT_BY_URL_CHOICES =
	    	{SORT_BY_BEST_URL, SORT_BY_HOT_URL, SORT_BY_NEW_URL,
	    	SORT_BY_CONTROVERSIAL_URL, SORT_BY_TOP_URL, SORT_BY_OLD_URL};
    }
    
    
    // JSON values
    public static final String JSON_AFTER = "after";
    public static final String JSON_AUTHOR = "author";
    public static final String JSON_BEFORE = "before";
    public static final String JSON_BODY = "body";
    public static final String JSON_CHILDREN = "children";
    public static final String JSON_DATA = "data";
    public static final String JSON_ERRORS = "errors";
    public static final String JSON_JSON = "json";
    public static final String JSON_KIND = "kind";
    public static final String JSON_LISTING = "Listing";
    public static final String JSON_MEDIA = "media";
    public static final String JSON_MEDIA_EMBED = "media_embed";
    public static final String JSON_MODHASH = "modhash";
    public static final String JSON_NEW = "new";
    public static final String JSON_NUM_COMMENTS = "num_comments";
    public static final String JSON_TITLE = "title";
    public static final String JSON_SUBREDDIT = "subreddit";
	public static final String JSON_REPLIES = "replies";
	public static final String JSON_SELFTEXT = "selftext";
	public static final String JSON_SELFTEXT_HTML = "selftext_html";
	public static final String JSON_SUBJECT = "subject";
    
    // TabSpec tags
    public static final String TAB_LINK = "tab_link";
    public static final String TAB_TEXT = "tab_text";
    
    // Preference keys and values
    public static final String PREF_HOMEPAGE = "homepage";
    public static final String PREF_USE_EXTERNAL_BROWSER = "use_external_browser";
    public static final String PREF_CONFIRM_QUIT = "confirm_quit";
    public static final String PREF_SAVE_HISTORY = "save_history";
    public static final String PREF_ALWAYS_SHOW_NEXT_PREVIOUS = "always_show_next_previous";
    public static final String PREF_COMMENTS_SORT_BY_URL = "sort_by_url";
    public static final String PREF_THEME = "theme";
    public static final String PREF_THEME_LIGHT = "THEME_LIGHT";
    public static final String PREF_THEME_DARK	 = "THEME_DARK";
    public static final String PREF_TEXT_SIZE = "text_size";
    public static final String PREF_TEXT_SIZE_MEDIUM = "TEXT_SIZE_MEDIUM";
    public static final String PREF_TEXT_SIZE_LARGE = "TEXT_SIZE_LARGE";
    public static final String PREF_TEXT_SIZE_LARGER = "TEXT_SIZE_LARGER";
    public static final String PREF_TEXT_SIZE_HUGE = "TEXT_SIZE_HUGE";
    public static final String PREF_SHOW_COMMENT_GUIDE_LINES = "show_comment_guide_lines";
    public static final String PREF_ROTATION = "rotation";
    public static final String PREF_ROTATION_UNSPECIFIED = "ROTATION_UNSPECIFIED";
    public static final String PREF_ROTATION_PORTRAIT = "ROTATION_PORTRAIT";
    public static final String PREF_ROTATION_LANDSCAPE = "ROTATION_LANDSCAPE";
    public static final String PREF_LOAD_THUMBNAILS = "load_thumbnails";
    public static final String PREF_LOAD_THUMBNAILS_ONLY_WIFI = "load_thumbnails_only_wifi";
    public static final String PREF_MAIL_NOTIFICATION_STYLE = "mail_notification_style";
    public static final String PREF_MAIL_NOTIFICATION_STYLE_DEFAULT = "MAIL_NOTIFICATION_STYLE_DEFAULT";
    public static final String PREF_MAIL_NOTIFICATION_STYLE_BIG_ENVELOPE = "MAIL_NOTIFICATION_STYLE_BIG_ENVELOPE";
    public static final String PREF_MAIL_NOTIFICATION_STYLE_OFF = "MAIL_NOTIFICATION_STYLE_OFF";
    public static final String PREF_MAIL_NOTIFICATION_SERVICE = "mail_notification_service";
    public static final String PREF_MAIL_NOTIFICATION_SERVICE_OFF = "MAIL_NOTIFICATION_SERVICE_OFF";
    public static final String PREF_MAIL_NOTIFICATION_SERVICE_5MIN = "MAIL_NOTIFICATION_SERVICE_5MIN";
    public static final String PREF_MAIL_NOTIFICATION_SERVICE_30MIN = "MAIL_NOTIFICATION_SERVICE_30MIN";
    public static final String PREF_MAIL_NOTIFICATION_SERVICE_1HOUR = "MAIL_NOTIFICATION_SERVICE_1HOUR";
    public static final String PREF_MAIL_NOTIFICATION_SERVICE_6HOURS = "MAIL_NOTIFICATION_SERVICE_6HOURS";
    public static final String PREF_MAIL_NOTIFICATION_SERVICE_1DAY = "MAIL_NOTIFICATION_SERVICE_1DAY";
    
    // Reddit's base URL, without trailing slash
    public static final String REDDIT_BASE_URL = "http://www.reddit.com";
    public static final String REDDIT_SSL_BASE_URL = "https://pay.reddit.com";
	public static final String REDDIT_LOGIN_URL = "https://ssl.reddit.com/api/login";
	
    // A short HTML file returned by reddit, so we can get the modhash
    public static final String MODHASH_URL = REDDIT_BASE_URL + "/r";
} public class ProgressInputStream extends FilterInputStream {
    private final PropertyChangeSupport propertyChangeSupport;
    private final long maxNumBytes;
    private volatile long totalNumBytesRead;

    public ProgressInputStream(InputStream in, long maxNumBytes) {
        super(in);
        this.propertyChangeSupport = new PropertyChangeSupport(this);
        this.maxNumBytes = maxNumBytes;
    }

    public long getMaxNumBytes() {
        return maxNumBytes;
    }

    public long getTotalNumBytesRead() {
        return totalNumBytesRead;
    }

    public void addPropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.addPropertyChangeListener(l);
    }

    public void removePropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.removePropertyChangeListener(l);
    }

    @Override
    public int read() throws IOException {
        return (int)updateProgress(super.read());
    }

    @Override
    public int read(byte[] b) throws IOException {
        return (int)updateProgress(super.read(b));
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        return (int)updateProgress(super.read(b, off, len));
    }

    @Override
    public long skip(long n) throws IOException {
        return updateProgress(super.skip(n));
    }

    @Override
    public void mark(int readlimit) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void reset() throws IOException {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean markSupported() {
        return false;
    }

    private long updateProgress(long numBytesRead) {
        if (numBytesRead > 0) {
            long oldTotalNumBytesRead = this.totalNumBytesRead;
            this.totalNumBytesRead += numBytesRead;
            propertyChangeSupport.firePropertyChange("totalNumBytesRead", oldTotalNumBytesRead, this.totalNumBytesRead);
        }

        return numBytesRead;
    }
} public class StringUtils {

	public static boolean isEmpty(CharSequence s) {
		return s == null || "".equals(s);
	}

	public static boolean listContainsIgnoreCase(ArrayList<String> list, String str){
		for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
			String string = (String) iterator.next();
			
			if(string.equalsIgnoreCase(str))
				return true;
		}
		return false;
	}

} public class Util {
	
	private static final String TAG = "Util";
	
	public static ArrayList<String> extractUris(URLSpan[] spans) {
        int size = spans.length;
        ArrayList<String> accumulator = new ArrayList<String>();

        for (int i = 0; i < size; i++) {
            accumulator.add(spans[i].getURL());
        }
        return accumulator;
    }
	
	/**
	 * Convert HTML tags so they will be properly recognized by
	 * android.text.Html.fromHtml()
	 * @param html unescaped HTML
	 * @return converted HTML
	 */
	public static String convertHtmlTags(String html) {
		// Handle <code>
		html = html.replaceAll("<code>", "<tt>").replaceAll("</code>", "</tt>");
		
		// Handle <pre>
		int preIndex = html.indexOf("<pre>");
		int preEndIndex = -6;  // -"</pre>".length()
		StringBuilder bodyConverted = new StringBuilder();
		while (preIndex != -1) {
			// get the text between previous </pre> and next <pre>.
			bodyConverted = bodyConverted.append(html.substring(preEndIndex + 6, preIndex));
			preEndIndex = html.indexOf("</pre>", preIndex);
			// Replace newlines with <br> inside the <pre></pre>
			// Retain <pre> tags since android.text.Html.fromHtml() will ignore them anyway.
			bodyConverted = bodyConverted.append(html.substring(preIndex, preEndIndex).replaceAll("\n", "<br>"))
				.append("</pre>");
			preIndex = html.indexOf("<pre>", preEndIndex);
		}
		html = bodyConverted.append(html.substring(preEndIndex + 6)).toString();
		
		// Handle <li>
		html = html.replaceAll("<li>(<p>)?", "&#8226; ")
		           .replaceAll("(</p>)?</li>", "<br>");
		
		// Handle <strong> and <em>, which are normally <b> and <i> respectively, but reversed in Android.
		// ANDROID BUG: http://code.google.com/p/android/issues/detail?id=3473
		html = html.replaceAll("<strong>", "<b>").replaceAll("</strong>", "</b>")
		           .replaceAll("<em>", "<i>").replaceAll("</em>", "</i>");
		
		return html;
	}
	
	/**
	 * To the second, not millisecond like reddit
	 * @param timeSeconds
	 * @return
	 */
	public static String getTimeAgo(long utcTimeSeconds) {
		long systime = System.currentTimeMillis() / 1000;
		long diff = systime - utcTimeSeconds;
		if (diff <= 0)
			return "very recently";
		else if (diff < 60) {
			if (diff == 1)
				return "1 second ago";
			else
				return diff + " seconds ago";
		}
		else if (diff < 3600) {
			if ((diff / 60) == 1)
				return "1 minute ago";
			else
				return (diff / 60) + " minutes ago";
		}
		else if (diff < 86400) { // 86400 seconds in a day
			if ((diff / 3600) == 1)
				return "1 hour ago";
			else
				return (diff / 3600) + " hours ago";
		}
		else if (diff < 604800) { // 86400 * 7
			if ((diff / 86400) == 1)
				return "1 day ago";
			else
				return (diff / 86400) + " days ago";
		}
		else if (diff < 2592000) { // 86400 * 30
			if ((diff / 604800) == 1)
				return "1 week ago";
			else
				return (diff / 604800) + " weeks ago";
		}
		else if (diff < 31536000) { // 86400 * 365
			if ((diff / 2592000) == 1)
				return "1 month ago";
			else
				return (diff / 2592000) + " months ago";
		}
		else {
			if ((diff / 31536000) == 1)
				return "1 year ago";
			else
				return (diff / 31536000) + " years ago";
		}
	}
	
	public static String getTimeAgo(double utcTimeSeconds) {
		return getTimeAgo((long)utcTimeSeconds);
	}
	
	public static String showNumComments(int comments) {
		if (comments == 1) {
			return "1 comment";
		} else {
			return comments + " comments";
		}
	}
	
	public static String showNumPoints(int score) {
		if (score == 1) {
			return "1 point";
		} else {
			return score + " points";
		}
	}
	
	public static String absolutePathToURL(String path) {
		if (path.startsWith("/"))
			return Constants.REDDIT_BASE_URL + path;
		return path;
	}
	
	public static String nameToId(String name) {
		// indexOf('_') == -1 if not found; -1 + 1 == 0
		return name.substring(name.indexOf('_') + 1);
	}
	
	public static boolean isHttpStatusOK(HttpResponse response) {
		if (response == null || response.getStatusLine() == null) {
			return false;
		}
		return response.getStatusLine().getStatusCode() == 200;
	}
	
	public static String getResponseErrorMessage(String line) throws Exception{
    	String error = null;
		
		if (StringUtils.isEmpty(line)) {
			error = "Connection error when subscribing. Try again.";
    		throw new HttpException("No content returned from subscribe POST");
    	}
    	if (line.contains("WRONG_PASSWORD")) {
    		error = "Wrong password.";
    		throw new Exception("Wrong password.");
    	}
    	if (line.contains("USER_REQUIRED")) {
    		// The modhash probably expired
    		throw new Exception("User required. Huh?");
    	}
    	
    	Common.logDLong(TAG, line);
		return error;
	}

	// ===============
	//      Theme
	// ===============

	public static boolean isLightTheme(int theme) {
		return theme == R.style.Reddit_Light_Medium || theme == R.style.Reddit_Light_Large || theme == R.style.Reddit_Light_Larger || theme == R.style.Reddit_Light_Huge;
	}
	
	public static boolean isDarkTheme(int theme) {
		return theme == R.style.Reddit_Dark_Medium || theme == R.style.Reddit_Dark_Large || theme == R.style.Reddit_Dark_Larger || theme == R.style.Reddit_Dark_Huge;
	}
	
	public static int getInvertedTheme(int theme) {
		switch (theme) {
		case R.style.Reddit_Light_Medium:
			return R.style.Reddit_Dark_Medium;
		case R.style.Reddit_Light_Large:
			return R.style.Reddit_Dark_Large;
		case R.style.Reddit_Light_Larger:
			return R.style.Reddit_Dark_Larger;
		case R.style.Reddit_Light_Huge:
			return R.style.Reddit_Dark_Huge;
		case R.style.Reddit_Dark_Medium:
			return R.style.Reddit_Light_Medium;
		case R.style.Reddit_Dark_Large:
			return R.style.Reddit_Light_Large;
		case R.style.Reddit_Dark_Larger:
			return R.style.Reddit_Light_Larger;
		case R.style.Reddit_Dark_Huge:
			return R.style.Reddit_Light_Huge;
		default:
			return R.style.Reddit_Light_Medium;	
		}
	}
	
	public static int getThemeResourceFromPrefs(String themePref, String textSizePref) {
		if (Constants.PREF_THEME_LIGHT.equals(themePref)) {
			if (Constants.PREF_TEXT_SIZE_MEDIUM.equals(textSizePref))
				return R.style.Reddit_Light_Medium;
			else if (Constants.PREF_TEXT_SIZE_LARGE.equals(textSizePref))
				return R.style.Reddit_Light_Large;
			else if (Constants.PREF_TEXT_SIZE_LARGER.equals(textSizePref))
				return R.style.Reddit_Light_Larger;
			else if (Constants.PREF_TEXT_SIZE_HUGE.equals(textSizePref))
				return R.style.Reddit_Light_Huge;
		} else /* if (Constants.PREF_THEME_DARK.equals(themePref)) */ {
			if (Constants.PREF_TEXT_SIZE_MEDIUM.equals(textSizePref))
				return R.style.Reddit_Dark_Medium;
			else if (Constants.PREF_TEXT_SIZE_LARGE.equals(textSizePref))
				return R.style.Reddit_Dark_Large;
			else if (Constants.PREF_TEXT_SIZE_LARGER.equals(textSizePref))
				return R.style.Reddit_Dark_Larger;
			else if (Constants.PREF_TEXT_SIZE_HUGE.equals(textSizePref))
				return R.style.Reddit_Dark_Huge;
		}
		return R.style.Reddit_Light_Medium;
	}
	
	/**
	 * Return the theme and textSize String prefs
	 */
	public static String[] getPrefsFromThemeResource(int theme) {
		switch (theme) {
		case R.style.Reddit_Light_Medium:
			return new String[] { Constants.PREF_THEME_LIGHT, Constants.PREF_TEXT_SIZE_MEDIUM };
		case R.style.Reddit_Light_Large:
			return new String[] { Constants.PREF_THEME_LIGHT, Constants.PREF_TEXT_SIZE_LARGE };
		case R.style.Reddit_Light_Larger:
			return new String[] { Constants.PREF_THEME_LIGHT, Constants.PREF_TEXT_SIZE_LARGER };
		case R.style.Reddit_Light_Huge:
			return new String[] { Constants.PREF_THEME_LIGHT, Constants.PREF_TEXT_SIZE_HUGE };
		case R.style.Reddit_Dark_Medium:
			return new String[] { Constants.PREF_THEME_DARK, Constants.PREF_TEXT_SIZE_MEDIUM };
		case R.style.Reddit_Dark_Large:
			return new String[] { Constants.PREF_THEME_DARK, Constants.PREF_TEXT_SIZE_LARGE };
		case R.style.Reddit_Dark_Larger:
			return new String[] { Constants.PREF_THEME_DARK, Constants.PREF_TEXT_SIZE_LARGER };
		case R.style.Reddit_Dark_Huge:
			return new String[] { Constants.PREF_THEME_DARK, Constants.PREF_TEXT_SIZE_HUGE };
		default:
			return new String[] { Constants.PREF_THEME_LIGHT, Constants.PREF_TEXT_SIZE_MEDIUM };
		}
	}
	
	public static int getTextAppearanceResource(int themeResource, int androidTextAppearanceStyle) {
		switch (themeResource) {
		case R.style.Reddit_Light_Medium:
		case R.style.Reddit_Dark_Medium:
			switch (androidTextAppearanceStyle) {
			case android.R.style.TextAppearance_Small:
				return R.style.TextAppearance_Medium_Small;
			case android.R.style.TextAppearance_Medium:
				return R.style.TextAppearance_Medium_Medium;
			case android.R.style.TextAppearance_Large:
				return R.style.TextAppearance_Medium_Large;
			default:
				return R.style.TextAppearance_Medium_Medium;
			}
		case R.style.Reddit_Light_Large:
		case R.style.Reddit_Dark_Large:
			switch (androidTextAppearanceStyle) {
			case android.R.style.TextAppearance_Small:
				return R.style.TextAppearance_Large_Small;
			case android.R.style.TextAppearance_Medium:
				return R.style.TextAppearance_Large_Medium;
			case android.R.style.TextAppearance_Large:
				return R.style.TextAppearance_Large_Large;
			default:
				return R.style.TextAppearance_Large_Medium;
			}
		case R.style.Reddit_Light_Larger:
		case R.style.Reddit_Dark_Larger:
			switch (androidTextAppearanceStyle) {
			case android.R.style.TextAppearance_Small:
				return R.style.TextAppearance_Larger_Small;
			case android.R.style.TextAppearance_Medium:
				return R.style.TextAppearance_Larger_Medium;
			case android.R.style.TextAppearance_Large:
				return R.style.TextAppearance_Larger_Large;
			default:
				return R.style.TextAppearance_Larger_Medium;
			}
		case R.style.Reddit_Light_Huge: 
		case R.style.Reddit_Dark_Huge:
			switch (androidTextAppearanceStyle) {
			case android.R.style.TextAppearance_Small:
				return R.style.TextAppearance_Huge_Small;
			case android.R.style.TextAppearance_Medium:
				return R.style.TextAppearance_Huge_Medium;
			case android.R.style.TextAppearance_Large:
				return R.style.TextAppearance_Huge_Large;
			default:
				return R.style.TextAppearance_Huge_Medium;
			}
		default:
			return R.style.TextAppearance_Medium_Medium;	
		}
	}
	
	
	// =======================
	//    Mail Notification
	// =======================
	
	public static long getMillisFromMailNotificationPref(String pref) {
		if (Constants.PREF_MAIL_NOTIFICATION_SERVICE_OFF.equals(pref)) {
			return 0;
		} else if (Constants.PREF_MAIL_NOTIFICATION_SERVICE_5MIN.equals(pref)) {
			return 5 * 60 * 1000;
		} else if (Constants.PREF_MAIL_NOTIFICATION_SERVICE_30MIN.equals(pref)) {
			return 30 * 60 * 1000;
		} else if (Constants.PREF_MAIL_NOTIFICATION_SERVICE_1HOUR.equals(pref)) {
			return 1 * 3600 * 1000;
		} else if (Constants.PREF_MAIL_NOTIFICATION_SERVICE_6HOURS.equals(pref)) {
			return 6 * 3600 * 1000;
		} else /* if (Constants.PREF_MAIL_NOTIFICATION_SERVICE_1DAY.equals(pref)) */ {
			return 24 * 3600 * 1000;
		}
	}
	
	
	// ===============
	//   Transitions
	// ===============
	
	public static void overridePendingTransition(Method activity_overridePendingTransition, Activity act, int enterAnim, int exitAnim) {
		// only available in Android 2.0 (SDK Level 5) and later
		if (activity_overridePendingTransition != null) {
			try {
				activity_overridePendingTransition.invoke(act, enterAnim, exitAnim);
			} catch (Exception ex) {
				if (Constants.LOGGING) Log.e(TAG, "overridePendingTransition", ex);
			}
		}
	}
	
	// ===============
	//       Uri
	// ===============
	
	static Uri createCommentUri(String linkId, String commentId, int commentContext) {
		return Uri.parse(new StringBuilder(Constants.REDDIT_BASE_URL + "/comments/")
			.append(linkId)
			.append("/z/")
			.append(commentId)
			.append("?context=")
			.append(commentContext)
			.toString());
	}
	
	public static Uri createCommentUri(ThingInfo commentThingInfo, int commentContext) {
		if (commentThingInfo.getContext() != null)
			return Uri.parse(absolutePathToURL(commentThingInfo.getContext()));
		if (commentThingInfo.getLink_id() != null)
			return createCommentUri(nameToId(commentThingInfo.getLink_id()), commentThingInfo.getId(), commentContext);
		return null;
	}
	
	public static Uri createProfileUri(String username) {
		return Uri.parse(new StringBuilder(Constants.REDDIT_BASE_URL + "/user/")
			.append(username)
			.toString());
	}
	
	public static Uri createSubmitUri(String subreddit) {
		if (Constants.FRONTPAGE_STRING.equals(subreddit))
			return Uri.parse(Constants.REDDIT_BASE_URL + "/submit");
		
		return Uri.parse(new StringBuilder(Constants.REDDIT_BASE_URL + "/r/")
			.append(subreddit)
			.append("/submit")
			.toString());
	}
	
	static Uri createSubmitUri(ThingInfo thingInfo) {
		return createSubmitUri(thingInfo.getSubreddit());
	}
	
	public static Uri createSubredditUri(String subreddit) {
		if (Constants.FRONTPAGE_STRING.equals(subreddit))
			return Uri.parse(Constants.REDDIT_BASE_URL + "/");
		
		return Uri.parse(new StringBuilder(Constants.REDDIT_BASE_URL + "/r/")
			.append(subreddit)
			.toString());
	}
	
	static Uri createSubredditUri(ThingInfo thingInfo) {
		return createSubredditUri(thingInfo.getSubreddit());
	}
	
	static Uri createThreadUri(String subreddit, String threadId) {
		return Uri.parse(new StringBuilder(Constants.REDDIT_BASE_URL + "/r/")
			.append(subreddit)
			.append("/comments/")
			.append(threadId)
			.toString());
	}
	
	public static Uri createThreadUri(ThingInfo threadThingInfo) {
		return createThreadUri(threadThingInfo.getSubreddit(), threadThingInfo.getId());
	}
	
	public static boolean isRedditUri(Uri uri) {
		if (uri == null) return false;
		String host = uri.getHost();
		return host != null && (host.equals("reddit.com") || host.endsWith(".reddit.com"));
	}
	
	public static boolean isRedditShortenedUri(Uri uri) {
		if (uri == null) return false;
		String host = uri.getHost();
		return host != null && host.equals("redd.it");
	}
	
    /**
     * Creates mobile version of <code>uri</code> if applicable.
     * 
     * @return original uri if no mobile version of uri is known
     */
    public static Uri optimizeMobileUri(Uri uri) {
    	if (isNonMobileWikipediaUri(uri)) {
    		uri = createMobileWikpediaUri(uri);
    	}
    	return uri;
    }
    
    /**
     * @return if uri points to a non-mobile wikpedia uri.
     */
    static boolean isNonMobileWikipediaUri(Uri uri) {
    	if (uri == null) return false;
    	String host = uri.getHost();
    	return host != null && host.endsWith(".wikipedia.org") && !host.contains(".m.wikipedia.org");
    }
    
    /**
     * @return mobile version of a wikipedia uri
     */
    static Uri createMobileWikpediaUri(Uri uri) {
    	String uriString = uri.toString();
    	return Uri.parse(uriString.replace(".wikipedia.org/", ".m.wikipedia.org/"));
    }
    
    public static boolean isYoutubeUri(Uri uri) {
    	if (uri == null) return false;
    	String host = uri.getHost();
    	return host != null && (host.endsWith(".youtube.com") || host.equals("youtu.be"));
    }
    
    public static boolean isAndroidMarketUri(Uri uri) {
    	if (uri == null) return false;
    	String host = uri.getHost();
    	return host != null && host.equals("market.android.com");
    }
    
} public class Markdown {
//    private Random rnd = new Random();
//    private static final MarkdownCharacterProtector CHAR_PROTECTOR = new MarkdownCharacterProtector();
	
	static final String TAG = "Markdown";
    
    static final RunAutomaton inlineLinkAutomaton = new RunAutomaton(new RegExp("(" + // Whole match = $1
            "\\[([^\\]]*)\\]" + // Link text = $2
            "\\(" +
            "([^\\)\\\\]|(\\\\.))+" +  // 3 cases: 1. simple URL. 2. escaped right-paren in URL. 3. escaped anything in URL. 
            "\\)" +
            ")", RegExp.NONE).toAutomaton());
    // Use inlineLinkAutomaton to check for whole match, then use inlineLink to capture groups
    static final Pattern inlineLink = Pattern.compile("(" + // Whole match = $1
            "\\[(.*?)\\]" + // Link text = $2
            "\\(" +
            "[ \\t]*" +
            "<?(.*?)>?" + // href = $3
            "[ \\t]*" +
            "(" +
            "(['\"])" + // Quote character = $5
            "(.*?)" + // Title = $6
            "\\5" +
            ")?" +
            "\\)" +
            ")", Pattern.DOTALL);
    
    static final RunAutomaton autoLinkUrlAutomaton = new RunAutomaton(new RegExp("((https?|ftp):([^'\"> \t\r\n])+)", RegExp.NONE).toAutomaton());
//    static final Pattern autoLinkEmail = Pattern.compile("<([-.\\w]+\\@[-a-z0-9]+(\\.[-a-z0-9]+)*\\.[a-z]+)>");
    
    static final RunAutomaton subredditAutomaton = new RunAutomaton(new RegExp("/[rR]/[a-zA-Z0-9]+/?", RegExp.NONE).toAutomaton());
	
    /**
     * @param txt input
     * @param urls out URLs
     */
    public void getURLs(String txt, ArrayList<MarkdownURL> urls) {
    	if (txt == null) {
    		txt = "";
    	}
        // Standardize line endings:
        txt.replaceAll("\\r\\n", "\n"); // DOS to Unix
        txt.replaceAll("\\r", "\n"); // Mac to Unix
        txt.replaceAll("^[ \\t]+$", "");
 
//        // Make sure $text ends with a couple of newlines:
//        text.append("\n\n");
 
//        text.detabify();
        txt.replaceAll("^[ ]+$", "");
        
        urls.clear();
        
        TreeMap<Integer, Integer> startToEndOffsetMap = new TreeMap<Integer, Integer>();
        
        // doAnchors originally called from runBlockGamut -> formParagraphs -> runSpanGamut 
        txt = doAnchorURLs(txt, urls, startToEndOffsetMap);
        txt = doAutoLinkURLs(txt, urls, startToEndOffsetMap);
        txt = doAutoLinkSubredditURLs(txt, urls, startToEndOffsetMap);
        
        startToEndOffsetMap.clear();
        
        Collections.sort(urls);
    }
    
    /**
	* Perform the conversion from Markdown to HTML.
	*
	* @param txt - input in markdown format
	* @return HTML block corresponding to txt passed in.
	*/
    @Deprecated
    public SpannableStringBuilder markdown(String txt, SpannableStringBuilder ssb, ArrayList<MarkdownURL> urls) {
    	if (txt == null) {
            txt = "";
        }
        
        // Standardize line endings:
        txt.replaceAll("\\r\\n", "\n"); // DOS to Unix
        txt.replaceAll("\\r", "\n"); // Mac to Unix
        txt.replaceAll("^[ \\t]+$", "");
 
//        // Make sure $text ends with a couple of newlines:
//        text.append("\n\n");
 
//        text.detabify();
        txt.replaceAll("^[ ]+$", "");
        
        ssb.append(txt);
        // doAnchors originally called from runBlockGamut -> formParagraphs -> runSpanGamut 
        urls.clear();
        doAnchors(ssb, urls);
        doAutoLinks(ssb, urls);
        
//	        hashHTMLBlocks(text);
//	        stripLinkDefinitions(text);
//	        text = runBlockGamut(text);
//	        unEscapeSpecialChars(text);
 
//	        text.append("\n");
        
        Collections.sort(urls);
        return ssb;
    }
    
    private boolean isOverlapping(int start, int end, TreeMap<Integer, Integer> startToEndOffsetMap) {
    	for (Map.Entry<Integer, Integer> startEnd : startToEndOffsetMap.entrySet()) {
    		int entryStart = startEnd.getKey();
    		int entryEnd = startEnd.getValue();
    		if (entryStart <= start && entryEnd > start)
    			return true;
    		else if (entryStart >= start && entryEnd <= end)
    			return true;
    		else if (entryStart >= end)
    			return false;
    	}
    	return false;
    }
    
    private void saveStartAndEnd(int start, int end, TreeMap<Integer, Integer> startToEndOffsetMap) {
    	startToEndOffsetMap.put(start, end);
    }
    
    /**
     * @param txt input text
     * @param urls Out URLs from anchors
     * @return updated text with anchors replaced
     */
    private String doAnchorURLs(String txt, ArrayList<MarkdownURL> urls, TreeMap<Integer, Integer> startToEndOffsetMap) {
    	// Inline-style links: [link text](url "optional title")
        AutomatonMatcher am = inlineLinkAutomaton.newMatcher(txt);
        // The offset into the entire original string 
        int start = 0;
        while (am.find()) {
        	// The offsets from start (offset into orig. string = start + anchorStart)
        	int anchorStart = am.start();
        	int anchorEnd = am.end();
        	Matcher m = inlineLink.matcher(am.group());
        	if (!m.find())
        		continue;
        	String linkText = m.group(2);
	        String url = m.group(3);
	        String title = m.group(6);
	        int linkTextLength = linkText.length();
	        
	        if (Constants.LOGGING) Log.d(TAG, "pos="+(start + anchorStart) + " linkText="+linkText + " url="+url + " title="+ title);
	        
	        // protect emphasis (* and _) within urls
//	        url = url.replaceAll("\\*", CHAR_PROTECTOR.encode("*"));
//	        url = url.replaceAll("_", CHAR_PROTECTOR.encode("_"));
	        
	        if (!isOverlapping(start + anchorStart, start + anchorStart + linkTextLength, startToEndOffsetMap)) {
	        	saveStartAndEnd(start + anchorStart, start + anchorStart + linkTextLength, startToEndOffsetMap);
	        	urls.add(new MarkdownURL(start + anchorStart, Util.absolutePathToURL(url), linkText));
	        }
	        	
//	        StringBuffer result = new StringBuffer();
	        // TODO: Show title (if any) alongside url in popup menu
//	        if (title != null) {
//	            // protect emphasis (* and _) within urls
//	            title = title.replaceAll("\\*", CHAR_PROTECTOR.encode("*"));
//	            title = title.replaceAll("_", CHAR_PROTECTOR.encode("_"));
//	            title.replaceAll("\"", "&quot;");
//	            result.append(" title=\"");
//	            result.append(title);
//	            result.append("\"");
//	        }
	        
        	txt = new StringBuilder(txt.substring(0, start + anchorStart))
        		.append(linkText)
        		.append(txt.substring(start + anchorEnd, txt.length()))
        		.toString();
        	// Skip past what we just replaced
        	am = inlineLinkAutomaton.newMatcher(txt, start + anchorStart + linkTextLength, txt.length());
        	start += anchorStart + linkTextLength;
        }
        return txt;
    }
    
    /**
     * @param txt input text
     * @param urls Out URLs from autolinks
     * @return txt, unchanged
     */
    private String doAutoLinkURLs(String txt, ArrayList<MarkdownURL> urls, TreeMap<Integer, Integer> startToEndOffsetMap) {
        // Colorize URLs
        AutomatonMatcher am = autoLinkUrlAutomaton.newMatcher(txt);
        while (am.find()) {
        	String linkText = am.group();
        	String url = Util.absolutePathToURL(am.group());
	        if (Constants.LOGGING) Log.d(TAG, "pos="+am.start() + " linkText="+linkText + " url="+url);
	        if (!isOverlapping(am.start(), am.start() + linkText.length(), startToEndOffsetMap)) {
	        	saveStartAndEnd(am.start(), am.start() + linkText.length(), startToEndOffsetMap);
	        	urls.add(new MarkdownURL(am.start(), url, null));
	        }
        }
        // Don't autolink emails for now. Neither does reddit.com
//        m = autoLinkEmail.matcher(ssb);
//        int start = 0;
//        while (m.find(start)) {
//        	String address = m.group(1);
//            TextEditor ed = new TextEditor(address);
//            unEscapeSpecialChars(ed);
//            String addr = encodeEmail(ed.toString());
//            String url = encodeEmail("mailto:" + ed.toString());
//        	
//        	urls.add(url);
//        	ssb.replace(m.start(), m.end(), addr);
//        	ForegroundColorSpan fcs = new ForegroundColorSpan(Constants.MARKDOWN_LINK_COLOR);
//        	ssb.setSpan(fcs, m.start(), m.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//        	// Skip what we just replaced
//        	m = autoLinkEmail.matcher(ssb);
//        	start = m.start() + addr.length();
//        }
        return txt;
    }
    
    /**
     * @param txt input text
     * @param urls Out URLs from subreddit references
     * @return txt, unchanged
     */
    private String doAutoLinkSubredditURLs(String txt, ArrayList<MarkdownURL> urls, TreeMap<Integer, Integer> startToEndOffsetMap) {
        AutomatonMatcher am = subredditAutomaton.newMatcher(txt);
        while (am.find()) {
        	String subreddit = am.group();
        	if (Constants.LOGGING) Log.d(TAG, "pos="+am.start() + " subreddit="+subreddit);
	        if (!isOverlapping(am.start(), am.start() + subreddit.length(), startToEndOffsetMap)) {
	        	saveStartAndEnd(am.start(), am.start() + subreddit.length(), startToEndOffsetMap);
	        	urls.add(new MarkdownURL(am.start(), Util.absolutePathToURL(subreddit), subreddit));
	        }
        }
        return txt;
    }
    
	
	/** Adapted from MarkdownJ. Convert links, return URL */
    private SpannableStringBuilder doAnchors(SpannableStringBuilder ssb, ArrayList<MarkdownURL> urls) {
    	// Inline-style links: [link text](url "optional title")
        AutomatonMatcher am = inlineLinkAutomaton.newMatcher(ssb);
        // The offset into the entire original string 
        int start = 0;
        while (am.find()) {
        	// The offsets from start (offset into orig. string = start + anchorStart)
        	int anchorStart = am.start();
        	int anchorEnd = am.end();
        	Matcher m = inlineLink.matcher(am.group());
        	if (!m.find())
        		continue;
        	String linkText = m.group(2);
	        String url = m.group(3);
	        String title = m.group(6);
	        int linkTextLength = linkText.length();
	        
	        if (Constants.LOGGING) Log.d(TAG, "pos="+am.start() + " linkText="+linkText + " url="+url + " title="+ title);
	        
	        // protect emphasis (* and _) within urls
//	        url = url.replaceAll("\\*", CHAR_PROTECTOR.encode("*"));
//	        url = url.replaceAll("_", CHAR_PROTECTOR.encode("_"));
	        urls.add(new MarkdownURL(start + anchorStart, url, linkText));
//	        StringBuffer result = new StringBuffer();
	        // TODO: Show title (if any) alongside url in popup menu
//	        if (title != null) {
//	            // protect emphasis (* and _) within urls
//	            title = title.replaceAll("\\*", CHAR_PROTECTOR.encode("*"));
//	            title = title.replaceAll("_", CHAR_PROTECTOR.encode("_"));
//	            title.replaceAll("\"", "&quot;");
//	            result.append(" title=\"");
//	            result.append(title);
//	            result.append("\"");
//	        }
	        
	        // Replace whole anchor thing with just linkText, colored different color
	        SpannableString ss = new SpannableString(linkText);
	        ForegroundColorSpan fcs = new ForegroundColorSpan(Constants.MARKDOWN_LINK_COLOR);
        	ss.setSpan(fcs, 0, linkTextLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        	ssb = ssb.replace(start + anchorStart, start + anchorEnd, ss);
        	// Skip past what we just replaced
        	am = inlineLinkAutomaton.newMatcher(ssb, start + anchorStart + linkTextLength, ssb.length());
        	start += anchorStart + linkTextLength;
        }
        return ssb;
    }

    private SpannableStringBuilder doAutoLinks(SpannableStringBuilder ssb, ArrayList<MarkdownURL> urls) {
        // Colorize URLs
        AutomatonMatcher am = autoLinkUrlAutomaton.newMatcher(ssb);
        while (am.find()) {
        	ForegroundColorSpan fcs = new ForegroundColorSpan(Constants.MARKDOWN_LINK_COLOR);
        	ssb.setSpan(fcs, am.start(), am.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
	        urls.add(new MarkdownURL(am.start(), am.group(), null));
        }
        // Don't autolink emails for now. Neither does reddit.com
//        m = autoLinkEmail.matcher(ssb);
//        int start = 0;
//        while (m.find(start)) {
//        	String address = m.group(1);
//            TextEditor ed = new TextEditor(address);
//            unEscapeSpecialChars(ed);
//            String addr = encodeEmail(ed.toString());
//            String url = encodeEmail("mailto:" + ed.toString());
//        	
//        	urls.add(url);
//        	ssb.replace(m.start(), m.end(), addr);
//        	ForegroundColorSpan fcs = new ForegroundColorSpan(Constants.MARKDOWN_LINK_COLOR);
//        	ssb.setSpan(fcs, m.start(), m.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//        	// Skip what we just replaced
//        	m = autoLinkEmail.matcher(ssb);
//        	start = m.start() + addr.length();
//        }
        return ssb;
    }
    
//    private String encodeEmail(String s) {
//        StringBuffer sb = new StringBuffer();
//        char[] email = s.toCharArray();
//        for (char ch : email) {
//            double r = rnd.nextDouble();
//            if (r < 0.45) { // Decimal
//                sb.append("&#");
//                sb.append((int) ch);
//                sb.append(';');
//            } else if (r < 0.9) { // Hex
//                sb.append("&#x");
//                sb.append(Integer.toString((int) ch, 16));
//                sb.append(';');
//            } else {
//                sb.append(ch);
//            }
//        }
//        return sb.toString();
//    }
//    
//    private void unEscapeSpecialChars(TextEditor ed) {
//        for (String hash : CHAR_PROTECTOR.getAllEncodedTokens()) {
//            String plaintext = CHAR_PROTECTOR.decode(hash);
//            ed.replaceAllLiteral(hash, plaintext);
//        }
//    }
} public class RedditSettings {
	
	private static final String TAG = "RedditSettings";
	
	private String username = null;
	private Cookie redditSessionCookie = null;
	private String modhash = null;
	private String homepage = Constants.FRONTPAGE_STRING;
	private boolean useExternalBrowser = false;
	private boolean showCommentGuideLines = true;
	private boolean confirmQuitOrLogout = true;
	private boolean saveHistory = true;
	private boolean alwaysShowNextPrevious = true;
	
	private int threadDownloadLimit = Constants.DEFAULT_THREAD_DOWNLOAD_LIMIT;
	private String commentsSortByUrl = Constants.CommentsSort.SORT_BY_BEST_URL;
	
    
	// --- Themes ---
	private int theme = R.style.Reddit_Light_Medium;
	private int rotation = -1;  // -1 means unspecified
	private boolean loadThumbnails = true;
	private boolean loadThumbnailsOnlyWifi = false;
	
	private String mailNotificationStyle = Constants.PREF_MAIL_NOTIFICATION_STYLE_DEFAULT;
	private String mailNotificationService = Constants.PREF_MAIL_NOTIFICATION_SERVICE_OFF;
	
	
	
	//
	// --- Methods ---
	//
	
	// --- Preferences ---
	public static class Rotation {
		/* From http://developer.android.com/reference/android/R.attr.html#screenOrientation
		 * unspecified -1
		 * landscape 0
		 * portrait 1
		 * user 2
		 * behind 3
		 * sensor 4
		 * nosensor 5
		 */
		public static int valueOf(String valueString) {
			if (Constants.PREF_ROTATION_UNSPECIFIED.equals(valueString))
				return -1;
			if (Constants.PREF_ROTATION_PORTRAIT.equals(valueString))
				return 1;
			if (Constants.PREF_ROTATION_LANDSCAPE.equals(valueString))
				return 0;
			return -1;
		}
		public static String toString(int value) {
			switch (value) {
			case -1:
				return Constants.PREF_ROTATION_UNSPECIFIED;
			case 1:
				return Constants.PREF_ROTATION_PORTRAIT;
			case 0:
				return Constants.PREF_ROTATION_LANDSCAPE;
			default:
				return Constants.PREF_ROTATION_UNSPECIFIED;
			}
		}
	}
	
    public void saveRedditPreferences(Context context) {
    	SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context);
    	SharedPreferences.Editor editor = settings.edit();
    	
    	// Session
    	if (this.username != null)
    		editor.putString("username", this.username);
    	else
    		editor.remove("username");
    	if (this.redditSessionCookie != null) {
    		editor.putString("reddit_sessionValue",  this.redditSessionCookie.getValue());
    		editor.putString("reddit_sessionDomain", this.redditSessionCookie.getDomain());
    		editor.putString("reddit_sessionPath",   this.redditSessionCookie.getPath());
    		if (this.redditSessionCookie.getExpiryDate() != null)
    			editor.putLong("reddit_sessionExpiryDate", this.redditSessionCookie.getExpiryDate().getTime());
    	}
    	if (this.modhash != null)
    		editor.putString("modhash", this.modhash.toString());
    	
    	// Default subreddit
    	editor.putString(Constants.PREF_HOMEPAGE, this.homepage.toString());
    	
    	// Use external browser instead of BrowserActivity
    	editor.putBoolean(Constants.PREF_USE_EXTERNAL_BROWSER, this.useExternalBrowser);

    	// Show confirmation dialog when backing out of root Activity
    	editor.putBoolean(Constants.PREF_CONFIRM_QUIT, this.confirmQuitOrLogout);

    	// Save reddit history to Browser history
    	editor.putBoolean(Constants.PREF_SAVE_HISTORY, this.saveHistory);
    	
    	// Whether to always show the next/previous buttons, or only at bottom of list
    	editor.putBoolean(Constants.PREF_ALWAYS_SHOW_NEXT_PREVIOUS, this.alwaysShowNextPrevious);
    	
    	// Comments sort order
    	editor.putString(Constants.PREF_COMMENTS_SORT_BY_URL, this.commentsSortByUrl);
    	
    	// Theme and text size
    	String[] themeTextSize = Util.getPrefsFromThemeResource(this.theme);
    	editor.putString(Constants.PREF_THEME, themeTextSize[0]);
    	editor.putString(Constants.PREF_TEXT_SIZE, themeTextSize[1]);
    	
    	// Comment guide lines
    	editor.putBoolean(Constants.PREF_SHOW_COMMENT_GUIDE_LINES, this.showCommentGuideLines);
    	
    	// Rotation
    	editor.putString(Constants.PREF_ROTATION, RedditSettings.Rotation.toString(this.rotation));
    	
    	// Thumbnails
    	editor.putBoolean(Constants.PREF_LOAD_THUMBNAILS, this.loadThumbnails);
    	editor.putBoolean(Constants.PREF_LOAD_THUMBNAILS_ONLY_WIFI, this.loadThumbnailsOnlyWifi);
    	
    	// Notifications
    	editor.putString(Constants.PREF_MAIL_NOTIFICATION_STYLE, this.mailNotificationStyle);
    	editor.putString(Constants.PREF_MAIL_NOTIFICATION_SERVICE, this.mailNotificationService);

    	editor.commit();
    }
    
    public void loadRedditPreferences(Context context, HttpClient client) {
        // Session
    	SharedPreferences sessionPrefs = PreferenceManager.getDefaultSharedPreferences(context);
    	this.setUsername(sessionPrefs.getString("username", null));
    	this.setModhash(sessionPrefs.getString("modhash", null));
        String cookieValue = sessionPrefs.getString("reddit_sessionValue", null);
        String cookieDomain = sessionPrefs.getString("reddit_sessionDomain", null);
        String cookiePath = sessionPrefs.getString("reddit_sessionPath", null);
        long cookieExpiryDate = sessionPrefs.getLong("reddit_sessionExpiryDate", -1);
        if (cookieValue != null) {
        	BasicClientCookie redditSessionCookie = new BasicClientCookie("reddit_session", cookieValue);
        	redditSessionCookie.setDomain(cookieDomain);
        	redditSessionCookie.setPath(cookiePath);
        	if (cookieExpiryDate != -1)
        		redditSessionCookie.setExpiryDate(new Date(cookieExpiryDate));
        	else
        		redditSessionCookie.setExpiryDate(null);
        	this.setRedditSessionCookie(redditSessionCookie);
    		RedditIsFunHttpClientFactory.getCookieStore().addCookie(redditSessionCookie);
    		try {
    			CookieSyncManager.getInstance().sync();
    		} catch (IllegalStateException ex) {
    			if (Constants.LOGGING) Log.e(TAG, "CookieSyncManager.getInstance().sync()", ex);
    		}
        }
        
        // Default subreddit
        String homepage = sessionPrefs.getString(Constants.PREF_HOMEPAGE, Constants.FRONTPAGE_STRING).trim();
        if (StringUtils.isEmpty(homepage))
        	this.setHomepage(Constants.FRONTPAGE_STRING);
        else
        	this.setHomepage(homepage);
        
    	// Use external browser instead of BrowserActivity
        this.setUseExternalBrowser(sessionPrefs.getBoolean(Constants.PREF_USE_EXTERNAL_BROWSER, false));

    	// Show confirmation dialog when backing out of root Activity
        this.setConfirmQuitOrLogout(sessionPrefs.getBoolean(Constants.PREF_CONFIRM_QUIT, true));

        // Save reddit history to Browser history
        this.setSaveHistory(sessionPrefs.getBoolean(Constants.PREF_SAVE_HISTORY, true));
        
    	// Whether to always show the next/previous buttons, or only at bottom of list
        this.setAlwaysShowNextPrevious(sessionPrefs.getBoolean(Constants.PREF_ALWAYS_SHOW_NEXT_PREVIOUS, true));
        
    	// Comments sort order
        this.setCommentsSortByUrl(sessionPrefs.getString(Constants.PREF_COMMENTS_SORT_BY_URL, Constants.CommentsSort.SORT_BY_BEST_URL));
        
        // Theme and text size
        this.setTheme(Util.getThemeResourceFromPrefs(
        		sessionPrefs.getString(Constants.PREF_THEME, Constants.PREF_THEME_LIGHT),
        		sessionPrefs.getString(Constants.PREF_TEXT_SIZE, Constants.PREF_TEXT_SIZE_MEDIUM)));
        
        // Comment guide lines
        this.setShowCommentGuideLines(sessionPrefs.getBoolean(Constants.PREF_SHOW_COMMENT_GUIDE_LINES, true));
        
        // Rotation
        this.setRotation(RedditSettings.Rotation.valueOf(
        		sessionPrefs.getString(Constants.PREF_ROTATION, Constants.PREF_ROTATION_UNSPECIFIED)));
        
        // Thumbnails
        this.setLoadThumbnails(sessionPrefs.getBoolean(Constants.PREF_LOAD_THUMBNAILS, true));
        // Thumbnails on Wifi
        this.setLoadThumbnailsOnlyWifi(sessionPrefs.getBoolean(Constants.PREF_LOAD_THUMBNAILS_ONLY_WIFI, false));
        
        // Notifications
        this.setMailNotificationStyle(sessionPrefs.getString(Constants.PREF_MAIL_NOTIFICATION_STYLE, Constants.PREF_MAIL_NOTIFICATION_STYLE_DEFAULT));
        this.setMailNotificationService(sessionPrefs.getString(Constants.PREF_MAIL_NOTIFICATION_SERVICE, Constants.PREF_MAIL_NOTIFICATION_SERVICE_OFF));
    }
    
    public int getDialogTheme() {
    	if (Util.isLightTheme(theme))
    		return R.style.Reddit_Light_Dialog;
    	else
    		return R.style.Reddit_Dark_Dialog;
    }
    
    public int getDialogNoTitleTheme() {
    	if (Util.isLightTheme(theme))
    		return R.style.Reddit_Light_Dialog_NoTitle;
    	else
    		return R.style.Reddit_Dark_Dialog_NoTitle;
    }

	public boolean isLoggedIn() {
		return username != null;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public Cookie getRedditSessionCookie() {
		return redditSessionCookie;
	}

	public void setRedditSessionCookie(Cookie redditSessionCookie) {
		this.redditSessionCookie = redditSessionCookie;
	}

	public String getModhash() {
		return modhash;
	}

	public void setModhash(String modhash) {
		this.modhash = modhash;
	}

	public String getHomepage() {
		return homepage;
	}

	public void setHomepage(String homepage) {
		this.homepage = homepage;
	}

	public boolean isUseExternalBrowser() {
		return useExternalBrowser;
	}

	public void setUseExternalBrowser(boolean useExternalBrowser) {
		this.useExternalBrowser = useExternalBrowser;
	}

	public boolean isShowCommentGuideLines() {
		return showCommentGuideLines;
	}

	public void setShowCommentGuideLines(boolean showCommentGuideLines) {
		this.showCommentGuideLines = showCommentGuideLines;
	}

	public boolean isConfirmQuitOrLogout() {
		return confirmQuitOrLogout;
	}

	public boolean isSaveHistory() {
		return saveHistory;
	}

	public void setConfirmQuitOrLogout(boolean confirmQuitOrLogout) {
		this.confirmQuitOrLogout = confirmQuitOrLogout;
	}

	public void setSaveHistory(boolean saveHistory) {
		this.saveHistory = saveHistory;
	}

	public boolean isAlwaysShowNextPrevious() {
		return alwaysShowNextPrevious;
	}

	public void setAlwaysShowNextPrevious(boolean alwaysShowNextPrevious) {
		this.alwaysShowNextPrevious = alwaysShowNextPrevious;
	}

	public int getThreadDownloadLimit() {
		return threadDownloadLimit;
	}

	public void setThreadDownloadLimit(int threadDownloadLimit) {
		this.threadDownloadLimit = threadDownloadLimit;
	}

	public String getCommentsSortByUrl() {
		return commentsSortByUrl;
	}

	public void setCommentsSortByUrl(String commentsSortByUrl) {
		this.commentsSortByUrl = commentsSortByUrl;
	}

	public int getTheme() {
		return theme;
	}

	public void setTheme(int theme) {
		this.theme = theme;
	}

	public int getRotation() {
		return rotation;
	}

	public void setRotation(int rotation) {
		this.rotation = rotation;
	}

	public boolean isLoadThumbnails() {
		return loadThumbnails;
	}

	public void setLoadThumbnails(boolean loadThumbnails) {
		this.loadThumbnails = loadThumbnails;
	}

	public boolean isLoadThumbnailsOnlyWifi() {
		return loadThumbnailsOnlyWifi;
	}

	public void setLoadThumbnailsOnlyWifi(boolean loadThumbnailsOnlyWifi) {
		this.loadThumbnailsOnlyWifi = loadThumbnailsOnlyWifi;
	}

	public String getMailNotificationStyle() {
		return mailNotificationStyle;
	}

	public void setMailNotificationStyle(String mailNotificationStyle) {
		this.mailNotificationStyle = mailNotificationStyle;
	}

	public String getMailNotificationService() {
		return mailNotificationService;
	}

	public void setMailNotificationService(String mailNotificationService) {
		this.mailNotificationService = mailNotificationService;
	}

} public class Listing {
	private String kind;
	private ListingData data;
	
	public Listing() {}
	
	public Listing(String stuff) {
		kind = null;
		data = null;
	}
	
	public void setKind(String kind) {
		this.kind = kind;
	}
	public String getKind() {
		return kind;
	}
	
	public void setData(ListingData data) {
		this.data = data;
	}
	public ListingData getData() {
		return data;
	}
} public class ListingData {
	private ThingListing[] children;
	private String after;
	private String before;
	private String modhash;
	
	public void setAfter(String after) {
		this.after = after;
	}
	public String getAfter() {
		return after;
	}
	
	public void setBefore(String before) {
		this.before = before;
	}
	public String getBefore() {
		return before;
	}
	
	public void setModhash(String modhash) {
		this.modhash = modhash;
	}
	public String getModhash() {
		return modhash;
	}
	
	public void setChildren(ThingListing[] children) {
		this.children = children;
	}
	public ThingListing[] getChildren() {
		return children;
	}
} public class ThingListing {
	private String kind;
	private ThingInfo data;
	
	public void setKind(String kind) {
		this.kind = kind;
	}
	public String getKind() {
		return kind;
	}
	
	public void setData(ThingInfo data) {
		this.data = data;
	}
	public ThingInfo getData() {
		return data;
	}
} public class ShowThumbnailsTask extends AsyncTask<ThumbnailLoadAction, ThumbnailLoadAction, Void> {
	
	private ListActivity mActivity;
	private HttpClient mClient;
	private Integer mDefaultThumbnailResource;
	
	private static HashMap<String, SoftReference<Bitmap>> cache = new HashMap<String, SoftReference<Bitmap>>();
	
	private static final String TAG = "ShowThumbnailsTask";

	public ShowThumbnailsTask(ListActivity activity, HttpClient client, Integer defaultThumbnailResource) {
		this.mActivity = activity;
		this.mClient = client;
		this.mDefaultThumbnailResource = defaultThumbnailResource;
	}
	
	public static class ThumbnailLoadAction {
    	public ThingInfo thingInfo;
    	public ImageView imageView;  // prefer imageView; if it's null, use threadIndex
    	public int threadIndex;
    	public ThumbnailLoadAction(ThingInfo thingInfo, ImageView imageView, int threadIndex) {
    		this.thingInfo = thingInfo;
    		this.imageView = imageView;
    		this.threadIndex = threadIndex;
    	}
    }
	
	@Override
	protected Void doInBackground(ThumbnailLoadAction... thumbnailLoadActions) {
		for (ThumbnailLoadAction thumbnailLoadAction : thumbnailLoadActions) {
			loadThumbnail(thumbnailLoadAction.thingInfo);
			publishProgress(thumbnailLoadAction);
		}
		return null;
	}
	
	// TODO use external storage cache if present
	private void loadThumbnail(ThingInfo thingInfo) {
		if ("default".equals(thingInfo.getThumbnail()) || "self".equals(thingInfo.getThumbnail()) || StringUtils.isEmpty(thingInfo.getThumbnail())) {
			thingInfo.setThumbnailResource(mDefaultThumbnailResource);
		}
		else {
			SoftReference<Bitmap> ref;
			Bitmap bitmap;
			
			ref = cache.get(thingInfo.getThumbnail());
			if (ref != null) {
				bitmap = ref.get();
				if (bitmap != null) {
					thingInfo.setThumbnailBitmap(bitmap);
					return;
				}
			}
			
			bitmap = readBitmapFromNetwork(thingInfo.getThumbnail());
			
			ref = new SoftReference<Bitmap>(bitmap);
			cache.put(thingInfo.getThumbnail(), ref);
			thingInfo.setThumbnailBitmap(ref.get());
		}
	}
	
    private InputStream fetch(String urlString) throws MalformedURLException, IOException {
    	HttpGet request = new HttpGet(urlString);
    	HttpResponse response = mClient.execute(request);
    	return response.getEntity().getContent();
    }
    
	private Bitmap readBitmapFromNetwork( String url ) {
		if (url == null)
			return null;
		
		InputStream is = null;
		BufferedInputStream bis = null;
		Bitmap bmp = null;
		try {
			// http://blog.donnfelker.com/2010/04/29/android-odd-error-in-defaulthttpclient/
			if (!url.startsWith("http://") && !url.startsWith("https://"))
				url = "http://" + url;
			
			is = fetch(url);
			bis = new BufferedInputStream(is);
			bmp = BitmapFactory.decodeStream(bis);
		} catch (MalformedURLException e) {
			Log.e(TAG, "Bad ad URL", e);
		} catch (IOException e) {
			Log.e(TAG, "Could not get remote ad image", e);
		} finally {
			try {
				if( is != null )
					is.close();
				if( bis != null )
					bis.close();
			} catch (IOException e) {
				Log.w(TAG, "Error closing stream.");
			}
		}
		return bmp;
	}
	
	@Override
	protected void onProgressUpdate(ThumbnailLoadAction... thumbnailLoadActions) {
		for (ThumbnailLoadAction thumbnailLoadAction : thumbnailLoadActions)
			refreshThumbnailUI(thumbnailLoadAction);
	}
	
	private void refreshThumbnailUI(ThumbnailLoadAction thumbnailLoadAction) {
		ImageView imageView = null;
		if (thumbnailLoadAction.imageView != null) {
			imageView = thumbnailLoadAction.imageView;
		}
		else {
			if (isCurrentlyOnScreenUI(thumbnailLoadAction.threadIndex)) {
				int positionOnScreen = thumbnailLoadAction.threadIndex - mActivity.getListView().getFirstVisiblePosition();
				View v = mActivity.getListView().getChildAt(positionOnScreen);
				if (v != null) {
					View thumbnailImageView = v.findViewById(R.id.thumbnail);
					if (thumbnailImageView != null) {
						imageView = (ImageView) thumbnailImageView;
					}
				}
			}
		}
		if (imageView != null) {
			ThingInfo thingInfo = thumbnailLoadAction.thingInfo;
			if (thingInfo.getThumbnailBitmap() != null)
				imageView.setImageBitmap(thingInfo.getThumbnailBitmap());
			else if (thingInfo.getThumbnailResource() != null)
				imageView.setImageResource(thingInfo.getThumbnailResource());
		}
	}
	
	private boolean isCurrentlyOnScreenUI(int position) {
		return position >= mActivity.getListView().getFirstVisiblePosition() &&
				position <= mActivity.getListView().getLastVisiblePosition();
	}
	
} public static class ThumbnailLoadAction {
   	public ThingInfo thingInfo;
   	public ImageView imageView;  // prefer imageView; if it's null, use threadIndex
   	public int threadIndex;
   	public ThumbnailLoadAction(ThingInfo thingInfo, ImageView imageView, int threadIndex) {
   		this.thingInfo = thingInfo;
   		this.imageView = imageView;
   		this.threadIndex = threadIndex;
   	}
   }
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.LinkedList;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.ObjectMapper;
import android.os.AsyncTask;
import android.text.Html;
import android.text.Spanned;
import android.util.Log;
import android.view.Window;
import android.widget.Toast;
import com.andrewshu.android.reddit.comments.ProcessCommentsTask.DeferredCommentProcessing;
import com.andrewshu.android.reddit.common.CacheInfo;
import com.andrewshu.android.reddit.common.Common;
import com.andrewshu.android.reddit.common.Constants;
import com.andrewshu.android.reddit.common.ProgressInputStream;
import com.andrewshu.android.reddit.common.util.Assert;
import com.andrewshu.android.reddit.common.util.StringUtils;
import com.andrewshu.android.reddit.common.util.Util;
import com.andrewshu.android.reddit.markdown.Markdown;
import com.andrewshu.android.reddit.settings.RedditSettings;
import com.andrewshu.android.reddit.things.Listing;
import com.andrewshu.android.reddit.things.ListingData;
import com.andrewshu.android.reddit.things.ThingInfo;
import com.andrewshu.android.reddit.things.ThingListing;
import com.andrewshu.android.reddit.threads.ShowThumbnailsTask;
import com.andrewshu.android.reddit.threads.ShowThumbnailsTask.ThumbnailLoadAction;
		return mPositionOffset == 0;
	}
	
	private void disableLoadingScreenKeepProgress() {
		mActivity.runOnUiThread(new Runnable() {
			@Override
			public void run() {
	    		mActivity.resetUI(mActivity.mCommentsAdapter);
			}
		});
	}
	
	private void parseCommentsJSON(
			InputStream in
	) throws IOException, JsonParseException {
		int insertedCommentIndex;
		String genericListingError = "Not a comments listing";
		try {
			Listing[] listings = mObjectMapper.readValue(in, Listing[].class);

			// listings[0] is a thread Listing for the OP.
			// process same as a thread listing more or less
			
			Assert.assertEquals(Constants.JSON_LISTING, listings[0].getKind(), genericListingError);
			
			// Save modhash, ignore "after" and "before" which are meaningless in this context (and probably null)
			ListingData threadListingData = listings[0].getData();
			if (StringUtils.isEmpty(threadListingData.getModhash()))
				mSettings.setModhash(null);
			else
				mSettings.setModhash(threadListingData.getModhash());
			
			if (Constants.LOGGING) Log.d(TAG, "Successfully got OP listing[0]: modhash "+mSettings.getModhash());
			
			ThingListing threadThingListing = threadListingData.getChildren()[0];
			Assert.assertEquals(Constants.THREAD_KIND, threadThingListing.getKind(), genericListingError);

			if (isInsertingEntireThread()) {
				parseOP(threadThingListing.getData());
				insertedCommentIndex = 0;  // we just inserted the OP into position 0
				
				// at this point we've started displaying comments, so disable the loading screen
				disableLoadingScreenKeepProgress();
			}
			else {
				insertedCommentIndex = mPositionOffset - 1;  // -1 because we +1 for the first comment
			}
			
			// listings[1] is a comment Listing for the comments
			// Go through the children and get the ThingInfos
			ListingData commentListingData = listings[1].getData();
			for (ThingListing commentThingListing : commentListingData.getChildren()) {
				// insert the comment and its replies, prefix traversal order
				insertedCommentIndex = insertNestedComment(commentThingListing, 0, insertedCommentIndex + 1);
			}
			
			mProcessCommentsTask.mergeLowPriorityListToMainList();
			
		} catch (Exception ex) {
			if (Constants.LOGGING) Log.e(TAG, "parseCommentsJSON", ex);
		}
	}
	
	private void parseOP(final ThingInfo data) {
		data.setIndent(0);
		data.setClicked(Common.isClicked(mActivity, data.getUrl()));
		
		mActivity.runOnUiThread(new Runnable() {
			@Override
			public void run() {
				mActivity.mCommentsList.add(0, data);
			}
		});

		if (data.isIs_self() && data.getSelftext_html() != null) {
			// HTML to Spanned
			String unescapedHtmlSelftext = Html.fromHtml(data.getSelftext_html()).toString();
			Spanned selftext = Html.fromHtml(Util.convertHtmlTags(unescapedHtmlSelftext));
			
    		// remove last 2 newline characters
			if (selftext.length() > 2)
				data.setSpannedSelftext(selftext.subSequence(0, selftext.length()-2));
			else
				data.setSpannedSelftext("");

			// Get URLs from markdown
			markdown.getURLs(data.getSelftext(), data.getUrls());
		}
		
		// We might not have a title if we've intercepted a plain link to a thread.
		mThreadTitle = data.getTitle();
		mActivity.setThreadTitle(mThreadTitle);
		mSubreddit = data.getSubreddit();
		mThreadId = data.getId();
		
		mOpThingInfo = data;
	}
	
	/**
	 * Recursive method to insert comment tree into the mCommentsList,
	 * with proper list order and indentation
	 */
	int insertNestedComment(ThingListing commentThingListing, int indentLevel, int insertedCommentIndex) {
		ThingInfo ci = commentThingListing.getData();
		
		// Add comment to deferred append/replace list
		if (isInsertingEntireThread())
			deferCommentAppend(ci);
		else
			deferCommentReplacement(ci);
		
		// Keep track of jump target
		if (isHasJumpTarget()) {
			if (!isFoundJumpTargetComment() && mJumpToCommentId.equals(ci.getId()))
				processJumpTarget(ci, insertedCommentIndex);
		}
		
		if (isHasJumpTarget()) {
			// if we have found the jump target, then we did the messy stuff already. just append to main processing list. 
			if (isFoundJumpTargetComment()) {
				mProcessCommentsTask.addDeferred(new DeferredCommentProcessing(ci, insertedCommentIndex));