/* * 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.comments; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.http.HttpEntity; 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.HttpPost; import org.apache.http.message.BasicNameValuePair; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.protocol.HTTP; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ListActivity; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.telephony.PhoneNumberUtils; import android.text.Html; import android.text.SpannableString; import android.text.Spanned; import android.text.style.ForegroundColorSpan; 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.CheckBox; 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.common.CacheInfo; import com.andrewshu.android.reddit.common.Common; import com.andrewshu.android.reddit.common.Constants; import com.andrewshu.android.reddit.common.RedditIsFunHttpClientFactory; import com.andrewshu.android.reddit.common.tasks.HideTask; import com.andrewshu.android.reddit.common.tasks.SaveTask; import com.andrewshu.android.reddit.common.util.CollectionUtils; import com.andrewshu.android.reddit.common.util.StringUtils; import com.andrewshu.android.reddit.common.util.Util; import com.andrewshu.android.reddit.login.LoginDialog; import com.andrewshu.android.reddit.login.LoginTask; import com.andrewshu.android.reddit.mail.InboxActivity; import com.andrewshu.android.reddit.mail.PeekEnvelopeTask; import com.andrewshu.android.reddit.markdown.MarkdownURL; import com.andrewshu.android.reddit.settings.RedditPreferencesPage; import com.andrewshu.android.reddit.settings.RedditSettings; import com.andrewshu.android.reddit.things.ThingInfo; import com.andrewshu.android.reddit.threads.ThreadsListActivity; import com.andrewshu.android.reddit.threads.ThumbnailOnClickListenerFactory; import com.andrewshu.android.reddit.user.ProfileActivity; /** * Main Activity class representing a Subreddit, i.e., a ThreadsList. * * @author TalkLittle * */ public class CommentsListActivity extends ListActivity implements View.OnCreateContextMenuListener { private static final String TAG = "CommentsListActivity"; // Group 2: subreddit name. Group 3: thread id36. Group 4: Comment id36. private final Pattern COMMENT_PATH_PATTERN = Pattern.compile(Constants.COMMENT_PATH_PATTERN_STRING); private final Pattern COMMENT_CONTEXT_PATTERN = Pattern.compile("context=(\\d+)"); /** Custom list adapter that fits our threads data into the list. */ CommentsListAdapter mCommentsAdapter = null; ArrayList<ThingInfo> mCommentsList = null; private final HttpClient mClient = RedditIsFunHttpClientFactory.getGzipHttpClient(); // Common settings are stored here private final RedditSettings mSettings = new RedditSettings(); private String mSubreddit = null; private String mThreadId = null; private String mThreadTitle = null; // UI State private ThingInfo mVoteTargetThing = null; private String mReportTargetName = null; private String mReplyTargetName = null; private String mEditTargetBody = null; private String mDeleteTargetKind = null; private boolean mShouldClearReply = false; private String last_search_string; private int last_found_position = -1; private boolean mCanChord = false; // override transition animation available Android 2.0 (SDK Level 5) and above private static Method mActivity_overridePendingTransition; static { initCompatibility(); }; private static void initCompatibility() { try { mActivity_overridePendingTransition = Activity.class.getMethod( "overridePendingTransition", new Class[] { Integer.TYPE, Integer.TYPE } ); /* success, this is a newer device */ } catch (NoSuchMethodException nsme) { /* failure, must be older device */ } } /** * Called when the activity starts up. Do activity initialization * here, not in a constructor. * * @see Activity#onCreate */ @SuppressWarnings("unchecked") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); CookieSyncManager.createInstance(getApplicationContext()); mSettings.loadRedditPreferences(this, mClient); setRequestedOrientation(mSettings.getRotation()); setTheme(mSettings.getTheme()); requestWindowFeature(Window.FEATURE_PROGRESS); requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); setContentView(R.layout.comments_list_content); registerForContextMenu(getListView()); if (savedInstanceState != null) { mReplyTargetName = savedInstanceState.getString(Constants.REPLY_TARGET_NAME_KEY); mReportTargetName = savedInstanceState.getString(Constants.REPORT_TARGET_NAME_KEY); mEditTargetBody = savedInstanceState.getString(Constants.EDIT_TARGET_BODY_KEY); mDeleteTargetKind = savedInstanceState.getString(Constants.DELETE_TARGET_KIND_KEY); mThreadTitle = savedInstanceState.getString(Constants.THREAD_TITLE_KEY); mSubreddit = savedInstanceState.getString(Constants.SUBREDDIT_KEY); mThreadId = savedInstanceState.getString(Constants.THREAD_ID_KEY); mVoteTargetThing = savedInstanceState.getParcelable(Constants.VOTE_TARGET_THING_INFO_KEY); if (mThreadTitle != null) { setTitle(mThreadTitle + " : " + mSubreddit); } mCommentsList = (ArrayList<ThingInfo>) getLastNonConfigurationInstance(); if (mCommentsList == null) { getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT); } else { // Orientation change. Use prior instance. resetUI(new CommentsListAdapter(this, mCommentsList)); } } // No saved state; use info from Intent.getData() else { String commentPath; String commentQuery; String jumpToCommentId = null; int jumpToCommentContext = 0; // We get the URL through getIntent().getData() Uri data = getIntent().getData(); if (data != null) { // Comment path: a URL pointing to a thread or a comment in a thread. commentPath = data.getPath(); commentQuery = data.getQuery(); } else { if (Constants.LOGGING) Log.e(TAG, "Quitting because no subreddit and thread id data was passed into the Intent."); finish(); return; } if (commentPath != null) { if (Constants.LOGGING) Log.d(TAG, "comment path: "+commentPath); if (Util.isRedditShortenedUri(data)) { // http://redd.it/abc12 mThreadId = commentPath.substring(1); } else { // http://www.reddit.com/... Matcher m = COMMENT_PATH_PATTERN.matcher(commentPath); if (m.matches()) { mSubreddit = m.group(1); mThreadId = m.group(2); jumpToCommentId = m.group(3); } } } else { if (Constants.LOGGING) Log.e(TAG, "Quitting because of bad comment path."); finish(); return; } if (commentQuery != null) { Matcher m = COMMENT_CONTEXT_PATTERN.matcher(commentQuery); if (m.find()) { jumpToCommentContext = m.group(1) != null ? Integer.valueOf(m.group(1)) : 0; } } // Extras: subreddit, threadTitle, numComments // subreddit is not always redundant to Intent.getData(), // since URL does not always contain the subreddit. (e.g., self posts) Bundle extras = getIntent().getExtras(); if (extras != null) { // subreddit could have already been set from the Intent.getData. don't overwrite with null here! String subreddit = extras.getString(Constants.EXTRA_SUBREDDIT); if (subreddit != null) mSubreddit = subreddit; // mThreadTitle has not been set yet, so no need for null check before setting it mThreadTitle = extras.getString(Constants.EXTRA_TITLE); if (mThreadTitle != null) { setTitle(mThreadTitle + " : " + mSubreddit); } // TODO: use extras.getInt(Constants.EXTRA_NUM_COMMENTS) somehow } if (!StringUtils.isEmpty(jumpToCommentId)) { getNewDownloadCommentsTask().prepareLoadAndJumpToComment(jumpToCommentId, jumpToCommentContext) .execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT); } else { getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT); } } } @Override protected void onResume() { super.onResume(); int previousTheme = mSettings.getTheme(); mSettings.loadRedditPreferences(this, mClient); if (mSettings.getTheme() != previousTheme) { relaunchActivity(); } else { CookieSyncManager.getInstance().startSync(); setRequestedOrientation(mSettings.getRotation()); if (mSettings.isLoggedIn()) new PeekEnvelopeTask(this, mClient, mSettings.getMailNotificationStyle()).execute(); } } private void relaunchActivity() { finish(); startActivity(getIntent()); } @Override protected void onPause() { super.onPause(); CookieSyncManager.getInstance().stopSync(); mSettings.saveRedditPreferences(this); } @Override public Object onRetainNonConfigurationInstance() { return mCommentsList; } private DownloadCommentsTask getNewDownloadCommentsTask() { return new DownloadCommentsTask( this, mSubreddit, mThreadId, mSettings, mClient ); } private boolean isHiddenCommentHeadPosition(int position) { return mCommentsAdapter != null && mCommentsAdapter.getItemViewType(position) == CommentsListAdapter.HIDDEN_ITEM_HEAD_VIEW_TYPE; } private boolean isHiddenCommentDescendantPosition(int position) { return mCommentsAdapter != null && mCommentsAdapter.getItem(position).isHiddenCommentDescendant(); } private boolean isLoadMoreCommentsPosition(int position) { return mCommentsAdapter != null && mCommentsAdapter.getItemViewType(position) == CommentsListAdapter.MORE_ITEM_VIEW_TYPE; } final class CommentsListAdapter extends ArrayAdapter<ThingInfo> { public static final int OP_ITEM_VIEW_TYPE = 0; public static final int COMMENT_ITEM_VIEW_TYPE = 1; public static final int MORE_ITEM_VIEW_TYPE = 2; public static final int HIDDEN_ITEM_HEAD_VIEW_TYPE = 3; // The number of view types public static final int VIEW_TYPE_COUNT = 4; public boolean mIsLoading = true; private LayoutInflater mInflater; private int mFrequentSeparatorPos = ListView.INVALID_POSITION; public CommentsListAdapter(Context context, List<ThingInfo> objects) { super(context, 0, objects); mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @Override public int getItemViewType(int position) { if (position == 0) return OP_ITEM_VIEW_TYPE; if (position == mFrequentSeparatorPos) { // We don't want the separator view to be recycled. return IGNORE_ITEM_VIEW_TYPE; } ThingInfo item = getItem(position); if (item.isHiddenCommentDescendant()) return IGNORE_ITEM_VIEW_TYPE; if (item.isHiddenCommentHead()) return HIDDEN_ITEM_HEAD_VIEW_TYPE; if (item.isLoadMoreCommentsPlaceholder()) return MORE_ITEM_VIEW_TYPE; return COMMENT_ITEM_VIEW_TYPE; } @Override public int getViewTypeCount() { return VIEW_TYPE_COUNT; } @Override public boolean isEmpty() { if (mIsLoading) return false; return super.isEmpty(); } @Override public View getView(int position, View convertView, ViewGroup parent) { View view = convertView; ThingInfo item = this.getItem(position); try { if (position == 0) { // The OP if (view == null) { view = mInflater.inflate(R.layout.threads_list_item, null); } ThreadsListActivity.fillThreadsListItemView( position, view, item, CommentsListActivity.this, mClient, mSettings, mThumbnailOnClickListenerFactory ); if (item.isIs_self()) { View thumbnailContainer = view.findViewById(R.id.thumbnail_view); if (thumbnailContainer != null) thumbnailContainer.setVisibility(View.GONE); } // In addition to stuff from ThreadsListActivity, // we want to show selftext in CommentsListActivity. TextView submissionStuffView = (TextView) view.findViewById(R.id.submissionTime_submitter); TextView selftextView = (TextView) view.findViewById(R.id.selftext); submissionStuffView.setVisibility(View.VISIBLE); submissionStuffView.setText( String.format(getResources().getString(R.string.thread_time_submitter), Util.getTimeAgo(item.getCreated_utc()), item.getAuthor())); if (!StringUtils.isEmpty(item.getSpannedSelftext())) { selftextView.setVisibility(View.VISIBLE); selftextView.setText(item.getSpannedSelftext()); } else { selftextView.setVisibility(View.GONE); } } else if (isHiddenCommentDescendantPosition(position)) { if (view == null) { // Doesn't matter which view we inflate since it's gonna be invisible view = mInflater.inflate(R.layout.zero_size_layout, null); } } else if (isHiddenCommentHeadPosition(position)) { if (view == null) { view = mInflater.inflate(R.layout.comments_list_item_hidden, null); } TextView votesView = (TextView) view.findViewById(R.id.votes); TextView submitterView = (TextView) view.findViewById(R.id.submitter); TextView submissionTimeView = (TextView) view.findViewById(R.id.submissionTime); try { votesView.setText(Util.showNumPoints(item.getUps() - item.getDowns())); } catch (NumberFormatException e) { // This happens because "ups" comes after the potentially long "replies" object, // so the ListView might try to display the View before "ups" in JSON has been parsed. if (Constants.LOGGING) Log.e(TAG, "getView, hidden comment heads", e); } if (getOpThingInfo() != null && item.getAuthor().equalsIgnoreCase(getOpThingInfo().getAuthor())) submitterView.setText(item.getAuthor() + " [S]"); else submitterView.setText(item.getAuthor()); submissionTimeView.setText(Util.getTimeAgo(item.getCreated_utc())); setCommentIndent(view, item.getIndent(), mSettings); } else if (isLoadMoreCommentsPosition(position)) { // "load more comments" if (view == null) { view = mInflater.inflate(R.layout.more_comments_view, null); } setCommentIndent(view, item.getIndent(), mSettings); } else { // Regular comment // Here view may be passed in for re-use, or we make a new one. if (view == null) { view = mInflater.inflate(R.layout.comments_list_item, null); } else { view = convertView; } // Sometimes (when in touch mode) the "selection" highlight disappears. // So we make our own persistent highlight. This background color must // be set explicitly on every element, however, or the "cached" list // item views will show up with the color. if (position == last_found_position) view.setBackgroundResource(R.color.translucent_yellow); else view.setBackgroundColor(Color.TRANSPARENT); fillCommentsListItemView(view, item, mSettings); } } catch (NullPointerException e) { if (Constants.LOGGING) Log.w(TAG, "NPE in getView()", e); // Probably means that the List is still being built, and OP probably got put in wrong position if (view == null) { if (position == 0) view = mInflater.inflate(R.layout.threads_list_item, null); else view = mInflater.inflate(R.layout.comments_list_item, null); } } return view; } } // End of CommentsListAdapter public ThingInfo getOpThingInfo() { if (!CollectionUtils.isEmpty(mCommentsList)) return mCommentsList.get(0); return null; } public void setThreadTitle(String threadTitle) { this.mThreadTitle = threadTitle; } public void setShouldClearReply(boolean shouldClearReply) { this.mShouldClearReply = shouldClearReply; } public static void setCommentIndent(View commentListItemView, int indentLevel, RedditSettings settings) { View[] indentViews = new View[] { commentListItemView.findViewById(R.id.left_indent1), commentListItemView.findViewById(R.id.left_indent2), commentListItemView.findViewById(R.id.left_indent3), commentListItemView.findViewById(R.id.left_indent4), commentListItemView.findViewById(R.id.left_indent5), commentListItemView.findViewById(R.id.left_indent6), commentListItemView.findViewById(R.id.left_indent7), commentListItemView.findViewById(R.id.left_indent8) }; for (int i = 0; i < indentLevel && i < indentViews.length; i++) { if (settings.isShowCommentGuideLines()) { indentViews[i].setVisibility(View.VISIBLE); if (Util.isLightTheme(settings.getTheme())) { indentViews[i].setBackgroundResource(R.color.light_light_gray); } else { indentViews[i].setBackgroundResource(R.color.dark_gray); } } else { indentViews[i].setVisibility(View.INVISIBLE); } } for (int i = indentLevel; i < indentViews.length; i++) { indentViews[i].setVisibility(View.GONE); } } /** * Called when user clicks an item in the list. Starts an activity to * open the url for that item. */ @Override protected void onListItemClick(ListView l, View v, int position, long id) { ThingInfo item = mCommentsAdapter.getItem(position); if (isHiddenCommentHeadPosition(position)) { showComment(position); return; } // Mark the OP post/regular comment as selected mVoteTargetThing = item; mReplyTargetName = mVoteTargetThing.getName(); if (isLoadMoreCommentsPosition(position)) { // Use this constructor to tell it to load more comments inline getNewDownloadCommentsTask().prepareLoadMoreComments(item.getId(), position, item.getIndent()) .execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT); } else { if (!"[deleted]".equals(item.getAuthor())) showDialog(Constants.DIALOG_COMMENT_CLICK); } } /** * Resets the output UI list contents, retains session state. * @param commentsAdapter A new CommentsListAdapter to use. Pass in null to create a new empty one. */ void resetUI(CommentsListAdapter commentsAdapter) { findViewById(R.id.loading_light).setVisibility(View.GONE); findViewById(R.id.loading_dark).setVisibility(View.GONE); if (commentsAdapter == null) { // Reset the list to be empty. mCommentsList = new ArrayList<ThingInfo>(); mCommentsAdapter = new CommentsListAdapter(this, mCommentsList); setListAdapter(mCommentsAdapter); } else if (mCommentsAdapter != commentsAdapter) { mCommentsAdapter = commentsAdapter; setListAdapter(commentsAdapter); } mCommentsAdapter.mIsLoading = false; mCommentsAdapter.notifyDataSetChanged(); // Just in case getListView().setDivider(null); Common.updateListDrawables(this, mSettings.getTheme()); } /** * Mark the OP submitter comments */ void markSubmitterComments() { if (getOpThingInfo() == null || mCommentsAdapter == null) return; SpannableString authorSS = new SpannableString(getOpThingInfo().getAuthor() + " [S]"); ForegroundColorSpan fcs; if (Util.isLightTheme(mSettings.getTheme())) fcs = new ForegroundColorSpan(getResources().getColor(R.color.blue)); else fcs = new ForegroundColorSpan(getResources().getColor(R.color.pale_blue)); authorSS.setSpan(fcs, 0, authorSS.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); for (int i = 0; i < mCommentsAdapter.getCount(); i++) { ThingInfo ci = mCommentsAdapter.getItem(i); // if it's the OP, mark his name if (getOpThingInfo().getAuthor().equalsIgnoreCase(ci.getAuthor())) ci.setSSAuthor(authorSS); } } void enableLoadingScreen() { if (Util.isLightTheme(mSettings.getTheme())) { findViewById(R.id.loading_light).setVisibility(View.VISIBLE); findViewById(R.id.loading_dark).setVisibility(View.GONE); } else { findViewById(R.id.loading_light).setVisibility(View.GONE); findViewById(R.id.loading_dark).setVisibility(View.VISIBLE); } if (mCommentsAdapter != null) mCommentsAdapter.mIsLoading = true; getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_START); } private class MyLoginTask extends LoginTask { public MyLoginTask(String username, String password) { super(username, password, mSettings, mClient, getApplicationContext()); } @Override protected void onPreExecute() { showDialog(Constants.DIALOG_LOGGING_IN); } @Override protected void onPostExecute(Boolean success) { removeDialog(Constants.DIALOG_LOGGING_IN); if (success) { Toast.makeText(CommentsListActivity.this, "Logged in as "+mUsername, Toast.LENGTH_SHORT).show(); // Check mail new PeekEnvelopeTask(CommentsListActivity.this, mClient, mSettings.getMailNotificationStyle()).execute(); // Refresh the comments list getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT); } else { Common.showErrorToast(mUserError, Toast.LENGTH_LONG, CommentsListActivity.this); } } } private class CommentReplyTask extends AsyncTask<String, Void, String> { private String _mParentThingId; String _mUserError = "Error submitting reply. Please try again."; CommentReplyTask(String parentThingId) { _mParentThingId = parentThingId; } @Override public String doInBackground(String... text) { HttpEntity entity = null; if (!mSettings.isLoggedIn()) { Common.showErrorToast("You must be logged in to reply.", Toast.LENGTH_LONG, CommentsListActivity.this); _mUserError = "Not logged in"; return null; } // Update the modhash if necessary if (mSettings.getModhash() == null) { String modhash = Common.doUpdateModhash(mClient); if (modhash == null) { // doUpdateModhash should have given an error about credentials Common.doLogout(mSettings, mClient, getApplicationContext()); if (Constants.LOGGING) Log.e(TAG, "Reply failed because doUpdateModhash() failed"); return null; } mSettings.setModhash(modhash); } try { // Construct data List<NameValuePair> nvps = new ArrayList<NameValuePair>(); nvps.add(new BasicNameValuePair("thing_id", _mParentThingId)); nvps.add(new BasicNameValuePair("text", text[0])); nvps.add(new BasicNameValuePair("r", mSubreddit)); nvps.add(new BasicNameValuePair("uh", mSettings.getModhash())); // Votehash is currently unused by reddit // nvps.add(new BasicNameValuePair("vh", "0d4ab0ffd56ad0f66841c15609e9a45aeec6b015")); HttpPost httppost = new HttpPost(Constants.REDDIT_BASE_URL + "/api/comment"); httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8)); HttpParams params = httppost.getParams(); HttpConnectionParams.setConnectionTimeout(params, 40000); HttpConnectionParams.setSoTimeout(params, 40000); if (Constants.LOGGING) Log.d(TAG, nvps.toString()); // Perform the HTTP POST request HttpResponse response = mClient.execute(httppost); entity = response.getEntity(); // Getting here means success. Create a new CommentInfo. return Common.checkIDResponse(response, entity); } catch (Exception e) { if (Constants.LOGGING) Log.e(TAG, "CommentReplyTask", e); _mUserError = e.getMessage(); } finally { if (entity != null) { try { entity.consumeContent(); } catch (Exception e2) { if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e2); } } } return null; } @Override public void onPreExecute() { showDialog(Constants.DIALOG_REPLYING); } @Override public void onPostExecute(String newId) { removeDialog(Constants.DIALOG_REPLYING); if (newId == null) { Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, CommentsListActivity.this); } else { // Refresh CacheInfo.invalidateCachedThread(getApplicationContext()); getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT); } } } private class EditTask extends AsyncTask<String, Void, String> { private String _mThingId; String _mUserError = "Error submitting edit. Please try again."; EditTask(String thingId) { _mThingId = thingId; } @Override public String doInBackground(String... text) { HttpEntity entity = null; if (!mSettings.isLoggedIn()) { _mUserError = "You must be logged in to edit."; return null; } // Update the modhash if necessary if (mSettings.getModhash() == null) { String modhash = Common.doUpdateModhash(mClient); if (modhash == null) { // doUpdateModhash should have given an error about credentials Common.doLogout(mSettings, mClient, getApplicationContext()); if (Constants.LOGGING) Log.e(TAG, "Reply failed because doUpdateModhash() failed"); return null; } mSettings.setModhash(modhash); } try { // Construct data List<NameValuePair> nvps = new ArrayList<NameValuePair>(); nvps.add(new BasicNameValuePair("thing_id", _mThingId.toString())); nvps.add(new BasicNameValuePair("text", text[0].toString())); nvps.add(new BasicNameValuePair("r", mSubreddit.toString())); nvps.add(new BasicNameValuePair("uh", mSettings.getModhash().toString())); HttpPost httppost = new HttpPost(Constants.REDDIT_BASE_URL + "/api/editusertext"); httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8)); HttpParams params = httppost.getParams(); HttpConnectionParams.setConnectionTimeout(params, 40000); HttpConnectionParams.setSoTimeout(params, 40000); if (Constants.LOGGING) Log.d(TAG, nvps.toString()); // Perform the HTTP POST request HttpResponse response = mClient.execute(httppost); entity = response.getEntity(); return Common.checkIDResponse(response, entity); } catch (Exception e) { if (Constants.LOGGING) Log.e(TAG, "EditTask", e); _mUserError = e.getMessage(); } finally { if (entity != null) { try { entity.consumeContent(); } catch (Exception e2) { if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e2); } } } return null; } @Override public void onPreExecute() { showDialog(Constants.DIALOG_EDITING); } @Override public void onPostExecute(String newId) { removeDialog(Constants.DIALOG_EDITING); if (newId == null) { Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, CommentsListActivity.this); } else { // Refresh CacheInfo.invalidateCachedThread(getApplicationContext()); getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT); } } } private class DeleteTask extends AsyncTask<String, Void, Boolean> { private String _mUserError = "Error deleting. Please try again."; private String _mKind; public DeleteTask(String kind) { _mKind = kind; } @Override public Boolean doInBackground(String... thingFullname) { // POSTDATA=id=t1_c0cxa7l&executed=deleted&r=test&uh=f7jb1yjwfqd4ffed8356eb63fcfbeeadad142f57c56e9cbd9e HttpEntity entity = null; if (!mSettings.isLoggedIn()) { _mUserError = "You must be logged in to delete."; return false; } // Update the modhash if necessary if (mSettings.getModhash() == null) { String modhash = Common.doUpdateModhash(mClient); if (modhash == null) { // doUpdateModhash should have given an error about credentials Common.doLogout(mSettings, mClient, getApplicationContext()); if (Constants.LOGGING) Log.e(TAG, "Reply failed because doUpdateModhash() failed"); return false; } mSettings.setModhash(modhash); } try { // Construct data List<NameValuePair> nvps = new ArrayList<NameValuePair>(); nvps.add(new BasicNameValuePair("id", thingFullname[0].toString())); nvps.add(new BasicNameValuePair("executed", "deleted")); nvps.add(new BasicNameValuePair("r", mSubreddit.toString())); nvps.add(new BasicNameValuePair("uh", mSettings.getModhash().toString())); HttpPost httppost = new HttpPost(Constants.REDDIT_BASE_URL + "/api/del"); httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8)); HttpParams params = httppost.getParams(); HttpConnectionParams.setConnectionTimeout(params, 40000); HttpConnectionParams.setSoTimeout(params, 40000); if (Constants.LOGGING) Log.d(TAG, nvps.toString()); // Perform the HTTP POST request HttpResponse response = mClient.execute(httppost); entity = response.getEntity(); String error = Common.checkResponseErrors(response, entity); if (error != null) throw new Exception(error); // Success return true; } catch (Exception e) { if (Constants.LOGGING) Log.e(TAG, "DeleteTask", e); _mUserError = e.getMessage(); } finally { if (entity != null) { try { entity.consumeContent(); } catch (Exception e2) { if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e2); } } } return false; } @Override public void onPreExecute() { showDialog(Constants.DIALOG_DELETING); } @Override public void onPostExecute(Boolean success) { removeDialog(Constants.DIALOG_DELETING); if (success) { CacheInfo.invalidateCachedThread(getApplicationContext()); if (Constants.THREAD_KIND.equals(_mKind)) { Toast.makeText(CommentsListActivity.this, "Deleted thread.", Toast.LENGTH_LONG).show(); finish(); return; } else { Toast.makeText(CommentsListActivity.this, "Deleted comment.", Toast.LENGTH_SHORT).show(); getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT); } } else { Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, CommentsListActivity.this); } } } private class VoteTask extends AsyncTask<Void, Void, Boolean> { private static final String TAG = "VoteWorker"; private String _mThingFullname; private int _mDirection; private String _mUserError = "Error voting."; private ThingInfo _mTargetThingInfo; // Save the previous arrow and score in case we need to revert private int _mPreviousUps, _mPreviousDowns; private Boolean _mPreviousLikes; VoteTask(String thingFullname, int direction) { _mThingFullname = thingFullname; _mDirection = direction; // Copy these because they can change while voting thread is running _mTargetThingInfo = mVoteTargetThing; } @Override public Boolean doInBackground(Void... v) { HttpEntity entity = null; if (!mSettings.isLoggedIn()) { _mUserError = "You must be logged in to vote."; return false; } // Update the modhash if necessary if (mSettings.getModhash() == null) { String modhash = Common.doUpdateModhash(mClient); if (modhash == null) { // doUpdateModhash should have given an error about credentials Common.doLogout(mSettings, mClient, getApplicationContext()); if (Constants.LOGGING) Log.e(TAG, "Vote failed because doUpdateModhash() failed"); return false; } mSettings.setModhash(modhash); } try { // Construct data List<NameValuePair> nvps = new ArrayList<NameValuePair>(); nvps.add(new BasicNameValuePair("id", _mThingFullname.toString())); nvps.add(new BasicNameValuePair("dir", String.valueOf(_mDirection))); nvps.add(new BasicNameValuePair("r", mSubreddit.toString())); nvps.add(new BasicNameValuePair("uh", mSettings.getModhash().toString())); // Votehash is currently unused by reddit // nvps.add(new BasicNameValuePair("vh", "0d4ab0ffd56ad0f66841c15609e9a45aeec6b015")); HttpPost httppost = new HttpPost(Constants.REDDIT_BASE_URL + "/api/vote"); httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8)); if (Constants.LOGGING) Log.d(TAG, nvps.toString()); // Perform the HTTP POST request HttpResponse response = mClient.execute(httppost); entity = response.getEntity(); String error = Common.checkResponseErrors(response, entity); if (error != null) throw new Exception(error); return true; } catch (Exception e) { if (Constants.LOGGING) Log.e(TAG, "VoteTask", e); _mUserError = e.getMessage(); } finally { if (entity != null) { try { entity.consumeContent(); } catch (Exception e2) { if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e2); } } } return false; } public void onPreExecute() { if (!mSettings.isLoggedIn()) { Common.showErrorToast("You must be logged in to vote.", Toast.LENGTH_LONG, CommentsListActivity.this); cancel(true); return; } if (_mDirection < -1 || _mDirection > 1) { if (Constants.LOGGING) Log.e(TAG, "WTF: _mDirection = " + _mDirection); throw new RuntimeException("How the hell did you vote something besides -1, 0, or 1?"); } int newUps, newDowns; Boolean newLikes; _mPreviousUps = Integer.valueOf(_mTargetThingInfo.getUps()); _mPreviousDowns = Integer.valueOf(_mTargetThingInfo.getDowns()); newUps = _mPreviousUps; newDowns = _mPreviousDowns; _mPreviousLikes = _mTargetThingInfo.getLikes(); if (_mPreviousLikes == null) { if (_mDirection == 1) { newUps = _mPreviousUps + 1; newLikes = true; } else if (_mDirection == -1) { newDowns = _mPreviousDowns + 1; newLikes = false; } else { cancel(true); return; } } else if (_mPreviousLikes == true) { if (_mDirection == 0) { newUps = _mPreviousUps - 1; newLikes = null; } else if (_mDirection == -1) { newUps = _mPreviousUps - 1; newDowns = _mPreviousDowns + 1; newLikes = false; } else { cancel(true); return; } } else { if (_mDirection == 1) { newUps = _mPreviousUps + 1; newDowns = _mPreviousDowns - 1; newLikes = true; } else if (_mDirection == 0) { newDowns = _mPreviousDowns - 1; newLikes = null; } else { cancel(true); return; } } _mTargetThingInfo.setLikes(newLikes); _mTargetThingInfo.setUps(newUps); _mTargetThingInfo.setDowns(newDowns); _mTargetThingInfo.setScore(newUps - newDowns); mCommentsAdapter.notifyDataSetChanged(); } public void onPostExecute(Boolean success) { if (success) { CacheInfo.invalidateCachedThread(getApplicationContext()); } else { // Vote failed. Undo the arrow and score. _mTargetThingInfo.setLikes(_mPreviousLikes); _mTargetThingInfo.setUps(_mPreviousUps); _mTargetThingInfo.setDowns(_mPreviousDowns); _mTargetThingInfo.setScore(_mPreviousUps - _mPreviousDowns); mCommentsAdapter.notifyDataSetChanged(); Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, CommentsListActivity.this); } } } private class ReportTask extends AsyncTask<Void, Void, Boolean> { private static final String TAG = "ReportTask"; private String _mUserError = "Error reporting."; private String _mFullId; ReportTask(String fullname) { this._mFullId = fullname; } @Override public Boolean doInBackground(Void... v) { HttpEntity entity = null; if (!mSettings.isLoggedIn()) { _mUserError = "You must be logged in to report something."; return false; } // Update the modhash if necessary if (mSettings.getModhash() == null) { String modhash = Common.doUpdateModhash(mClient); if (modhash == null) { // doUpdateModhash should have given an error about credentials Common.doLogout(mSettings, mClient, getApplicationContext()); if (Constants.LOGGING) Log.e(TAG, "Report failed because doUpdateModhash() failed"); return false; } mSettings.setModhash(modhash); } try { // Construct data List<NameValuePair> nvps = new ArrayList<NameValuePair>(); nvps.add(new BasicNameValuePair("id", _mFullId)); nvps.add(new BasicNameValuePair("executed", "reported")); nvps.add(new BasicNameValuePair("r", mSubreddit.toString())); nvps.add(new BasicNameValuePair("uh", mSettings.getModhash().toString())); // Votehash is currently unused by reddit // nvps.add(new BasicNameValuePair("vh", "0d4ab0ffd56ad0f66841c15609e9a45aeec6b015")); HttpPost httppost = new HttpPost(Constants.REDDIT_BASE_URL + "/api/report"); httppost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8)); if (Constants.LOGGING) Log.d(TAG, nvps.toString()); // Perform the HTTP POST request HttpResponse response = mClient.execute(httppost); entity = response.getEntity(); String error = Common.checkResponseErrors(response, entity); if (error != null) throw new Exception(error); // Success return true; } catch (Exception e) { if (Constants.LOGGING) Log.e(TAG, "ReportTask", e); } finally { if (entity != null) { try { entity.consumeContent(); } catch (Exception e2) { if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e2); } } } return false; } public void onPreExecute() { if (!mSettings.isLoggedIn()) { Common.showErrorToast("You must be logged in to report this.", Toast.LENGTH_LONG, CommentsListActivity.this); cancel(true); return; } } public void onPostExecute(Boolean success) { if (success) { Toast.makeText(CommentsListActivity.this, "Reported.", Toast.LENGTH_SHORT); } else { Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, CommentsListActivity.this); } } } /** * Populates the menu. */ @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.comments, menu); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { // This happens when the user begins to hold down the menu key, so // allow them to chord to get a shortcut. mCanChord = true; super.onPrepareOptionsMenu(menu); MenuItem src, dest; menu.findItem(R.id.find_next_menu_id).setVisible(last_search_string != null && last_search_string.length() > 0); // Login/Logout if (mSettings.isLoggedIn()) { menu.findItem(R.id.login_menu_id).setVisible(false); menu.findItem(R.id.inbox_menu_id).setVisible(true); menu.findItem(R.id.user_profile_menu_id).setVisible(true); menu.findItem(R.id.user_profile_menu_id).setTitle( String.format(getResources().getString(R.string.user_profile), mSettings.getUsername()) ); menu.findItem(R.id.logout_menu_id).setVisible(true); menu.findItem(R.id.logout_menu_id).setTitle( String.format(getResources().getString(R.string.logout), mSettings.getUsername()) ); } else { menu.findItem(R.id.login_menu_id).setVisible(true); menu.findItem(R.id.inbox_menu_id).setVisible(false); menu.findItem(R.id.user_profile_menu_id).setVisible(false); menu.findItem(R.id.logout_menu_id).setVisible(false); } // Edit and delete if (getOpThingInfo() != null) { if (mSettings.getUsername() != null && mSettings.getUsername().equalsIgnoreCase(getOpThingInfo().getAuthor())) { if (getOpThingInfo().isIs_self()) menu.findItem(R.id.op_edit_menu_id).setVisible(true); else menu.findItem(R.id.op_edit_menu_id).setVisible(false); menu.findItem(R.id.op_delete_menu_id).setVisible(true); } else { menu.findItem(R.id.op_edit_menu_id).setVisible(false); menu.findItem(R.id.op_delete_menu_id).setVisible(false); } } // Theme: Light/Dark src = Util.isLightTheme(mSettings.getTheme()) ? menu.findItem(R.id.dark_menu_id) : menu.findItem(R.id.light_menu_id); dest = menu.findItem(R.id.light_dark_menu_id); dest.setTitle(src.getTitle()); // Sort if (Constants.CommentsSort.SORT_BY_BEST_URL.equals(mSettings.getCommentsSortByUrl())) src = menu.findItem(R.id.sort_by_best_menu_id); else if (Constants.CommentsSort.SORT_BY_HOT_URL.equals(mSettings.getCommentsSortByUrl())) src = menu.findItem(R.id.sort_by_hot_menu_id); else if (Constants.CommentsSort.SORT_BY_NEW_URL.equals(mSettings.getCommentsSortByUrl())) src = menu.findItem(R.id.sort_by_new_menu_id); else if (Constants.CommentsSort.SORT_BY_CONTROVERSIAL_URL.equals(mSettings.getCommentsSortByUrl())) src = menu.findItem(R.id.sort_by_controversial_menu_id); else if (Constants.CommentsSort.SORT_BY_TOP_URL.equals(mSettings.getCommentsSortByUrl())) src = menu.findItem(R.id.sort_by_top_menu_id); else if (Constants.CommentsSort.SORT_BY_OLD_URL.equals(mSettings.getCommentsSortByUrl())) src = menu.findItem(R.id.sort_by_old_menu_id); dest = menu.findItem(R.id.sort_by_menu_id); dest.setTitle(src.getTitle()); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { if (!mCanChord) { // The user has already fired a shortcut with this hold down of the // menu key. return false; } switch (item.getItemId()) { case R.id.op_menu_id: if (getOpThingInfo() == null) break; mVoteTargetThing = getOpThingInfo(); mReplyTargetName = getOpThingInfo().getName(); showDialog(Constants.DIALOG_COMMENT_CLICK); break; case R.id.op_subreddit_menu_id: Intent intent = new Intent(getApplicationContext(), ThreadsListActivity.class); intent.setData(Util.createSubredditUri(mSubreddit)); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); Util.overridePendingTransition(mActivity_overridePendingTransition, this, android.R.anim.slide_in_left, android.R.anim.slide_out_right); break; case R.id.login_menu_id: showDialog(Constants.DIALOG_LOGIN); break; case R.id.logout_menu_id: Common.doLogout(mSettings, mClient, getApplicationContext()); Toast.makeText(this, "You have been logged out.", Toast.LENGTH_SHORT).show(); getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT); break; case R.id.find_next_menu_id: if (last_search_string != null && last_search_string.length() > 0) findCommentText(last_search_string, true, true); break; case R.id.find_base_id: // This case is needed because the "default" case throws // an error, otherwise precluding anonymous "parent" menu items break; case R.id.find_menu_id: showDialog(Constants.DIALOG_FIND); break; case R.id.refresh_menu_id: CacheInfo.invalidateCachedThread(getApplicationContext()); getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT); break; case R.id.sort_by_menu_id: showDialog(Constants.DIALOG_SORT_BY); break; case R.id.open_browser_menu_id: String url = new StringBuilder(Constants.REDDIT_BASE_URL + "/r/") .append(mSubreddit).append("/comments/").append(mThreadId).toString(); Common.launchBrowser(this, url, url, false, true, true, false); break; case R.id.op_delete_menu_id: mReplyTargetName = getOpThingInfo().getName(); mDeleteTargetKind = Constants.THREAD_KIND; showDialog(Constants.DIALOG_DELETE); break; case R.id.op_edit_menu_id: mReplyTargetName = getOpThingInfo().getName(); mEditTargetBody = getOpThingInfo().getSelftext(); showDialog(Constants.DIALOG_EDIT); break; case R.id.light_dark_menu_id: mSettings.setTheme(Util.getInvertedTheme(mSettings.getTheme())); relaunchActivity(); break; case R.id.inbox_menu_id: Intent inboxIntent = new Intent(getApplicationContext(), InboxActivity.class); startActivity(inboxIntent); break; case R.id.user_profile_menu_id: Intent profileIntent = new Intent(getApplicationContext(), ProfileActivity.class); startActivity(profileIntent); break; case R.id.preferences_menu_id: Intent prefsIntent = new Intent(getApplicationContext(), RedditPreferencesPage.class); startActivity(prefsIntent); break; case android.R.id.home: Common.goHome(this); break; default: throw new IllegalArgumentException("Unexpected action value "+item.getItemId()); } return true; } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; int rowId = (int) info.id; ThingInfo item = mCommentsAdapter.getItem(rowId); if (rowId == 0) { menu.add(0, Constants.SHARE_CONTEXT_ITEM, Menu.NONE, "Share"); if(getOpThingInfo().isSaved()){ menu.add(0, Constants.UNSAVE_CONTEXT_ITEM, Menu.NONE, "Unsave"); } else { menu.add(0, Constants.SAVE_CONTEXT_ITEM, Menu.NONE, "Save"); } if(getOpThingInfo().isHidden()){ menu.add(0, Constants.UNHIDE_CONTEXT_ITEM, Menu.NONE, "Unhide"); } else { menu.add(0, Constants.HIDE_CONTEXT_ITEM, Menu.NONE, "Hide"); } menu.add(0, Constants.DIALOG_VIEW_PROFILE, Menu.NONE, String.format(getResources().getString(R.string.user_profile), item.getAuthor())); } else if (isLoadMoreCommentsPosition(rowId)) { menu.add(0, Constants.DIALOG_GOTO_PARENT, Menu.NONE, "Go to parent"); } else if (isHiddenCommentHeadPosition(rowId)) { menu.add(0, Constants.DIALOG_SHOW_COMMENT, Menu.NONE, "Show comment"); menu.add(0, Constants.DIALOG_GOTO_PARENT, Menu.NONE, "Go to parent"); } else { if (mSettings.getUsername() != null && mSettings.getUsername().equalsIgnoreCase(item.getAuthor())) { menu.add(0, Constants.DIALOG_EDIT, Menu.NONE, "Edit"); menu.add(0, Constants.DIALOG_DELETE, Menu.NONE, "Delete"); } menu.add(0, Constants.DIALOG_HIDE_COMMENT, Menu.NONE, "Hide comment"); // if (mSettings.isLoggedIn()) // menu.add(0, Constants.DIALOG_REPORT, Menu.NONE, "Report comment"); menu.add(0, Constants.DIALOG_GOTO_PARENT, Menu.NONE, "Go to parent"); menu.add(0, Constants.DIALOG_VIEW_PROFILE, Menu.NONE, String.format(getResources().getString(R.string.user_profile), item.getAuthor())); } } @Override public boolean onContextItemSelected(MenuItem item) { AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); int rowId = (int) info.id; switch (item.getItemId()) { case Constants.SAVE_CONTEXT_ITEM: new SaveTask(true, getOpThingInfo(), mSettings, this).execute(); return true; case Constants.UNSAVE_CONTEXT_ITEM: new SaveTask(false, getOpThingInfo(), mSettings, this).execute(); return true; case Constants.HIDE_CONTEXT_ITEM: new HideTask(true, getOpThingInfo(), mSettings, this).execute(); return true; case Constants.UNHIDE_CONTEXT_ITEM: new HideTask(false, getOpThingInfo(), mSettings, this).execute(); return true; case Constants.SHARE_CONTEXT_ITEM: Intent intent = new Intent(); intent.setAction(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_TEXT, getOpThingInfo().getUrl()); try { startActivity(Intent.createChooser(intent, "Share Link")); } catch (android.content.ActivityNotFoundException ex) { } return true; case Constants.DIALOG_HIDE_COMMENT: hideComment(rowId); return true; case Constants.DIALOG_SHOW_COMMENT: showComment(rowId); return true; case Constants.DIALOG_GOTO_PARENT: int myIndent = mCommentsAdapter.getItem(rowId).getIndent(); int parentRowId; for (parentRowId = rowId - 1; parentRowId >= 0; parentRowId--) if (mCommentsAdapter.getItem(parentRowId).getIndent() < myIndent) break; getListView().setSelection(parentRowId); return true; case Constants.DIALOG_VIEW_PROFILE: Intent i = new Intent(this, ProfileActivity.class); i.setData(Util.createProfileUri(mCommentsAdapter.getItem(rowId).getAuthor())); startActivity(i); return true; case Constants.DIALOG_EDIT: mReplyTargetName = mCommentsAdapter.getItem(rowId).getName(); mEditTargetBody = mCommentsAdapter.getItem(rowId).getBody(); showDialog(Constants.DIALOG_EDIT); return true; case Constants.DIALOG_DELETE: mReplyTargetName = mCommentsAdapter.getItem(rowId).getName(); // It must be a comment, since the OP selftext is reached via options menu, not context menu mDeleteTargetKind = Constants.COMMENT_KIND; showDialog(Constants.DIALOG_DELETE); return true; case Constants.DIALOG_REPORT: mReportTargetName = mCommentsAdapter.getItem(rowId).getName(); showDialog(Constants.DIALOG_REPORT); return true; default: return super.onContextItemSelected(item); } } private void hideComment(int rowId) { ThingInfo headComment = mCommentsAdapter.getItem(rowId); int myIndent = headComment.getIndent(); headComment.setHiddenCommentHead(true); // Hide everything after the row. for (int i = rowId + 1; i < mCommentsAdapter.getCount(); i++) { ThingInfo ci = mCommentsAdapter.getItem(i); if (ci.getIndent() <= myIndent) break; ci.setHiddenCommentDescendant(true); } mCommentsAdapter.notifyDataSetChanged(); } private void showComment(int rowId) { ThingInfo headComment = mCommentsAdapter.getItem(rowId); headComment.setHiddenCommentHead(false); int stopIndent = headComment.getIndent(); int skipIndentAbove = -1; for (int i = rowId + 1; i < mCommentsAdapter.getCount(); i++) { ThingInfo ci = mCommentsAdapter.getItem(i); int ciIndent = ci.getIndent(); if (ciIndent <= stopIndent) break; if (skipIndentAbove != -1 && ciIndent > skipIndentAbove) continue; ci.setHiddenCommentDescendant(false); // skip nested hidden comments (e.g. you collapsed child first, then root. now expanding root, but don't expand child) if (ci.isHiddenCommentHead()) skipIndentAbove = ci.getIndent(); else skipIndentAbove = -1; } mCommentsAdapter.notifyDataSetChanged(); } private void findCommentText(String search_text, boolean wrap, boolean next) { last_search_string = search_text; int current_position = next ? (last_found_position + 1) % mCommentsAdapter.getCount() : Math.max(0, getSelectedItemPosition()); if ( getFoundPosition(current_position, mCommentsAdapter.getCount(), search_text) ) { mCommentsAdapter.notifyDataSetChanged(); return; } if ( wrap ) { if (Constants.LOGGING) Log.d(TAG, "Continuing search from top..."); if ( getFoundPosition(0, current_position, search_text) ) { mCommentsAdapter.notifyDataSetChanged(); return; } } mCommentsAdapter.notifyDataSetChanged(); String not_found_msg = getResources().getString(R.string.find_not_found, search_text); Toast.makeText(CommentsListActivity.this, not_found_msg, Toast.LENGTH_LONG).show(); } private boolean getFoundPosition(int start_index, int end_index, String search_text) { for (int i = start_index; i < end_index; i++) { ThingInfo ci = mCommentsAdapter.getItem(i); if (ci == null) continue; String comment_body = ci.getBody(); if (comment_body == null) continue; if (comment_body.toLowerCase().contains(search_text)) { final int position = i; getListView().post(new Runnable() { @Override public void run() { setSelection(position); getListView().requestFocus(); } }); last_found_position = i; return true; } } last_found_position = -1; return false; } @Override protected Dialog onCreateDialog(int id) { final Dialog dialog; ProgressDialog pdialog; AlertDialog.Builder builder; LayoutInflater inflater; switch (id) { case Constants.DIALOG_LOGIN: dialog = new LoginDialog(this, mSettings, false) { @Override public void onLoginChosen(String user, String password) { removeDialog(Constants.DIALOG_LOGIN); new MyLoginTask(user, password).execute(); } }; break; case Constants.DIALOG_COMMENT_CLICK: dialog = new CommentClickDialog(this, mSettings); break; case Constants.DIALOG_REPLY: { dialog = new Dialog(this, mSettings.getDialogTheme()); dialog.setContentView(R.layout.compose_reply_dialog); final EditText replyBody = (EditText) dialog.findViewById(R.id.body); final Button replySaveButton = (Button) dialog.findViewById(R.id.reply_save_button); final Button replyCancelButton = (Button) dialog.findViewById(R.id.reply_cancel_button); replySaveButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mReplyTargetName != null) { new CommentReplyTask(mReplyTargetName).execute(replyBody.getText().toString()); dialog.dismiss(); } else { Common.showErrorToast("Error replying. Please try again.", Toast.LENGTH_SHORT, CommentsListActivity.this); } } }); replyCancelButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mVoteTargetThing.setReplyDraft(replyBody.getText().toString()); dialog.cancel(); } }); dialog.setCancelable(false); // disallow the BACK key dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { replyBody.setText(""); } }); break; } case Constants.DIALOG_EDIT: { dialog = new Dialog(this, mSettings.getDialogTheme()); dialog.setContentView(R.layout.compose_reply_dialog); final EditText replyBody = (EditText) dialog.findViewById(R.id.body); final Button replySaveButton = (Button) dialog.findViewById(R.id.reply_save_button); final Button replyCancelButton = (Button) dialog.findViewById(R.id.reply_cancel_button); replyBody.setText(mEditTargetBody); replySaveButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { if (mReplyTargetName != null) { new EditTask(mReplyTargetName).execute(replyBody.getText().toString()); dialog.dismiss(); } else { Common.showErrorToast("Error editing. Please try again.", Toast.LENGTH_SHORT, CommentsListActivity.this); } } }); replyCancelButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { dialog.cancel(); } }); dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { replyBody.setText(""); } }); break; } case Constants.DIALOG_DELETE: builder = new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme())); builder.setTitle("Really delete this?"); builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int item) { removeDialog(Constants.DIALOG_DELETE); new DeleteTask(mDeleteTargetKind).execute(mReplyTargetName); } }) .setNegativeButton("No", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); dialog = builder.create(); break; case Constants.DIALOG_SORT_BY: builder = new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme())); builder.setTitle("Sort by:"); int selectedSortBy = -1; for (int i = 0; i < Constants.CommentsSort.SORT_BY_URL_CHOICES.length; i++) { if (Constants.CommentsSort.SORT_BY_URL_CHOICES[i].equals(mSettings.getCommentsSortByUrl())) { selectedSortBy = i; break; } } builder.setSingleChoiceItems(Constants.CommentsSort.SORT_BY_CHOICES, selectedSortBy, sortByOnClickListener); dialog = builder.create(); break; case Constants.DIALOG_REPORT: builder = new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme())); builder.setTitle("Really report this?"); builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int item) { removeDialog(Constants.DIALOG_REPORT); new ReportTask(mReportTargetName.toString()).execute(); } }) .setNegativeButton("No", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); dialog = builder.create(); break; // "Please wait" case Constants.DIALOG_DELETING: pdialog = new ProgressDialog(new ContextThemeWrapper(this, mSettings.getDialogTheme())); pdialog.setMessage("Deleting..."); pdialog.setIndeterminate(true); pdialog.setCancelable(true); dialog = pdialog; break; case Constants.DIALOG_EDITING: pdialog = new ProgressDialog(new ContextThemeWrapper(this, mSettings.getDialogTheme())); pdialog.setMessage("Submitting edit..."); pdialog.setIndeterminate(true); pdialog.setCancelable(true); dialog = pdialog; break; case Constants.DIALOG_LOGGING_IN: pdialog = new ProgressDialog(new ContextThemeWrapper(this, mSettings.getDialogTheme())); pdialog.setMessage("Logging in..."); pdialog.setIndeterminate(true); pdialog.setCancelable(true); dialog = pdialog; break; case Constants.DIALOG_REPLYING: pdialog = new ProgressDialog(new ContextThemeWrapper(this, mSettings.getDialogTheme())); pdialog.setMessage("Sending reply..."); pdialog.setIndeterminate(true); pdialog.setCancelable(true); dialog = pdialog; break; case Constants.DIALOG_FIND: inflater = (LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View content = inflater.inflate(R.layout.dialog_find, null); final EditText find_box = (EditText) content.findViewById(R.id.input_find_box); // final CheckBox wrap_box = (CheckBox) content.findViewById(R.id.find_wrap_checkbox); builder = new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme())); builder.setView(content); builder.setTitle(R.string.find) .setPositiveButton(R.string.find, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String search_text = find_box.getText().toString().toLowerCase(); // findCommentText(search_text, wrap_box.isChecked(), false); findCommentText(search_text, true, false); } }) .setNegativeButton("Cancel", null); dialog = builder.create(); break; default: throw new IllegalArgumentException("Unexpected dialog id "+id); } return dialog; } @Override protected void onPrepareDialog(int id, Dialog dialog) { super.onPrepareDialog(id, dialog); StringBuilder sb; switch (id) { case Constants.DIALOG_LOGIN: if (mSettings.getUsername() != null) { final TextView loginUsernameInput = (TextView) dialog.findViewById(R.id.login_username_input); loginUsernameInput.setText(mSettings.getUsername()); } final TextView loginPasswordInput = (TextView) dialog.findViewById(R.id.login_password_input); loginPasswordInput.setText(""); break; case Constants.DIALOG_COMMENT_CLICK: if (mVoteTargetThing == null) break; Boolean likes; final TextView titleView = (TextView) dialog.findViewById(R.id.title); final TextView urlView = (TextView) dialog.findViewById(R.id.url); final TextView submissionStuffView = (TextView) dialog.findViewById(R.id.submissionTime_submitter_subreddit); final Button linkButton = (Button) dialog.findViewById(R.id.thread_link_button); if (mVoteTargetThing == getOpThingInfo()) { likes = mVoteTargetThing.getLikes(); titleView.setVisibility(View.VISIBLE); titleView.setText(getOpThingInfo().getTitle()); urlView.setVisibility(View.VISIBLE); urlView.setText(getOpThingInfo().getUrl()); submissionStuffView.setVisibility(View.VISIBLE); sb = new StringBuilder(Util.getTimeAgo(getOpThingInfo().getCreated_utc())) .append(" by ").append(getOpThingInfo().getAuthor()); submissionStuffView.setText(sb); // For self posts, you're already there! if (getOpThingInfo().getDomain().toLowerCase().startsWith("self.")) { linkButton.setText(R.string.comment_links_button); linkToEmbeddedURLs(linkButton); } else { final String url = getOpThingInfo().getUrl(); linkButton.setText(R.string.thread_link_button); linkButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { removeDialog(Constants.DIALOG_COMMENT_CLICK); setLinkClicked(getOpThingInfo()); Common.launchBrowser(CommentsListActivity.this, url, Util.createThreadUri(getOpThingInfo()).toString(), false, false, mSettings.isUseExternalBrowser(), mSettings.isSaveHistory()); } }); linkButton.setEnabled(true); } } else { titleView.setText("Comment by " + mVoteTargetThing.getAuthor()); likes = mVoteTargetThing.getLikes(); urlView.setVisibility(View.INVISIBLE); submissionStuffView.setVisibility(View.INVISIBLE); // Get embedded URLs linkButton.setText(R.string.comment_links_button); linkToEmbeddedURLs(linkButton); } final CheckBox voteUpButton = (CheckBox) dialog.findViewById(R.id.vote_up_button); final CheckBox voteDownButton = (CheckBox) dialog.findViewById(R.id.vote_down_button); final Button replyButton = (Button) dialog.findViewById(R.id.reply_button); final Button loginButton = (Button) dialog.findViewById(R.id.login_button); // Only show upvote/downvote if user is logged in if (mSettings.isLoggedIn()) { loginButton.setVisibility(View.GONE); voteUpButton.setVisibility(View.VISIBLE); voteDownButton.setVisibility(View.VISIBLE); replyButton.setEnabled(true); // Make sure the setChecked() actions don't actually vote just yet. voteUpButton.setOnCheckedChangeListener(null); voteDownButton.setOnCheckedChangeListener(null); // Set initial states of the vote buttons based on user's past actions if (likes == null) { // User is currently neutral voteUpButton.setChecked(false); voteDownButton.setChecked(false); } else if (likes == true) { // User currenty likes it voteUpButton.setChecked(true); voteDownButton.setChecked(false); } else { // User currently dislikes it voteUpButton.setChecked(false); voteDownButton.setChecked(true); } // Now we want the user to be able to vote. voteUpButton.setOnCheckedChangeListener(voteUpOnCheckedChangeListener); voteDownButton.setOnCheckedChangeListener(voteDownOnCheckedChangeListener); // The "reply" button replyButton.setOnClickListener(replyOnClickListener); } else { replyButton.setEnabled(false); voteUpButton.setVisibility(View.GONE); voteDownButton.setVisibility(View.GONE); loginButton.setVisibility(View.VISIBLE); loginButton.setOnClickListener(loginOnClickListener); } break; case Constants.DIALOG_REPLY: if (mVoteTargetThing != null) { if (mVoteTargetThing.getReplyDraft() != null && !mShouldClearReply) { EditText replyBodyView = (EditText) dialog.findViewById(R.id.body); replyBodyView.setText(mVoteTargetThing.getReplyDraft()); } else { EditText replyBodyView = (EditText) dialog.findViewById(R.id.body); replyBodyView.setText(""); mShouldClearReply = false; } } break; case Constants.DIALOG_EDIT: EditText replyBodyView = (EditText) dialog.findViewById(R.id.body); replyBodyView.setText(mEditTargetBody); break; default: // No preparation based on app state is required. break; } } /** * Helper function to add links from mVoteTargetThing to the button * @param linkButton Button that should open list of links */ private void linkToEmbeddedURLs(Button linkButton) { final ArrayList<String> urls = new ArrayList<String>(); final ArrayList<MarkdownURL> vtUrls = mVoteTargetThing.getUrls(); int urlsCount = vtUrls.size(); for (int i = 0; i < urlsCount; i++) { urls.add(vtUrls.get(i).url); } if (urlsCount == 0) { linkButton.setEnabled(false); } else { linkButton.setEnabled(true); linkButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { removeDialog(Constants.DIALOG_COMMENT_CLICK); ArrayAdapter<MarkdownURL> adapter = new ArrayAdapter<MarkdownURL>(CommentsListActivity.this, android.R.layout.select_dialog_item, vtUrls) { public View getView(int position, View convertView, ViewGroup parent) { TextView tv; if (convertView == null) { tv = (TextView) ((LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE)) .inflate(android.R.layout.select_dialog_item, null); } else { tv = (TextView) convertView; } String url = getItem(position).url; String anchorText = getItem(position).anchorText; if (Constants.LOGGING) Log.d(TAG, "links url="+url + " anchorText="+anchorText); Drawable d = null; try { d = getPackageManager().getActivityIcon(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); } catch (NameNotFoundException ignore) { } if (d != null) { d.setBounds(0, 0, d.getIntrinsicHeight(), d.getIntrinsicHeight()); tv.setCompoundDrawablePadding(10); tv.setCompoundDrawables(d, null, null, null); } final String telPrefix = "tel:"; if (url.startsWith(telPrefix)) { url = PhoneNumberUtils.formatNumber(url.substring(telPrefix.length())); } if (anchorText != null) tv.setText(Html.fromHtml("<span>" + anchorText + "</span><br /><small>" + url + "</small>")); else tv.setText(Html.fromHtml(url)); return tv; } }; AlertDialog.Builder b = new AlertDialog.Builder(new ContextThemeWrapper(CommentsListActivity.this, mSettings.getDialogTheme())); DialogInterface.OnClickListener click = new DialogInterface.OnClickListener() { public final void onClick(DialogInterface dialog, int which) { if (which >= 0) { Common.launchBrowser(CommentsListActivity.this, urls.get(which), Util.createThreadUri(getOpThingInfo()).toString(), false, false, mSettings.isUseExternalBrowser(), mSettings.isSaveHistory()); } } }; b.setTitle(R.string.select_link_title); b.setCancelable(true); b.setAdapter(adapter, click); b.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public final void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); b.show(); } }); } } public static void fillCommentsListItemView(View view, ThingInfo item, RedditSettings settings) { // Set the values of the Views for the CommentsListItem TextView votesView = (TextView) view.findViewById(R.id.votes); TextView submitterView = (TextView) view.findViewById(R.id.submitter); TextView bodyView = (TextView) view.findViewById(R.id.body); TextView submissionTimeView = (TextView) view.findViewById(R.id.submissionTime); ImageView voteUpView = (ImageView) view.findViewById(R.id.vote_up_image); ImageView voteDownView = (ImageView) view.findViewById(R.id.vote_down_image); try { votesView.setText(Util.showNumPoints(item.getUps() - item.getDowns())); } catch (NumberFormatException e) { // This happens because "ups" comes after the potentially long "replies" object, // so the ListView might try to display the View before "ups" in JSON has been parsed. if (Constants.LOGGING) Log.e(TAG, "getView, normal comment", e); } if (item.getSSAuthor() != null) submitterView.setText(item.getSSAuthor()); else submitterView.setText(item.getAuthor()); submissionTimeView.setText(Util.getTimeAgo(item.getCreated_utc())); if (item.getSpannedBody() != null) bodyView.setText(item.getSpannedBody()); else bodyView.setText(item.getBody()); setCommentIndent(view, item.getIndent(), settings); if (voteUpView != null && voteDownView != null) { if (item.getLikes() == null || "[deleted]".equals(item.getAuthor())) { voteUpView.setVisibility(View.GONE); voteDownView.setVisibility(View.GONE); } else if (Boolean.TRUE.equals(item.getLikes())) { voteUpView.setVisibility(View.VISIBLE); voteDownView.setVisibility(View.GONE); } else if (Boolean.FALSE.equals(item.getLikes())) { voteUpView.setVisibility(View.GONE); voteDownView.setVisibility(View.VISIBLE); } } } private final CompoundButton.OnCheckedChangeListener voteUpOnCheckedChangeListener = new CompoundButton.OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { removeDialog(Constants.DIALOG_COMMENT_CLICK); String thingFullname = mVoteTargetThing.getName(); if (isChecked) new VoteTask(thingFullname, 1).execute(); else new VoteTask(thingFullname, 0).execute(); } }; private final CompoundButton.OnCheckedChangeListener voteDownOnCheckedChangeListener = new CompoundButton.OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { removeDialog(Constants.DIALOG_COMMENT_CLICK); String thingFullname = mVoteTargetThing.getName(); if (isChecked) new VoteTask(thingFullname, -1).execute(); else new VoteTask(thingFullname, 0).execute(); } }; private final OnClickListener replyOnClickListener = new OnClickListener() { public void onClick(View v) { removeDialog(Constants.DIALOG_COMMENT_CLICK); showDialog(Constants.DIALOG_REPLY); } }; private final OnClickListener loginOnClickListener = new OnClickListener() { public void onClick(View v) { removeDialog(Constants.DIALOG_COMMENT_CLICK); showDialog(Constants.DIALOG_LOGIN); } }; private final DialogInterface.OnClickListener sortByOnClickListener = new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int item) { dialog.dismiss(); mSettings.setCommentsSortByUrl(Constants.CommentsSort.SORT_BY_URL_CHOICES[item]); getNewDownloadCommentsTask().execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT); } }; private final ThumbnailOnClickListenerFactory mThumbnailOnClickListenerFactory = new ThumbnailOnClickListenerFactory() { @Override public OnClickListener getThumbnailOnClickListener(final ThingInfo threadThingInfo, final Activity activity) { return new OnClickListener() { public void onClick(View v) { setLinkClicked(threadThingInfo); Common.launchBrowser( activity, threadThingInfo.getUrl(), Util.createThreadUri(threadThingInfo).toString(), false, false, mSettings.isUseExternalBrowser(), mSettings.isSaveHistory() ); } }; } }; private void setLinkClicked(ThingInfo threadThingInfo) { threadThingInfo.setClicked(true); mCommentsAdapter.notifyDataSetChanged(); } @Override protected void onSaveInstanceState(Bundle state) { super.onSaveInstanceState(state); state.putString(Constants.REPLY_TARGET_NAME_KEY, mReplyTargetName); state.putString(Constants.REPORT_TARGET_NAME_KEY, mReportTargetName); state.putString(Constants.EDIT_TARGET_BODY_KEY, mEditTargetBody); state.putString(Constants.DELETE_TARGET_KIND_KEY, mDeleteTargetKind); state.putString(Constants.SUBREDDIT_KEY, mSubreddit); state.putString(Constants.THREAD_ID_KEY, mThreadId); state.putString(Constants.THREAD_TITLE_KEY, mThreadTitle); state.putParcelable(Constants.VOTE_TARGET_THING_INFO_KEY, mVoteTargetThing); } /** * Called to "thaw" re-animate the app from a previous onSaveInstanceState(). * * @see android.app.Activity#onRestoreInstanceState */ @Override protected void onRestoreInstanceState(Bundle state) { super.onRestoreInstanceState(state); final int[] myDialogs = { Constants.DIALOG_COMMENT_CLICK, Constants.DIALOG_DELETE, Constants.DIALOG_DELETING, Constants.DIALOG_EDIT, Constants.DIALOG_EDITING, Constants.DIALOG_LOGGING_IN, Constants.DIALOG_LOGIN, Constants.DIALOG_REPLY, Constants.DIALOG_REPLYING, Constants.DIALOG_SORT_BY, Constants.DIALOG_REPORT }; for (int dialog : myDialogs) { try { removeDialog(dialog); } catch (IllegalArgumentException e) { // Ignore. } } } }