com.andrewshu.android.reddit.mail.InboxListActivity.java Source code

Java tutorial

Introduction

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

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
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.Typeface;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Html;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
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.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.EditText;
import android.widget.ListView;
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.Common;
import com.andrewshu.android.reddit.common.Constants;
import com.andrewshu.android.reddit.common.ProgressInputStream;
import com.andrewshu.android.reddit.common.RedditIsFunHttpClientFactory;
import com.andrewshu.android.reddit.common.util.Assert;
import com.andrewshu.android.reddit.common.util.StringUtils;
import com.andrewshu.android.reddit.common.util.Util;
import com.andrewshu.android.reddit.login.LoginDialog;
import com.andrewshu.android.reddit.login.LoginTask;
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;

/**
 * Main Activity class representing a Subreddit, i.e., a ThreadsList.
 * 
 * @author TalkLittle
 *
 */
public final class InboxListActivity extends ListActivity implements View.OnCreateContextMenuListener {

    private static final String TAG = "InboxListActivity";

    private final ObjectMapper mObjectMapper = Common.getObjectMapper();

    /** Custom list adapter that fits our threads data into the list. */
    private MessagesListAdapter mMessagesAdapter;
    private ArrayList<ThingInfo> mMessagesList;
    // 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 String mReplyTargetName = null;
    private URLSpan[] mVoteTargetSpans = null;
    // TODO: String mVoteTargetId so when you rotate, you can find the TargetThingInfo again
    private DownloadMessagesTask mCurrentDownloadMessagesTask = null;
    private final Object mCurrentDownloadMessagesTaskLock = new Object();
    private View mNextPreviousView = null;

    private String mWhichInbox = "inbox";

    private String mAfter = null;
    private String mBefore = null;
    private int mCount = 0;
    private String mLastAfter = null;
    private String mLastBefore = null;
    private int mLastCount = 0;

    // 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.inbox_list_content);
        registerForContextMenu(getListView());

        if (mSettings.isLoggedIn()) {
            if (savedInstanceState != null) {
                mReplyTargetName = savedInstanceState.getString(Constants.REPLY_TARGET_NAME_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);
                mVoteTargetThingInfo = savedInstanceState.getParcelable(Constants.VOTE_TARGET_THING_INFO_KEY);
                mWhichInbox = savedInstanceState.getString(Constants.WHICH_INBOX_KEY);

                restoreLastNonConfigurationInstance();
                if (mMessagesList == null) {
                    // Load previous view of threads
                    if (mLastAfter != null) {
                        new DownloadMessagesTask(mWhichInbox, mLastAfter, null, mLastCount)
                                .execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
                    } else if (mLastBefore != null) {
                        new DownloadMessagesTask(mWhichInbox, null, mLastBefore, mLastCount)
                                .execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
                    } else {
                        new DownloadMessagesTask(mWhichInbox).execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
                    }
                } else {
                    // Orientation change. Use prior instance.
                    resetUI(new MessagesListAdapter(this, mMessagesList));
                }
            } else {
                Bundle extras = getIntent().getExtras();
                if (extras != null) {
                    if (extras.containsKey(Constants.WHICH_INBOX_KEY))
                        mWhichInbox = extras.getString(Constants.WHICH_INBOX_KEY);
                }
                new DownloadMessagesTask(mWhichInbox).execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
            }
        } else {
            showDialog(Constants.DIALOG_LOGIN);
        }
        setTitle(String.format(getResources().getString(R.string.inbox_title), mSettings.getUsername()));
    }

    private void returnStatus(int status) {
        Intent i = new Intent();
        setResult(status, i);
        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(mMessagesAdapter);
        }
        updateNextPreviousButtons();
        if (mSettings.isLoggedIn() != previousLoggedIn) {
            new DownloadMessagesTask(mWhichInbox).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 mMessagesList;
    }

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

    public void refresh() {
        new DownloadMessagesTask(mWhichInbox).execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
    }

    /**
     * Return the ThingInfo based on linear search over the names
     */
    private ThingInfo findThingInfoByName(String name) {
        if (name == null)
            return null;
        synchronized (MESSAGE_ADAPTER_LOCK) {
            for (int i = 0; i < mMessagesAdapter.getCount(); i++) {
                if (mMessagesAdapter.getItem(i).getName().equals(name))
                    return mMessagesAdapter.getItem(i);
            }
        }
        return null;
    }

    private final class MessagesListAdapter extends ArrayAdapter<ThingInfo> {
        public boolean mIsLoading = true;

        private LayoutInflater mInflater;

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

        public MessagesListAdapter(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;

            ThingInfo item = this.getItem(position);

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

            // Set the values of the Views for the CommentsListItem

            TextView fromInfoView = (TextView) view.findViewById(R.id.from_info);
            TextView subjectView = (TextView) view.findViewById(R.id.subject);
            TextView bodyView = (TextView) view.findViewById(R.id.body);

            // Highlight new messages in red
            if (item.isNew())
                fromInfoView.setTextColor(getResources().getColor(R.color.red));
            else
                fromInfoView.setTextColor(getResources().getColor(R.color.gray_50));
            // Build fromInfoView using Spans. Example (** means bold & different color):
            // from *talklittle_test* sent 20 hours ago
            SpannableStringBuilder builder = new SpannableStringBuilder();
            SpannableString authorSS = new SpannableString(item.getAuthor());
            builder.append("from ");
            // Make the author bold and a different color
            int authorLen = item.getAuthor().length();
            StyleSpan authorStyleSpan = new StyleSpan(Typeface.BOLD);
            authorSS.setSpan(authorStyleSpan, 0, authorLen, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            ForegroundColorSpan fcs;
            if (Util.isLightTheme(mSettings.getTheme()))
                fcs = new ForegroundColorSpan(getResources().getColor(R.color.dark_blue));
            else
                fcs = new ForegroundColorSpan(getResources().getColor(R.color.white));
            authorSS.setSpan(fcs, 0, authorLen, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            builder.append(authorSS);
            // When it was sent
            builder.append(" sent ");
            builder.append(Util.getTimeAgo(Double.valueOf(item.getCreated_utc())));
            fromInfoView.setText(builder);

            subjectView.setText(item.getSubject());
            bodyView.setText(item.getSpannedBody());

            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 = mMessagesAdapter.getItem(position);

        // Mark the message/comment as selected
        mVoteTargetThingInfo = item;
        mVoteTargetView = v;
        mReplyTargetName = item.getName();

        // If new, mark the message read. Otherwise handle it.
        if (item.isNew()) {
            new ReadMessageTask().execute();
        } 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 = mMessagesAdapter.getItem(rowId);

        // Mark the message/comment as selected
        mVoteTargetThingInfo = item;
        mVoteTargetView = v;
        mReplyTargetName = item.getName();

        if (item.isWas_comment()) {
            // TODO: include the context!
            menu.add(0, Constants.DIALOG_COMMENT_CLICK, Menu.NONE, "Go to comment");
        } else {
            menu.add(0, Constants.DIALOG_MESSAGE_CLICK, Menu.NONE, "Reply");
        }
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case Constants.DIALOG_COMMENT_CLICK:
            Intent 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_MESSAGE_CLICK:
            showDialog(Constants.DIALOG_REPLY);
            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(MessagesListAdapter 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.
                mMessagesList = new ArrayList<ThingInfo>();
                mMessagesAdapter = new MessagesListAdapter(this, mMessagesList);
            } else {
                mMessagesAdapter = messagesAdapter;
            }
            setListAdapter(mMessagesAdapter);
            mMessagesAdapter.mIsLoading = false;
            mMessagesAdapter.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 (mMessagesAdapter != null)
                mMessagesAdapter.mIsLoading = true;
        }
        getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_START);
    }

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

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

    /**
     * Task takes in a subreddit name string and thread id, downloads its data, parses
     * out the comments, and communicates them back to the UI as they are read.
     */
    private class DownloadMessagesTask extends AsyncTask<Integer, Long, Void> implements PropertyChangeListener {

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

        private String mAfter = null;
        private String mBefore = null;
        private int mCount = Constants.DEFAULT_THREAD_DOWNLOAD_LIMIT;
        private String mLastAfter = null;
        private String mLastBefore = null;
        private int mLastCount = 0;

        private String mWhichInbox = "inbox";

        public DownloadMessagesTask(String whichInbox) {
            this.mWhichInbox = whichInbox;
        }

        public DownloadMessagesTask(String whichInbox, String after, String before, int count) {
            this(whichInbox);

            mAfter = after;
            mBefore = before;
            mCount = count;
        }

        protected void saveState() {
            InboxListActivity.this.mLastAfter = mLastAfter;
            InboxListActivity.this.mLastBefore = mLastBefore;
            InboxListActivity.this.mLastCount = mLastCount;
            InboxListActivity.this.mAfter = mAfter;
            InboxListActivity.this.mBefore = mBefore;
            InboxListActivity.this.mCount = mCount;
        }

        // 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 {
                String url;
                StringBuilder sb = new StringBuilder(Constants.REDDIT_BASE_URL + "/message/").append(mWhichInbox)
                        .append("/.json?");

                // "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);

                parseInboxJSON(pin);

                // XXX: HACK: http://code.reddit.com/ticket/709
                // Marking messages as read is currently broken (even with mark=true)
                // For now, just send an extra request to the regular non-JSON inbox
                mClient.execute(new HttpGet(Constants.REDDIT_BASE_URL + "/message/" + mWhichInbox));

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

        private void parseInboxJSON(InputStream in) throws IOException, JsonParseException, IllegalStateException {

            String genericListingError = "Not an inbox listing";
            try {
                Listing listing = mObjectMapper.readValue(in, Listing.class);
                Assert.assertEquals(Constants.JSON_LISTING, listing.getKind(), 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()) {
                    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);
                }
            } catch (Exception ex) {
                if (Constants.LOGGING)
                    Log.e(TAG, "parseInboxJSON", ex);
            }
        }

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

        @Override
        public void onPostExecute(Void v) {
            synchronized (mCurrentDownloadMessagesTaskLock) {
                mCurrentDownloadMessagesTask = null;
            }
            synchronized (MESSAGE_ADAPTER_LOCK) {
                for (ThingInfo mi : _mThingInfos)
                    mMessagesAdapter.add(mi);
            }

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

            disableLoadingScreen();
            Common.cancelMailNotification(InboxListActivity.this.getApplicationContext());
        }

        @Override
        public void onProgressUpdate(Long... progress) {
            if (_mContentLength != -1)
                getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
                        progress[0].intValue() * (Window.PROGRESS_END - 1) / (int) _mContentLength);
        }

        public void propertyChange(PropertyChangeEvent event) {
            publishProgress((Long) event.getNewValue());
        }
    }

    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(InboxListActivity.this, "Logged in as " + mUsername, Toast.LENGTH_SHORT).show();
                // Refresh the threads list
                new DownloadMessagesTask(mWhichInbox).execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT);
            } else {
                Common.showErrorToast(mUserError, Toast.LENGTH_LONG, InboxListActivity.this);
                returnStatus(Constants.RESULT_LOGIN_REQUIRED);
            }
        }
    }

    private class ReadMessageTask extends AsyncTask<Void, Void, Boolean> {

        private static final String TAG = "ReadMessageTask";

        private String _mUserError = "Error marking messag read.";
        private ThingInfo _mTargetThingInfo;

        ReadMessageTask() {
            _mTargetThingInfo = mVoteTargetThingInfo;
        }

        @Override
        public Boolean doInBackground(Void... v) {
            String status = "";
            HttpEntity entity = null;

            if (!mSettings.isLoggedIn()) {
                _mUserError = "You must be logged in to read the message.";
                return false;
            }

            // Update the modhash if necessary
            if (mSettings.getModhash() == null) {
                String modhash = Common.doUpdateModhash(mClient);
                if (modhash == null) {
                    // doUpdateModhash should have given an error about credentials
                    Common.doLogout(mSettings, mClient, getApplicationContext());
                    if (Constants.LOGGING)
                        Log.e(TAG, "Read message failed because doUpdateModhash() failed");
                    return false;
                }
                mSettings.setModhash(modhash);
            }

            try {
                // Construct data
                List<NameValuePair> nvps = new ArrayList<NameValuePair>();
                nvps.add(new BasicNameValuePair("id", _mTargetThingInfo.getName()));
                nvps.add(new BasicNameValuePair("uh", mSettings.getModhash().toString()));
                // Votehash is currently unused by reddit 
                //                nvps.add(new BasicNameValuePair("vh", "0d4ab0ffd56ad0f66841c15609e9a45aeec6b015"));

                HttpPost httppost = new HttpPost(Constants.REDDIT_BASE_URL + "/api/read_message");
                httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));

                if (Constants.LOGGING)
                    Log.d(TAG, nvps.toString());

                // Perform the HTTP POST request
                HttpResponse response = mClient.execute(httppost);
                status = response.getStatusLine().toString();
                if (!status.contains("OK")) {
                    _mUserError = "HTTP error when marking message read. Try again.";
                    throw new HttpException(status);
                }

                entity = response.getEntity();

                BufferedReader in = new BufferedReader(new InputStreamReader(entity.getContent()));
                String line = in.readLine();
                in.close();
                if (StringUtils.isEmpty(line)) {
                    _mUserError = "Connection error when marking message read. Try again.";
                    throw new HttpException("No content returned from read_message POST");
                }
                if (line.contains("WRONG_PASSWORD")) {
                    _mUserError = "Wrong password.";
                    throw new Exception("Wrong password.");
                }
                if (line.contains("USER_REQUIRED")) {
                    // The modhash probably expired
                    throw new Exception("User required. Huh?");
                }

                if (Constants.LOGGING)
                    Common.logDLong(TAG, line);

                return true;

            } catch (Exception e) {
                if (Constants.LOGGING)
                    Log.e(TAG, "ReadMessageTask", e);
            } finally {
                if (entity != null) {
                    try {
                        entity.consumeContent();
                    } catch (Exception e2) {
                        if (Constants.LOGGING)
                            Log.e(TAG, "entity.consumeContent()", e2);
                    }
                }
            }
            return false;
        }

        @Override
        public void onPreExecute() {
            if (!mSettings.isLoggedIn()) {
                Common.showErrorToast("You must be logged in to read message.", Toast.LENGTH_LONG,
                        InboxListActivity.this);
                cancel(true);
                return;
            }
            _mTargetThingInfo.setNew(false);
            mMessagesAdapter.notifyDataSetChanged();
        }

        @Override
        public void onPostExecute(Boolean success) {
            if (!success) {
                // Read message failed. Mark new again...
                _mTargetThingInfo.setLikes(true);
                mMessagesAdapter.notifyDataSetChanged();

                Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, InboxListActivity.this);
            }
        }
    }

    private class MessageReplyTask extends AsyncTask<String, Void, Boolean> {
        private String _mParentThingId;
        String _mUserError = "Error submitting reply. Please try again.";

        MessageReplyTask(String parentThingId) {
            _mParentThingId = parentThingId;
        }

        @Override
        public Boolean doInBackground(String... text) {
            HttpEntity entity = null;

            if (!mSettings.isLoggedIn()) {
                Common.showErrorToast("You must be logged in to reply.", Toast.LENGTH_LONG, InboxListActivity.this);
                _mUserError = "Not logged in";
                return false;
            }
            // Update the modhash if necessary
            if (mSettings.getModhash() == null) {
                String modhash = Common.doUpdateModhash(mClient);
                if (modhash == null) {
                    // doUpdateModhash should have given an error about credentials
                    Common.doLogout(mSettings, mClient, getApplicationContext());
                    if (Constants.LOGGING)
                        Log.e(TAG, "Reply failed because doUpdateModhash() failed");
                    return false;
                }
                mSettings.setModhash(modhash);
            }

            try {
                // Construct data
                List<NameValuePair> nvps = new ArrayList<NameValuePair>();
                nvps.add(new BasicNameValuePair("thing_id", _mParentThingId));
                nvps.add(new BasicNameValuePair("text", text[0]));
                nvps.add(new BasicNameValuePair("uh", mSettings.getModhash()));
                // Votehash is currently unused by reddit 
                //                nvps.add(new BasicNameValuePair("vh", "0d4ab0ffd56ad0f66841c15609e9a45aeec6b015"));

                HttpPost httppost = new HttpPost(Constants.REDDIT_BASE_URL + "/api/comment");
                httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));

                if (Constants.LOGGING)
                    Log.d(TAG, nvps.toString());

                // Perform the HTTP POST request
                HttpResponse response = mClient.execute(httppost);
                entity = response.getEntity();

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

                return true;

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

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

        @Override
        public void onPostExecute(Boolean success) {
            removeDialog(Constants.DIALOG_REPLYING);
            if (success) {
                Toast.makeText(InboxListActivity.this, "Reply 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, InboxListActivity.this);
            }
        }
    }

    @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, true) {
                @Override
                public void onLoginChosen(String user, String password) {
                    removeDialog(Constants.DIALOG_LOGIN);
                    new MyLoginTask(user, password).execute();
                }
            };
            break;

        case Constants.DIALOG_REPLY:
            dialog = new Dialog(this, mSettings.getDialogTheme());
            dialog.setContentView(R.layout.compose_reply_dialog);
            final EditText replyBody = (EditText) dialog.findViewById(R.id.body);
            final Button replySaveButton = (Button) dialog.findViewById(R.id.reply_save_button);
            final Button replyCancelButton = (Button) dialog.findViewById(R.id.reply_cancel_button);
            replySaveButton.setOnClickListener(new OnClickListener() {
                public void onClick(View v) {
                    if (mReplyTargetName != null) {
                        new MessageReplyTask(mReplyTargetName).execute(replyBody.getText().toString());
                        removeDialog(Constants.DIALOG_REPLY);
                    } else {
                        Common.showErrorToast("Error replying. Please try again.", Toast.LENGTH_SHORT,
                                InboxListActivity.this);
                    }
                }
            });
            replyCancelButton.setOnClickListener(new OnClickListener() {
                public void onClick(View v) {
                    removeDialog(Constants.DIALOG_REPLY);
                }
            });
            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;

        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_REPLY:
            if (mVoteTargetThingInfo != null && mVoteTargetThingInfo.getReplyDraft() != null) {
                EditText replyBodyView = (EditText) dialog.findViewById(R.id.body);
                replyBodyView.setText(mVoteTargetThingInfo.getReplyDraft());
            }
            break;

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

    private final OnClickListener downloadAfterOnClickListener = new OnClickListener() {
        public void onClick(View v) {
            new DownloadMessagesTask(mWhichInbox, mAfter, null, mCount).execute();
        }
    };
    private final OnClickListener downloadBeforeOnClickListener = new OnClickListener() {
        public void onClick(View v) {
            new DownloadMessagesTask(mWhichInbox, null, mBefore, mCount).execute();
        }
    };

    @Override
    protected void onSaveInstanceState(Bundle state) {
        super.onSaveInstanceState(state);
        state.putString(Constants.REPLY_TARGET_NAME_KEY, mReplyTargetName);
        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, mVoteTargetThingInfo);
        state.putString(Constants.WHICH_INBOX_KEY, mWhichInbox);
    }

    /**
     * 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.
            }
        }
    }
}