com.andrewshu.android.reddit.threads.ThreadsListActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.andrewshu.android.reddit.threads.ThreadsListActivity.java

Source

/*
 * Copyright 2009 Andrew Shu
 *
 * This file is part of "reddit is fun".
 *
 * "reddit is fun" is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * "reddit is fun" is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with "reddit is fun".  If not, see <http://www.gnu.org/licenses/>.
 */

package com.andrewshu.android.reddit.threads;

import java.beans.PropertyChangeEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.http.client.HttpClient;
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.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.text.style.TextAppearanceSpan;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.ContextThemeWrapper;
import android.view.KeyEvent;
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;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import com.andrewshu.android.reddit.R;
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.RedditIsFunHttpClientFactory;
import com.andrewshu.android.reddit.common.tasks.HideTask;
import com.andrewshu.android.reddit.common.tasks.SaveTask;
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.InboxActivity;
import com.andrewshu.android.reddit.mail.PeekEnvelopeTask;
import com.andrewshu.android.reddit.reddits.PickSubredditActivity;
import com.andrewshu.android.reddit.reddits.SubscribeTask;
import com.andrewshu.android.reddit.reddits.UnsubscribeTask;
import com.andrewshu.android.reddit.settings.RedditPreferencesPage;
import com.andrewshu.android.reddit.settings.RedditSettings;
import com.andrewshu.android.reddit.submit.SubmitLinkActivity;
import com.andrewshu.android.reddit.things.ThingInfo;
import com.andrewshu.android.reddit.threads.ShowThumbnailsTask.ThumbnailLoadAction;
import com.andrewshu.android.reddit.user.ProfileActivity;

/**
 * Main Activity class representing a Subreddit, i.e., a ThreadsList.
 * 
 * @author TalkLittle
 *
 */
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.
            }
        }
    }
}