com.andrewshu.android.reddit.user.ProfileActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.andrewshu.android.reddit.user.ProfileActivity.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.user;

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;

/**
 * Activity to view user submissions and comments.
 * Also check their link and comment karma.
 * 
 * @author TalkLittle
 *
 */
public final class ProfileActivity extends ListActivity implements View.OnCreateContextMenuListener {

    private static final String TAG = "ProfileActivity";

    static final Pattern USER_PATH_PATTERN = Pattern.compile(Constants.USER_PATH_PATTERN_STRING);

    private final ObjectMapper mObjectMapper = Common.getObjectMapper();

    /** Custom list adapter that fits our threads data into the list. */
    private ThingsListAdapter mThingsAdapter;
    private ArrayList<ThingInfo> mThingsList;
    // Lock used when modifying the mMessagesAdapter
    private static final Object MESSAGE_ADAPTER_LOCK = new Object();

    private final HttpClient mClient = RedditIsFunHttpClientFactory.getGzipHttpClient();

    // Common settings are stored here
    private final RedditSettings mSettings = new RedditSettings();

    // UI State
    private View mVoteTargetView = null;
    private ThingInfo mVoteTargetThingInfo = null;
    private URLSpan[] mVoteTargetSpans = null;
    // TODO: String mVoteTargetId so when you rotate, you can find the TargetThingInfo again
    private DownloadProfileTask mCurrentDownloadThingsTask = null;
    private final Object mCurrentDownloadThingsTaskLock = new Object();
    private View mNextPreviousView = null;

    private String mUsername = null;

    private String mAfter = null;
    private String mBefore = null;
    private int mCount = 0;
    private String mLastAfter = null;
    private String mLastBefore = null;
    private int mLastCount = 0;
    private int[] mKarma = null;
    private String mSortByUrl = null;
    private String mSortByUrlExtra = null;

    private String mJumpToThreadId = null;

    private volatile String mCaptchaIden = null;
    private volatile String mCaptchaUrl = null;

    // ProgressDialogs with percentage bars
    //    private AutoResetProgressDialog mLoadingCommentsProgress;
    //    private int mNumVisibleMessages;

    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(this, mClient);
        setRequestedOrientation(mSettings.getRotation());
        setTheme(mSettings.getTheme());
        requestWindowFeature(Window.FEATURE_PROGRESS);
        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);

        setContentView(R.layout.profile_list_content);
        registerForContextMenu(getListView());

        if (savedInstanceState != null) {
            if (Constants.LOGGING)
                Log.d(TAG, "using savedInstanceState");
            mUsername = savedInstanceState.getString(Constants.USERNAME_KEY);
            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);
            mKarma = savedInstanceState.getIntArray(Constants.KARMA_KEY);
            mSortByUrl = savedInstanceState.getString(Constants.CommentsSort.SORT_BY_KEY);
            mJumpToThreadId = savedInstanceState.getString(Constants.JUMP_TO_THREAD_ID_KEY);
            mVoteTargetThingInfo = savedInstanceState.getParcelable(Constants.VOTE_TARGET_THING_INFO_KEY);

            // try to restore mThingsList using getLastNonConfigurationInstance()
            // (separate function to avoid a compiler warning casting ArrayList<ThingInfo>
            restoreLastNonConfigurationInstance();
            if (mThingsList == null) {
                // Load previous page of profile items
                if (mLastAfter != null) {
                    new DownloadProfileTask(mUsername, mLastAfter, null, mLastCount).execute();
                } else if (mLastBefore != null) {
                    new DownloadProfileTask(mUsername, null, mLastBefore, mLastCount).execute();
                } else {
                    new DownloadProfileTask(mUsername).execute();
                }
            } else {
                // Orientation change. Use prior instance.
                resetUI(new ThingsListAdapter(this, mThingsList));
                setTitle(mUsername + "'s profile");
            }
            return;
        }
        // Handle subreddit Uri passed via Intent
        else if (getIntent().getData() != null) {
            Matcher userPathMatcher = USER_PATH_PATTERN.matcher(getIntent().getData().getPath());
            if (userPathMatcher.matches()) {
                mUsername = userPathMatcher.group(1);
                new DownloadProfileTask(mUsername).execute();
                return;
            }
        }

        // No username specified by Intent, so load the logged in user's profile
        if (mSettings.isLoggedIn()) {
            mUsername = mSettings.getUsername();
            new DownloadProfileTask(mUsername).execute();
            return;
        }

        // Can't find a username to use. Quit.
        if (Constants.LOGGING)
            Log.e(TAG, "Could not find a username to use for ProfileActivity");
        finish();
    }

    @Override
    protected void onResume() {
        super.onResume();
        CookieSyncManager.getInstance().startSync();
        int previousTheme = mSettings.getTheme();
        boolean previousLoggedIn = mSettings.isLoggedIn();
        mSettings.loadRedditPreferences(this, mClient);
        setRequestedOrientation(mSettings.getRotation());
        if (mSettings.getTheme() != previousTheme) {
            resetUI(mThingsAdapter);
        }
        updateNextPreviousButtons();
        updateKarma();
        if (mSettings.isLoggedIn() != previousLoggedIn) {
            new DownloadProfileTask(mSettings.getUsername()).execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
        }
    }

    @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 messages list
        // when rotating or opening keyboard.
        return mThingsList;
    }

    @SuppressWarnings("unchecked")
    private void restoreLastNonConfigurationInstance() {
        mThingsList = (ArrayList<ThingInfo>) getLastNonConfigurationInstance();
    }

    private final class ThingsListAdapter extends ArrayAdapter<ThingInfo> {
        static final int THREAD_ITEM_VIEW_TYPE = 0;
        static final int COMMENT_ITEM_VIEW_TYPE = 1;

        private static final int VIEW_TYPE_COUNT = 2;

        public boolean mIsLoading = true;

        private LayoutInflater mInflater;

        @Override
        public int getItemViewType(int position) {
            ThingInfo item = getItem(position);
            if (item.getName().startsWith(Constants.THREAD_KIND)) {
                return THREAD_ITEM_VIEW_TYPE;
            }
            if (item.getName().startsWith(Constants.COMMENT_KIND)) {
                return COMMENT_ITEM_VIEW_TYPE;
            }
            return COMMENT_ITEM_VIEW_TYPE;
        }

        @Override
        public int getViewTypeCount() {
            return VIEW_TYPE_COUNT;
        }

        public boolean isEmpty() {
            if (mIsLoading)
                return false;
            return super.isEmpty();
        }

        public ThingsListAdapter(Context context, List<ThingInfo> objects) {
            super(context, 0, objects);
            mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View view = null;

            ThingInfo item = this.getItem(position);

            if (getItemViewType(position) == THREAD_ITEM_VIEW_TYPE) {
                // 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;
                }

                ThreadsListActivity.fillThreadsListItemView(position, view, item, ProfileActivity.this, mClient,
                        mSettings, mThumbnailOnClickListenerFactory);
            }

            else if (getItemViewType(position) == COMMENT_ITEM_VIEW_TYPE) {
                // Here view may be passed in for re-use, or we make a new one.
                if (convertView == null) {
                    view = mInflater.inflate(R.layout.comments_list_item, null);
                } else {
                    view = convertView;
                }

                CommentsListActivity.fillCommentsListItemView(view, item, mSettings);
                view.setPadding(15, 5, 0, 5);
            }

            return view;
        }
    } // End of MessagesListAdapter

    /**
     * Called when user clicks an item in the list. Mark message read.
     * If item was already focused, open a dialog.
     */
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        ThingInfo item = mThingsAdapter.getItem(position);

        // Mark the message/comment as selected
        mVoteTargetThingInfo = item;
        mVoteTargetView = v;

        if (item.getName().startsWith(Constants.THREAD_KIND)) {
            showDialog(Constants.DIALOG_THREAD_CLICK);
        } else {
            openContextMenu(v);
        }
    }

    @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 = mThingsAdapter.getItem(rowId);

        // Mark the message/comment as selected
        mVoteTargetThingInfo = item;
        mVoteTargetView = v;

        if (item.getName().startsWith(Constants.THREAD_KIND)) {
            menu.add(0, Constants.DIALOG_THREAD_CLICK, Menu.NONE, "Go to thread");
        } else {
            // TODO: include the context!
            menu.add(0, Constants.DIALOG_COMMENT_CLICK, Menu.NONE, "Go to comment");
        }
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        Intent i;

        switch (item.getItemId()) {
        case Constants.DIALOG_COMMENT_CLICK:
            i = new Intent(getApplicationContext(), CommentsListActivity.class);
            i.setData(Util.createCommentUri(mVoteTargetThingInfo, 0));
            i.putExtra(Constants.EXTRA_SUBREDDIT, mVoteTargetThingInfo.getSubreddit());
            i.putExtra(Constants.EXTRA_TITLE, mVoteTargetThingInfo.getTitle());
            startActivity(i);
            return true;
        case Constants.DIALOG_THREAD_CLICK:
            // Launch an Intent for CommentsListActivity
            CacheInfo.invalidateCachedThread(getApplicationContext());
            i = new Intent(getApplicationContext(), CommentsListActivity.class);
            i.setData(Util.createThreadUri(mVoteTargetThingInfo));
            i.putExtra(Constants.EXTRA_SUBREDDIT, mVoteTargetThingInfo.getSubreddit());
            i.putExtra(Constants.EXTRA_TITLE, mVoteTargetThingInfo.getTitle());
            i.putExtra(Constants.EXTRA_NUM_COMMENTS, Integer.valueOf(mVoteTargetThingInfo.getNum_comments()));
            startActivity(i);
            return true;
        default:
            return super.onContextItemSelected(item);
        }
    }

    /**
     * Resets the output UI list contents, retains session state.
     * @param messagesAdapter A MessagesListAdapter to use. Pass in null if you want a new empty one created.
     */
    void resetUI(ThingsListAdapter messagesAdapter) {
        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 (MESSAGE_ADAPTER_LOCK) {
            if (messagesAdapter == null) {
                // Reset the list to be empty.
                mThingsList = new ArrayList<ThingInfo>();
                mThingsAdapter = new ThingsListAdapter(this, mThingsList);
            } else {
                mThingsAdapter = messagesAdapter;
            }

            setListAdapter(mThingsAdapter);
            mThingsAdapter.mIsLoading = false;
            mThingsAdapter.notifyDataSetChanged(); // Just in case
        }
        getListView().setDivider(null);
        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 (MESSAGE_ADAPTER_LOCK) {
            if (mThingsAdapter != null)
                mThingsAdapter.mIsLoading = true;
        }
        getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_START);
    }

    private void disableLoadingScreen() {
        resetUI(mThingsAdapter);
        getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_END);
    }

    private void updateNextPreviousButtons() {
        Common.updateNextPreviousButtons(this, mNextPreviousView, mAfter, mBefore, mCount, mSettings,
                downloadAfterOnClickListener, downloadBeforeOnClickListener);
    }

    private void updateKarma() {
        if (mKarma == null)
            return;
        View karmaLayout = findViewById(R.id.karma_layout);
        View karmaLayoutBorder = findViewById(R.id.karma_border_bottom);
        if (karmaLayout != null && karmaLayoutBorder != null) {
            karmaLayout.setVisibility(View.VISIBLE);
            if (Util.isLightTheme(mSettings.getTheme())) {
                karmaLayout.setBackgroundResource(android.R.color.background_light);
                karmaLayoutBorder.setBackgroundResource(R.color.black);
            } else {
                karmaLayoutBorder.setBackgroundResource(R.color.white);
            }
            TextView linkKarma = (TextView) findViewById(R.id.link_karma);
            TextView commentKarma = (TextView) findViewById(R.id.comment_karma);
            linkKarma.setText(mKarma[0] + " link karma");
            commentKarma.setText(mKarma[1] + " comment karma");
        }
    }

    private class DownloadProfileTask extends AsyncTask<Integer, Long, Void> implements PropertyChangeListener {

        private ArrayList<ThingInfo> _mThingInfos = new ArrayList<ThingInfo>();
        private long _mContentLength;

        private String mUsername;
        private String mAfter;
        private String mBefore;
        private int mCount;
        private String mLastAfter = null;
        private String mLastBefore = null;
        private int mLastCount = 0;
        private int[] mKarma;
        private String mSortByUrl;
        private String mSortByUrlExtra;

        public DownloadProfileTask(String username) {
            this(username, null, null, Constants.DEFAULT_THREAD_DOWNLOAD_LIMIT, ProfileActivity.this.mSortByUrl,
                    ProfileActivity.this.mSortByUrlExtra);
        }

        public DownloadProfileTask(String username, String after, String before, int count) {
            this(username, after, before, count, ProfileActivity.this.mSortByUrl,
                    ProfileActivity.this.mSortByUrlExtra);
        }

        /**
         * The real constructor
         * @param username
         * @param after
         * @param before
         * @param count
         * @param sortByUrl
         * @param sortByUrlExtra
         */
        public DownloadProfileTask(String username, String after, String before, int count, String sortByUrl,
                String sortByUrlExtra) {
            mUsername = username;
            mAfter = after;
            mBefore = before;
            mCount = count;
            mSortByUrl = sortByUrl;
            mSortByUrlExtra = sortByUrlExtra;
        }

        protected void saveState() {
            ProfileActivity.this.mLastAfter = mLastAfter;
            ProfileActivity.this.mLastBefore = mLastBefore;
            ProfileActivity.this.mLastCount = mLastCount;
            ProfileActivity.this.mAfter = mAfter;
            ProfileActivity.this.mBefore = mBefore;
            ProfileActivity.this.mCount = mCount;
            ProfileActivity.this.mKarma = mKarma;
            ProfileActivity.this.mSortByUrl = mSortByUrl;
            ProfileActivity.this.mSortByUrlExtra = mSortByUrlExtra;
        }

        // XXX: maxComments is unused for now
        public Void doInBackground(Integer... maxComments) {
            HttpEntity entity = null;
            boolean isAfter = false;
            boolean isBefore = false;
            InputStream in = null;
            ProgressInputStream pin = null;

            try {

                if (mKarma == null)
                    mKarma = getKarma();

                String url;
                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);

                if (!Constants.JSON_LISTING.equals(listing.getKind()))
                    throw new IllegalStateException(genericListingError);
                // Save the modhash, after, and before
                ListingData data = listing.getData();
                if (StringUtils.isEmpty(data.getModhash()))
                    mSettings.setModhash(null);
                else
                    mSettings.setModhash(data.getModhash());

                mLastAfter = mAfter;
                mLastBefore = mBefore;
                mAfter = data.getAfter();
                mBefore = data.getBefore();

                // Go through the children and get the ThingInfos
                for (ThingListing tiContainer : data.getChildren()) {
                    if (Constants.COMMENT_KIND.equals(tiContainer.getKind())) {
                        ThingInfo ti = tiContainer.getData();
                        // HTML to Spanned
                        String unescapedHtmlBody = Html.fromHtml(ti.getBody_html()).toString();
                        Spanned body = Html.fromHtml(Util.convertHtmlTags(unescapedHtmlBody));
                        // remove last 2 newline characters
                        if (body.length() > 2)
                            ti.setSpannedBody(body.subSequence(0, body.length() - 2));
                        else
                            ti.setSpannedBody("");
                        _mThingInfos.add(ti);
                    } else if (Constants.THREAD_KIND.equals(tiContainer.getKind())) {
                        ThingInfo ti = tiContainer.getData();
                        ti.setClicked(Common.isClicked(ProfileActivity.this, ti.getUrl()));
                        _mThingInfos.add(ti);
                    }
                }
            } catch (Exception ex) {
                if (Constants.LOGGING)
                    Log.e(TAG, "parseThingsJSON", ex);
            }
        }

        @Override
        public void onPreExecute() {
            synchronized (mCurrentDownloadThingsTaskLock) {
                if (mCurrentDownloadThingsTask != null)
                    mCurrentDownloadThingsTask.cancel(true);
                mCurrentDownloadThingsTask = this;
            }
            resetUI(null);
            enableLoadingScreen();
            if (_mContentLength == -1)
                getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_INDETERMINATE_ON);
        }

        @Override
        public void onPostExecute(Void v) {
            synchronized (mCurrentDownloadThingsTaskLock) {
                mCurrentDownloadThingsTask = null;
            }
            synchronized (MESSAGE_ADAPTER_LOCK) {
                for (ThingInfo mi : _mThingInfos)
                    mThingsAdapter.add(mi);
            }

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

            showThumbnails(_mThingInfos);

            disableLoadingScreen();
            setTitle(String.format(getResources().getString(R.string.user_profile), mUsername));
            updateNextPreviousButtons();
            updateKarma();
        }

        @Override
        public void onProgressUpdate(Long... progress) {
            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) {
        int size = thingInfos.size();
        ThumbnailLoadAction[] thumbnailLoadActions = new ThumbnailLoadAction[size];
        for (int i = 0; i < thumbnailLoadActions.length; i++) {
            thumbnailLoadActions[i] = new ThumbnailLoadAction(thingInfos.get(i), null, i);
        }
        new ShowThumbnailsTask(this, mClient, R.drawable.go_arrow).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(ProfileActivity.this, "Logged in as " + mUsername, Toast.LENGTH_SHORT).show();
                showDialog(Constants.DIALOG_COMPOSE);
            } else {
                Common.showErrorToast(mUserError, Toast.LENGTH_LONG, ProfileActivity.this);
            }
        }
    }

    private class MyMessageComposeTask extends MessageComposeTask {
        MyMessageComposeTask(Dialog dialog, ThingInfo targetThingInfo, String captcha) {
            super(dialog, targetThingInfo, captcha, mCaptchaIden, mSettings, mClient, getApplicationContext());
        }

        @Override
        public void onPreExecute() {
            showDialog(Constants.DIALOG_COMPOSING);
        }

        @Override
        public void onPostExecute(Boolean success) {
            removeDialog(Constants.DIALOG_COMPOSING);
            if (success) {
                Toast.makeText(ProfileActivity.this, "Message sent.", Toast.LENGTH_SHORT).show();
                // TODO: add the reply beneath the original, OR redirect to sent messages page
            } else {
                Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, ProfileActivity.this);
                new MyCaptchaDownloadTask(_mDialog).execute();
            }
        }
    }

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

        @Override
        public void onPostExecute(Boolean success) {
            if (success) {
                CacheInfo.invalidateCachedSubreddit(_mContext);
            } else {
                // Vote failed. Undo the score.
                _mTargetThingInfo.setLikes(_mPreviousLikes);
                _mTargetThingInfo.setScore(_mPreviousScore);
                mThingsAdapter.notifyDataSetChanged();

                Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, _mContext);
            }
        }
    }

    private class MyCaptchaCheckRequiredTask extends CaptchaCheckRequiredTask {

        Dialog _mDialog;

        public MyCaptchaCheckRequiredTask(Dialog dialog) {
            super(Constants.REDDIT_BASE_URL + "/message/compose/", mClient);
            _mDialog = dialog;
        }

        @Override
        protected void saveState() {
            ProfileActivity.this.mCaptchaIden = _mCaptchaIden;
            ProfileActivity.this.mCaptchaUrl = _mCaptchaUrl;
        }

        @Override
        public void onPreExecute() {
            // Hide send button so user can't send until we know whether he needs captcha
            final Button sendButton = (Button) _mDialog.findViewById(R.id.compose_send_button);
            sendButton.setVisibility(View.INVISIBLE);
            // Show "loading captcha" label
            final TextView loadingCaptcha = (TextView) _mDialog.findViewById(R.id.compose_captcha_loading);
            loadingCaptcha.setVisibility(View.VISIBLE);
        }

        @Override
        public void onPostExecute(Boolean required) {
            final TextView captchaLabel = (TextView) _mDialog.findViewById(R.id.compose_captcha_textview);
            final ImageView captchaImage = (ImageView) _mDialog.findViewById(R.id.compose_captcha_image);
            final EditText captchaEdit = (EditText) _mDialog.findViewById(R.id.compose_captcha_input);
            final TextView loadingCaptcha = (TextView) _mDialog.findViewById(R.id.compose_captcha_loading);
            final Button sendButton = (Button) _mDialog.findViewById(R.id.compose_send_button);
            if (required == null) {
                Common.showErrorToast("Error retrieving captcha. Use the menu to try again.", Toast.LENGTH_LONG,
                        ProfileActivity.this);
                return;
            }
            if (required) {
                captchaLabel.setVisibility(View.VISIBLE);
                captchaImage.setVisibility(View.VISIBLE);
                captchaEdit.setVisibility(View.VISIBLE);
                // Launch a task to download captcha and display it
                new MyCaptchaDownloadTask(_mDialog).execute();
            } else {
                captchaLabel.setVisibility(View.GONE);
                captchaImage.setVisibility(View.GONE);
                captchaEdit.setVisibility(View.GONE);
            }
            loadingCaptcha.setVisibility(View.GONE);
            sendButton.setVisibility(View.VISIBLE);
        }
    }

    private class MyCaptchaDownloadTask extends CaptchaDownloadTask {

        Dialog _mDialog;

        public MyCaptchaDownloadTask(Dialog dialog) {
            super(mCaptchaUrl, mClient);
            _mDialog = dialog;
        }

        @Override
        public void onPostExecute(Drawable captcha) {
            if (captcha == null) {
                Common.showErrorToast("Error retrieving captcha. Use the menu to try again.", Toast.LENGTH_LONG,
                        ProfileActivity.this);
                return;
            }
            final ImageView composeCaptchaView = (ImageView) _mDialog.findViewById(R.id.compose_captcha_image);
            composeCaptchaView.setVisibility(View.VISIBLE);
            composeCaptchaView.setImageDrawable(captcha);
        }
    }

    /**
     * Populates the menu.
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);

        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.profile, menu);

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {

        switch (item.getItemId()) {
        case R.id.compose_message_menu_id:
            if (mSettings.isLoggedIn()) {
                showDialog(Constants.DIALOG_COMPOSE);
            } else {
                showDialog(Constants.DIALOG_LOGIN);
            }
            break;
        case R.id.refresh_menu_id:
            new DownloadProfileTask(mUsername).execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
            break;
        case android.R.id.home:
            Common.goHome(this);
            break;
        }

        return true;
    }

    @Override
    protected Dialog onCreateDialog(int id) {
        Dialog dialog;
        ProgressDialog pdialog;
        AlertDialog.Builder builder;
        LayoutInflater inflater;
        View layout; // used for inflated views for AlertDialog.Builder.setView()

        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_COMPOSE:
            inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            builder = new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme()));
            layout = inflater.inflate(R.layout.compose_dialog, null);

            Common.setTextColorFromTheme(mSettings.getTheme(), getResources(),
                    (TextView) layout.findViewById(R.id.compose_destination_textview),
                    (TextView) layout.findViewById(R.id.compose_subject_textview),
                    (TextView) layout.findViewById(R.id.compose_message_textview),
                    (TextView) layout.findViewById(R.id.compose_captcha_textview),
                    (TextView) layout.findViewById(R.id.compose_captcha_loading));

            final EditText composeDestination = (EditText) layout.findViewById(R.id.compose_destination_input);
            final EditText composeSubject = (EditText) layout.findViewById(R.id.compose_subject_input);
            final EditText composeText = (EditText) layout.findViewById(R.id.compose_text_input);
            final Button composeSendButton = (Button) layout.findViewById(R.id.compose_send_button);
            final Button composeCancelButton = (Button) layout.findViewById(R.id.compose_cancel_button);
            final EditText composeCaptcha = (EditText) layout.findViewById(R.id.compose_captcha_input);
            composeDestination.setText(mUsername);

            dialog = builder.setView(layout).create();
            final Dialog composeDialog = dialog;
            composeSendButton.setOnClickListener(new OnClickListener() {
                public void onClick(View v) {
                    ThingInfo hi = new ThingInfo();

                    if (!FormValidation.validateComposeMessageInputFields(ProfileActivity.this, composeDestination,
                            composeSubject, composeText, composeCaptcha))
                        return;

                    hi.setDest(composeDestination.getText().toString().trim());
                    hi.setSubject(composeSubject.getText().toString().trim());
                    new MyMessageComposeTask(composeDialog, hi, composeCaptcha.getText().toString().trim())
                            .execute(composeText.getText().toString().trim());
                    removeDialog(Constants.DIALOG_COMPOSE);
                }
            });
            composeCancelButton.setOnClickListener(new OnClickListener() {
                public void onClick(View v) {
                    removeDialog(Constants.DIALOG_COMPOSE);
                }
            });
            break;

        case Constants.DIALOG_THREAD_CLICK:
            dialog = new ThreadClickDialog(this, mSettings);
            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;
        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_COMPOSING:
            pdialog = new ProgressDialog(new ContextThemeWrapper(this, mSettings.getDialogTheme()));
            pdialog.setMessage("Composing message...");
            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.username != null) {
        //             final TextView loginUsernameInput = (TextView) dialog.findViewById(R.id.login_username_input);
        //             loginUsernameInput.setText(mSettings.username);
        //          }
        //          final TextView loginPasswordInput = (TextView) dialog.findViewById(R.id.login_password_input);
        //          loginPasswordInput.setText("");
        //          break;

        case Constants.DIALOG_COMPOSE:
            final EditText composeDestination = (EditText) dialog.findViewById(R.id.compose_destination_input);
            composeDestination.setText(mUsername);
            new MyCaptchaCheckRequiredTask(dialog).execute();
            break;

        case Constants.DIALOG_THREAD_CLICK:
            ThreadsListActivity.fillThreadClickDialog(dialog, mVoteTargetThingInfo, mSettings,
                    mThreadClickDialogOnClickListenerFactory);
            break;

        default:
            // No preparation based on app state is required.
            break;
        }
    }

    private void setLinkClicked(ThingInfo threadThingInfo) {
        threadThingInfo.setClicked(true);
        mThingsAdapter.notifyDataSetChanged();
    }

    private final OnClickListener downloadAfterOnClickListener = new OnClickListener() {
        public void onClick(View v) {
            new DownloadProfileTask(mUsername, mAfter, null, mCount).execute();
        }
    };
    private final OnClickListener downloadBeforeOnClickListener = new OnClickListener() {
        public void onClick(View v) {
            new DownloadProfileTask(mUsername, null, mBefore, mCount).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 = jumpToId;
                    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(ThingInfo thingInfo, boolean useExternalBrowser) {
            final ThingInfo info = thingInfo;
            final boolean fUseExternalBrowser = useExternalBrowser;
            return new OnClickListener() {
                public void onClick(View v) {
                    removeDialog(Constants.DIALOG_THREAD_CLICK);
                    // Launch Intent to goto the URL
                    Common.launchBrowser(ProfileActivity.this, info.getUrl(), Util.createThreadUri(info).toString(),
                            false, false, fUseExternalBrowser, mSettings.isSaveHistory());
                }
            };
        }

        @Override
        public OnClickListener getCommentsOnClickListener(ThingInfo thingInfo) {
            final ThingInfo info = thingInfo;
            return new OnClickListener() {
                public void onClick(View v) {
                    removeDialog(Constants.DIALOG_THREAD_CLICK);
                    // Launch an Intent for CommentsListActivity
                    CacheInfo.invalidateCachedThread(ProfileActivity.this);
                    Intent i = new Intent(ProfileActivity.this, CommentsListActivity.class);
                    i.setData(Util.createThreadUri(info));
                    i.putExtra(Constants.EXTRA_SUBREDDIT, info.getSubreddit());
                    i.putExtra(Constants.EXTRA_TITLE, info.getTitle());
                    i.putExtra(Constants.EXTRA_NUM_COMMENTS, Integer.valueOf(info.getNum_comments()));
                    startActivity(i);
                }
            };
        }

        @Override
        public CompoundButton.OnCheckedChangeListener getVoteUpOnCheckedChangeListener(ThingInfo thingInfo) {
            final ThingInfo info = thingInfo;
            return new CompoundButton.OnCheckedChangeListener() {
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    removeDialog(Constants.DIALOG_THREAD_CLICK);
                    if (isChecked) {
                        new MyVoteTask(info, 1, info.getSubreddit()).execute();
                    } else {
                        new MyVoteTask(info, 0, info.getSubreddit()).execute();
                    }
                }
            };
        }

        @Override
        public CompoundButton.OnCheckedChangeListener getVoteDownOnCheckedChangeListener(ThingInfo thingInfo) {
            final ThingInfo info = thingInfo;
            return new CompoundButton.OnCheckedChangeListener() {
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    removeDialog(Constants.DIALOG_THREAD_CLICK);
                    if (isChecked) {
                        new MyVoteTask(info, -1, info.getSubreddit()).execute();
                    } else {
                        new MyVoteTask(info, 0, info.getSubreddit()).execute();
                    }
                }
            };
        }
    };

    @Override
    protected void onSaveInstanceState(Bundle state) {
        super.onSaveInstanceState(state);
        state.putString(Constants.USERNAME_KEY, mUsername);
        state.putString(Constants.CommentsSort.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.putIntArray(Constants.KARMA_KEY, mKarma);
        state.putParcelable(Constants.VOTE_TARGET_THING_INFO_KEY, mVoteTargetThingInfo);
    }

    /**
     * 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_LOGGING_IN,
                Constants.DIALOG_LOGIN, Constants.DIALOG_MESSAGE_CLICK, Constants.DIALOG_REPLY,
                Constants.DIALOG_REPLYING, };
        for (int dialog : myDialogs) {
            try {
                removeDialog(dialog);
            } catch (IllegalArgumentException e) {
                // Ignore.
            }
        }
    }
}