Java tutorial
/* * Copyright 2009 Andrew Shu * * This file is part of "diode". * * "diode" 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. * * "diode" 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 "diode". If not, see <http://www.gnu.org/licenses/>. */ package in.shick.diode.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 in.shick.diode.markdown.Markdown; 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.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.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 in.shick.diode.R; import in.shick.diode.comments.CommentsListActivity; import in.shick.diode.common.Common; import in.shick.diode.common.Constants; import in.shick.diode.common.ProgressInputStream; import in.shick.diode.common.RedditIsFunHttpClientFactory; import in.shick.diode.common.util.Assert; import in.shick.diode.common.util.StringUtils; import in.shick.diode.common.util.Util; import in.shick.diode.login.LoginDialog; import in.shick.diode.login.LoginTask; import in.shick.diode.settings.RedditSettings; import in.shick.diode.things.Listing; import in.shick.diode.things.ListingData; import in.shick.diode.things.ThingInfo; import in.shick.diode.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 ThingInfo mVoteTargetThingInfo = null; private String mReplyTargetName = 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; /** * 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); } 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(); String authorString = item.getAuthor(); if (authorString == null) { authorString = "<other>"; } SpannableString authorSS = new SpannableString(authorString); builder.append("from "); // Make the author bold and a different color int authorLen = authorString.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(item.getCreated_utc(), getResources())); 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; 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; mReplyTargetName = item.getName(); if (item.isWas_comment()) { menu.add(0, Constants.DIALOG_COMMENT_CLICK, Menu.NONE, R.string.view_context); } else { menu.add(0, Constants.DIALOG_MESSAGE_CLICK, Menu.NONE, "Reply"); } // Lazy-load the URL list to make the list-loading more 'snappy'. if (mVoteTargetThingInfo.getUrls() != null && mVoteTargetThingInfo.getUrls().isEmpty()) { Markdown.getURLs(mVoteTargetThingInfo.getBody(), mVoteTargetThingInfo.getUrls()); } if (mVoteTargetThingInfo.getUrls() != null && !mVoteTargetThingInfo.getUrls().isEmpty()) { menu.add(0, Constants.DIALOG_LINKS, Menu.NONE, R.string.links_menu_item); } } @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; case Constants.DIALOG_LINKS: Common.showLinksDialog(InboxListActivity.this, mSettings, mVoteTargetThingInfo); 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())); // 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; 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. } } } }