public abstract class CaptchaCheckRequiredTask extends AsyncTask<Void, Void, Boolean> {
	
	private static final String TAG = "CaptchaCheckRequiredTask";
	
	// Captcha "iden"
    private static final Pattern CAPTCHA_IDEN_PATTERN
    	= Pattern.compile("name=\"iden\" value=\"([^\"]+)\"");
    // Group 2: Captcha image absolute path
    private static final Pattern CAPTCHA_IMAGE_PATTERN
    	= Pattern.compile("<img class=\"capimage\"( alt=\"[^\"]*\")? src=\"(/captcha/[^\"]+)\"");

    
    protected String _mCaptchaIden;
    protected String _mCaptchaUrl;
    
    private String _mCheckUrl;
    private HttpClient _mClient;
    
	public CaptchaCheckRequiredTask(String checkUrl, HttpClient client) {
		_mCheckUrl = checkUrl;
		_mClient = client;
	}
	
	@Override
	public Boolean doInBackground(Void... voidz) {
		HttpEntity entity = null;
		BufferedReader in = null;
		try {
			HttpGet request = new HttpGet(_mCheckUrl);
			HttpResponse response = _mClient.execute(request);
			if (!Util.isHttpStatusOK(response)) {
				throw new HttpException("bad HTTP response: "+response);
			}
			entity = response.getEntity();
    		in = new BufferedReader(new InputStreamReader(entity.getContent()));
        	String line;
        	
        	while ((line = in.readLine()) != null) {
	        	Matcher idenMatcher = CAPTCHA_IDEN_PATTERN.matcher(line);
	        	Matcher urlMatcher = CAPTCHA_IMAGE_PATTERN.matcher(line);
	        	if (idenMatcher.find() && urlMatcher.find()) {
	        		_mCaptchaIden = idenMatcher.group(1);
	        		_mCaptchaUrl = urlMatcher.group(2);
	        		saveState();
	        		return true;
	        	}
        	}
        	
    		_mCaptchaIden = null;
    		_mCaptchaUrl = null;
    		saveState();
    		return false;
    		
		} catch (Exception e) {
			if (Constants.LOGGING) Log.e(TAG, "Error accessing "+_mCheckUrl+" to check for CAPTCHA", e);
    	} finally {
    		if (in != null) {
    			try {
    				in.close();
    			} catch (Exception e2) {
    				if (Constants.LOGGING) Log.e(TAG, "in.Close()", e2);
    			}
    		}
    		if (entity != null) {
    			try {
    				entity.consumeContent();
    			} catch (Exception e2) {
    				if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e2);
    			}
    		}
		}
    	// mCaptchaIden and mCaptchaUrl are null if not required
    	// so on error, set them to some non-null dummy value
    	_mCaptchaIden = "";
    	_mCaptchaUrl = "";
    	saveState();
		return null;
	}
	
	abstract protected void saveState();
} public abstract class CaptchaDownloadTask extends AsyncTask<Void, Void, Drawable> {
	
	private static final String TAG = "CaptchaDownloadTask";
	
	private String _mCaptchaUrl;
	private HttpClient _mClient;
	
	public CaptchaDownloadTask(String captchaUrl, HttpClient client) {
		_mCaptchaUrl = captchaUrl;
		_mClient = client;
	}
	@Override
	public Drawable doInBackground(Void... voidz) {
		try {
			HttpGet request = new HttpGet(Constants.REDDIT_BASE_URL + "/" + _mCaptchaUrl);
			HttpResponse response = _mClient.execute(request);
    	
			InputStream in = response.getEntity().getContent();
			
			//get image as bitmap
			Bitmap captchaOrg  = BitmapFactory.decodeStream(in);

			// create matrix for the manipulation
			Matrix matrix = new Matrix();
			// resize the bit map
			matrix.postScale(2f, 2f);

			// recreate the new Bitmap
			Bitmap resizedBitmap = Bitmap.createScaledBitmap (captchaOrg,
					captchaOrg.getWidth() * 3, captchaOrg.getHeight() * 3, true);
		 
			BitmapDrawable bmd = new BitmapDrawable(resizedBitmap);
			
			return bmd;
		
		} catch (Exception e) {
			if (Constants.LOGGING) Log.e(TAG, "download captcha", e);
		}
		
		return null;
	}
	

} public class CommentsListActivity extends ListActivity
		implements View.OnCreateContextMenuListener {

	private static final String TAG = "CommentsListActivity";
	
    // Group 2: subreddit name. Group 3: thread id36. Group 4: Comment id36.
    private final Pattern COMMENT_PATH_PATTERN = Pattern.compile(Constants.COMMENT_PATH_PATTERN_STRING);
    private final Pattern COMMENT_CONTEXT_PATTERN = Pattern.compile("context=(\\d+)");

    /** Custom list adapter that fits our threads data into the list. */
    CommentsListAdapter mCommentsAdapter = null;
    ArrayList<ThingInfo> mCommentsList = null;
    
    private final HttpClient mClient = RedditIsFunHttpClientFactory.getGzipHttpClient();
    
    // Common settings are stored here
    private final RedditSettings mSettings = new RedditSettings();
    
    private String mSubreddit = null;
    private String mThreadId = null;
    private String mThreadTitle = null;
	
    // UI State
    private ThingInfo mVoteTargetThing = null;
    private String mReportTargetName = null;
    private String mReplyTargetName = null;
    private String mEditTargetBody = null;
    private String mDeleteTargetKind = null;
    private boolean mShouldClearReply = false;

    private String last_search_string;
    private int last_found_position = -1;
    
    private boolean mCanChord = false;
    
    // override transition animation available Android 2.0 (SDK Level 5) and above
    private static Method mActivity_overridePendingTransition;
    
    static {
        initCompatibility();
    };

    private static void initCompatibility() {
        try {
            mActivity_overridePendingTransition = Activity.class.getMethod(
                    "overridePendingTransition", new Class[] { Integer.TYPE, Integer.TYPE } );
            /* success, this is a newer device */
        } catch (NoSuchMethodException nsme) {
            /* failure, must be older device */
        }
    }
    
    /**
     * Called when the activity starts up. Do activity initialization
     * here, not in a constructor.
     * 
     * @see Activity#onCreate
     */
    @SuppressWarnings("unchecked")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

		CookieSyncManager.createInstance(getApplicationContext());
		
		mSettings.loadRedditPreferences(this, mClient);

        setRequestedOrientation(mSettings.getRotation());
        setTheme(mSettings.getTheme());
        requestWindowFeature(Window.FEATURE_PROGRESS);
        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
    	
        setContentView(R.layout.comments_list_content);
        registerForContextMenu(getListView());
        
        if (savedInstanceState != null) {
        	mReplyTargetName = savedInstanceState.getString(Constants.REPLY_TARGET_NAME_KEY);
        	mReportTargetName = savedInstanceState.getString(Constants.REPORT_TARGET_NAME_KEY);
        	mEditTargetBody = savedInstanceState.getString(Constants.EDIT_TARGET_BODY_KEY);
        	mDeleteTargetKind = savedInstanceState.getString(Constants.DELETE_TARGET_KIND_KEY);
        	mThreadTitle = savedInstanceState.getString(Constants.THREAD_TITLE_KEY);
        	mSubreddit = savedInstanceState.getString(Constants.SUBREDDIT_KEY);
        	mThreadId = savedInstanceState.getString(Constants.THREAD_ID_KEY);
        	mVoteTargetThing = savedInstanceState.getParcelable(Constants.VOTE_TARGET_THING_INFO_KEY);
        	
        	if (mThreadTitle != null) {
        	    setTitle(mThreadTitle + " : " + mSubreddit);
        	}
        	
		    mCommentsList = (ArrayList<ThingInfo>) getLastNonConfigurationInstance();
        	if (mCommentsList == null) {
        		getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
		    } else {
		    	// Orientation change. Use prior instance.
			    resetUI(new CommentsListAdapter(this, mCommentsList));
		    }
    	}
        
        // No saved state; use info from Intent.getData()
        else {
        	String commentPath;
        	String commentQuery;
        	String jumpToCommentId = null;
        	int jumpToCommentContext = 0;
    		// We get the URL through getIntent().getData()
            Uri data = getIntent().getData();
            if (data != null) {
            	// Comment path: a URL pointing to a thread or a comment in a thread.
            	commentPath = data.getPath();
            	commentQuery = data.getQuery();
            } else {
        		if (Constants.LOGGING) Log.e(TAG, "Quitting because no subreddit and thread id data was passed into the Intent.");
        		finish();
        		return;
            }
            
        	if (commentPath != null) {
        		if (Constants.LOGGING) Log.d(TAG, "comment path: "+commentPath);
        		
        		if (Util.isRedditShortenedUri(data)) {
        			// http://redd.it/abc12
        			mThreadId = commentPath.substring(1);
        		} else {
        			// http://www.reddit.com/...
	        		Matcher m = COMMENT_PATH_PATTERN.matcher(commentPath);
	        		if (m.matches()) {
	            		mSubreddit = m.group(1);
	        			mThreadId = m.group(2);
	        			jumpToCommentId = m.group(3);
	        		}
        		}
        	} else {
    			if (Constants.LOGGING) Log.e(TAG, "Quitting because of bad comment path.");
    			finish();
    			return;
    		}
        	
        	if (commentQuery != null) {
        		Matcher m = COMMENT_CONTEXT_PATTERN.matcher(commentQuery);
        		if (m.find()) {
        			jumpToCommentContext = m.group(1) != null ? Integer.valueOf(m.group(1)) : 0;
        		}
        	}
        	
        	// Extras: subreddit, threadTitle, numComments
        	// subreddit is not always redundant to Intent.getData(),
        	// since URL does not always contain the subreddit. (e.g., self posts)
        	Bundle extras = getIntent().getExtras();
        	if (extras != null) {
        		// subreddit could have already been set from the Intent.getData. don't overwrite with null here!
        		String subreddit = extras.getString(Constants.EXTRA_SUBREDDIT);
        		if (subreddit != null)
        			mSubreddit = subreddit;
        		// mThreadTitle has not been set yet, so no need for null check before setting it
        		mThreadTitle = extras.getString(Constants.EXTRA_TITLE);
        		if (mThreadTitle != null) {
            	    setTitle(mThreadTitle + " : " + mSubreddit);
            	}
        		// TODO: use extras.getInt(Constants.EXTRA_NUM_COMMENTS) somehow
        	}
        	
        	if (!StringUtils.isEmpty(jumpToCommentId)) {
        		getNewDownloadCommentsTask().prepareLoadAndJumpToComment(jumpToCommentId, jumpToCommentContext)
        				.execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
        	}
        	else {
        		getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
        	}
        }
    }
    
    @Override
    protected void onResume() {
    	super.onResume();
		int previousTheme = mSettings.getTheme();
		
    	mSettings.loadRedditPreferences(this, mClient);

    	if (mSettings.getTheme() != previousTheme) {
    		relaunchActivity();
    	}
    	else {
	    	CookieSyncManager.getInstance().startSync();
	    	setRequestedOrientation(mSettings.getRotation());
	
	    	if (mSettings.isLoggedIn())
	    		new PeekEnvelopeTask(this, mClient, mSettings.getMailNotificationStyle()).execute();
    	}
    }
    
    private void relaunchActivity() {
    	finish();
    	startActivity(getIntent());
    }
    
    @Override
    protected void onPause() {
    	super.onPause();
		CookieSyncManager.getInstance().stopSync();
		mSettings.saveRedditPreferences(this);
    }
    
    @Override
    public Object onRetainNonConfigurationInstance() {
    	return mCommentsList;
    }
    
    private DownloadCommentsTask getNewDownloadCommentsTask() {
    	return new DownloadCommentsTask(
				this,
				mSubreddit,
				mThreadId,
				mSettings,
				mClient
		);
    }
    
    private boolean isHiddenCommentHeadPosition(int position) {
    	return mCommentsAdapter != null && mCommentsAdapter.getItemViewType(position) == CommentsListAdapter.HIDDEN_ITEM_HEAD_VIEW_TYPE;
    }
    
    private boolean isHiddenCommentDescendantPosition(int position) {
    	return mCommentsAdapter != null && mCommentsAdapter.getItem(position).isHiddenCommentDescendant();
    }
    
    private boolean isLoadMoreCommentsPosition(int position) {
    	return mCommentsAdapter != null && mCommentsAdapter.getItemViewType(position) == CommentsListAdapter.MORE_ITEM_VIEW_TYPE;
    }
    
    final class CommentsListAdapter extends ArrayAdapter<ThingInfo> {
    	public static final int OP_ITEM_VIEW_TYPE = 0;
    	public static final int COMMENT_ITEM_VIEW_TYPE = 1;
    	public static final int MORE_ITEM_VIEW_TYPE = 2;
    	public static final int HIDDEN_ITEM_HEAD_VIEW_TYPE = 3;
    	// The number of view types
    	public static final int VIEW_TYPE_COUNT = 4;
    	
    	public boolean mIsLoading = true;
    	
    	private LayoutInflater mInflater;
        private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
        
        public CommentsListAdapter(Context context, List<ThingInfo> objects) {
            super(context, 0, objects);
            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }

        @Override
        public int getItemViewType(int position) {
        	if (position == 0)
        		return OP_ITEM_VIEW_TYPE;
        	if (position == mFrequentSeparatorPos) {
                // We don't want the separator view to be recycled.
                return IGNORE_ITEM_VIEW_TYPE;
            }
        	
            ThingInfo item = getItem(position);
            if (item.isHiddenCommentDescendant())
            	return IGNORE_ITEM_VIEW_TYPE;
            if (item.isHiddenCommentHead())
            	return HIDDEN_ITEM_HEAD_VIEW_TYPE;
            if (item.isLoadMoreCommentsPlaceholder())
            	return MORE_ITEM_VIEW_TYPE;
            
            return COMMENT_ITEM_VIEW_TYPE;
        }
        
        @Override
        public int getViewTypeCount() {
        	return VIEW_TYPE_COUNT;
        }
        
        @Override
        public boolean isEmpty() {
        	if (mIsLoading)
        		return false;
        	return super.isEmpty();
        }
        
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View view = convertView;
            
            ThingInfo item = this.getItem(position);
            
            try {
	            if (position == 0) {
	            	// The OP
	            	if (view == null) {
	            		view = mInflater.inflate(R.layout.threads_list_item, null);
	            	}
	            	
	            	ThreadsListActivity.fillThreadsListItemView(
	            			position, view, item, CommentsListActivity.this, mClient, mSettings, mThumbnailOnClickListenerFactory
        			);
	            	if (item.isIs_self()) {
	            		View thumbnailContainer = view.findViewById(R.id.thumbnail_view);
	            		if (thumbnailContainer != null)
	            			thumbnailContainer.setVisibility(View.GONE);
	            	}
	                
	                // In addition to stuff from ThreadsListActivity,
	            	// we want to show selftext in CommentsListActivity.
	                
	            	TextView submissionStuffView = (TextView) view.findViewById(R.id.submissionTime_submitter);
	                TextView selftextView = (TextView) view.findViewById(R.id.selftext);
	                
	                submissionStuffView.setVisibility(View.VISIBLE);
	                submissionStuffView.setText(
	                		String.format(getResources().getString(R.string.thread_time_submitter),
	                				Util.getTimeAgo(item.getCreated_utc()), item.getAuthor()));
	                
	            	if (!StringUtils.isEmpty(item.getSpannedSelftext())) {
	            		selftextView.setVisibility(View.VISIBLE);
		                selftextView.setText(item.getSpannedSelftext());
	            	} else {
	            		selftextView.setVisibility(View.GONE);
	            	}
	            	
	            } else if (isHiddenCommentDescendantPosition(position)) { 
	            	if (view == null) {
	            		// Doesn't matter which view we inflate since it's gonna be invisible
	            		view = mInflater.inflate(R.layout.zero_size_layout, null);
	            	}
	            } else if (isHiddenCommentHeadPosition(position)) {
	            	if (view == null) {
	            		view = mInflater.inflate(R.layout.comments_list_item_hidden, null);
	            	}
	            	TextView votesView = (TextView) view.findViewById(R.id.votes);
		            TextView submitterView = (TextView) view.findViewById(R.id.submitter);
	                TextView submissionTimeView = (TextView) view.findViewById(R.id.submissionTime);
		            
		            try {
		            	votesView.setText(Util.showNumPoints(item.getUps() - item.getDowns()));
		            } catch (NumberFormatException e) {
		            	// This happens because "ups" comes after the potentially long "replies" object,
		            	// so the ListView might try to display the View before "ups" in JSON has been parsed.
		            	if (Constants.LOGGING) Log.e(TAG, "getView, hidden comment heads", e);
		            }
		            if (getOpThingInfo() != null && item.getAuthor().equalsIgnoreCase(getOpThingInfo().getAuthor()))
		            	submitterView.setText(item.getAuthor() + " [S]");
		            else
		            	submitterView.setText(item.getAuthor());
		            submissionTimeView.setText(Util.getTimeAgo(item.getCreated_utc()));
		            
		            setCommentIndent(view, item.getIndent(), mSettings);
		            
            	} else if (isLoadMoreCommentsPosition(position)) {
	            	// "load more comments"
	            	if (view == null) {
	            		view = mInflater.inflate(R.layout.more_comments_view, null);
	            	}

	            	setCommentIndent(view, item.getIndent(), mSettings);
	            	
	            } else {  // Regular comment
	            	// Here view may be passed in for re-use, or we make a new one.
		            if (view == null) {
		                view = mInflater.inflate(R.layout.comments_list_item, null);
		            } else {
		                view = convertView;
		            }

					// Sometimes (when in touch mode) the "selection" highlight disappears.
					// So we make our own persistent highlight. This background color must
					// be set explicitly on every element, however, or the "cached" list
					// item views will show up with the color.
					if (position == last_found_position)
						view.setBackgroundResource(R.color.translucent_yellow);
					else
						view.setBackgroundColor(Color.TRANSPARENT);

		            fillCommentsListItemView(view, item, mSettings);
	            }
            } catch (NullPointerException e) {
            	if (Constants.LOGGING) Log.w(TAG, "NPE in getView()", e);
            	// Probably means that the List is still being built, and OP probably got put in wrong position
            	if (view == null) {
            		if (position == 0)
            			view = mInflater.inflate(R.layout.threads_list_item, null);
            		else
            			view = mInflater.inflate(R.layout.comments_list_item, null);
	            }
            }
            return view;
        }
    } // End of CommentsListAdapter
    
    public ThingInfo getOpThingInfo() {
    	if (!CollectionUtils.isEmpty(mCommentsList))
    		return mCommentsList.get(0);
    	return null;
    }

    public void setThreadTitle(String threadTitle) {
    	this.mThreadTitle = threadTitle;
    }
    
    public void setShouldClearReply(boolean shouldClearReply) {
    	this.mShouldClearReply = shouldClearReply;
    }
    
    public static void setCommentIndent(View commentListItemView, int indentLevel, RedditSettings settings) {
        View[] indentViews = new View[] {
        	commentListItemView.findViewById(R.id.left_indent1),
        	commentListItemView.findViewById(R.id.left_indent2),
        	commentListItemView.findViewById(R.id.left_indent3),
        	commentListItemView.findViewById(R.id.left_indent4),
        	commentListItemView.findViewById(R.id.left_indent5),
        	commentListItemView.findViewById(R.id.left_indent6),
        	commentListItemView.findViewById(R.id.left_indent7),
        	commentListItemView.findViewById(R.id.left_indent8)
        };
        for (int i = 0; i < indentLevel && i < indentViews.length; i++) {
        	if (settings.isShowCommentGuideLines()) {
            	indentViews[i].setVisibility(View.VISIBLE);
            	if (Util.isLightTheme(settings.getTheme())) {
            		indentViews[i].setBackgroundResource(R.color.light_light_gray);
            	} else {
            		indentViews[i].setBackgroundResource(R.color.dark_gray);
            	}
        	} else {
        		indentViews[i].setVisibility(View.INVISIBLE);
        	}
        }
        for (int i = indentLevel; i < indentViews.length; i++) {
        	indentViews[i].setVisibility(View.GONE);
        }
    }

    
    /**
     * Called when user clicks an item in the list. Starts an activity to
     * open the url for that item.
     */
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        ThingInfo item = mCommentsAdapter.getItem(position);
        
        if (isHiddenCommentHeadPosition(position)) {
        	showComment(position);
        	return;
        }
        
        // Mark the OP post/regular comment as selected
        mVoteTargetThing = item;
        mReplyTargetName = mVoteTargetThing.getName();
		
        if (isLoadMoreCommentsPosition(position)) {
        	// Use this constructor to tell it to load more comments inline
        	getNewDownloadCommentsTask().prepareLoadMoreComments(item.getId(), position, item.getIndent())
        			.execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
        } else {
        	if (!"[deleted]".equals(item.getAuthor()))
        		showDialog(Constants.DIALOG_COMMENT_CLICK);
        }
    }
    
    /**
     * Resets the output UI list contents, retains session state.
     * @param commentsAdapter A new CommentsListAdapter to use. Pass in null to create a new empty one.
     */
    void resetUI(CommentsListAdapter commentsAdapter) {
    	findViewById(R.id.loading_light).setVisibility(View.GONE);
    	findViewById(R.id.loading_dark).setVisibility(View.GONE);
    	
    	if (commentsAdapter == null) {
    		// Reset the list to be empty.
    		mCommentsList = new ArrayList<ThingInfo>();
            mCommentsAdapter = new CommentsListAdapter(this, mCommentsList);
            setListAdapter(mCommentsAdapter);
    	} else if (mCommentsAdapter != commentsAdapter) {
    		mCommentsAdapter = commentsAdapter;
    		setListAdapter(commentsAdapter);
    	}
        
        mCommentsAdapter.mIsLoading = false;
        mCommentsAdapter.notifyDataSetChanged();  // Just in case
        getListView().setDivider(null);
        Common.updateListDrawables(this, mSettings.getTheme());
    }
    
    /**
     * Mark the OP submitter comments
     */
    void markSubmitterComments() {
    	if (getOpThingInfo() == null || mCommentsAdapter == null)
    		return;
    	
		SpannableString authorSS = new SpannableString(getOpThingInfo().getAuthor() + " [S]");
        ForegroundColorSpan fcs;
        if (Util.isLightTheme(mSettings.getTheme()))
        	fcs = new ForegroundColorSpan(getResources().getColor(R.color.blue));
        else
        	fcs = new ForegroundColorSpan(getResources().getColor(R.color.pale_blue));
        authorSS.setSpan(fcs, 0, authorSS.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
	
		for (int i = 0; i < mCommentsAdapter.getCount(); i++) {
			ThingInfo ci = mCommentsAdapter.getItem(i);
			// if it's the OP, mark his name
			if (getOpThingInfo().getAuthor().equalsIgnoreCase(ci.getAuthor()))
	            ci.setSSAuthor(authorSS);
		}
    }
    
    void enableLoadingScreen() {
    	if (Util.isLightTheme(mSettings.getTheme())) {
    		findViewById(R.id.loading_light).setVisibility(View.VISIBLE);
    		findViewById(R.id.loading_dark).setVisibility(View.GONE);
    	} else {
    		findViewById(R.id.loading_light).setVisibility(View.GONE);
    		findViewById(R.id.loading_dark).setVisibility(View.VISIBLE);
    	}
    	if (mCommentsAdapter != null)
    		mCommentsAdapter.mIsLoading = true;
    	getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_START);
    }
    
    
    
    private class MyLoginTask extends LoginTask {
    	public MyLoginTask(String username, String password) {
    		super(username, password, mSettings, mClient, getApplicationContext());
    	}
    	
    	@Override
    	protected void onPreExecute() {
    		showDialog(Constants.DIALOG_LOGGING_IN);
    	}
    	
    	@Override
    	protected void onPostExecute(Boolean success) {
    		removeDialog(Constants.DIALOG_LOGGING_IN);
    		if (success) {
    			Toast.makeText(CommentsListActivity.this, "Logged in as "+mUsername, Toast.LENGTH_SHORT).show();
    			// Check mail
    			new PeekEnvelopeTask(CommentsListActivity.this, mClient, mSettings.getMailNotificationStyle()).execute();
	    		// Refresh the comments list
    			getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
    		} else {
            	Common.showErrorToast(mUserError, Toast.LENGTH_LONG, CommentsListActivity.this);
    		}
    	}
    }
    
    
    
    
    private class CommentReplyTask extends AsyncTask<String, Void, String> {
    	private String _mParentThingId;
    	String _mUserError = "Error submitting reply. Please try again.";
    	
    	CommentReplyTask(String parentThingId) {
    		_mParentThingId = parentThingId;
    	}
    	
    	@Override
        public String doInBackground(String... text) {
        	HttpEntity entity = null;
        	
        	if (!mSettings.isLoggedIn()) {
        		Common.showErrorToast("You must be logged in to reply.", Toast.LENGTH_LONG, CommentsListActivity.this);
        		_mUserError = "Not logged in";
        		return null;
        	}
        	// Update the modhash if necessary
        	if (mSettings.getModhash() == null) {
        		String modhash = Common.doUpdateModhash(mClient);
        		if (modhash == null) {
        			// doUpdateModhash should have given an error about credentials
        			Common.doLogout(mSettings, mClient, getApplicationContext());
        			if (Constants.LOGGING) Log.e(TAG, "Reply failed because doUpdateModhash() failed");
        			return null;
        		}
        		mSettings.setModhash(modhash);
        	}
        	
        	try {
        		// Construct data
    			List<NameValuePair> nvps = new ArrayList<NameValuePair>();
    			nvps.add(new BasicNameValuePair("thing_id", _mParentThingId));
    			nvps.add(new BasicNameValuePair("text", text[0]));
    			nvps.add(new BasicNameValuePair("r", mSubreddit));
    			nvps.add(new BasicNameValuePair("uh", mSettings.getModhash()));
    			// Votehash is currently unused by reddit 
//    				nvps.add(new BasicNameValuePair("vh", "0d4ab0ffd56ad0f66841c15609e9a45aeec6b015"));
    			
    			HttpPost httppost = new HttpPost(Constants.REDDIT_BASE_URL + "/api/comment");
    	        httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
    	        
    	        HttpParams params = httppost.getParams();
    	        HttpConnectionParams.setConnectionTimeout(params, 40000);
    	        HttpConnectionParams.setSoTimeout(params, 40000);
    	        
    	        if (Constants.LOGGING) Log.d(TAG, nvps.toString());
    	        
                // Perform the HTTP POST request
    	    	HttpResponse response = mClient.execute(httppost);
    	    	entity = response.getEntity();
    	    	
            	// Getting here means success. Create a new CommentInfo.
            	return Common.checkIDResponse(response, entity);
            	
        	} catch (Exception e) {
        		if (Constants.LOGGING) Log.e(TAG, "CommentReplyTask", e);
        		_mUserError = e.getMessage();
        	} finally {
        		if (entity != null) {
        			try {
        				entity.consumeContent();
        			} catch (Exception e2) {
        				if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e2);
        			}
        		}
        	}
        	return null;
        }
    	
    	@Override
    	public void onPreExecute() {
    		showDialog(Constants.DIALOG_REPLYING);
    	}
    	
    	@Override
    	public void onPostExecute(String newId) {
    		removeDialog(Constants.DIALOG_REPLYING);
    		if (newId == null) {
    			Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, CommentsListActivity.this);
    		} else {
    			// Refresh
    			CacheInfo.invalidateCachedThread(getApplicationContext());
    			getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
    		}
    	}
    }
    
    private class EditTask extends AsyncTask<String, Void, String> {
    	private String _mThingId;
    	String _mUserError = "Error submitting edit. Please try again.";
    	
    	EditTask(String thingId) {
    		_mThingId = thingId;
    	}
    	
    	@Override
        public String doInBackground(String... text) {
        	HttpEntity entity = null;
        	
        	if (!mSettings.isLoggedIn()) {
        		_mUserError = "You must be logged in to edit.";
        		return null;
        	}
        	// Update the modhash if necessary
        	if (mSettings.getModhash() == null) {
        		String modhash = Common.doUpdateModhash(mClient);
        		if (modhash == null) {
        			// doUpdateModhash should have given an error about credentials
        			Common.doLogout(mSettings, mClient, getApplicationContext());
        			if (Constants.LOGGING) Log.e(TAG, "Reply failed because doUpdateModhash() failed");
        			return null;
        		}
        		mSettings.setModhash(modhash);
        	}
        	
        	try {
        		// Construct data
    			List<NameValuePair> nvps = new ArrayList<NameValuePair>();
    			nvps.add(new BasicNameValuePair("thing_id", _mThingId.toString()));
    			nvps.add(new BasicNameValuePair("text", text[0].toString()));
    			nvps.add(new BasicNameValuePair("r", mSubreddit.toString()));
    			nvps.add(new BasicNameValuePair("uh", mSettings.getModhash().toString()));
    			
    			HttpPost httppost = new HttpPost(Constants.REDDIT_BASE_URL + "/api/editusertext");
    	        httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
    	        
    	        HttpParams params = httppost.getParams();
    	        HttpConnectionParams.setConnectionTimeout(params, 40000);
    	        HttpConnectionParams.setSoTimeout(params, 40000);
    	        
    	        if (Constants.LOGGING) Log.d(TAG, nvps.toString());
    	        
                // Perform the HTTP POST request
    	    	HttpResponse response = mClient.execute(httppost);
    	    	entity = response.getEntity();
    	    	
    	    	return Common.checkIDResponse(response, entity);
            	
        	} catch (Exception e) {
        		if (Constants.LOGGING) Log.e(TAG, "EditTask", e);
        		_mUserError = e.getMessage();
        	} finally {
        		if (entity != null) {
        			try {
        				entity.consumeContent();
        			} catch (Exception e2) {
        				if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e2);
        			}
        		}
        	}
        	return null;
        }
    	
    	@Override
    	public void onPreExecute() {
    		showDialog(Constants.DIALOG_EDITING);
    	}
    	
    	@Override
    	public void onPostExecute(String newId) {
    		removeDialog(Constants.DIALOG_EDITING);
    		if (newId == null) {
    			Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, CommentsListActivity.this);
    		} else {
    			// Refresh
    			CacheInfo.invalidateCachedThread(getApplicationContext());
    			getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
    		}
    	}
    }
    
    private class DeleteTask extends AsyncTask<String, Void, Boolean> {
    	private String _mUserError = "Error deleting. Please try again.";
    	private String _mKind;
    	
    	public DeleteTask(String kind) {
    		_mKind = kind;
    	}
    	
    	@Override
    	public Boolean doInBackground(String... thingFullname) {
//    		POSTDATA=id=t1_c0cxa7l&executed=deleted&r=test&uh=f7jb1yjwfqd4ffed8356eb63fcfbeeadad142f57c56e9cbd9e
    		
    		HttpEntity entity = null;
        	
        	if (!mSettings.isLoggedIn()) {
        		_mUserError = "You must be logged in to delete.";
        		return false;
        	}
        	// Update the modhash if necessary
        	if (mSettings.getModhash() == null) {
        		String modhash = Common.doUpdateModhash(mClient);
        		if (modhash == null) {
        			// doUpdateModhash should have given an error about credentials
        			Common.doLogout(mSettings, mClient, getApplicationContext());
        			if (Constants.LOGGING) Log.e(TAG, "Reply failed because doUpdateModhash() failed");
        			return false;
        		}
        		mSettings.setModhash(modhash);
        	}
        	
        	try {
        		// Construct data
    			List<NameValuePair> nvps = new ArrayList<NameValuePair>();
    			nvps.add(new BasicNameValuePair("id", thingFullname[0].toString()));
    			nvps.add(new BasicNameValuePair("executed", "deleted"));
    			nvps.add(new BasicNameValuePair("r", mSubreddit.toString()));
    			nvps.add(new BasicNameValuePair("uh", mSettings.getModhash().toString()));
    			
    			HttpPost httppost = new HttpPost(Constants.REDDIT_BASE_URL + "/api/del");
    	        httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
    	        
    	        HttpParams params = httppost.getParams();
    	        HttpConnectionParams.setConnectionTimeout(params, 40000);
    	        HttpConnectionParams.setSoTimeout(params, 40000);
    	        
    	        if (Constants.LOGGING) Log.d(TAG, nvps.toString());
    	        
                // Perform the HTTP POST request
    	    	HttpResponse response = mClient.execute(httppost);
            	entity = response.getEntity();

            	String error = Common.checkResponseErrors(response, entity);
            	if (error != null)
            		throw new Exception(error);

            	// Success
            	return true;
            	
        	} catch (Exception e) {
        		if (Constants.LOGGING) Log.e(TAG, "DeleteTask", e);
        		_mUserError = e.getMessage();
        	} finally {
        		if (entity != null) {
        			try {
        				entity.consumeContent();
        			} catch (Exception e2) {
        				if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e2);
        			}
        		}
        	}
        	return false;
    	}
    	
    	@Override
    	public void onPreExecute() {
    		showDialog(Constants.DIALOG_DELETING);
    	}
    	
    	@Override
    	public void onPostExecute(Boolean success) {
    		removeDialog(Constants.DIALOG_DELETING);
    		if (success) {
    			CacheInfo.invalidateCachedThread(getApplicationContext());
    			if (Constants.THREAD_KIND.equals(_mKind)) {
    				Toast.makeText(CommentsListActivity.this, "Deleted thread.", Toast.LENGTH_LONG).show();
    				finish();
    				return;
    			} else {
    				Toast.makeText(CommentsListActivity.this, "Deleted comment.", Toast.LENGTH_SHORT).show();
    				getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
    			}
    		} else {
    			Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, CommentsListActivity.this);
    		}
    	}
    }

    private class VoteTask extends AsyncTask<Void, Void, Boolean> {
    	
    	private static final String TAG = "VoteWorker";
    	
    	private String _mThingFullname;
    	private int _mDirection;
    	private String _mUserError = "Error voting.";
    	private ThingInfo _mTargetThingInfo;
    	
    	// Save the previous arrow and score in case we need to revert
    	private int _mPreviousUps, _mPreviousDowns;
    	private Boolean _mPreviousLikes;
    	
    	VoteTask(String thingFullname, int direction) {
    		_mThingFullname = thingFullname;
    		_mDirection = direction;
    		// Copy these because they can change while voting thread is running
    		_mTargetThingInfo = mVoteTargetThing;
    	}
    	
    	@Override
    	public Boolean doInBackground(Void... v) {
        	HttpEntity entity = null;
        	
        	if (!mSettings.isLoggedIn()) {
        		_mUserError = "You must be logged in to vote.";
        		return false;
        	}
        	
        	// Update the modhash if necessary
        	if (mSettings.getModhash() == null) {
        		String modhash = Common.doUpdateModhash(mClient); 
        		if (modhash == null) {
        			// doUpdateModhash should have given an error about credentials
        			Common.doLogout(mSettings, mClient, getApplicationContext());
        			if (Constants.LOGGING) Log.e(TAG, "Vote failed because doUpdateModhash() failed");
        			return false;
        		}
        		mSettings.setModhash(modhash);
        	}
        	
        	try {
        		// Construct data
    			List<NameValuePair> nvps = new ArrayList<NameValuePair>();
    			nvps.add(new BasicNameValuePair("id", _mThingFullname.toString()));
    			nvps.add(new BasicNameValuePair("dir", String.valueOf(_mDirection)));
    			nvps.add(new BasicNameValuePair("r", mSubreddit.toString()));
    			nvps.add(new BasicNameValuePair("uh", mSettings.getModhash().toString()));
    			// Votehash is currently unused by reddit 
//    				nvps.add(new BasicNameValuePair("vh", "0d4ab0ffd56ad0f66841c15609e9a45aeec6b015"));
    			
    			HttpPost httppost = new HttpPost(Constants.REDDIT_BASE_URL + "/api/vote");
    	        httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
    	        
    	        if (Constants.LOGGING) Log.d(TAG, nvps.toString());
    	        
                // Perform the HTTP POST request
    	    	HttpResponse response = mClient.execute(httppost);
            	entity = response.getEntity();

            	String error = Common.checkResponseErrors(response, entity);
            	if (error != null)
            		throw new Exception(error);

            	return true;
        	} catch (Exception e) {
        		if (Constants.LOGGING) Log.e(TAG, "VoteTask", e);
        		_mUserError = e.getMessage();
        	} finally {
        		if (entity != null) {
        			try {
        				entity.consumeContent();
        			} catch (Exception e2) {
        				if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e2);
        			}
        		}
        	}
        	return false;
        }
    	
    	public void onPreExecute() {
        	if (!mSettings.isLoggedIn()) {
        		Common.showErrorToast("You must be logged in to vote.", Toast.LENGTH_LONG, CommentsListActivity.this);
        		cancel(true);
        		return;
        	}
        	if (_mDirection < -1 || _mDirection > 1) {
        		if (Constants.LOGGING) Log.e(TAG, "WTF: _mDirection = " + _mDirection);
        		throw new RuntimeException("How the hell did you vote something besides -1, 0, or 1?");
        	}

    		int newUps, newDowns;
        	Boolean newLikes;
        	_mPreviousUps = Integer.valueOf(_mTargetThingInfo.getUps());
        	_mPreviousDowns = Integer.valueOf(_mTargetThingInfo.getDowns());
    	    newUps = _mPreviousUps;
    	    newDowns = _mPreviousDowns;
    	    _mPreviousLikes = _mTargetThingInfo.getLikes();
        	
    	    if (_mPreviousLikes == null) {
	    		if (_mDirection == 1) {
	    			newUps = _mPreviousUps + 1;
	    			newLikes = true;
	    		} else if (_mDirection == -1) {
	    			newDowns = _mPreviousDowns + 1;
	    			newLikes = false;
	    		} else {
	    			cancel(true);
	    			return;
	    		}
    	    } else if (_mPreviousLikes == true) {
    	    	if (_mDirection == 0) {
	    			newUps = _mPreviousUps - 1;
	    			newLikes = null;
	    		} else if (_mDirection == -1) {
	    			newUps = _mPreviousUps - 1;
	    			newDowns = _mPreviousDowns + 1;
	    			newLikes = false;
	    		} else {
	    			cancel(true);
	    			return;
	    		}
	    	} else {
	    		if (_mDirection == 1) {
	    			newUps = _mPreviousUps + 1;
	    			newDowns = _mPreviousDowns - 1;
	    			newLikes = true;
	    		} else if (_mDirection == 0) {
	    			newDowns = _mPreviousDowns - 1;
	    			newLikes = null;
	    		} else {
	    			cancel(true);
	    			return;
	    		}
	    	}

    		_mTargetThingInfo.setLikes(newLikes);
    		_mTargetThingInfo.setUps(newUps);
    		_mTargetThingInfo.setDowns(newDowns);
    		_mTargetThingInfo.setScore(newUps - newDowns);
        	mCommentsAdapter.notifyDataSetChanged();
    	}
    	
    	public void onPostExecute(Boolean success) {
    		if (success) {
    			CacheInfo.invalidateCachedThread(getApplicationContext());
    		} else {
    			// Vote failed. Undo the arrow and score.
            	_mTargetThingInfo.setLikes(_mPreviousLikes);
       			_mTargetThingInfo.setUps(_mPreviousUps);
       			_mTargetThingInfo.setDowns(_mPreviousDowns);
       			_mTargetThingInfo.setScore(_mPreviousUps - _mPreviousDowns);
        		mCommentsAdapter.notifyDataSetChanged();
        		
    			Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, CommentsListActivity.this);
    		}
    	}
    }
    
    

    private class ReportTask extends AsyncTask<Void, Void, Boolean> {
    	
    	private static final String TAG = "ReportTask";
    	
    	private String _mUserError = "Error reporting.";
    	private String _mFullId;
    	
    	ReportTask(String fullname) {
    		this._mFullId = fullname;
    	}
    	
    	@Override
    	public Boolean doInBackground(Void... v) {
        	HttpEntity entity = null;
        	
        	if (!mSettings.isLoggedIn()) {
        		_mUserError = "You must be logged in to report something.";
        		return false;
        	}
        	
        	// Update the modhash if necessary
        	if (mSettings.getModhash() == null) {
        		String modhash = Common.doUpdateModhash(mClient); 
        		if (modhash == null) {
        			// doUpdateModhash should have given an error about credentials
        			Common.doLogout(mSettings, mClient, getApplicationContext());
        			if (Constants.LOGGING) Log.e(TAG, "Report failed because doUpdateModhash() failed");
        			return false;
        		}
        		mSettings.setModhash(modhash);
        	}
        	
        	try {
        		// Construct data
    			List<NameValuePair> nvps = new ArrayList<NameValuePair>();
    			nvps.add(new BasicNameValuePair("id", _mFullId));
    			nvps.add(new BasicNameValuePair("executed", "reported"));
    			nvps.add(new BasicNameValuePair("r", mSubreddit.toString()));
    			nvps.add(new BasicNameValuePair("uh", mSettings.getModhash().toString()));
    			// Votehash is currently unused by reddit 
//    				nvps.add(new BasicNameValuePair("vh", "0d4ab0ffd56ad0f66841c15609e9a45aeec6b015"));
    			
    			HttpPost httppost = new HttpPost(Constants.REDDIT_BASE_URL + "/api/report");
    	        httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
    	        
    	        if (Constants.LOGGING) Log.d(TAG, nvps.toString());
    	        
                // Perform the HTTP POST request
    	    	HttpResponse response = mClient.execute(httppost);
    	    	entity = response.getEntity();

    	    	String error = Common.checkResponseErrors(response, entity);
            	if (error != null)
            		throw new Exception(error);

            	// Success
            	return true;

        	} catch (Exception e) {
        		if (Constants.LOGGING) Log.e(TAG, "ReportTask", e);
        	} finally {
        		if (entity != null) {
        			try {
        				entity.consumeContent();
        			} catch (Exception e2) {
        				if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e2);
        			}
        		}
        	}
        	return false;
        }
    	
    	public void onPreExecute() {
	        if (!mSettings.isLoggedIn()) {
	        	Common.showErrorToast("You must be logged in to report this.", Toast.LENGTH_LONG, CommentsListActivity.this);
	        	cancel(true);
	        	return;
	        }
    	}
    	
    	public void onPostExecute(Boolean success) {
    		if (success) {
    			Toast.makeText(CommentsListActivity.this, "Reported.", Toast.LENGTH_SHORT);
    		} else {
    			Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, CommentsListActivity.this);
    		}
    	}
    }

    /**
     * Populates the menu.
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.comments, menu);
        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        // This happens when the user begins to hold down the menu key, so
        // allow them to chord to get a shortcut.
        mCanChord = true;

    	super.onPrepareOptionsMenu(menu);
    	
    	MenuItem src, dest;

        menu.findItem(R.id.find_next_menu_id).setVisible(last_search_string != null && last_search_string.length() > 0);

        // Login/Logout
    	if (mSettings.isLoggedIn()) {
    		menu.findItem(R.id.login_menu_id).setVisible(false);
	        menu.findItem(R.id.inbox_menu_id).setVisible(true);
	        menu.findItem(R.id.user_profile_menu_id).setVisible(true);
	        menu.findItem(R.id.user_profile_menu_id).setTitle(
	        		String.format(getResources().getString(R.string.user_profile), mSettings.getUsername())
    		);
    		menu.findItem(R.id.logout_menu_id).setVisible(true);
	        menu.findItem(R.id.logout_menu_id).setTitle(
	        		String.format(getResources().getString(R.string.logout), mSettings.getUsername())
    		);
    	} else {
            menu.findItem(R.id.login_menu_id).setVisible(true);
            menu.findItem(R.id.inbox_menu_id).setVisible(false);
	        menu.findItem(R.id.user_profile_menu_id).setVisible(false);
            menu.findItem(R.id.logout_menu_id).setVisible(false);
    	}
    	
    	// Edit and delete
    	if (getOpThingInfo() != null) {
	    	if (mSettings.getUsername() != null && mSettings.getUsername().equalsIgnoreCase(getOpThingInfo().getAuthor())) {
				if (getOpThingInfo().isIs_self())
					menu.findItem(R.id.op_edit_menu_id).setVisible(true);
				else
					menu.findItem(R.id.op_edit_menu_id).setVisible(false);
				menu.findItem(R.id.op_delete_menu_id).setVisible(true);
			} else {
				menu.findItem(R.id.op_edit_menu_id).setVisible(false);
				menu.findItem(R.id.op_delete_menu_id).setVisible(false);
			}
    	}
    	
    	// Theme: Light/Dark
    	src = Util.isLightTheme(mSettings.getTheme()) ?
        		menu.findItem(R.id.dark_menu_id) :
        			menu.findItem(R.id.light_menu_id);
        dest = menu.findItem(R.id.light_dark_menu_id);
        dest.setTitle(src.getTitle());
        
        // Sort
        if (Constants.CommentsSort.SORT_BY_BEST_URL.equals(mSettings.getCommentsSortByUrl()))
        	src = menu.findItem(R.id.sort_by_best_menu_id);
        else if (Constants.CommentsSort.SORT_BY_HOT_URL.equals(mSettings.getCommentsSortByUrl()))
        	src = menu.findItem(R.id.sort_by_hot_menu_id);
        else if (Constants.CommentsSort.SORT_BY_NEW_URL.equals(mSettings.getCommentsSortByUrl()))
        	src = menu.findItem(R.id.sort_by_new_menu_id);
        else if (Constants.CommentsSort.SORT_BY_CONTROVERSIAL_URL.equals(mSettings.getCommentsSortByUrl()))
        	src = menu.findItem(R.id.sort_by_controversial_menu_id);
        else if (Constants.CommentsSort.SORT_BY_TOP_URL.equals(mSettings.getCommentsSortByUrl()))
        	src = menu.findItem(R.id.sort_by_top_menu_id);
        else if (Constants.CommentsSort.SORT_BY_OLD_URL.equals(mSettings.getCommentsSortByUrl()))
        	src = menu.findItem(R.id.sort_by_old_menu_id);
        dest = menu.findItem(R.id.sort_by_menu_id);
        dest.setTitle(src.getTitle());
    	
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (!mCanChord) {
            // The user has already fired a shortcut with this hold down of the
            // menu key.
            return false;
        }
        
        switch (item.getItemId()) {
        case R.id.op_menu_id:
        	if (getOpThingInfo() == null)
        		break;
    		mVoteTargetThing = getOpThingInfo();
        	mReplyTargetName = getOpThingInfo().getName();
    		showDialog(Constants.DIALOG_COMMENT_CLICK);
    		break;
    	case R.id.op_subreddit_menu_id:
			Intent intent = new Intent(getApplicationContext(), ThreadsListActivity.class);
			intent.setData(Util.createSubredditUri(mSubreddit));
			intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
			startActivity(intent);
			Util.overridePendingTransition(mActivity_overridePendingTransition, this,
					android.R.anim.slide_in_left, android.R.anim.slide_out_right);
			break;
    	case R.id.login_menu_id:
    		showDialog(Constants.DIALOG_LOGIN);
    		break;
    	case R.id.logout_menu_id:
    		Common.doLogout(mSettings, mClient, getApplicationContext());
    		Toast.makeText(this, "You have been logged out.", Toast.LENGTH_SHORT).show();
    		getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
            break;
        case R.id.find_next_menu_id:
            if (last_search_string != null && last_search_string.length() > 0)
                findCommentText(last_search_string, true, true);
            break;
        case R.id.find_base_id:
            // This case is needed because the "default" case throws
            // an error, otherwise precluding anonymous "parent" menu items
            break;
        case R.id.find_menu_id:
            showDialog(Constants.DIALOG_FIND);
            break;
    	case R.id.refresh_menu_id:
    		CacheInfo.invalidateCachedThread(getApplicationContext());
    		getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
    		break;
    	case R.id.sort_by_menu_id:
    		showDialog(Constants.DIALOG_SORT_BY);
    		break;
    	case R.id.open_browser_menu_id:
    		String url = new StringBuilder(Constants.REDDIT_BASE_URL + "/r/")
				.append(mSubreddit).append("/comments/").append(mThreadId).toString();
    		Common.launchBrowser(this, url, url, false, true, true, false);
    		break;
    	case R.id.op_delete_menu_id:
    		mReplyTargetName = getOpThingInfo().getName();
    		mDeleteTargetKind = Constants.THREAD_KIND;
    		showDialog(Constants.DIALOG_DELETE);
    		break;
    	case R.id.op_edit_menu_id:
    		mReplyTargetName = getOpThingInfo().getName();
    		mEditTargetBody = getOpThingInfo().getSelftext();
    		showDialog(Constants.DIALOG_EDIT);
    		break;
    	case R.id.light_dark_menu_id:
    		mSettings.setTheme(Util.getInvertedTheme(mSettings.getTheme()));
    		relaunchActivity();
    		break;
        case R.id.inbox_menu_id:
        	Intent inboxIntent = new Intent(getApplicationContext(), InboxActivity.class);
        	startActivity(inboxIntent);
        	break;
        case R.id.user_profile_menu_id:
        	Intent profileIntent = new Intent(getApplicationContext(), ProfileActivity.class);
        	startActivity(profileIntent);
        	break;
    	case R.id.preferences_menu_id:
            Intent prefsIntent = new Intent(getApplicationContext(), RedditPreferencesPage.class);
            startActivity(prefsIntent);
            break;
    	case android.R.id.home:
    		Common.goHome(this);
    		break;

    	default:
    		throw new IllegalArgumentException("Unexpected action value "+item.getItemId());
    	}
    	
        return true;
    }



    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
    	super.onCreateContextMenu(menu, v, menuInfo);
    	AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
    	int rowId = (int) info.id;
    	
    	ThingInfo item = mCommentsAdapter.getItem(rowId);
    	
    	if (rowId == 0) {
    		menu.add(0, Constants.SHARE_CONTEXT_ITEM, Menu.NONE, "Share");

    		if(getOpThingInfo().isSaved()){
    			menu.add(0, Constants.UNSAVE_CONTEXT_ITEM, Menu.NONE, "Unsave");
    		} else {
    			menu.add(0, Constants.SAVE_CONTEXT_ITEM, Menu.NONE, "Save");
    		}
    		if(getOpThingInfo().isHidden()){
    			menu.add(0, Constants.UNHIDE_CONTEXT_ITEM, Menu.NONE, "Unhide");
    		} else {
    			menu.add(0, Constants.HIDE_CONTEXT_ITEM, Menu.NONE, "Hide");
    		}
    		
    		menu.add(0, Constants.DIALOG_VIEW_PROFILE, Menu.NONE,
    				String.format(getResources().getString(R.string.user_profile), item.getAuthor()));
    		
    	} else if (isLoadMoreCommentsPosition(rowId)) {
    		menu.add(0, Constants.DIALOG_GOTO_PARENT, Menu.NONE, "Go to parent");
    	} else if (isHiddenCommentHeadPosition(rowId)) {
    		menu.add(0, Constants.DIALOG_SHOW_COMMENT, Menu.NONE, "Show comment");
    		menu.add(0, Constants.DIALOG_GOTO_PARENT, Menu.NONE, "Go to parent");
    	} else {
    		if (mSettings.getUsername() != null && mSettings.getUsername().equalsIgnoreCase(item.getAuthor())) {
    			menu.add(0, Constants.DIALOG_EDIT, Menu.NONE, "Edit");
    			menu.add(0, Constants.DIALOG_DELETE, Menu.NONE, "Delete");
    		}
    		menu.add(0, Constants.DIALOG_HIDE_COMMENT, Menu.NONE, "Hide comment");
//    		if (mSettings.isLoggedIn())
//    			menu.add(0, Constants.DIALOG_REPORT, Menu.NONE, "Report comment");
    		menu.add(0, Constants.DIALOG_GOTO_PARENT, Menu.NONE, "Go to parent");
    		menu.add(0, Constants.DIALOG_VIEW_PROFILE, Menu.NONE,
    				String.format(getResources().getString(R.string.user_profile), item.getAuthor()));
    	}
    }
    
    @Override
    public boolean onContextItemSelected(MenuItem item) {
    	AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
    	int rowId = (int) info.id;
    	
    	switch (item.getItemId()) {
    	case Constants.SAVE_CONTEXT_ITEM:
    		new SaveTask(true, getOpThingInfo(), mSettings, this).execute();
    		return true;
    		
    	case Constants.UNSAVE_CONTEXT_ITEM:
    		new SaveTask(false, getOpThingInfo(), mSettings, this).execute();
    		return true;
    		
    	case Constants.HIDE_CONTEXT_ITEM:
    		new HideTask(true, getOpThingInfo(), mSettings, this).execute();
    		return true;
    		
    	case Constants.UNHIDE_CONTEXT_ITEM:
    		new HideTask(false, getOpThingInfo(), mSettings, this).execute();
    		return true;
    		
    	case Constants.SHARE_CONTEXT_ITEM:
    		Intent intent = new Intent();
			intent.setAction(Intent.ACTION_SEND);
			intent.setType("text/plain");

			intent.putExtra(Intent.EXTRA_TEXT, getOpThingInfo().getUrl());
			
			try {
				startActivity(Intent.createChooser(intent, "Share Link"));
			} catch (android.content.ActivityNotFoundException ex) {
				
			}
			
			return true;
			
    	case Constants.DIALOG_HIDE_COMMENT:
    		hideComment(rowId);
    		return true;
    		
    	case Constants.DIALOG_SHOW_COMMENT:
    		showComment(rowId);
    		return true;
    		
    	case Constants.DIALOG_GOTO_PARENT:
			int myIndent = mCommentsAdapter.getItem(rowId).getIndent();
    		int parentRowId;
    		for (parentRowId = rowId - 1; parentRowId >= 0; parentRowId--)
    			if (mCommentsAdapter.getItem(parentRowId).getIndent() < myIndent)
    				break;
    		getListView().setSelection(parentRowId);
    		return true;
    		
    	case Constants.DIALOG_VIEW_PROFILE:
    		Intent i = new Intent(this, ProfileActivity.class);
    		i.setData(Util.createProfileUri(mCommentsAdapter.getItem(rowId).getAuthor()));
    		startActivity(i);
    		return true;
    		
    	case Constants.DIALOG_EDIT:
    		mReplyTargetName = mCommentsAdapter.getItem(rowId).getName();
    		mEditTargetBody = mCommentsAdapter.getItem(rowId).getBody();
    		showDialog(Constants.DIALOG_EDIT);
    		return true;
    		
    	case Constants.DIALOG_DELETE:
			mReplyTargetName = mCommentsAdapter.getItem(rowId).getName();
    		// It must be a comment, since the OP selftext is reached via options menu, not context menu
    		mDeleteTargetKind = Constants.COMMENT_KIND;
    		showDialog(Constants.DIALOG_DELETE);
    		return true;
    		
    	case Constants.DIALOG_REPORT:
			mReportTargetName = mCommentsAdapter.getItem(rowId).getName();
    		showDialog(Constants.DIALOG_REPORT);
    		return true;
    		
		default:
    		return super.onContextItemSelected(item);	
    	}
    }
    
    private void hideComment(int rowId) {
		ThingInfo headComment = mCommentsAdapter.getItem(rowId);
    	int myIndent = headComment.getIndent();
    	headComment.setHiddenCommentHead(true);
    	
    	// Hide everything after the row.
    	for (int i = rowId + 1; i < mCommentsAdapter.getCount(); i++) {
    		ThingInfo ci = mCommentsAdapter.getItem(i);
    		if (ci.getIndent() <= myIndent)
    			break;
    		ci.setHiddenCommentDescendant(true);
    	}
    	mCommentsAdapter.notifyDataSetChanged();
    }
    
    private void showComment(int rowId) {
		ThingInfo headComment = mCommentsAdapter.getItem(rowId);
		headComment.setHiddenCommentHead(false);
    	int stopIndent = headComment.getIndent();
    	int skipIndentAbove = -1;
    	for (int i = rowId + 1; i < mCommentsAdapter.getCount(); i++) {
    		ThingInfo ci = mCommentsAdapter.getItem(i);
    		int ciIndent = ci.getIndent();
    		if (ciIndent <= stopIndent)
    			break;
    		if (skipIndentAbove != -1 && ciIndent > skipIndentAbove)
    			continue;

    		ci.setHiddenCommentDescendant(false);
    		
    		// skip nested hidden comments (e.g. you collapsed child first, then root. now expanding root, but don't expand child) 
    		if (ci.isHiddenCommentHead())
    			skipIndentAbove = ci.getIndent();
    		else
	    		skipIndentAbove = -1;
    	}
    	mCommentsAdapter.notifyDataSetChanged();
    }

	private void findCommentText(String search_text, boolean wrap, boolean next) {
		last_search_string = search_text;
		int current_position = next
			? (last_found_position + 1) % mCommentsAdapter.getCount()
			: Math.max(0, getSelectedItemPosition());

		if ( getFoundPosition(current_position, mCommentsAdapter.getCount(), search_text) ) {
			mCommentsAdapter.notifyDataSetChanged();
			return;
		}

		if ( wrap ) {
			if (Constants.LOGGING) Log.d(TAG, "Continuing search from top...");
			if ( getFoundPosition(0, current_position, search_text) ) {
				mCommentsAdapter.notifyDataSetChanged();
				return;
			}
		}

		mCommentsAdapter.notifyDataSetChanged();

		String not_found_msg = getResources().getString(R.string.find_not_found, search_text);
    	Toast.makeText(CommentsListActivity.this, not_found_msg, Toast.LENGTH_LONG).show();
	}

	private boolean getFoundPosition(int start_index, int end_index, String search_text) {
    	for (int i = start_index; i < end_index; i++) {
    		ThingInfo ci = mCommentsAdapter.getItem(i);

    		if (ci == null) continue;

    		String comment_body = ci.getBody();
    		if (comment_body == null) continue;

    		if (comment_body.toLowerCase().contains(search_text)) {
    			final int position = i;
    			getListView().post(new Runnable() {
	    			@Override
	    			public void run() {
	    				setSelection(position);
	    				getListView().requestFocus();
	    			}
    			});

				last_found_position = i;
				return true;
    		}
    	}
		last_found_position = -1;
    	return false;
	}

    @Override
    protected Dialog onCreateDialog(int id) {
    	final Dialog dialog;
    	ProgressDialog pdialog;
    	AlertDialog.Builder builder;
    	LayoutInflater inflater;
    	
    	switch (id) {
    	case Constants.DIALOG_LOGIN:
    		dialog = new LoginDialog(this, mSettings, false) {
				@Override
				public void onLoginChosen(String user, String password) {
					removeDialog(Constants.DIALOG_LOGIN);
    				new MyLoginTask(user, password).execute();
				}
			};
    		break;
    		
    	case Constants.DIALOG_COMMENT_CLICK:
    		dialog = new CommentClickDialog(this, mSettings);
    		break;

    	case Constants.DIALOG_REPLY:
    	{
    		dialog = new Dialog(this, mSettings.getDialogTheme());
    		dialog.setContentView(R.layout.compose_reply_dialog);
    		final EditText replyBody = (EditText) dialog.findViewById(R.id.body);
    		final Button replySaveButton = (Button) dialog.findViewById(R.id.reply_save_button);
    		final Button replyCancelButton = (Button) dialog.findViewById(R.id.reply_cancel_button);
    		
			replySaveButton.setOnClickListener(new OnClickListener() {
    			@Override
				public void onClick(View v) {
    				if (mReplyTargetName != null) {
	    				new CommentReplyTask(mReplyTargetName).execute(replyBody.getText().toString());
	    				dialog.dismiss();
    				}
    				else {
    					Common.showErrorToast("Error replying. Please try again.", Toast.LENGTH_SHORT, CommentsListActivity.this);
    				}
    			}
    		});
    		replyCancelButton.setOnClickListener(new OnClickListener() {
    			@Override
    			public void onClick(View v) {
    				mVoteTargetThing.setReplyDraft(replyBody.getText().toString());
    				dialog.cancel();
    			}
    		});
    		dialog.setCancelable(false);  // disallow the BACK key
    		dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
				public void onCancel(DialogInterface dialog) {
					replyBody.setText("");
				}
    		});
    		break;
    	}
    		
    	case Constants.DIALOG_EDIT:
    	{
    		dialog = new Dialog(this, mSettings.getDialogTheme());
    		dialog.setContentView(R.layout.compose_reply_dialog);
    		final EditText replyBody = (EditText) dialog.findViewById(R.id.body);
    		final Button replySaveButton = (Button) dialog.findViewById(R.id.reply_save_button);
    		final Button replyCancelButton = (Button) dialog.findViewById(R.id.reply_cancel_button);

			replyBody.setText(mEditTargetBody);
			
			replySaveButton.setOnClickListener(new OnClickListener() {
    			public void onClick(View v) {
    				if (mReplyTargetName != null) {
	    				new EditTask(mReplyTargetName).execute(replyBody.getText().toString());
	    				dialog.dismiss();
    				}
    				else {
    					Common.showErrorToast("Error editing. Please try again.", Toast.LENGTH_SHORT, CommentsListActivity.this);
    				}
    			}
    		});
			replyCancelButton.setOnClickListener(new OnClickListener() {
    			public void onClick(View v) {
    				dialog.cancel();
    			}
    		});
    		dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
				public void onCancel(DialogInterface dialog) {
					replyBody.setText("");
				}
    		});
    		break;
		}
    		
    	case Constants.DIALOG_DELETE:
    		builder = new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme()));
    		builder.setTitle("Really delete this?");
    		builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
    			public void onClick(DialogInterface dialog, int item) {
    				removeDialog(Constants.DIALOG_DELETE);
    				new DeleteTask(mDeleteTargetKind).execute(mReplyTargetName);
    			}
    		})
    		.setNegativeButton("No", new DialogInterface.OnClickListener() {
				public void onClick(DialogInterface dialog, int id) {
					dialog.cancel();
				}
			});
    		dialog = builder.create();
    		break;
    		
    	case Constants.DIALOG_SORT_BY:
    		builder = new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme()));
    		builder.setTitle("Sort by:");
			int selectedSortBy = -1;
			for (int i = 0; i < Constants.CommentsSort.SORT_BY_URL_CHOICES.length; i++) {
				if (Constants.CommentsSort.SORT_BY_URL_CHOICES[i].equals(mSettings.getCommentsSortByUrl())) {
					selectedSortBy = i;
					break;
				}
			}
    		builder.setSingleChoiceItems(Constants.CommentsSort.SORT_BY_CHOICES, selectedSortBy, sortByOnClickListener);
    		dialog = builder.create();
    		break;
    		
    	case Constants.DIALOG_REPORT:
    		builder = new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme()));
    		builder.setTitle("Really report this?");
    		builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
    			public void onClick(DialogInterface dialog, int item) {
    				removeDialog(Constants.DIALOG_REPORT);
    				new ReportTask(mReportTargetName.toString()).execute();
    			}
    		})
    		.setNegativeButton("No", new DialogInterface.OnClickListener() {
				public void onClick(DialogInterface dialog, int id) {
					dialog.cancel();
				}
			});
    		dialog = builder.create();
    		break;
    		
   		// "Please wait"
    	case Constants.DIALOG_DELETING:
    		pdialog = new ProgressDialog(new ContextThemeWrapper(this, mSettings.getDialogTheme()));
    		pdialog.setMessage("Deleting...");
    		pdialog.setIndeterminate(true);
    		pdialog.setCancelable(true);
    		dialog = pdialog;
    		break;
    	case Constants.DIALOG_EDITING:
    		pdialog = new ProgressDialog(new ContextThemeWrapper(this, mSettings.getDialogTheme()));
    		pdialog.setMessage("Submitting edit...");
    		pdialog.setIndeterminate(true);
    		pdialog.setCancelable(true);
    		dialog = pdialog;
    		break;
    	case Constants.DIALOG_LOGGING_IN:
    		pdialog = new ProgressDialog(new ContextThemeWrapper(this, mSettings.getDialogTheme()));
    		pdialog.setMessage("Logging in...");
    		pdialog.setIndeterminate(true);
    		pdialog.setCancelable(true);
    		dialog = pdialog;
    		break;
    	case Constants.DIALOG_REPLYING:
    		pdialog = new ProgressDialog(new ContextThemeWrapper(this, mSettings.getDialogTheme()));
    		pdialog.setMessage("Sending reply...");
    		pdialog.setIndeterminate(true);
    		pdialog.setCancelable(true);
    		dialog = pdialog;
    		break;
    	case Constants.DIALOG_FIND:
    		inflater = (LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

    		View content = inflater.inflate(R.layout.dialog_find, null);
    		final EditText find_box = (EditText) content.findViewById(R.id.input_find_box);
//    		final CheckBox wrap_box = (CheckBox) content.findViewById(R.id.find_wrap_checkbox);

    		builder = new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme()));
    		builder.setView(content);
    		builder.setTitle(R.string.find)
    		.setPositiveButton(R.string.find, new DialogInterface.OnClickListener() {
				@Override
				public void onClick(DialogInterface dialog, int which) {
					String search_text = find_box.getText().toString().toLowerCase();
//					findCommentText(search_text, wrap_box.isChecked(), false);
					findCommentText(search_text, true, false);
				}
    		})
    		.setNegativeButton("Cancel", null);
    		dialog = builder.create();
    		break;
    	default:
    		throw new IllegalArgumentException("Unexpected dialog id "+id);
    	}
    	return dialog;
    }
    
    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
    	super.onPrepareDialog(id, dialog);
    	StringBuilder sb;
    	    	
    	switch (id) {
    	case Constants.DIALOG_LOGIN:
    		if (mSettings.getUsername() != null) {
	    		final TextView loginUsernameInput = (TextView) dialog.findViewById(R.id.login_username_input);
	    		loginUsernameInput.setText(mSettings.getUsername());
    		}
    		final TextView loginPasswordInput = (TextView) dialog.findViewById(R.id.login_password_input);
    		loginPasswordInput.setText("");
    		break;
    		
    	case Constants.DIALOG_COMMENT_CLICK:
    		if (mVoteTargetThing == null)
    			break;
    		Boolean likes;
    		final TextView titleView = (TextView) dialog.findViewById(R.id.title);
    		final TextView urlView = (TextView) dialog.findViewById(R.id.url);
    		final TextView submissionStuffView = (TextView) dialog.findViewById(R.id.submissionTime_submitter_subreddit);
    		final Button linkButton = (Button) dialog.findViewById(R.id.thread_link_button);
			
    		if (mVoteTargetThing == getOpThingInfo()) {
				likes = mVoteTargetThing.getLikes();
    			titleView.setVisibility(View.VISIBLE);
    			titleView.setText(getOpThingInfo().getTitle());
    			urlView.setVisibility(View.VISIBLE);
    			urlView.setText(getOpThingInfo().getUrl());
    			submissionStuffView.setVisibility(View.VISIBLE);
        		sb = new StringBuilder(Util.getTimeAgo(getOpThingInfo().getCreated_utc()))
	    			.append(" by ").append(getOpThingInfo().getAuthor());
        		submissionStuffView.setText(sb);
    			// For self posts, you're already there!
    			if (getOpThingInfo().getDomain().toLowerCase().startsWith("self.")) {
    				linkButton.setText(R.string.comment_links_button);
    				linkToEmbeddedURLs(linkButton);
    			} else {
    				final String url = getOpThingInfo().getUrl();
    				linkButton.setText(R.string.thread_link_button);
	    			linkButton.setOnClickListener(new OnClickListener() {
	    				public void onClick(View v) {
	    					removeDialog(Constants.DIALOG_COMMENT_CLICK);
	    					setLinkClicked(getOpThingInfo());
	    					Common.launchBrowser(CommentsListActivity.this, url,
	    							Util.createThreadUri(getOpThingInfo()).toString(),
	    							false, false, mSettings.isUseExternalBrowser(),
	    							mSettings.isSaveHistory());
	    				}
	    			});
	    			linkButton.setEnabled(true);
    			}
    		} else {
    			titleView.setText("Comment by " + mVoteTargetThing.getAuthor());
    			likes = mVoteTargetThing.getLikes();
    			urlView.setVisibility(View.INVISIBLE);
    			submissionStuffView.setVisibility(View.INVISIBLE);

    			// Get embedded URLs
    			linkButton.setText(R.string.comment_links_button);
    	    	linkToEmbeddedURLs(linkButton);
    		}
    		final CheckBox voteUpButton = (CheckBox) dialog.findViewById(R.id.vote_up_button);
    		final CheckBox voteDownButton = (CheckBox) dialog.findViewById(R.id.vote_down_button);
    		final Button replyButton = (Button) dialog.findViewById(R.id.reply_button);
    		final Button loginButton = (Button) dialog.findViewById(R.id.login_button);
    		
    		// Only show upvote/downvote if user is logged in
    		if (mSettings.isLoggedIn()) {
    			loginButton.setVisibility(View.GONE);
    			voteUpButton.setVisibility(View.VISIBLE);
    			voteDownButton.setVisibility(View.VISIBLE);
    			replyButton.setEnabled(true);
    			
    			// Make sure the setChecked() actions don't actually vote just yet.
    			voteUpButton.setOnCheckedChangeListener(null);
    			voteDownButton.setOnCheckedChangeListener(null);
    			
    			// Set initial states of the vote buttons based on user's past actions
	    		if (likes == null) {
	    			// User is currently neutral
	    			voteUpButton.setChecked(false);
	    			voteDownButton.setChecked(false);
	    		} else if (likes == true) {
	    			// User currenty likes it
	    			voteUpButton.setChecked(true);
	    			voteDownButton.setChecked(false);
	    		} else {
	    			// User currently dislikes it
	    			voteUpButton.setChecked(false);
	    			voteDownButton.setChecked(true);
	    		}
	    		// Now we want the user to be able to vote.
	    		voteUpButton.setOnCheckedChangeListener(voteUpOnCheckedChangeListener);
	    		voteDownButton.setOnCheckedChangeListener(voteDownOnCheckedChangeListener);

	    		// The "reply" button
    			replyButton.setOnClickListener(replyOnClickListener);	
	    	} else {
	    		replyButton.setEnabled(false);
    			
	    		voteUpButton.setVisibility(View.GONE);
    			voteDownButton.setVisibility(View.GONE);
    			loginButton.setVisibility(View.VISIBLE);
    			loginButton.setOnClickListener(loginOnClickListener);
    		}
    		break;
    		
    	case Constants.DIALOG_REPLY:
    		if (mVoteTargetThing != null) {
        		if (mVoteTargetThing.getReplyDraft() != null && !mShouldClearReply) {
        			EditText replyBodyView = (EditText) dialog.findViewById(R.id.body);
        			replyBodyView.setText(mVoteTargetThing.getReplyDraft());
        		}
        		else {
        			EditText replyBodyView = (EditText) dialog.findViewById(R.id.body);
        			replyBodyView.setText("");
        			mShouldClearReply = false;
        		}
    		}
    		break;
    		
    	case Constants.DIALOG_EDIT:
    		EditText replyBodyView = (EditText) dialog.findViewById(R.id.body);
			replyBodyView.setText(mEditTargetBody);
    		break;
    		
		default:
			// No preparation based on app state is required.
			break;
    	}
    }
    
    /**
     * Helper function to add links from mVoteTargetThing to the button
     * @param linkButton Button that should open list of links
     */
    private void linkToEmbeddedURLs(Button linkButton) {
		final ArrayList<String> urls = new ArrayList<String>();
		final ArrayList<MarkdownURL> vtUrls = mVoteTargetThing.getUrls();
		int urlsCount = vtUrls.size();
		for (int i = 0; i < urlsCount; i++) {
			urls.add(vtUrls.get(i).url);
		}
		if (urlsCount == 0) {
			linkButton.setEnabled(false);
        } else {
        	linkButton.setEnabled(true);
        	linkButton.setOnClickListener(new OnClickListener() {
        		public void onClick(View v) {
        			removeDialog(Constants.DIALOG_COMMENT_CLICK);      
        			
    	            ArrayAdapter<MarkdownURL> adapter = 
    	                new ArrayAdapter<MarkdownURL>(CommentsListActivity.this, android.R.layout.select_dialog_item, vtUrls) {
    	                public View getView(int position, View convertView, ViewGroup parent) {
    	                	TextView tv;
    	                    if (convertView == null) {
    	                        tv = (TextView) ((LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE))
    	                        		.inflate(android.R.layout.select_dialog_item, null);
    	                    } else {
    	                        tv = (TextView) convertView;
    	                    }

	                        String url = getItem(position).url;
	                        String anchorText = getItem(position).anchorText;
	                        if (Constants.LOGGING) Log.d(TAG, "links url="+url + " anchorText="+anchorText);
	                        
	                        Drawable d = null;
							try {
								d = getPackageManager().getActivityIcon(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
							} catch (NameNotFoundException ignore) {
							}
	                        if (d != null) {
	                            d.setBounds(0, 0, d.getIntrinsicHeight(), d.getIntrinsicHeight());
	                            tv.setCompoundDrawablePadding(10);
	                            tv.setCompoundDrawables(d, null, null, null);
	                        }
	                        
	                        final String telPrefix = "tel:";
	                        if (url.startsWith(telPrefix)) {
	                            url = PhoneNumberUtils.formatNumber(url.substring(telPrefix.length()));
	                        }
	                        
							if (anchorText != null)
								tv.setText(Html.fromHtml("<span>" + anchorText + "</span><br /><small>" + url + "</small>"));
							else
								tv.setText(Html.fromHtml(url));
							
    	                    return tv;
    	                }
    	            };

    	            AlertDialog.Builder b = new AlertDialog.Builder(new ContextThemeWrapper(CommentsListActivity.this, mSettings.getDialogTheme()));

    	            DialogInterface.OnClickListener click = new DialogInterface.OnClickListener() {
    	                public final void onClick(DialogInterface dialog, int which) {
    	                    if (which >= 0) {
    	                        Common.launchBrowser(CommentsListActivity.this, urls.get(which),
    	                        		Util.createThreadUri(getOpThingInfo()).toString(),
    	                        		false, false, mSettings.isUseExternalBrowser(),
    	                        		mSettings.isSaveHistory());
    	                    }
    	                }
    	            };
    	                
    	            b.setTitle(R.string.select_link_title);
    	            b.setCancelable(true);
    	            b.setAdapter(adapter, click);

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

    	            b.show();
        		}
        	});
        }
    }
    
    public static void fillCommentsListItemView(View view, ThingInfo item, RedditSettings settings) {
        // Set the values of the Views for the CommentsListItem
        
        TextView votesView = (TextView) view.findViewById(R.id.votes);
        TextView submitterView = (TextView) view.findViewById(R.id.submitter);
        TextView bodyView = (TextView) view.findViewById(R.id.body);
        
        TextView submissionTimeView = (TextView) view.findViewById(R.id.submissionTime);
        ImageView voteUpView = (ImageView) view.findViewById(R.id.vote_up_image);
        ImageView voteDownView = (ImageView) view.findViewById(R.id.vote_down_image);
        
        try {
        	votesView.setText(Util.showNumPoints(item.getUps() - item.getDowns()));
        } catch (NumberFormatException e) {
        	// This happens because "ups" comes after the potentially long "replies" object,
        	// so the ListView might try to display the View before "ups" in JSON has been parsed.
        	if (Constants.LOGGING) Log.e(TAG, "getView, normal comment", e);
        }
        if (item.getSSAuthor() != null)
        	submitterView.setText(item.getSSAuthor());
        else
        	submitterView.setText(item.getAuthor());
        submissionTimeView.setText(Util.getTimeAgo(item.getCreated_utc()));
        
    	if (item.getSpannedBody() != null)
    		bodyView.setText(item.getSpannedBody());
    	else
    		bodyView.setText(item.getBody());
        
        setCommentIndent(view, item.getIndent(), settings);
        
        if (voteUpView != null && voteDownView != null) {
	        if (item.getLikes() == null || "[deleted]".equals(item.getAuthor())) {
	        	voteUpView.setVisibility(View.GONE);
	        	voteDownView.setVisibility(View.GONE);
	    	}
	        else if (Boolean.TRUE.equals(item.getLikes())) {
	    		voteUpView.setVisibility(View.VISIBLE);
	    		voteDownView.setVisibility(View.GONE);
	    	}
	        else if (Boolean.FALSE.equals(item.getLikes())) {
	    		voteUpView.setVisibility(View.GONE);
	    		voteDownView.setVisibility(View.VISIBLE);
	    	}
        }
    }

    
    private final CompoundButton.OnCheckedChangeListener voteUpOnCheckedChangeListener = new CompoundButton.OnCheckedChangeListener() {
    	public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
	    	removeDialog(Constants.DIALOG_COMMENT_CLICK);
	    	String thingFullname = mVoteTargetThing.getName();
			if (isChecked)
				new VoteTask(thingFullname, 1).execute();
			else
				new VoteTask(thingFullname, 0).execute();
		}
    };
    private final CompoundButton.OnCheckedChangeListener voteDownOnCheckedChangeListener = new CompoundButton.OnCheckedChangeListener() {
	    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
	    	removeDialog(Constants.DIALOG_COMMENT_CLICK);
	    	String thingFullname = mVoteTargetThing.getName();
			if (isChecked)
				new VoteTask(thingFullname, -1).execute();
			else
				new VoteTask(thingFullname, 0).execute();
		}
    };
    
    private final OnClickListener replyOnClickListener = new OnClickListener() {
		public void onClick(View v) {
			removeDialog(Constants.DIALOG_COMMENT_CLICK);
			showDialog(Constants.DIALOG_REPLY);
		}
	};
	
	private final OnClickListener loginOnClickListener = new OnClickListener() {
		public void onClick(View v) {
			removeDialog(Constants.DIALOG_COMMENT_CLICK);
			showDialog(Constants.DIALOG_LOGIN);
		}
	};
    
	private final DialogInterface.OnClickListener sortByOnClickListener = new DialogInterface.OnClickListener() {
		public void onClick(DialogInterface dialog, int item) {
			dialog.dismiss();
			mSettings.setCommentsSortByUrl(Constants.CommentsSort.SORT_BY_URL_CHOICES[item]);
			getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
		}
	};
	
	private final ThumbnailOnClickListenerFactory mThumbnailOnClickListenerFactory
			= new ThumbnailOnClickListenerFactory() {
		@Override
		public OnClickListener getThumbnailOnClickListener(final ThingInfo threadThingInfo, final Activity activity) {
			return new OnClickListener() {
				public void onClick(View v) {
					setLinkClicked(threadThingInfo);
					Common.launchBrowser(
							activity,
							threadThingInfo.getUrl(),
							Util.createThreadUri(threadThingInfo).toString(),
							false,
							false,
							mSettings.isUseExternalBrowser(),
							mSettings.isSaveHistory()
					);
				}
			};
		}
	};
	
	private void setLinkClicked(ThingInfo threadThingInfo) {
		threadThingInfo.setClicked(true);
		mCommentsAdapter.notifyDataSetChanged();
	}


    
    @Override
    protected void onSaveInstanceState(Bundle state) {
    	super.onSaveInstanceState(state);
    	state.putString(Constants.REPLY_TARGET_NAME_KEY, mReplyTargetName);
    	state.putString(Constants.REPORT_TARGET_NAME_KEY, mReportTargetName);
    	state.putString(Constants.EDIT_TARGET_BODY_KEY, mEditTargetBody);
    	state.putString(Constants.DELETE_TARGET_KIND_KEY, mDeleteTargetKind);
    	state.putString(Constants.SUBREDDIT_KEY, mSubreddit);
    	state.putString(Constants.THREAD_ID_KEY, mThreadId);
    	state.putString(Constants.THREAD_TITLE_KEY, mThreadTitle);
    	state.putParcelable(Constants.VOTE_TARGET_THING_INFO_KEY, mVoteTargetThing);
    }
    
    /**
     * Called to "thaw" re-animate the app from a previous onSaveInstanceState().
     * 
     * @see android.app.Activity#onRestoreInstanceState
     */
    @Override
    protected void onRestoreInstanceState(Bundle state) {
        super.onRestoreInstanceState(state);
        final int[] myDialogs = {
    		Constants.DIALOG_COMMENT_CLICK,
        	Constants.DIALOG_DELETE,
        	Constants.DIALOG_DELETING,
        	Constants.DIALOG_EDIT,
        	Constants.DIALOG_EDITING,
        	Constants.DIALOG_LOGGING_IN,
        	Constants.DIALOG_LOGIN,
        	Constants.DIALOG_REPLY,
        	Constants.DIALOG_REPLYING,
        	Constants.DIALOG_SORT_BY,
        	Constants.DIALOG_REPORT
        };
        for (int dialog : myDialogs) {
	        try {
	        	removeDialog(dialog);
		    } catch (IllegalArgumentException e) {
		    	// Ignore.
		    }
        }
    }
} 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 FormValidation {

    public static boolean validateComposeMessageInputFields(
    		Context context,
    		final EditText composeDestination,
			final EditText composeSubject,
			final EditText composeText,
			final EditText composeCaptcha
	) {
		// reddit.com performs these sanity checks too.
		if ("".equals(composeDestination.getText().toString().trim())) {
			Toast.makeText(context, "please enter a username", Toast.LENGTH_LONG).show();
			return false;
		}
		if ("".equals(composeSubject.getText().toString().trim())) {
			Toast.makeText(context, "please enter a subject", Toast.LENGTH_LONG).show();
			return false;
		}
		if ("".equals(composeText.getText().toString().trim())) {
			Toast.makeText(context, "you need to enter a message", Toast.LENGTH_LONG).show();
			return false;
		}
		if (composeCaptcha.getVisibility() == View.VISIBLE && "".equals(composeCaptcha.getText().toString().trim())) {
			Toast.makeText(context, "", Toast.LENGTH_LONG).show();
			return false;
		}
		return true;
	}


} 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 RedditIsFunHttpClientFactory {
	
	private static final String TAG = "RedditIsFunHttpClientFactory";
	
	private static final DefaultHttpClient mGzipHttpClient = createGzipHttpClient();
	private static final CookieStore mCookieStore = mGzipHttpClient.getCookieStore();

	// Default connection and socket timeout of 60 seconds.  Tweak to taste.
	private static final int SOCKET_OPERATION_TIMEOUT = 60 * 1000;

	static DefaultHttpClient createGzipHttpClient() {
		HttpParams params = new BasicHttpParams();
		params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1);
		
		DefaultHttpClient httpclient = new DefaultHttpClient(params) {
		    @Override
		    protected ClientConnectionManager createClientConnectionManager() {
		        SchemeRegistry registry = new SchemeRegistry();
		        registry.register(
		                new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
		        registry.register(
		        		new Scheme("https", getHttpsSocketFactory(), 443));
		        HttpParams params = getParams();
				HttpConnectionParams.setConnectionTimeout(params, SOCKET_OPERATION_TIMEOUT);
				HttpConnectionParams.setSoTimeout(params, SOCKET_OPERATION_TIMEOUT);
				HttpConnectionParams.setSocketBufferSize(params, 8192);
		        return new ThreadSafeClientConnManager(params, registry);
		    }
		    
		    /** Gets an HTTPS socket factory with SSL Session Caching if such support is available, otherwise falls back to a non-caching factory
		     * @return
		     */
		    protected SocketFactory getHttpsSocketFactory(){
				try {
					Class<?> sslSessionCacheClass = Class.forName("android.net.SSLSessionCache");
			    	Object sslSessionCache = sslSessionCacheClass.getConstructor(Context.class).newInstance(RedditIsFunApplication.getApplication());
			    	Method getHttpSocketFactory = Class.forName("android.net.SSLCertificateSocketFactory").getMethod("getHttpSocketFactory", new Class<?>[]{int.class, sslSessionCacheClass});
			    	return (SocketFactory) getHttpSocketFactory.invoke(null, SOCKET_OPERATION_TIMEOUT, sslSessionCache);
				}catch(Exception e){
					return SSLSocketFactory.getSocketFactory();
				}
		    }
		};
		
		
        httpclient.addRequestInterceptor(new HttpRequestInterceptor() {
            public void process(
                    final HttpRequest request,
                    final HttpContext context
            ) throws HttpException, IOException {
            	RedditIsFunApplication app = RedditIsFunApplication.getApplication();
            	String version;
				try {
					version = app.getPackageManager().getPackageInfo(app.getPackageName(), 0).versionName;
				} catch (NameNotFoundException e) {
					Log.e(TAG, "Package name not found.", e);
					version = "1";
				}
            	String userAgent = app.getString(R.string.user_agent, version);
                request.setHeader("User-Agent", userAgent);
            	
                if (!request.containsHeader("Accept-Encoding"))
                    request.addHeader("Accept-Encoding", "gzip");
            }
        });
        httpclient.addResponseInterceptor(new HttpResponseInterceptor() {
            public void process(
                    final HttpResponse response, 
                    final HttpContext context) throws HttpException, IOException {
                HttpEntity entity = response.getEntity();
                Header ceheader = entity.getContentEncoding();
                if (ceheader != null) {
                    HeaderElement[] codecs = ceheader.getElements();
                    for (int i = 0; i < codecs.length; i++) {
                        if (codecs[i].getName().equalsIgnoreCase("gzip")) {
                            response.setEntity(
                                    new GzipDecompressingEntity(response.getEntity())); 
                            return;
                        }
                    }
                }
            }
        });
        return httpclient;
	}
    static class GzipDecompressingEntity extends HttpEntityWrapper {
        public GzipDecompressingEntity(final HttpEntity entity) {
            super(entity);
        }
        @Override
        public InputStream getContent()
            throws IOException, IllegalStateException {
            // the wrapped entity's getContent() decides about repeatability
            InputStream wrappedin = wrappedEntity.getContent();
            return new GZIPInputStream(wrappedin);
        }
        @Override
        public long getContentLength() {
            // length of ungzipped content is not known
            return -1;
        }
    }
	/**
	 * http://hc.apache.org/httpcomponents-client/examples.html
	 * @return a Gzip-enabled DefaultHttpClient
	 */
	public static HttpClient getGzipHttpClient() {
		return mGzipHttpClient;
	}
	public static CookieStore getCookieStore() {
		return mCookieStore;
	}

} public class VoteTask extends AsyncTask<Void, Void, Boolean> {
	private static final String TAG = "VoteTask";
	
	protected String _mThingFullname, _mSubreddit;
	protected int _mDirection;
	protected String _mUserError = "Error voting.";
	
	protected RedditSettings _mSettings;
	protected HttpClient _mClient;
	protected Context _mContext;
	
	public VoteTask(String thingFullname, int direction, String subreddit,
			Context context, RedditSettings settings, HttpClient client) {
		_mClient = client;
		_mSettings = settings;
		_mContext = context;
		_mThingFullname = thingFullname;
		_mDirection = direction;
		_mSubreddit = subreddit;
	}
	
	@Override
	public Boolean doInBackground(Void... v) {
    	HttpEntity entity = null;
    	
    	if (!_mSettings.isLoggedIn()) {
    		_mUserError = "You must be logged in to vote.";
    		return false;
    	}
    	
    	// Update the modhash if necessary
    	if (_mSettings.getModhash() == null) {
    		String modhash = Common.doUpdateModhash(_mClient);
    		if (modhash == null) {
    			// doUpdateModhash should have given an error about credentials
    			Common.doLogout(_mSettings, _mClient, _mContext);
    			if (Constants.LOGGING) Log.e(TAG, "Vote failed because doUpdateModhash() failed");
    			return false;
    		}
    		_mSettings.setModhash(modhash);
    	}
    	
    	try {
    		// Construct data
			List<NameValuePair> nvps = new ArrayList<NameValuePair>();
			nvps.add(new BasicNameValuePair("id", _mThingFullname));
			nvps.add(new BasicNameValuePair("dir", String.valueOf(_mDirection)));
			nvps.add(new BasicNameValuePair("r", _mSubreddit));
			nvps.add(new BasicNameValuePair("uh", _mSettings.getModhash()));
			// Votehash is currently unused by reddit 
//				nvps.add(new BasicNameValuePair("vh", "0d4ab0ffd56ad0f66841c15609e9a45aeec6b015"));
			
			HttpPost httppost = new HttpPost(Constants.REDDIT_BASE_URL + "/api/vote");
	        httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
	        
	        if (Constants.LOGGING) Log.d(TAG, nvps.toString());
	        
            // Perform the HTTP POST request
	    	HttpResponse response = _mClient.execute(httppost);
	    	entity = response.getEntity();
	    	
	    	String error = Common.checkResponseErrors(response, entity);
	    	if (error != null)
	    		throw new Exception(error);
	    	
        	return true;
        	
    	} catch (Exception e) {
    		if (Constants.LOGGING) Log.e(TAG, "VoteTask", e);
    		_mUserError = e.getMessage();
    	} finally {
    		if (entity != null) {
    			try {
    				entity.consumeContent();
    			} catch (Exception e2) {
    				if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent", e2);
    			}
    		}
    	}
    	return false;
    }
} 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 LoginTask extends AsyncTask<Void, Void, Boolean> {
	
	private static final String TAG = "LoginTask";
	
	protected String mUsername;
	private String mPassword;
	protected String mUserError = null;
	
	private RedditSettings mSettings;
	private HttpClient mClient;
	private Context mContext;
	
	protected LoginTask(String username, String password, RedditSettings settings, HttpClient client, Context context) {
		mUsername = username;
		mPassword = password;
		mSettings = settings;
		mClient = client;
		mContext = context;
	}
	
	@Override
	public Boolean doInBackground(Void... v) {
		return doLogin(mUsername, mPassword, mSettings, mClient, mContext);
    }
	
    /**
     * On success stores the session cookie and modhash in your RedditSettings.
     * On failure does not modify RedditSettings. 
     * Should be called from a background thread.
     * @return Error message, or null on success
     */
    private Boolean doLogin(String username, String password, RedditSettings settings, HttpClient client, Context context) {
		String status = "";
    	String userError = "Error logging in. Please try again.";
    	HttpEntity entity = null;
    	try {
    		// Construct data
    		List<NameValuePair> nvps = new ArrayList<NameValuePair>();
    		nvps.add(new BasicNameValuePair("user", username.toString()));
    		nvps.add(new BasicNameValuePair("passwd", password.toString()));
    		nvps.add(new BasicNameValuePair("api_type", "json"));
    		
            HttpPost httppost = new HttpPost(Constants.REDDIT_LOGIN_URL);
            httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
            
            // Set timeout to 45 seconds for login
            HttpParams params = httppost.getParams();
	        HttpConnectionParams.setConnectionTimeout(params, 45000);
	        HttpConnectionParams.setSoTimeout(params, 45000);
	        
            // Perform the HTTP POST request
        	HttpResponse response = client.execute(httppost);
        	status = response.getStatusLine().toString();
        	if (!status.contains("OK")) {
        		throw new HttpException(status);
        	}
        	
        	entity = response.getEntity();
        	
        	BufferedReader in = new BufferedReader(new InputStreamReader(entity.getContent()));
        	String line = in.readLine();
        	in.close();
        	entity.consumeContent();
        	if (StringUtils.isEmpty(line)) {
        		throw new HttpException("No content returned from login POST");
        	}
        	
        	if (Constants.LOGGING) Common.logDLong(TAG, line);
        	
        	if (RedditIsFunHttpClientFactory.getCookieStore().getCookies().isEmpty())
        		throw new HttpException("Failed to login: No cookies");
        	
        	final JsonFactory jsonFactory = new JsonFactory();
        	final JsonParser jp = jsonFactory.createJsonParser(line);
        	
        	// Go to the errors
        	while (jp.nextToken() != JsonToken.FIELD_NAME || !Constants.JSON_ERRORS.equals(jp.getCurrentName()))
        		;
        	if (jp.nextToken() != JsonToken.START_ARRAY)
        		throw new IllegalStateException("Login: expecting errors START_ARRAY");
        	if (jp.nextToken() != JsonToken.END_ARRAY) {
	        	if (line.contains("WRONG_PASSWORD")) {
	        		userError = "Bad password.";
	        		throw new Exception("Wrong password");
	        	} else {
	        		// Could parse for error code and error description but using whole line is easier.
	        		throw new Exception(line);
	        	}
        	}

        	// Getting here means you successfully logged in.
        	// Congratulations!
        	// You are a true reddit master!
        	
        	// Get modhash
        	while (jp.nextToken() != JsonToken.FIELD_NAME || !Constants.JSON_MODHASH.equals(jp.getCurrentName()))
        		;
        	jp.nextToken();
        	settings.setModhash(jp.getText());

        	// Could grab cookie from JSON too, but it lacks expiration date and stuff. So grab from HttpClient.
			List<Cookie> cookies = RedditIsFunHttpClientFactory.getCookieStore().getCookies();
        	for (Cookie c : cookies) {
        		if (c.getName().equals("reddit_session")) {
        			settings.setRedditSessionCookie(c);
        			break;
        		}
        	}
        	settings.setUsername(username);
        	
        	CookieSyncManager.getInstance().sync();
        	CacheInfo.invalidateAllCaches(context);
        	
        	return true;

    	} catch (Exception e) {
    		mUserError = userError;
    		if (entity != null) {
    			try {
    				entity.consumeContent();
    			} catch (Exception e2) {
    				if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e);
    			}
    		}
    		if (Constants.LOGGING) Log.e(TAG, "doLogin()", e);
        }
        return false;
    }
    
} public abstract class MessageComposeTask extends AsyncTask<String, Void, Boolean> {
	
	private static final String TAG = "MessageComposeTask";
	
	protected Dialog _mDialog;  // needed to update CAPTCHA on failure
	ThingInfo _mTargetThingInfo;
	protected String _mUserError = "Error composing message. Please try again.";
	String _mCaptcha;
	String _mCaptchaIden;
	
	RedditSettings _mSettings;
	HttpClient _mClient;
	Context _mContext;
	
	protected MessageComposeTask(Dialog dialog, ThingInfo targetThingInfo, String captcha, String captchaIden,
			RedditSettings settings, HttpClient client, Context context) {
		_mDialog = dialog;
		_mTargetThingInfo = targetThingInfo;
		_mCaptcha = captcha;
		_mCaptchaIden = captchaIden;
		_mSettings = settings;
		_mClient = client;
		_mContext = context;
	}
	
	@Override
    public Boolean doInBackground(String... text) {
    	HttpEntity entity = null;
    	
    	if (!_mSettings.isLoggedIn()) {
    		Common.showErrorToast("You must be logged in to compose a message.", Toast.LENGTH_LONG, _mContext);
    		_mUserError = "Not logged in";
    		return false;
    	}
    	// Update the modhash if necessary
    	if (_mSettings.getModhash() == null) {
    		String modhash = Common.doUpdateModhash(_mClient);
    		if (modhash == null) {
    			// doUpdateModhash should have given an error about credentials
    			Common.doLogout(_mSettings, _mClient, _mContext);
    			if (Constants.LOGGING) Log.e(TAG, "Message compose failed because doUpdateModhash() failed");
    			return false;
    		}
    		_mSettings.setModhash(modhash);
    	}
    	
    	try {
    		// Construct data
			List<NameValuePair> nvps = new ArrayList<NameValuePair>();
			nvps.add(new BasicNameValuePair("text", text[0].toString()));
			nvps.add(new BasicNameValuePair("subject", _mTargetThingInfo.getSubject()));
			nvps.add(new BasicNameValuePair("to", _mTargetThingInfo.getDest()));
			nvps.add(new BasicNameValuePair("uh", _mSettings.getModhash().toString()));
			nvps.add(new BasicNameValuePair("thing_id", ""));
			if (_mCaptchaIden != null) {
				nvps.add(new BasicNameValuePair("iden", _mCaptchaIden));
				nvps.add(new BasicNameValuePair("captcha", _mCaptcha.toString()));
			}
			
			HttpPost httppost = new HttpPost(Constants.REDDIT_BASE_URL + "/api/compose");
	        httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
	        
	        if (Constants.LOGGING) Log.d(TAG, nvps.toString());
	        
            // Perform the HTTP POST request
	    	HttpResponse response = _mClient.execute(httppost);
        	entity = response.getEntity();

       		// Don't need the return value ID since reply isn't posted to inbox
        	Common.checkIDResponse(response, entity);

        	return true;
        	
    	} catch (CaptchaException e) {
    		if (Constants.LOGGING) Log.e(TAG, "CaptchaException", e);
    		_mUserError = e.getMessage();
    	} catch (Exception e) {
    		if (Constants.LOGGING) Log.e(TAG, "MessageComposeTask", e);
    		_mUserError = e.getMessage();
    	} finally {
    		if (entity != null) {
    			try {
    				entity.consumeContent();
    			} catch (IOException e2) {
    				if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e2);
    			}
    		}
    	}
    	return false;
    }
	

} 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;
   	}
   } public class ThreadClickDialog extends Dialog {

	public ThreadClickDialog(Context context, RedditSettings settings) {
		super(context, settings.getDialogNoTitleTheme());
	}

	@Override
	public void onCreate(Bundle savedInstanceState) 
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.thread_click_dialog);

		Display display = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();

		LayoutParams params = getWindow().getAttributes(); 
		params.width = LayoutParams.FILL_PARENT;
		if (display.getOrientation() == Configuration.ORIENTATION_LANDSCAPE)
			params.height = LayoutParams.FILL_PARENT;
		getWindow().setAttributes((android.view.WindowManager.LayoutParams) params);
		
		setCanceledOnTouchOutside(true);
	}

} public interface ThreadClickDialogOnClickListenerFactory {
	OnClickListener getLoginOnClickListener();
	OnClickListener getLinkOnClickListener(ThingInfo thingInfo, boolean useExternalBrowser);
	OnClickListener getCommentsOnClickListener(ThingInfo thingInfo);
	CompoundButton.OnCheckedChangeListener getVoteUpOnCheckedChangeListener(ThingInfo thingInfo);
	CompoundButton.OnCheckedChangeListener getVoteDownOnCheckedChangeListener(ThingInfo thingInfo);
} public final class ThreadsListActivity extends ListActivity {

	private static final String TAG = "ThreadsListActivity";
	private final Pattern REDDIT_PATH_PATTERN = Pattern.compile(Constants.REDDIT_PATH_PATTERN_STRING);
	
	private final ObjectMapper mObjectMapper = Common.getObjectMapper();

    /** Custom list adapter that fits our threads data into the list. */
    private ThreadsListAdapter mThreadsAdapter = null;
    private ArrayList<ThingInfo> mThreadsList = null;
    private static final Object THREAD_ADAPTER_LOCK = new Object();

    private final HttpClient mClient = RedditIsFunHttpClientFactory.getGzipHttpClient();
	
   
    private final RedditSettings mSettings = new RedditSettings();
    
    // UI State
    private ThingInfo mVoteTargetThing = null;
    private DownloadThreadsTask mCurrentDownloadThreadsTask = null;
    private final Object mCurrentDownloadThreadsTaskLock = new Object();
    private View mNextPreviousView = null;
    
    private ShowThumbnailsTask mCurrentShowThumbnailsTask = null;
    private final Object mCurrentShowThumbnailsTaskLock = new Object();
    
    // Navigation that can be cached
    private String mSubreddit = Constants.FRONTPAGE_STRING;
    // The after, before, and count to navigate away from current page of results
    private String mAfter = null;
    private String mBefore = null;
    private volatile int mCount = Constants.DEFAULT_THREAD_DOWNLOAD_LIMIT;
    // The after, before, and count to navigate to current page
    private String mLastAfter = null;
    private String mLastBefore = null;
    private volatile int mLastCount = 0;
    private String mSortByUrl = Constants.ThreadsSort.SORT_BY_HOT_URL;
    private String mSortByUrlExtra = "";
    private String mJumpToThreadId = null;
    // End navigation variables
    
    // Menu
    private boolean mCanChord = false;
    
    
    /**
     * Called when the activity starts up. Do activity initialization
     * here, not in a constructor.
     * 
     * @see Activity#onCreate
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
		CookieSyncManager.createInstance(getApplicationContext());
		
        mSettings.loadRedditPreferences(getApplicationContext(), mClient);
        setRequestedOrientation(mSettings.getRotation());
        setTheme(mSettings.getTheme());
        requestWindowFeature(Window.FEATURE_PROGRESS);
        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
        
        setContentView(R.layout.threads_list_content);
        registerForContextMenu(getListView());
    	
		if (savedInstanceState != null) {
        	if (Constants.LOGGING) Log.d(TAG, "using savedInstanceState");
			mSubreddit = savedInstanceState.getString(Constants.SUBREDDIT_KEY);
	        if (mSubreddit == null)
	        	mSubreddit = mSettings.getHomepage();
	        mAfter = savedInstanceState.getString(Constants.AFTER_KEY);
	        mBefore = savedInstanceState.getString(Constants.BEFORE_KEY);
	        mCount = savedInstanceState.getInt(Constants.THREAD_COUNT_KEY);
	        mLastAfter = savedInstanceState.getString(Constants.LAST_AFTER_KEY);
	        mLastBefore = savedInstanceState.getString(Constants.LAST_BEFORE_KEY);
	        mLastCount = savedInstanceState.getInt(Constants.THREAD_LAST_COUNT_KEY);
	        mSortByUrl = savedInstanceState.getString(Constants.ThreadsSort.SORT_BY_KEY);
		    mJumpToThreadId = savedInstanceState.getString(Constants.JUMP_TO_THREAD_ID_KEY);
		    mVoteTargetThing = savedInstanceState.getParcelable(Constants.VOTE_TARGET_THING_INFO_KEY);
		    
		    // try to restore mThreadsList using getLastNonConfigurationInstance()
		    // (separate function to avoid a compiler warning casting ArrayList<ThingInfo>
		    restoreLastNonConfigurationInstance();
		    if (mThreadsList == null) {
	        	// Load previous view of threads
		        if (mLastAfter != null) {
		        	new MyDownloadThreadsTask(mSubreddit, mLastAfter, null, mLastCount).execute();
		        } else if (mLastBefore != null) {
		        	new MyDownloadThreadsTask(mSubreddit, null, mLastBefore, mLastCount).execute();
		        } else {
		        	new MyDownloadThreadsTask(mSubreddit).execute();
		        }
		    } else {
		    	// Orientation change. Use prior instance.
		    	resetUI(new ThreadsListAdapter(this, mThreadsList));
		    	if (Constants.FRONTPAGE_STRING.equals(mSubreddit))
		    		setTitle("reddit.com: what's new online!");
		    	else
		    		setTitle("/r/" + mSubreddit.trim());
		    }
        }
		// Handle subreddit Uri passed via Intent
        else if (getIntent().getData() != null) {
	    	Matcher redditContextMatcher = REDDIT_PATH_PATTERN.matcher(getIntent().getData().getPath());
			if (redditContextMatcher.matches()) {
				new MyDownloadThreadsTask(redditContextMatcher.group(1)).execute();
			} else {
				new MyDownloadThreadsTask(mSettings.getHomepage()).execute();
			}
		}
		// No subreddit specified by Intent, so load the user's home reddit
		else {
        	new MyDownloadThreadsTask(mSettings.getHomepage()).execute();
        }
    }
    
    @Override
    protected void onResume() {
    	super.onResume();
    	
        // SOPA blackout: Jan 18, 2012 from 8am-8pm EST (1300-0100 UTC)
    	long timeMillis = System.currentTimeMillis();
        if (timeMillis >= 1326891600000L && timeMillis <= 1326934800000L) {
        	Toast.makeText(this, "Let's Protest SOPA", Toast.LENGTH_LONG).show();
        	Common.launchBrowser(this, "http://www.reddit.com", null, false, true, false, false);
        	finish();
        	return;
        }
        
		int previousTheme = mSettings.getTheme();

    	mSettings.loadRedditPreferences(this, mClient);

    	if (mSettings.getTheme() != previousTheme) {
    		relaunchActivity();
    	}
    	else {
	    	CookieSyncManager.getInstance().startSync();
	    	setRequestedOrientation(mSettings.getRotation());
	    	
	    	updateNextPreviousButtons();
	    	
	    	if (mThreadsAdapter != null)
	    		jumpToThread();
	    	
	    	if (mSettings.isLoggedIn())
	    		new PeekEnvelopeTask(this, mClient, mSettings.getMailNotificationStyle()).execute();
    	}
    }
    
    private void relaunchActivity() {
    	finish();
    	startActivity(getIntent());
    }
    
    @Override
    protected void onPause() {
    	super.onPause();
		CookieSyncManager.getInstance().stopSync();
		mSettings.saveRedditPreferences(this);
    }
    
    @Override
    public Object onRetainNonConfigurationInstance() {
        // Avoid having to re-download and re-parse the threads list
    	// when rotating or opening keyboard.
    	return mThreadsList;
    }
    
    @SuppressWarnings("unchecked")
	private void restoreLastNonConfigurationInstance() {
    	mThreadsList = (ArrayList<ThingInfo>) getLastNonConfigurationInstance();
    }
    

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
    	super.onActivityResult(requestCode, resultCode, intent);
    	
    	switch(requestCode) {
    	case Constants.ACTIVITY_PICK_SUBREDDIT:
    		if (resultCode == Activity.RESULT_OK) {
    	    	Matcher redditContextMatcher = REDDIT_PATH_PATTERN.matcher(intent.getData().getPath());
    			if (redditContextMatcher.matches()) {
    				new MyDownloadThreadsTask(redditContextMatcher.group(1)).execute();
    			}
    		}
    		break;
    	default:
    		break;
    	}
    }
    
    /**
     * http://stackoverflow.com/questions/2257963/android-how-to-show-dialog-to-confirm-user-wishes-to-exit-activity
     */
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        //Handle the back button
        if(mSettings.isConfirmQuitOrLogout() && keyCode == KeyEvent.KEYCODE_BACK && isTaskRoot()) {
            //Ask the user if they want to quit
            new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme()))
            .setIcon(android.R.drawable.ic_dialog_alert)
            .setTitle(R.string.quit)
            .setMessage(R.string.really_quit)
            .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) {
                    //Stop the activity
                    finish();    
                }
            })
            .setNegativeButton(R.string.no, null)
            .show();

            return true;
        }
        else {
            return super.onKeyDown(keyCode, event);
        }
    }
    
    
    final class ThreadsListAdapter extends ArrayAdapter<ThingInfo> {
    	static final int THREAD_ITEM_VIEW_TYPE = 0;
    	// The number of view types
    	static final int VIEW_TYPE_COUNT = 1;
    	public boolean mIsLoading = true;
    	private LayoutInflater mInflater;
    	private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
        
        public ThreadsListAdapter(Context context, List<ThingInfo> objects) {
            super(context, 0, objects);
            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }
        
        @Override
        public int getItemViewType(int position) {
        	if (position == mFrequentSeparatorPos) {
                // We don't want the separator view to be recycled.
                return IGNORE_ITEM_VIEW_TYPE;
            }
    		return THREAD_ITEM_VIEW_TYPE;
        }
        
        @Override
        public int getViewTypeCount() {
        	return VIEW_TYPE_COUNT;
        }

        @Override
        public boolean isEmpty() {
        	if (mIsLoading)
        		return false;
        	return super.isEmpty();
        }
        
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View view;

            // Here view may be passed in for re-use, or we make a new one.
            if (convertView == null) {
                view = mInflater.inflate(R.layout.threads_list_item, null);
            } else {
                view = convertView;
            }
            
            ThingInfo item = this.getItem(position);
            
            // Set the values of the Views for the ThreadsListItem
            fillThreadsListItemView(
            		position, view, item, ThreadsListActivity.this, mClient, mSettings, mThumbnailOnClickListenerFactory
    		);
            
            return view;
        }
    }
    
    public static void fillThreadsListItemView(
    		int position,
    		View view,
    		ThingInfo item,
    		ListActivity activity,
    		HttpClient client,
    		RedditSettings settings,
    		ThumbnailOnClickListenerFactory thumbnailOnClickListenerFactory
	) {
    	
    	Resources res = activity.getResources();
    	
    	TextView titleView = (TextView) view.findViewById(R.id.title);
        TextView votesView = (TextView) view.findViewById(R.id.votes);
        TextView numCommentsSubredditView = (TextView) view.findViewById(R.id.numCommentsSubreddit);
        TextView nsfwView = (TextView) view.findViewById(R.id.nsfw);
//        TextView submissionTimeView = (TextView) view.findViewById(R.id.submissionTime);
        ImageView voteUpView = (ImageView) view.findViewById(R.id.vote_up_image);
        ImageView voteDownView = (ImageView) view.findViewById(R.id.vote_down_image);
        View thumbnailContainer = view.findViewById(R.id.thumbnail_view);
        FrameLayout thumbnailFrame = (FrameLayout) view.findViewById(R.id.thumbnail_frame);
        ImageView thumbnailImageView = (ImageView) view.findViewById(R.id.thumbnail);
        ProgressBar indeterminateProgressBar = (ProgressBar) view.findViewById(R.id.indeterminate_progress);
        
        // Set the title and domain using a SpannableStringBuilder
        SpannableStringBuilder builder = new SpannableStringBuilder();
        String title = item.getTitle();
        if (title == null)
        	title = "";
        SpannableString titleSS = new SpannableString(title);
        int titleLen = title.length();
        titleSS.setSpan(new TextAppearanceSpan(activity,
        		Util.getTextAppearanceResource(settings.getTheme(), android.R.style.TextAppearance_Large)),
        		0, titleLen, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        
        String domain = item.getDomain();
        if (domain == null)
        	domain = "";
        int domainLen = domain.length();
        SpannableString domainSS = new SpannableString("("+item.getDomain()+")");
        domainSS.setSpan(new TextAppearanceSpan(activity,
        		Util.getTextAppearanceResource(settings.getTheme(), android.R.style.TextAppearance_Small)),
        		0, domainLen+2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

        if (Util.isLightTheme(settings.getTheme())) {
            if (item.isClicked()) {
            	ForegroundColorSpan fcs = new ForegroundColorSpan(res.getColor(R.color.purple));
            	titleSS.setSpan(fcs, 0, titleLen, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            } else {
            	ForegroundColorSpan fcs = new ForegroundColorSpan(res.getColor(R.color.blue));
            	titleSS.setSpan(fcs, 0, titleLen, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
            domainSS.setSpan(new ForegroundColorSpan(res.getColor(R.color.gray_50)),
            		0, domainLen+2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        } else {
            if (item.isClicked()) {
            	ForegroundColorSpan fcs = new ForegroundColorSpan(res.getColor(R.color.gray_50));
            	titleSS.setSpan(fcs, 0, titleLen, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        	domainSS.setSpan(new ForegroundColorSpan(res.getColor(R.color.gray_75)),
            		0, domainLen+2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        
        builder.append(titleSS).append(" ").append(domainSS);
        titleView.setText(builder);
        
        votesView.setText("" + item.getScore());
        numCommentsSubredditView.setText(Util.showNumComments(item.getNum_comments()) + "  " + item.getSubreddit());
        
        if(item.isOver_18()){
            nsfwView.setVisibility(View.VISIBLE);
        } else {
            nsfwView.setVisibility(View.GONE);
        }
        
        // Set the up and down arrow colors based on whether user likes
        if (settings.isLoggedIn()) {
        	if (item.getLikes() == null) {
        		voteUpView.setImageResource(R.drawable.vote_up_gray);
        		voteDownView.setImageResource(R.drawable.vote_down_gray);
        		votesView.setTextColor(res.getColor(R.color.gray_75));
        	} else if (item.getLikes() == true) {
        		voteUpView.setImageResource(R.drawable.vote_up_red);
        		voteDownView.setImageResource(R.drawable.vote_down_gray);
        		votesView.setTextColor(res.getColor(R.color.arrow_red));
        	} else {
        		voteUpView.setImageResource(R.drawable.vote_up_gray);
        		voteDownView.setImageResource(R.drawable.vote_down_blue);
        		votesView.setTextColor(res.getColor(R.color.arrow_blue));
        	}
        } else {
    		voteUpView.setImageResource(R.drawable.vote_up_gray);
    		voteDownView.setImageResource(R.drawable.vote_down_gray);
    		votesView.setTextColor(res.getColor(R.color.gray_75));
        }
        
        // Thumbnails open links
        if (thumbnailContainer != null) {
        	if (Common.shouldLoadThumbnails(activity, settings)) {
        		thumbnailContainer.setVisibility(View.VISIBLE);
        		
            	if (item.getUrl() != null) {
            		OnClickListener thumbnailOnClickListener = thumbnailOnClickListenerFactory.getThumbnailOnClickListener(item, activity);
            		if (thumbnailOnClickListener != null) {
		            	thumbnailFrame.setOnClickListener(thumbnailOnClickListener);
            		}
            	}
            	
            	// Show thumbnail based on ThingInfo
            	if ("default".equals(item.getThumbnail()) || "self".equals(item.getThumbnail()) || StringUtils.isEmpty(item.getThumbnail())) {
        			indeterminateProgressBar.setVisibility(View.GONE);
            		thumbnailImageView.setVisibility(View.VISIBLE);
            		thumbnailImageView.setImageResource(R.drawable.go_arrow);
            	}
            	else {
            		indeterminateProgressBar.setVisibility(View.GONE);
            		thumbnailImageView.setVisibility(View.VISIBLE);
            		if (item.getThumbnailBitmap() != null) {
	            		thumbnailImageView.setImageBitmap(item.getThumbnailBitmap());
            		}
            		else {
            			thumbnailImageView.setImageBitmap(null);
            			new ShowThumbnailsTask(activity, client, R.drawable.go_arrow).execute(new ThumbnailLoadAction(item, thumbnailImageView, position));
            		}
            	}
            	
            	// Set thumbnail background based on current theme
            	if (Util.isLightTheme(settings.getTheme()))
            		thumbnailFrame.setBackgroundResource(R.drawable.thumbnail_background_light);
            	else
            		thumbnailFrame.setBackgroundResource(R.drawable.thumbnail_background_dark);
        	} else {
        		// if thumbnails disabled, hide thumbnail icon
        		thumbnailContainer.setVisibility(View.GONE);
        	}
        }
    }
    
	public static void fillThreadClickDialog(Dialog dialog, ThingInfo thingInfo, RedditSettings settings,
			ThreadClickDialogOnClickListenerFactory threadClickDialogOnClickListenerFactory) {
		
		final CheckBox voteUpButton = (CheckBox) dialog.findViewById(R.id.vote_up_button);
		final CheckBox voteDownButton = (CheckBox) dialog.findViewById(R.id.vote_down_button);
		final TextView titleView = (TextView) dialog.findViewById(R.id.title);
		final TextView urlView = (TextView) dialog.findViewById(R.id.url);
		final TextView submissionStuffView = (TextView) dialog.findViewById(R.id.submissionTime_submitter_subreddit);
		final Button loginButton = (Button) dialog.findViewById(R.id.login_button);
		final Button linkButton = (Button) dialog.findViewById(R.id.thread_link_button);
		final Button commentsButton = (Button) dialog.findViewById(R.id.thread_comments_button);
		
		titleView.setText(thingInfo.getTitle());
		urlView.setText(thingInfo.getUrl());
		StringBuilder sb = new StringBuilder(Util.getTimeAgo(thingInfo.getCreated_utc()))
			.append(" by ").append(thingInfo.getAuthor())
			.append(" to ").append(thingInfo.getSubreddit());
		submissionStuffView.setText(sb);
        
		// Only show upvote/downvote if user is logged in
		if (settings.isLoggedIn()) {
			loginButton.setVisibility(View.GONE);
			voteUpButton.setVisibility(View.VISIBLE);
			voteDownButton.setVisibility(View.VISIBLE);
			
			// Remove the OnCheckedChangeListeners because we are about to setChecked(),
			// and I think the Buttons are recycled, so old listeners will fire
			// for the previous vote target ThingInfo.
			voteUpButton.setOnCheckedChangeListener(null);
			voteDownButton.setOnCheckedChangeListener(null);
			
			// Set initial states of the vote buttons based on user's past actions
    		if (thingInfo.getLikes() == null) {
    			// User is currently neutral
    			voteUpButton.setChecked(false);
    			voteDownButton.setChecked(false);
    		} else if (thingInfo.getLikes() == true) {
    			// User currenty likes it
    			voteUpButton.setChecked(true);
    			voteDownButton.setChecked(false);
    		} else {
    			// User currently dislikes it
    			voteUpButton.setChecked(false);
    			voteDownButton.setChecked(true);
    		}
    		voteUpButton.setOnCheckedChangeListener(
    				threadClickDialogOnClickListenerFactory.getVoteUpOnCheckedChangeListener(thingInfo));
    		voteDownButton.setOnCheckedChangeListener(
    				threadClickDialogOnClickListenerFactory.getVoteDownOnCheckedChangeListener(thingInfo));
		} else {
			voteUpButton.setVisibility(View.GONE);
			voteDownButton.setVisibility(View.GONE);
			loginButton.setVisibility(View.VISIBLE);
			loginButton.setOnClickListener(
					threadClickDialogOnClickListenerFactory.getLoginOnClickListener());
		}

		// "link" button behaves differently for regular links vs. self posts and links to comments pages (e.g., bestof)
        if (thingInfo.isIs_self()) {
        	// It's a self post. Both buttons do the same thing.
        	linkButton.setEnabled(false);
        } else {
        	linkButton.setOnClickListener(
        			threadClickDialogOnClickListenerFactory.getLinkOnClickListener(thingInfo, settings.isUseExternalBrowser()));
        	linkButton.setEnabled(true);
        }
        
        // "comments" button is easy: always does the same thing
        commentsButton.setOnClickListener(
        		threadClickDialogOnClickListenerFactory.getCommentsOnClickListener(thingInfo));
	}

	
	/**
     * Jump to thread whose id is mJumpToThreadId. Then clear mJumpToThreadId.
     */
    private void jumpToThread() {
    	if (mJumpToThreadId == null || mThreadsAdapter == null)
    		return;
		for (int k = 0; k < mThreadsAdapter.getCount(); k++) {
			if (mJumpToThreadId.equals(mThreadsAdapter.getItem(k).getId())) {
				getListView().setSelection(k);
				mJumpToThreadId = null;
				break;
			}
		}
    }
    
    
    /**
     * Called when user clicks an item in the list. Starts an activity to
     * open the url for that item.
     */
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        ThingInfo item = mThreadsAdapter.getItem(position);
        
    	// Mark the thread as selected
    	mVoteTargetThing = item;
    	mJumpToThreadId = item.getId();
    	
    	showDialog(Constants.DIALOG_THREAD_CLICK);
    }

    /**
     * Resets the output UI list contents, retains session state.
     * @param threadsAdapter A ThreadsListAdapter to use. Pass in null if you want a new empty one created.
     */
    void resetUI(ThreadsListAdapter threadsAdapter) {
    	findViewById(R.id.loading_light).setVisibility(View.GONE);
    	findViewById(R.id.loading_dark).setVisibility(View.GONE);

    	if (mSettings.isAlwaysShowNextPrevious()) {
    		if (mNextPreviousView != null) {
    			getListView().removeFooterView(mNextPreviousView);
    			mNextPreviousView = null;
    		}
        } else {
        	findViewById(R.id.next_previous_layout).setVisibility(View.GONE);
        	if (getListView().getFooterViewsCount() == 0) {
	            // If we are not using the persistent navbar, then show as ListView footer instead
		        LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
		        mNextPreviousView = inflater.inflate(R.layout.next_previous_list_item, null);
		        getListView().addFooterView(mNextPreviousView);
        	}
        }

    	synchronized (THREAD_ADAPTER_LOCK) {
	    	if (threadsAdapter == null) {
	            // Reset the list to be empty.
		    	mThreadsList = new ArrayList<ThingInfo>();
				mThreadsAdapter = new ThreadsListAdapter(this, mThreadsList);
	    	} else {
	    		mThreadsAdapter = threadsAdapter;
	    	}
		    setListAdapter(mThreadsAdapter);
		    mThreadsAdapter.mIsLoading = false;
		    mThreadsAdapter.notifyDataSetChanged();  // Just in case
		}
	    Common.updateListDrawables(this, mSettings.getTheme());
	    updateNextPreviousButtons();
    }
    
    private void enableLoadingScreen() {
    	if (Util.isLightTheme(mSettings.getTheme())) {
        	findViewById(R.id.loading_light).setVisibility(View.VISIBLE);
        	findViewById(R.id.loading_dark).setVisibility(View.GONE);
    	} else {
        	findViewById(R.id.loading_light).setVisibility(View.GONE);
        	findViewById(R.id.loading_dark).setVisibility(View.VISIBLE);
    	}
    	synchronized (THREAD_ADAPTER_LOCK) {
	    	if (mThreadsAdapter != null)
	    		mThreadsAdapter.mIsLoading = true;
    	}
    	getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_START);
    }
    
    private void disableLoadingScreen() {
    	resetUI(mThreadsAdapter);
    	getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_END);
    }
    
    private void updateNextPreviousButtons() {
    	Common.updateNextPreviousButtons(this, mNextPreviousView, mAfter, mBefore, mCount, mSettings,
    			downloadAfterOnClickListener, downloadBeforeOnClickListener);
    }
    
    
    /**
     * Given a subreddit name string, starts the threadlist-download-thread going.
     * 
     * @param subreddit The name of a subreddit ("android", "gaming", etc.)
     *        If the number of elements in subreddit is >= 2, treat 2nd element as "after" 
     */
    private class MyDownloadThreadsTask extends DownloadThreadsTask {
    	
    	public MyDownloadThreadsTask(String subreddit) {
			super(getApplicationContext(),
					ThreadsListActivity.this.mClient,
					ThreadsListActivity.this.mObjectMapper,
					ThreadsListActivity.this.mSortByUrl,
					ThreadsListActivity.this.mSortByUrlExtra,
					subreddit);
		}
    	
    	public MyDownloadThreadsTask(String subreddit,
				String after, String before, int count) {
			super(getApplicationContext(),
					ThreadsListActivity.this.mClient,
					ThreadsListActivity.this.mObjectMapper,
					ThreadsListActivity.this.mSortByUrl,
					ThreadsListActivity.this.mSortByUrlExtra,
					subreddit, after, before, count);
		}

		@Override
    	protected void saveState() {
			mSettings.setModhash(mModhash);
			ThreadsListActivity.this.mSubreddit = mSubreddit;
			ThreadsListActivity.this.mLastAfter = mLastAfter;
			ThreadsListActivity.this.mLastBefore = mLastBefore;
			ThreadsListActivity.this.mLastCount = mLastCount;
			ThreadsListActivity.this.mAfter = mAfter;
			ThreadsListActivity.this.mBefore = mBefore;
			ThreadsListActivity.this.mCount = mCount;
			ThreadsListActivity.this.mSortByUrl = mSortByUrl;
			ThreadsListActivity.this.mSortByUrlExtra = mSortByUrlExtra;
    	}
    	
    	@Override
    	public void onPreExecute() {
    		synchronized (mCurrentDownloadThreadsTaskLock) {
	    		if (mCurrentDownloadThreadsTask != null) {
	    			this.cancel(true);
	    			return;
	    		}
    			mCurrentDownloadThreadsTask = this;
    		}
    		resetUI(null);
    		enableLoadingScreen();
			
    		if (mContentLength == -1) {
    			getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_INDETERMINATE_ON);
    		}
    		else {
    			getWindow().setFeatureInt(Window.FEATURE_PROGRESS, 0);
    		}
    		
	    	if (Constants.FRONTPAGE_STRING.equals(mSubreddit))
	    		setTitle("reddit.com: what's new online!");
	    	else
	    		setTitle("/r/" + mSubreddit.trim());
    	}
    	
    	@Override
    	public void onPostExecute(Boolean success) {
    		synchronized (mCurrentDownloadThreadsTaskLock) {
    			mCurrentDownloadThreadsTask = null;
    		}

    		disableLoadingScreen();

    		if (mContentLength == -1)
    			getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_INDETERMINATE_OFF);
    		else
    			getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_END);

    		if (success) {
    			synchronized (THREAD_ADAPTER_LOCK) {
    				mThreadsList.addAll(mThingInfos);
		    		mThreadsAdapter.notifyDataSetChanged();
    			}
    			
    			showThumbnails(mThingInfos);
    			
    			updateNextPreviousButtons();

	    		// Point the list to last thread user was looking at, if any
	    		jumpToThread();
    		} else {
    			if (!isCancelled())
    				Common.showErrorToast(mUserError, Toast.LENGTH_LONG, ThreadsListActivity.this);
    		}
    	}
    	
    	@Override
    	public void onProgressUpdate(Long... progress) {
    		if (mContentLength == -1) {
    			getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_INDETERMINATE_ON);
    		}
    		else {
    			getWindow().setFeatureInt(Window.FEATURE_PROGRESS, progress[0].intValue() * (Window.PROGRESS_END-1) / (int) mContentLength);
    		}
    	}
    	
    	public void propertyChange(PropertyChangeEvent event) {
    		publishProgress((Long) event.getNewValue());
    	}
    }
    
    private void showThumbnails(List<ThingInfo> thingInfos) {
    	if (Common.shouldLoadThumbnails(this, mSettings)) {
	    	int size = thingInfos.size();
	    	ThumbnailLoadAction[] thumbnailLoadActions = new ThumbnailLoadAction[size];
	    	for (int i = 0; i < size; i++) {
	    		thumbnailLoadActions[i] = new ThumbnailLoadAction(thingInfos.get(i), null, i);
	    	}
	    	synchronized (mCurrentShowThumbnailsTaskLock) {
	    		if (mCurrentShowThumbnailsTask != null)
	    			mCurrentShowThumbnailsTask.cancel(true);
	    		mCurrentShowThumbnailsTask = new ShowThumbnailsTask(this, mClient, R.drawable.go_arrow);
	    	}
	    	mCurrentShowThumbnailsTask.execute(thumbnailLoadActions);
    	}
    }
    
    private class MyLoginTask extends LoginTask {
    	public MyLoginTask(String username, String password) {
    		super(username, password, mSettings, mClient, getApplicationContext());
    	}
    	
    	@Override
    	protected void onPreExecute() {
    		showDialog(Constants.DIALOG_LOGGING_IN);
    	}
    	
    	@Override
    	protected void onPostExecute(Boolean success) {
    		removeDialog(Constants.DIALOG_LOGGING_IN);
    		if (success) {
    			Toast.makeText(ThreadsListActivity.this, "Logged in as "+mUsername, Toast.LENGTH_SHORT).show();
    			// Check mail
    			new PeekEnvelopeTask(getApplicationContext(), mClient, mSettings.getMailNotificationStyle()).execute();
    			// Refresh the threads list
    			new MyDownloadThreadsTask(mSubreddit).execute();
        	} else {
            	Common.showErrorToast(mUserError, Toast.LENGTH_LONG, ThreadsListActivity.this);
    		}
    	}
    }
    
    
    private class MyVoteTask extends VoteTask {
    	
    	private int _mPreviousScore;
    	private Boolean _mPreviousLikes;
    	private ThingInfo _mTargetThingInfo;
    	
    	public MyVoteTask(ThingInfo thingInfo, int direction, String subreddit) {
    		super(thingInfo.getName(), direction, subreddit, getApplicationContext(), mSettings, mClient);
    		_mTargetThingInfo = thingInfo;
    		_mPreviousScore = thingInfo.getScore();
    		_mPreviousLikes = thingInfo.getLikes();
    	}
    	
    	@Override
    	public void onPreExecute() {
    		if (!_mSettings.isLoggedIn()) {
        		Common.showErrorToast("You must be logged in to vote.", Toast.LENGTH_LONG, _mContext);
        		cancel(true);
        		return;
        	}
        	if (_mDirection < -1 || _mDirection > 1) {
        		if (Constants.LOGGING) Log.e(TAG, "WTF: _mDirection = " + _mDirection);
        		throw new RuntimeException("How the hell did you vote something besides -1, 0, or 1?");
        	}
        	int newScore;
        	Boolean newLikes;
        	_mPreviousScore = Integer.valueOf(_mTargetThingInfo.getScore());
        	_mPreviousLikes = _mTargetThingInfo.getLikes();
        	if (_mPreviousLikes == null) {
        		if (_mDirection == 1) {
        			newScore = _mPreviousScore + 1;
        			newLikes = true;
        		} else if (_mDirection == -1) {
        			newScore = _mPreviousScore - 1;
        			newLikes = false;
        		} else {
        			cancel(true);
        			return;
        		}
        	} else if (_mPreviousLikes == true) {
        		if (_mDirection == 0) {
        			newScore = _mPreviousScore - 1;
        			newLikes = null;
        		} else if (_mDirection == -1) {
        			newScore = _mPreviousScore - 2;
        			newLikes = false;
        		} else {
        			cancel(true);
        			return;
        		}
        	} else {
        		if (_mDirection == 1) {
        			newScore = _mPreviousScore + 2;
        			newLikes = true;
        		} else if (_mDirection == 0) {
        			newScore = _mPreviousScore + 1;
        			newLikes = null;
        		} else {
        			cancel(true);
        			return;
        		}
        	}
    		_mTargetThingInfo.setLikes(newLikes);
    		_mTargetThingInfo.setScore(newScore);
    		mThreadsAdapter.notifyDataSetChanged();
    	}
    	
    	@Override
    	public void onPostExecute(Boolean success) {
    		if (success) {
    			CacheInfo.invalidateCachedSubreddit(_mContext);
    		} else {
    			// Vote failed. Undo the score.
            	_mTargetThingInfo.setLikes(_mPreviousLikes);
        		_mTargetThingInfo.setScore(_mPreviousScore);
        		mThreadsAdapter.notifyDataSetChanged();
        		
    			Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, _mContext);
    		}
    	}
    }
    
    private final class MyHideTask extends HideTask {

		public MyHideTask(boolean hide, ThingInfo mVoteTargetThreadInfo,
				RedditSettings mSettings, Context mContext) {
			super(hide, mVoteTargetThreadInfo, mSettings, mContext);
		}
		
		@Override
		public void onPostExecute(Boolean success) {
			// super shows error on success==false
			super.onPostExecute(success);
			
			if (success) {
				synchronized (THREAD_ADAPTER_LOCK) {
					// Remove from list even if unhiding--because the only place you can
					// unhide from is the list of Hidden threads.
					mThreadsAdapter.remove(mTargetThreadInfo);
					mThreadsAdapter.notifyDataSetChanged();
				}
			}
		}
    	
    }
    
    
    /**
     * Populates the menu.
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.subreddit, menu);
        return true;
    }
    
    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo){
    	super.onCreateContextMenu(menu, v, menuInfo);
    	
    	AdapterView.AdapterContextMenuInfo info;
    	info = (AdapterView.AdapterContextMenuInfo) menuInfo;
    	ThingInfo _item = mThreadsAdapter.getItem(info.position);
    	
    	mVoteTargetThing = _item;
    	
    	menu.add(0, Constants.VIEW_SUBREDDIT_CONTEXT_ITEM, 0, R.string.view_subreddit);
    	menu.add(0, Constants.SHARE_CONTEXT_ITEM, 0, R.string.share);
    	menu.add(0, Constants.OPEN_IN_BROWSER_CONTEXT_ITEM, 0, R.string.open_browser);
    	
    	if(mSettings.isLoggedIn()){
    		if(!_item.isSaved()){
    			menu.add(0, Constants.SAVE_CONTEXT_ITEM, 0, "Save");
    		} else {
    			menu.add(0, Constants.UNSAVE_CONTEXT_ITEM, 0, "Unsave");
    		}
    		menu.add(0, Constants.HIDE_CONTEXT_ITEM, 0, "Hide");
    	}
    	
		menu.add(0, Constants.DIALOG_VIEW_PROFILE, Menu.NONE,
				String.format(getResources().getString(R.string.user_profile), _item.getAuthor()));
    }
    
    @Override
    public boolean onContextItemSelected(MenuItem item) {
    	AdapterView.AdapterContextMenuInfo info;
        info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
        
        ThingInfo _item = mThreadsAdapter.getItem(info.position);
        
        switch (item.getItemId()) {
        case Constants.VIEW_SUBREDDIT_CONTEXT_ITEM:
        	new MyDownloadThreadsTask(_item.getSubreddit()).execute();
        	return true;
        
        case Constants.SHARE_CONTEXT_ITEM:
			Intent intent = new Intent();
			intent.setAction(Intent.ACTION_SEND);
			intent.setType("text/plain");
			intent.putExtra(Intent.EXTRA_TEXT, _item.getUrl());
			try {
				startActivity(Intent.createChooser(intent, "Share Link"));
			} catch (android.content.ActivityNotFoundException ex) {
				if (Constants.LOGGING) Log.e(TAG, "Share Link", ex);
			}
			return true;
			
		case Constants.OPEN_IN_BROWSER_CONTEXT_ITEM:
			setLinkClicked(_item);
			Common.launchBrowser(this, _item.getUrl(), Util.createThreadUri(_item).toString(), false, true, true, mSettings.isSaveHistory());
			return true;
			
		case Constants.SAVE_CONTEXT_ITEM:
			new SaveTask(true, _item, mSettings, getApplicationContext()).execute();
			return true;
			
		case Constants.UNSAVE_CONTEXT_ITEM:
			new SaveTask(false, _item, mSettings, getApplicationContext()).execute();
			return true;
			
		case Constants.HIDE_CONTEXT_ITEM:
			new MyHideTask(true, _item, mSettings, getApplicationContext()).execute();
			return true;
			
		case Constants.UNHIDE_CONTEXT_ITEM:
			new MyHideTask(false, _item, mSettings, getApplicationContext()).execute();
			
    	case Constants.DIALOG_VIEW_PROFILE:
    		Intent i = new Intent(this, ProfileActivity.class);
    		i.setData(Util.createProfileUri(_item.getAuthor()));
    		startActivity(i);
    		return true;
    		
		default:
			return super.onContextItemSelected(item);
		}
    	
    }
    
	@Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        // This happens when the user begins to hold down the menu key, so
        // allow them to chord to get a shortcut.
        mCanChord = true;

    	super.onPrepareOptionsMenu(menu);
    	
    	MenuItem src, dest;
    	
        // Login/Logout
    	if (mSettings.isLoggedIn()) {
    		menu.findItem(R.id.login_menu_id).setVisible(false);

    		if(!mSubreddit.equals(Constants.FRONTPAGE_STRING)){
    			ArrayList<String> mSubredditsList = CacheInfo.getCachedSubredditList(getApplicationContext());	
    			
    			if(mSubredditsList != null && StringUtils.listContainsIgnoreCase(mSubredditsList, mSubreddit)){
	    			menu.findItem(R.id.unsubscribe_menu_id).setVisible(true);
	    			menu.findItem(R.id.subscribe_menu_id).setVisible(false);
	    		}
	    		else {
	    			menu.findItem(R.id.subscribe_menu_id).setVisible(true);
	    			menu.findItem(R.id.unsubscribe_menu_id).setVisible(false);
	    		}
    		}
    		
	        menu.findItem(R.id.inbox_menu_id).setVisible(true);
	        menu.findItem(R.id.user_profile_menu_id).setVisible(true);
	        menu.findItem(R.id.user_profile_menu_id).setTitle(
	        		String.format(getResources().getString(R.string.user_profile), mSettings.getUsername())
    		);
	        menu.findItem(R.id.logout_menu_id).setVisible(true);
	        menu.findItem(R.id.logout_menu_id).setTitle(
	        		String.format(getResources().getString(R.string.logout), mSettings.getUsername())
    		);
    	}
    	else {
			menu.findItem(R.id.login_menu_id).setVisible(true);

			menu.findItem(R.id.unsubscribe_menu_id).setVisible(false);
			menu.findItem(R.id.subscribe_menu_id).setVisible(false);

            menu.findItem(R.id.inbox_menu_id).setVisible(false);
            menu.findItem(R.id.user_profile_menu_id).setVisible(false);
            menu.findItem(R.id.logout_menu_id).setVisible(false);
    	}
    	
    	// Theme: Light/Dark
    	src = Util.isLightTheme(mSettings.getTheme()) ?
        		menu.findItem(R.id.dark_menu_id) :
        			menu.findItem(R.id.light_menu_id);
        dest = menu.findItem(R.id.light_dark_menu_id);
        dest.setTitle(src.getTitle());
        
        // Sort
        if (Constants.ThreadsSort.SORT_BY_HOT_URL.equals(mSortByUrl))
        	src = menu.findItem(R.id.sort_by_hot_menu_id);
        else if (Constants.ThreadsSort.SORT_BY_NEW_URL.equals(mSortByUrl))
        	src = menu.findItem(R.id.sort_by_new_menu_id);
        else if (Constants.ThreadsSort.SORT_BY_CONTROVERSIAL_URL.equals(mSortByUrl))
        	src = menu.findItem(R.id.sort_by_controversial_menu_id);
        else if (Constants.ThreadsSort.SORT_BY_TOP_URL.equals(mSortByUrl))
        	src = menu.findItem(R.id.sort_by_top_menu_id);
        dest = menu.findItem(R.id.sort_by_menu_id);
        dest.setTitle(src.getTitle());
    	
        return true;
    }
    
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (!mCanChord) {
            // The user has already fired a shortcut with this hold down of the
            // menu key.
            return false;
        }
        
        switch (item.getItemId()) {
        case R.id.pick_subreddit_menu_id:
    		Intent pickSubredditIntent = new Intent(getApplicationContext(), PickSubredditActivity.class);
    		startActivityForResult(pickSubredditIntent, Constants.ACTIVITY_PICK_SUBREDDIT);
    		break;
    	case R.id.login_menu_id:
    		showDialog(Constants.DIALOG_LOGIN);
    		break;
    	case R.id.logout_menu_id:
			if (mSettings.isConfirmQuitOrLogout()) {
				// Ask the user if they want to logout
				new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme()))
						.setIcon(android.R.drawable.ic_dialog_alert)
						.setTitle(R.string.confirm_logout_title)
						.setMessage(R.string.confirm_logout)
						.setPositiveButton(R.string.yes,
								new DialogInterface.OnClickListener() {
									public void onClick(DialogInterface dialog,
											int which) {
										ThreadsListActivity.this.logout();
									}
								}
						)
						.setNegativeButton(R.string.no, null)
						.show();
			} else {
				logout();
			}
			break;
    	case R.id.refresh_menu_id:
    		CacheInfo.invalidateCachedSubreddit(getApplicationContext());
    		new MyDownloadThreadsTask(mSubreddit).execute();
    		break;
    	case R.id.submit_link_menu_id:
    		Intent submitLinkIntent = new Intent(getApplicationContext(), SubmitLinkActivity.class);
    		submitLinkIntent.setData(Util.createSubmitUri(mSubreddit));
    		startActivity(submitLinkIntent);
    		break;
    	case R.id.sort_by_menu_id:
    		showDialog(Constants.DIALOG_SORT_BY);
    		break;
    	case R.id.open_browser_menu_id:
    		String url;
    		if (mSubreddit.equals(Constants.FRONTPAGE_STRING))
    			url = Constants.REDDIT_BASE_URL;
    		else
        		url = new StringBuilder(Constants.REDDIT_BASE_URL + "/r/").append(mSubreddit).toString();
    		Common.launchBrowser(this, url, null, false, true, true, false);
    		break;
        case R.id.light_dark_menu_id:
    		mSettings.setTheme(Util.getInvertedTheme(mSettings.getTheme()));
    		relaunchActivity();
    		break;
        case R.id.inbox_menu_id:
        	Intent inboxIntent = new Intent(getApplicationContext(), InboxActivity.class);
        	startActivity(inboxIntent);
        	break;
        case R.id.user_profile_menu_id:
        	Intent profileIntent = new Intent(getApplicationContext(), ProfileActivity.class);
        	startActivity(profileIntent);
        	break;
    	case R.id.preferences_menu_id:
            Intent prefsIntent = new Intent(getApplicationContext(), RedditPreferencesPage.class);
            startActivity(prefsIntent);
            break;
    	case R.id.subscribe_menu_id:
    		CacheInfo.invalidateCachedSubreddit(getApplicationContext());
    		new SubscribeTask(mSubreddit, getApplicationContext(), mSettings).execute();
    		break;
    	case R.id.unsubscribe_menu_id:
    		CacheInfo.invalidateCachedSubreddit(getApplicationContext());
    		new UnsubscribeTask(mSubreddit, getApplicationContext(), mSettings).execute();
    		break;
    	case android.R.id.home:
    		Common.goHome(this);
    		break;
    		
    	default:
    		throw new IllegalArgumentException("Unexpected action value "+item.getItemId());
    	}
    	
        return true;
    }
    
	private void logout() {
		Common.doLogout(mSettings, mClient, getApplicationContext());
		Toast.makeText(this, "You have been logged out.", Toast.LENGTH_SHORT)
				.show();
		new MyDownloadThreadsTask(mSubreddit).execute();
	}

    @Override
    protected Dialog onCreateDialog(int id) {
    	Dialog dialog;
    	ProgressDialog pdialog;
    	AlertDialog.Builder builder;
    	
    	switch (id) {
    	case Constants.DIALOG_LOGIN:
    		dialog = new LoginDialog(this, mSettings, false) {
				public void onLoginChosen(String user, String password) {
					removeDialog(Constants.DIALOG_LOGIN);
		        	new MyLoginTask(user, password).execute(); 
				}
			};
    		break;
    		
    	case Constants.DIALOG_THREAD_CLICK:
    		dialog = new ThreadClickDialog(this, mSettings);
    		break;
    		
    	case Constants.DIALOG_SORT_BY:
    		builder = new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme()));
    		builder.setTitle("Sort by:");
    		builder.setSingleChoiceItems(Constants.ThreadsSort.SORT_BY_CHOICES,
    				getSelectedSortBy(), sortByOnClickListener);
    		dialog = builder.create();
    		break;
    	case Constants.DIALOG_SORT_BY_NEW:
    		builder = new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme()));
    		builder.setTitle("what's new");
    		builder.setSingleChoiceItems(Constants.ThreadsSort.SORT_BY_NEW_CHOICES,
    				getSelectedSortByNew(), sortByNewOnClickListener);
    		dialog = builder.create();
    		break;
    	case Constants.DIALOG_SORT_BY_CONTROVERSIAL:
    		builder = new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme()));
    		builder.setTitle("most controversial");
    		builder.setSingleChoiceItems(Constants.ThreadsSort.SORT_BY_CONTROVERSIAL_CHOICES,
    				getSelectedSortByControversial(), sortByControversialOnClickListener);
    		dialog = builder.create();
    		break;
    	case Constants.DIALOG_SORT_BY_TOP:
    		builder = new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme()));
    		builder.setTitle("top scoring");
    		builder.setSingleChoiceItems(Constants.ThreadsSort.SORT_BY_TOP_CHOICES,
    				getSelectedSortByTop(), sortByTopOnClickListener);
    		dialog = builder.create();
    		break;

    	// "Please wait"
    	case Constants.DIALOG_LOGGING_IN:
    		pdialog = new ProgressDialog(new ContextThemeWrapper(this, mSettings.getDialogTheme()));
    		pdialog.setMessage("Logging in...");
    		pdialog.setIndeterminate(true);
    		pdialog.setCancelable(true);
    		dialog = pdialog;
    		break;
    	
    	default:
    		throw new IllegalArgumentException("Unexpected dialog id "+id);
    	}
    	return dialog;
    }
    
    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
    	super.onPrepareDialog(id, dialog);
    	
    	switch (id) {
    	case Constants.DIALOG_LOGIN:
    		if (mSettings.getUsername() != null) {
	    		final TextView loginUsernameInput = (TextView) dialog.findViewById(R.id.login_username_input);
	    		loginUsernameInput.setText(mSettings.getUsername());
    		}
    		final TextView loginPasswordInput = (TextView) dialog.findViewById(R.id.login_password_input);
    		loginPasswordInput.setText("");
    		break;
    		
    	case Constants.DIALOG_THREAD_CLICK:
    		if (mVoteTargetThing == null)
    			break;
    		fillThreadClickDialog(dialog, mVoteTargetThing, mSettings, mThreadClickDialogOnClickListenerFactory);
    		break;
    		
    	case Constants.DIALOG_SORT_BY:
    		((AlertDialog) dialog).getListView().setItemChecked(getSelectedSortBy(), true);
    		break;
    	case Constants.DIALOG_SORT_BY_NEW:
    		((AlertDialog) dialog).getListView().setItemChecked(getSelectedSortByNew(), true);
    		break;
    	case Constants.DIALOG_SORT_BY_CONTROVERSIAL:
    		((AlertDialog) dialog).getListView().setItemChecked(getSelectedSortByControversial(), true);
    		break;
    	case Constants.DIALOG_SORT_BY_TOP:
    		((AlertDialog) dialog).getListView().setItemChecked(getSelectedSortByTop(), true);
    		break;
    	
		default:
			// No preparation based on app state is required.
			break;
    	}
    }
    
	private int getSelectedSortBy() {
		for (int i = 0; i < Constants.ThreadsSort.SORT_BY_URL_CHOICES.length; i++) {
			if (Constants.ThreadsSort.SORT_BY_URL_CHOICES[i].equals(mSortByUrl)) {
				return i;
			}
		}
		return -1;
	}
	private int getSelectedSortByNew() {
		for (int i = 0; i < Constants.ThreadsSort.SORT_BY_NEW_URL_CHOICES.length; i++) {
			if (Constants.ThreadsSort.SORT_BY_NEW_URL_CHOICES[i].equals(mSortByUrlExtra)) {
				return i;
			}
		}
		return -1;
	}
	private int getSelectedSortByControversial() {
		for (int i = 0; i < Constants.ThreadsSort.SORT_BY_CONTROVERSIAL_URL_CHOICES.length; i++) {
			if (Constants.ThreadsSort.SORT_BY_CONTROVERSIAL_URL_CHOICES[i].equals(mSortByUrlExtra)) {
				return i;
			}
		}
		return -1;
	}
	private int getSelectedSortByTop() {
		for (int i = 0; i < Constants.ThreadsSort.SORT_BY_TOP_URL_CHOICES.length; i++) {
			if (Constants.ThreadsSort.SORT_BY_TOP_URL_CHOICES[i].equals(mSortByUrlExtra)) {
				return i;
			}
		}
		return -1;
	}

	private final OnClickListener downloadAfterOnClickListener = new OnClickListener() {
		public void onClick(View v) {
			new MyDownloadThreadsTask(mSubreddit, mAfter, null, mCount).execute();
		}
	};
	private final OnClickListener downloadBeforeOnClickListener = new OnClickListener() {
		public void onClick(View v) {
			new MyDownloadThreadsTask(mSubreddit, null, mBefore, mCount).execute();
		}
	};
    
    
	private final DialogInterface.OnClickListener sortByOnClickListener = new DialogInterface.OnClickListener() {
		public void onClick(DialogInterface dialog, int item) {
			dialog.dismiss();
			String itemString = Constants.ThreadsSort.SORT_BY_CHOICES[item];
			if (Constants.ThreadsSort.SORT_BY_HOT.equals(itemString)) {
				mSortByUrl = Constants.ThreadsSort.SORT_BY_HOT_URL;
				mSortByUrlExtra = "";
				new MyDownloadThreadsTask(mSubreddit).execute();
			} else if (Constants.ThreadsSort.SORT_BY_NEW.equals(itemString)) {
				showDialog(Constants.DIALOG_SORT_BY_NEW);
			} else if (Constants.ThreadsSort.SORT_BY_CONTROVERSIAL.equals(itemString)) {
				showDialog(Constants.DIALOG_SORT_BY_CONTROVERSIAL);
			} else if (Constants.ThreadsSort.SORT_BY_TOP.equals(itemString)) {
				showDialog(Constants.DIALOG_SORT_BY_TOP);
			}
		}
	};
	private final DialogInterface.OnClickListener sortByNewOnClickListener = new DialogInterface.OnClickListener() {
		public void onClick(DialogInterface dialog, int item) {
			dialog.dismiss();
			mSortByUrl = Constants.ThreadsSort.SORT_BY_NEW_URL;
			mSortByUrlExtra = Constants.ThreadsSort.SORT_BY_NEW_URL_CHOICES[item];
			new MyDownloadThreadsTask(mSubreddit).execute();
		}
	};
	private final DialogInterface.OnClickListener sortByControversialOnClickListener = new DialogInterface.OnClickListener() {
		public void onClick(DialogInterface dialog, int item) {
			dialog.dismiss();
			mSortByUrl = Constants.ThreadsSort.SORT_BY_CONTROVERSIAL_URL;
			mSortByUrlExtra = Constants.ThreadsSort.SORT_BY_CONTROVERSIAL_URL_CHOICES[item];
			new MyDownloadThreadsTask(mSubreddit).execute();
		}
	};
	private final DialogInterface.OnClickListener sortByTopOnClickListener = new DialogInterface.OnClickListener() {
		public void onClick(DialogInterface dialog, int item) {
			dialog.dismiss();
			mSortByUrl = Constants.ThreadsSort.SORT_BY_TOP_URL;
			mSortByUrlExtra = Constants.ThreadsSort.SORT_BY_TOP_URL_CHOICES[item];
			new MyDownloadThreadsTask(mSubreddit).execute();
		}
	};
	
	private final ThumbnailOnClickListenerFactory mThumbnailOnClickListenerFactory
			= new ThumbnailOnClickListenerFactory() {
		@Override
		public OnClickListener getThumbnailOnClickListener(final ThingInfo threadThingInfo, final Activity activity) {
			return new OnClickListener() {
				public void onClick(View v) {
					mJumpToThreadId = threadThingInfo.getId();
					setLinkClicked(threadThingInfo);
					Common.launchBrowser(
							activity,
							threadThingInfo.getUrl(),
							Util.createThreadUri(threadThingInfo).toString(),
							false,
							false,
							mSettings.isUseExternalBrowser(),
							mSettings.isSaveHistory()
					);
				}
			};
		}
	};
	
	private final ThreadClickDialogOnClickListenerFactory mThreadClickDialogOnClickListenerFactory
			= new ThreadClickDialogOnClickListenerFactory() {
		@Override
		public OnClickListener getLoginOnClickListener() {
			return new OnClickListener() {
				public void onClick(View v) {
					removeDialog(Constants.DIALOG_THREAD_CLICK);
					showDialog(Constants.DIALOG_LOGIN);
				}
			};
		}
		@Override
		public OnClickListener getLinkOnClickListener(final ThingInfo thingInfo, final boolean useExternalBrowser) {
    		return new OnClickListener() {
				public void onClick(View v) {
					removeDialog(Constants.DIALOG_THREAD_CLICK);
					setLinkClicked(thingInfo);
					Common.launchBrowser(ThreadsListActivity.this, thingInfo.getUrl(),
							Util.createThreadUri(thingInfo).toString(),
							false, false, useExternalBrowser, mSettings.isSaveHistory());
				}
			};
    	}
		@Override
		public OnClickListener getCommentsOnClickListener(final ThingInfo thingInfo) {
			return new OnClickListener() {
				public void onClick(View v) {
					removeDialog(Constants.DIALOG_THREAD_CLICK);

					CacheInfo.invalidateCachedThread(ThreadsListActivity.this);
					
					// Launch an Intent for CommentsListActivity
					Intent i = new Intent(ThreadsListActivity.this, CommentsListActivity.class);
					i.setData(Util.createThreadUri(thingInfo));
					i.putExtra(Constants.EXTRA_SUBREDDIT, thingInfo.getSubreddit());
					i.putExtra(Constants.EXTRA_TITLE, thingInfo.getTitle());
					i.putExtra(Constants.EXTRA_NUM_COMMENTS, Integer.valueOf(thingInfo.getNum_comments()));
					startActivity(i);
				}
			};
		}
		@Override
		public CompoundButton.OnCheckedChangeListener getVoteUpOnCheckedChangeListener(final ThingInfo thingInfo) {
			return new CompoundButton.OnCheckedChangeListener() {
		    	public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
		    		removeDialog(Constants.DIALOG_THREAD_CLICK);
			    	if (isChecked) {
						new MyVoteTask(thingInfo, 1, thingInfo.getSubreddit()).execute();
					} else {
						new MyVoteTask(thingInfo, 0, thingInfo.getSubreddit()).execute();
					}
				}
		    };
		}
		@Override
	    public CompoundButton.OnCheckedChangeListener getVoteDownOnCheckedChangeListener(final ThingInfo thingInfo) {
	    	return new CompoundButton.OnCheckedChangeListener() {
		        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
			    	removeDialog(Constants.DIALOG_THREAD_CLICK);
					if (isChecked) {
						new MyVoteTask(thingInfo, -1, thingInfo.getSubreddit()).execute();
					} else {
						new MyVoteTask(thingInfo, 0, thingInfo.getSubreddit()).execute();
					}
				}
		    };
	    }
	};
	
	private void setLinkClicked(ThingInfo threadThingInfo) {
		threadThingInfo.setClicked(true);
		mThreadsAdapter.notifyDataSetChanged();
	}
	
	@Override
    protected void onSaveInstanceState(Bundle state) {
    	super.onSaveInstanceState(state);
    	state.putString(Constants.SUBREDDIT_KEY, mSubreddit);
    	state.putString(Constants.ThreadsSort.SORT_BY_KEY, mSortByUrl);
    	state.putString(Constants.JUMP_TO_THREAD_ID_KEY, mJumpToThreadId);
    	state.putString(Constants.AFTER_KEY, mAfter);
    	state.putString(Constants.BEFORE_KEY, mBefore);
    	state.putInt(Constants.THREAD_COUNT_KEY, mCount);
    	state.putString(Constants.LAST_AFTER_KEY, mLastAfter);
    	state.putString(Constants.LAST_BEFORE_KEY, mLastBefore);
    	state.putInt(Constants.THREAD_LAST_COUNT_KEY, mLastCount);
    	state.putParcelable(Constants.VOTE_TARGET_THING_INFO_KEY, mVoteTargetThing);
    }
    
    /**
     * Called to "thaw" re-animate the app from a previous onSaveInstanceState().
     * 
     * @see android.app.Activity#onRestoreInstanceState
     */
    @Override
    protected void onRestoreInstanceState(Bundle state) {
        super.onRestoreInstanceState(state);
        final int[] myDialogs = {
        	Constants.DIALOG_LOGGING_IN,
        	Constants.DIALOG_LOGIN,
        	Constants.DIALOG_SORT_BY,
        	Constants.DIALOG_SORT_BY_CONTROVERSIAL,
        	Constants.DIALOG_SORT_BY_NEW,
        	Constants.DIALOG_SORT_BY_TOP,
        	Constants.DIALOG_THREAD_CLICK,
        };
        for (int dialog : myDialogs) {
	        try {
	        	removeDialog(dialog);
		    } catch (IllegalArgumentException e) {
		    	// Ignore.
		    }
        }
    }
} public interface ThumbnailOnClickListenerFactory {
	OnClickListener getThumbnailOnClickListener(ThingInfo threadThingInfo, Activity activity);
}
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ListActivity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Html;
import android.text.Spanned;
import android.text.style.URLSpan;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.Window;
import android.webkit.CookieSyncManager;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.andrewshu.android.reddit.R;
import com.andrewshu.android.reddit.captcha.CaptchaCheckRequiredTask;
import com.andrewshu.android.reddit.captcha.CaptchaDownloadTask;
import com.andrewshu.android.reddit.comments.CommentsListActivity;
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.FormValidation;
import com.andrewshu.android.reddit.common.ProgressInputStream;
import com.andrewshu.android.reddit.common.RedditIsFunHttpClientFactory;
import com.andrewshu.android.reddit.common.tasks.VoteTask;
import com.andrewshu.android.reddit.common.util.StringUtils;
import com.andrewshu.android.reddit.common.util.Util;
import com.andrewshu.android.reddit.login.LoginDialog;
import com.andrewshu.android.reddit.login.LoginTask;
import com.andrewshu.android.reddit.mail.MessageComposeTask;
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;
import com.andrewshu.android.reddit.threads.ThreadClickDialog;
import com.andrewshu.android.reddit.threads.ThreadClickDialogOnClickListenerFactory;
import com.andrewshu.android.reddit.threads.ThreadsListActivity;
import com.andrewshu.android.reddit.threads.ThumbnailOnClickListenerFactory;
        		StringBuilder sb = new StringBuilder(Constants.REDDIT_BASE_URL).append("/user/").append(mUsername.trim()).append("/.json?");
        		
        		if (mSortByUrl != null)
        			sb = sb.append(mSortByUrl).append("&");
        		if (mSortByUrlExtra != null)
        			sb = sb.append(mSortByUrlExtra).append("&");
        		
    			// "before" always comes back null unless you provide correct "count"
        		if (mAfter != null) {
        			// count: 25, 50, ...
    				sb = sb.append("count=").append(mCount)
    					.append("&after=").append(mAfter).append("&");
    				isAfter = true;
        		}
        		else if (mBefore != null) {
        			// count: nothing, 26, 51, ...
        			sb = sb.append("count=").append(mCount + 1 - Constants.DEFAULT_THREAD_DOWNLOAD_LIMIT)
        				.append("&before=").append(mBefore).append("&");
        			isBefore = true;
        		}
        		
        		url = sb.toString();
        		if (Constants.LOGGING) Log.d(TAG, "url=" + url);
        		
        		HttpGet request = new HttpGet(url);
            	HttpResponse response = mClient.execute(request);
            	
            	// Read the header to get Content-Length since entity.getContentLength() returns -1
            	Header contentLengthHeader = response.getFirstHeader("Content-Length");
            	if (contentLengthHeader != null) {
	            	_mContentLength = Long.valueOf(contentLengthHeader.getValue());
	            	if (Constants.LOGGING) Log.d(TAG, "Content length: "+_mContentLength);
            	} else {
            		_mContentLength = -1;
            		if (Constants.LOGGING) Log.d(TAG, "Content length: UNAVAILABLE");
            	}

            	entity = response.getEntity();
            	in = entity.getContent();
            	
            	// setup a special InputStream to report progress
            	pin = new ProgressInputStream(in, _mContentLength);
            	pin.addPropertyChangeListener(this);
            	
                parseThingsJSON(pin);
                
            	mLastCount = mCount;
            	if (isAfter)
            		mCount += Constants.DEFAULT_THREAD_DOWNLOAD_LIMIT;
            	else if (isBefore)
            		mCount -= Constants.DEFAULT_THREAD_DOWNLOAD_LIMIT;
            	
                saveState();

            } catch (Exception e) {
            	if (Constants.LOGGING) Log.e(TAG, "failed", e);
        	} finally {
        		if (pin != null) {
        			try {
						pin.close();
					} catch (IOException e2) {
						if (Constants.LOGGING) Log.e(TAG, "pin.close()", e2);
					}
        		}
        		if (in != null) {
        			try {
						in.close();
					} catch (IOException e2) {
						if (Constants.LOGGING) Log.e(TAG, "in.close()", e2);
					}
        		}
        		if (entity != null) {
        			try {
        				entity.consumeContent();
        			} catch (Exception e2) {
        				if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e2);
        			}
        		}
            }
            return null;
	    }
    	
    	/**
    	 * @return [linkKarma, commentKarma]
    	 */
    	private int[] getKarma() throws IOException {
        	String url = new StringBuilder(Constants.REDDIT_BASE_URL).append("/user/").append(mUsername.trim()).append("/about.json").toString();
    		
    		if (Constants.LOGGING) Log.d(TAG, "karma url=" + url);
    		
    		HttpGet request = new HttpGet(url);
        	HttpResponse response = mClient.execute(request);
        	
        	HttpEntity entity = null;
        	InputStream in = null;
        	try {
	        	entity = response.getEntity();
	        	in = entity.getContent();
	        	
	        	UserInfo userInfo = UserInfoParser.parseJSON(in);
	        	if (userInfo != null)
	        		return new int[] { userInfo.getLink_karma(), userInfo.getComment_karma() };
	        	
        	} finally {
        		try {
        			in.close();
        		} catch (Exception ignore) {}
        		try {
        			entity.consumeContent();
        		} catch (Exception ignore) {}
        	}
        	
        	return null;
    	}
    	
    	private void parseThingsJSON(InputStream in) throws IOException,
		    	JsonParseException, IllegalStateException {
		
    		String genericListingError = "Not a user page listing";
    		try {
    			Listing listing = mObjectMapper.readValue(in, Listing.class);