/* * Copyright 2009 Andrew Shu * * This file is part of "reddit is fun". * * "reddit is fun" is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * "reddit is fun" is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with "reddit is fun". If not, see <http://www.gnu.org/licenses/>. */ package com.andrewshu.android.reddit.user; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.codehaus.jackson.JsonParseException; import org.codehaus.jackson.map.ObjectMapper; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ListActivity; import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Bundle; import android.text.Html; import android.text.Spanned; import android.text.style.URLSpan; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.Window; import android.webkit.CookieSyncManager; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.andrewshu.android.reddit.R; import com.andrewshu.android.reddit.captcha.CaptchaCheckRequiredTask; import com.andrewshu.android.reddit.captcha.CaptchaDownloadTask; import com.andrewshu.android.reddit.comments.CommentsListActivity; import com.andrewshu.android.reddit.common.CacheInfo; import com.andrewshu.android.reddit.common.Common; import com.andrewshu.android.reddit.common.Constants; import com.andrewshu.android.reddit.common.FormValidation; import com.andrewshu.android.reddit.common.ProgressInputStream; import com.andrewshu.android.reddit.common.RedditIsFunHttpClientFactory; import com.andrewshu.android.reddit.common.tasks.VoteTask; import com.andrewshu.android.reddit.common.util.StringUtils; import com.andrewshu.android.reddit.common.util.Util; import com.andrewshu.android.reddit.login.LoginDialog; import com.andrewshu.android.reddit.login.LoginTask; import com.andrewshu.android.reddit.mail.MessageComposeTask; import com.andrewshu.android.reddit.settings.RedditSettings; import com.andrewshu.android.reddit.things.Listing; import com.andrewshu.android.reddit.things.ListingData; import com.andrewshu.android.reddit.things.ThingInfo; import com.andrewshu.android.reddit.things.ThingListing; import com.andrewshu.android.reddit.threads.ShowThumbnailsTask; import com.andrewshu.android.reddit.threads.ShowThumbnailsTask.ThumbnailLoadAction; import com.andrewshu.android.reddit.threads.ThreadClickDialog; import com.andrewshu.android.reddit.threads.ThreadClickDialogOnClickListenerFactory; import com.andrewshu.android.reddit.threads.ThreadsListActivity; import com.andrewshu.android.reddit.threads.ThumbnailOnClickListenerFactory; /** * Activity to view user submissions and comments. * Also check their link and comment karma. * * @author TalkLittle * */ public final class ProfileActivity extends ListActivity implements View.OnCreateContextMenuListener { private static final String TAG = "ProfileActivity"; static final Pattern USER_PATH_PATTERN = Pattern.compile(Constants.USER_PATH_PATTERN_STRING); private final ObjectMapper mObjectMapper = Common.getObjectMapper(); /** Custom list adapter that fits our threads data into the list. */ private ThingsListAdapter mThingsAdapter; private ArrayList<ThingInfo> mThingsList; // Lock used when modifying the mMessagesAdapter private static final Object MESSAGE_ADAPTER_LOCK = new Object(); private final HttpClient mClient = RedditIsFunHttpClientFactory.getGzipHttpClient(); // Common settings are stored here private final RedditSettings mSettings = new RedditSettings(); // UI State private View mVoteTargetView = null; private ThingInfo mVoteTargetThingInfo = null; private URLSpan[] mVoteTargetSpans = null; // TODO: String mVoteTargetId so when you rotate, you can find the TargetThingInfo again private DownloadProfileTask mCurrentDownloadThingsTask = null; private final Object mCurrentDownloadThingsTaskLock = new Object(); private View mNextPreviousView = null; private String mUsername = null; private String mAfter = null; private String mBefore = null; private int mCount = 0; private String mLastAfter = null; private String mLastBefore = null; private int mLastCount = 0; private int[] mKarma = null; private String mSortByUrl = null; private String mSortByUrlExtra = null; private String mJumpToThreadId = null; private volatile String mCaptchaIden = null; private volatile String mCaptchaUrl = null; // ProgressDialogs with percentage bars // private AutoResetProgressDialog mLoadingCommentsProgress; // private int mNumVisibleMessages; private boolean mCanChord = false; /** * Called when the activity starts up. Do activity initialization * here, not in a constructor. * * @see Activity#onCreate */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); CookieSyncManager.createInstance(getApplicationContext()); mSettings.loadRedditPreferences(this, mClient); setRequestedOrientation(mSettings.getRotation()); setTheme(mSettings.getTheme()); requestWindowFeature(Window.FEATURE_PROGRESS); requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); setContentView(R.layout.profile_list_content); registerForContextMenu(getListView()); if (savedInstanceState != null) { if (Constants.LOGGING) Log.d(TAG, "using savedInstanceState"); mUsername = savedInstanceState.getString(Constants.USERNAME_KEY); mAfter = savedInstanceState.getString(Constants.AFTER_KEY); mBefore = savedInstanceState.getString(Constants.BEFORE_KEY); mCount = savedInstanceState.getInt(Constants.THREAD_COUNT_KEY); mLastAfter = savedInstanceState.getString(Constants.LAST_AFTER_KEY); mLastBefore = savedInstanceState.getString(Constants.LAST_BEFORE_KEY); mLastCount = savedInstanceState.getInt(Constants.THREAD_LAST_COUNT_KEY); mKarma = savedInstanceState.getIntArray(Constants.KARMA_KEY); mSortByUrl = savedInstanceState.getString(Constants.CommentsSort.SORT_BY_KEY); mJumpToThreadId = savedInstanceState.getString(Constants.JUMP_TO_THREAD_ID_KEY); mVoteTargetThingInfo = savedInstanceState.getParcelable(Constants.VOTE_TARGET_THING_INFO_KEY); // try to restore mThingsList using getLastNonConfigurationInstance() // (separate function to avoid a compiler warning casting ArrayList<ThingInfo> restoreLastNonConfigurationInstance(); if (mThingsList == null) { // Load previous page of profile items if (mLastAfter != null) { new DownloadProfileTask(mUsername, mLastAfter, null, mLastCount).execute(); } else if (mLastBefore != null) { new DownloadProfileTask(mUsername, null, mLastBefore, mLastCount).execute(); } else { new DownloadProfileTask(mUsername).execute(); } } else { // Orientation change. Use prior instance. resetUI(new ThingsListAdapter(this, mThingsList)); setTitle(mUsername + "'s profile"); } return; } // Handle subreddit Uri passed via Intent else if (getIntent().getData() != null) { Matcher userPathMatcher = USER_PATH_PATTERN.matcher(getIntent().getData().getPath()); if (userPathMatcher.matches()) { mUsername = userPathMatcher.group(1); new DownloadProfileTask(mUsername).execute(); return; } } // No username specified by Intent, so load the logged in user's profile if (mSettings.isLoggedIn()) { mUsername = mSettings.getUsername(); new DownloadProfileTask(mUsername).execute(); return; } // Can't find a username to use. Quit. if (Constants.LOGGING) Log.e(TAG, "Could not find a username to use for ProfileActivity"); finish(); } @Override protected void onResume() { super.onResume(); CookieSyncManager.getInstance().startSync(); int previousTheme = mSettings.getTheme(); boolean previousLoggedIn = mSettings.isLoggedIn(); mSettings.loadRedditPreferences(this, mClient); setRequestedOrientation(mSettings.getRotation()); if (mSettings.getTheme() != previousTheme) { resetUI(mThingsAdapter); } updateNextPreviousButtons(); updateKarma(); if (mSettings.isLoggedIn() != previousLoggedIn) { new DownloadProfileTask(mSettings.getUsername()).execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT); } } @Override protected void onPause() { super.onPause(); CookieSyncManager.getInstance().stopSync(); mSettings.saveRedditPreferences(this); } @Override public Object onRetainNonConfigurationInstance() { // Avoid having to re-download and re-parse the messages list // when rotating or opening keyboard. return mThingsList; } @SuppressWarnings("unchecked") private void restoreLastNonConfigurationInstance() { mThingsList = (ArrayList<ThingInfo>) getLastNonConfigurationInstance(); } private final class ThingsListAdapter extends ArrayAdapter<ThingInfo> { static final int THREAD_ITEM_VIEW_TYPE = 0; static final int COMMENT_ITEM_VIEW_TYPE = 1; private static final int VIEW_TYPE_COUNT = 2; public boolean mIsLoading = true; private LayoutInflater mInflater; @Override public int getItemViewType(int position) { ThingInfo item = getItem(position); if (item.getName().startsWith(Constants.THREAD_KIND)) { return THREAD_ITEM_VIEW_TYPE; } if (item.getName().startsWith(Constants.COMMENT_KIND)) { return COMMENT_ITEM_VIEW_TYPE; } return COMMENT_ITEM_VIEW_TYPE; } @Override public int getViewTypeCount() { return VIEW_TYPE_COUNT; } public boolean isEmpty() { if (mIsLoading) return false; return super.isEmpty(); } public ThingsListAdapter(Context context, List<ThingInfo> objects) { super(context, 0, objects); mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } @Override public View getView(int position, View convertView, ViewGroup parent) { View view = null; ThingInfo item = this.getItem(position); if (getItemViewType(position) == THREAD_ITEM_VIEW_TYPE) { // Here view may be passed in for re-use, or we make a new one. if (convertView == null) { view = mInflater.inflate(R.layout.threads_list_item, null); } else { view = convertView; } ThreadsListActivity.fillThreadsListItemView( position, view, item, ProfileActivity.this, mClient, mSettings, mThumbnailOnClickListenerFactory ); } else if (getItemViewType(position) == COMMENT_ITEM_VIEW_TYPE) { // Here view may be passed in for re-use, or we make a new one. if (convertView == null) { view = mInflater.inflate(R.layout.comments_list_item, null); } else { view = convertView; } CommentsListActivity.fillCommentsListItemView(view, item, mSettings); view.setPadding(15, 5, 0, 5); } return view; } } // End of MessagesListAdapter /** * Called when user clicks an item in the list. Mark message read. * If item was already focused, open a dialog. */ @Override protected void onListItemClick(ListView l, View v, int position, long id) { ThingInfo item = mThingsAdapter.getItem(position); // Mark the message/comment as selected mVoteTargetThingInfo = item; mVoteTargetView = v; if (item.getName().startsWith(Constants.THREAD_KIND)) { showDialog(Constants.DIALOG_THREAD_CLICK); } else { openContextMenu(v); } } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; int rowId = (int) info.id; ThingInfo item = mThingsAdapter.getItem(rowId); // Mark the message/comment as selected mVoteTargetThingInfo = item; mVoteTargetView = v; if (item.getName().startsWith(Constants.THREAD_KIND)) { menu.add(0, Constants.DIALOG_THREAD_CLICK, Menu.NONE, "Go to thread"); } else { // TODO: include the context! menu.add(0, Constants.DIALOG_COMMENT_CLICK, Menu.NONE, "Go to comment"); } } @Override public boolean onContextItemSelected(MenuItem item) { Intent i; switch (item.getItemId()) { case Constants.DIALOG_COMMENT_CLICK: i = new Intent(getApplicationContext(), CommentsListActivity.class); i.setData(Util.createCommentUri(mVoteTargetThingInfo, 0)); i.putExtra(Constants.EXTRA_SUBREDDIT, mVoteTargetThingInfo.getSubreddit()); i.putExtra(Constants.EXTRA_TITLE, mVoteTargetThingInfo.getTitle()); startActivity(i); return true; case Constants.DIALOG_THREAD_CLICK: // Launch an Intent for CommentsListActivity CacheInfo.invalidateCachedThread(getApplicationContext()); i = new Intent(getApplicationContext(), CommentsListActivity.class); i.setData(Util.createThreadUri(mVoteTargetThingInfo)); i.putExtra(Constants.EXTRA_SUBREDDIT, mVoteTargetThingInfo.getSubreddit()); i.putExtra(Constants.EXTRA_TITLE, mVoteTargetThingInfo.getTitle()); i.putExtra(Constants.EXTRA_NUM_COMMENTS, Integer.valueOf(mVoteTargetThingInfo.getNum_comments())); startActivity(i); return true; default: return super.onContextItemSelected(item); } } /** * Resets the output UI list contents, retains session state. * @param messagesAdapter A MessagesListAdapter to use. Pass in null if you want a new empty one created. */ void resetUI(ThingsListAdapter messagesAdapter) { findViewById(R.id.loading_light).setVisibility(View.GONE); findViewById(R.id.loading_dark).setVisibility(View.GONE); if (mSettings.isAlwaysShowNextPrevious()) { if (mNextPreviousView != null) { getListView().removeFooterView(mNextPreviousView); mNextPreviousView = null; } } else { findViewById(R.id.next_previous_layout).setVisibility(View.GONE); if (getListView().getFooterViewsCount() == 0) { // If we are not using the persistent navbar, then show as ListView footer instead LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); mNextPreviousView = inflater.inflate(R.layout.next_previous_list_item, null); getListView().addFooterView(mNextPreviousView); } } synchronized (MESSAGE_ADAPTER_LOCK) { if (messagesAdapter == null) { // Reset the list to be empty. mThingsList = new ArrayList<ThingInfo>(); mThingsAdapter = new ThingsListAdapter(this, mThingsList); } else { mThingsAdapter = messagesAdapter; } setListAdapter(mThingsAdapter); mThingsAdapter.mIsLoading = false; mThingsAdapter.notifyDataSetChanged(); // Just in case } getListView().setDivider(null); Common.updateListDrawables(this, mSettings.getTheme()); updateNextPreviousButtons(); } private void enableLoadingScreen() { if (Util.isLightTheme(mSettings.getTheme())) { findViewById(R.id.loading_light).setVisibility(View.VISIBLE); findViewById(R.id.loading_dark).setVisibility(View.GONE); } else { findViewById(R.id.loading_light).setVisibility(View.GONE); findViewById(R.id.loading_dark).setVisibility(View.VISIBLE); } synchronized (MESSAGE_ADAPTER_LOCK) { if (mThingsAdapter != null) mThingsAdapter.mIsLoading = true; } getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_START); } private void disableLoadingScreen() { resetUI(mThingsAdapter); getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_END); } private void updateNextPreviousButtons() { Common.updateNextPreviousButtons(this, mNextPreviousView, mAfter, mBefore, mCount, mSettings, downloadAfterOnClickListener, downloadBeforeOnClickListener); } private void updateKarma() { if (mKarma == null) return; View karmaLayout = findViewById(R.id.karma_layout); View karmaLayoutBorder = findViewById(R.id.karma_border_bottom); if (karmaLayout != null && karmaLayoutBorder != null) { karmaLayout.setVisibility(View.VISIBLE); if (Util.isLightTheme(mSettings.getTheme())) { karmaLayout.setBackgroundResource(android.R.color.background_light); karmaLayoutBorder.setBackgroundResource(R.color.black); } else { karmaLayoutBorder.setBackgroundResource(R.color.white); } TextView linkKarma = (TextView) findViewById(R.id.link_karma); TextView commentKarma = (TextView) findViewById(R.id.comment_karma); linkKarma.setText(mKarma[0] + " link karma"); commentKarma.setText(mKarma[1] + " comment karma"); } } private class DownloadProfileTask extends AsyncTask<Integer, Long, Void> implements PropertyChangeListener { private ArrayList<ThingInfo> _mThingInfos = new ArrayList<ThingInfo>(); private long _mContentLength; private String mUsername; private String mAfter; private String mBefore; private int mCount; private String mLastAfter = null; private String mLastBefore = null; private int mLastCount = 0; private int[] mKarma; private String mSortByUrl; private String mSortByUrlExtra; public DownloadProfileTask(String username) { this(username, null, null, Constants.DEFAULT_THREAD_DOWNLOAD_LIMIT, ProfileActivity.this.mSortByUrl, ProfileActivity.this.mSortByUrlExtra); } public DownloadProfileTask(String username, String after, String before, int count) { this(username, after, before, count, ProfileActivity.this.mSortByUrl, ProfileActivity.this.mSortByUrlExtra); } /** * The real constructor * @param username * @param after * @param before * @param count * @param sortByUrl * @param sortByUrlExtra */ public DownloadProfileTask(String username, String after, String before, int count, String sortByUrl, String sortByUrlExtra) { mUsername = username; mAfter = after; mBefore = before; mCount = count; mSortByUrl = sortByUrl; mSortByUrlExtra = sortByUrlExtra; } protected void saveState() { ProfileActivity.this.mLastAfter = mLastAfter; ProfileActivity.this.mLastBefore = mLastBefore; ProfileActivity.this.mLastCount = mLastCount; ProfileActivity.this.mAfter = mAfter; ProfileActivity.this.mBefore = mBefore; ProfileActivity.this.mCount = mCount; ProfileActivity.this.mKarma = mKarma; ProfileActivity.this.mSortByUrl = mSortByUrl; ProfileActivity.this.mSortByUrlExtra = mSortByUrlExtra; } // XXX: maxComments is unused for now public Void doInBackground(Integer... maxComments) { HttpEntity entity = null; boolean isAfter = false; boolean isBefore = false; InputStream in = null; ProgressInputStream pin = null; try { if (mKarma == null) mKarma = getKarma(); String url; StringBuilder sb = new StringBuilder(Constants.REDDIT_BASE_URL).append("/user/").append(mUsername.trim()).append("/.json?"); if (mSortByUrl != null) sb = sb.append(mSortByUrl).append("&"); if (mSortByUrlExtra != null) sb = sb.append(mSortByUrlExtra).append("&"); // "before" always comes back null unless you provide correct "count" if (mAfter != null) { // count: 25, 50, ... sb = sb.append("count=").append(mCount) .append("&after=").append(mAfter).append("&"); isAfter = true; } else if (mBefore != null) { // count: nothing, 26, 51, ... sb = sb.append("count=").append(mCount + 1 - Constants.DEFAULT_THREAD_DOWNLOAD_LIMIT) .append("&before=").append(mBefore).append("&"); isBefore = true; } url = sb.toString(); if (Constants.LOGGING) Log.d(TAG, "url=" + url); HttpGet request = new HttpGet(url); HttpResponse response = mClient.execute(request); // Read the header to get Content-Length since entity.getContentLength() returns -1 Header contentLengthHeader = response.getFirstHeader("Content-Length"); if (contentLengthHeader != null) { _mContentLength = Long.valueOf(contentLengthHeader.getValue()); if (Constants.LOGGING) Log.d(TAG, "Content length: "+_mContentLength); } else { _mContentLength = -1; if (Constants.LOGGING) Log.d(TAG, "Content length: UNAVAILABLE"); } entity = response.getEntity(); in = entity.getContent(); // setup a special InputStream to report progress pin = new ProgressInputStream(in, _mContentLength); pin.addPropertyChangeListener(this); parseThingsJSON(pin); mLastCount = mCount; if (isAfter) mCount += Constants.DEFAULT_THREAD_DOWNLOAD_LIMIT; else if (isBefore) mCount -= Constants.DEFAULT_THREAD_DOWNLOAD_LIMIT; saveState(); } catch (Exception e) { if (Constants.LOGGING) Log.e(TAG, "failed", e); } finally { if (pin != null) { try { pin.close(); } catch (IOException e2) { if (Constants.LOGGING) Log.e(TAG, "pin.close()", e2); } } if (in != null) { try { in.close(); } catch (IOException e2) { if (Constants.LOGGING) Log.e(TAG, "in.close()", e2); } } if (entity != null) { try { entity.consumeContent(); } catch (Exception e2) { if (Constants.LOGGING) Log.e(TAG, "entity.consumeContent()", e2); } } } return null; } /** * @return [linkKarma, commentKarma] */ private int[] getKarma() throws IOException { String url = new StringBuilder(Constants.REDDIT_BASE_URL).append("/user/").append(mUsername.trim()).append("/about.json").toString(); if (Constants.LOGGING) Log.d(TAG, "karma url=" + url); HttpGet request = new HttpGet(url); HttpResponse response = mClient.execute(request); HttpEntity entity = null; InputStream in = null; try { entity = response.getEntity(); in = entity.getContent(); UserInfo userInfo = UserInfoParser.parseJSON(in); if (userInfo != null) return new int[] { userInfo.getLink_karma(), userInfo.getComment_karma() }; } finally { try { in.close(); } catch (Exception ignore) {} try { entity.consumeContent(); } catch (Exception ignore) {} } return null; } private void parseThingsJSON(InputStream in) throws IOException, JsonParseException, IllegalStateException { String genericListingError = "Not a user page listing"; try { Listing listing = mObjectMapper.readValue(in, Listing.class); if (!Constants.JSON_LISTING.equals(listing.getKind())) throw new IllegalStateException(genericListingError); // Save the modhash, after, and before ListingData data = listing.getData(); if (StringUtils.isEmpty(data.getModhash())) mSettings.setModhash(null); else mSettings.setModhash(data.getModhash()); mLastAfter = mAfter; mLastBefore = mBefore; mAfter = data.getAfter(); mBefore = data.getBefore(); // Go through the children and get the ThingInfos for (ThingListing tiContainer : data.getChildren()) { if (Constants.COMMENT_KIND.equals(tiContainer.getKind())) { ThingInfo ti = tiContainer.getData(); // HTML to Spanned String unescapedHtmlBody = Html.fromHtml(ti.getBody_html()).toString(); Spanned body = Html.fromHtml(Util.convertHtmlTags(unescapedHtmlBody)); // remove last 2 newline characters if (body.length() > 2) ti.setSpannedBody(body.subSequence(0, body.length()-2)); else ti.setSpannedBody(""); _mThingInfos.add(ti); } else if (Constants.THREAD_KIND.equals(tiContainer.getKind())) { ThingInfo ti = tiContainer.getData(); ti.setClicked(Common.isClicked(ProfileActivity.this, ti.getUrl())); _mThingInfos.add(ti); } } } catch (Exception ex) { if (Constants.LOGGING) Log.e(TAG, "parseThingsJSON", ex); } } @Override public void onPreExecute() { synchronized (mCurrentDownloadThingsTaskLock) { if (mCurrentDownloadThingsTask != null) mCurrentDownloadThingsTask.cancel(true); mCurrentDownloadThingsTask = this; } resetUI(null); enableLoadingScreen(); if (_mContentLength == -1) getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_INDETERMINATE_ON); } @Override public void onPostExecute(Void v) { synchronized (mCurrentDownloadThingsTaskLock) { mCurrentDownloadThingsTask = null; } synchronized(MESSAGE_ADAPTER_LOCK) { for (ThingInfo mi : _mThingInfos) mThingsAdapter.add(mi); } if (_mContentLength == -1) getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_INDETERMINATE_OFF); else getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_END); showThumbnails(_mThingInfos); disableLoadingScreen(); setTitle(String.format(getResources().getString(R.string.user_profile), mUsername)); updateNextPreviousButtons(); updateKarma(); } @Override public void onProgressUpdate(Long... progress) { getWindow().setFeatureInt(Window.FEATURE_PROGRESS, progress[0].intValue() * (Window.PROGRESS_END-1) / (int) _mContentLength); } public void propertyChange(PropertyChangeEvent event) { publishProgress((Long) event.getNewValue()); } } private void showThumbnails(List<ThingInfo> thingInfos) { int size = thingInfos.size(); ThumbnailLoadAction[] thumbnailLoadActions = new ThumbnailLoadAction[size]; for (int i = 0; i < thumbnailLoadActions.length; i++) { thumbnailLoadActions[i] = new ThumbnailLoadAction(thingInfos.get(i), null, i); } new ShowThumbnailsTask(this, mClient, R.drawable.go_arrow).execute(thumbnailLoadActions); } private class MyLoginTask extends LoginTask { public MyLoginTask(String username, String password) { super(username, password, mSettings, mClient, getApplicationContext()); } @Override protected void onPreExecute() { showDialog(Constants.DIALOG_LOGGING_IN); } @Override protected void onPostExecute(Boolean success) { removeDialog(Constants.DIALOG_LOGGING_IN); if (success) { Toast.makeText(ProfileActivity.this, "Logged in as "+mUsername, Toast.LENGTH_SHORT).show(); showDialog(Constants.DIALOG_COMPOSE); } else { Common.showErrorToast(mUserError, Toast.LENGTH_LONG, ProfileActivity.this); } } } private class MyMessageComposeTask extends MessageComposeTask { MyMessageComposeTask(Dialog dialog, ThingInfo targetThingInfo, String captcha) { super(dialog, targetThingInfo, captcha, mCaptchaIden, mSettings, mClient, getApplicationContext()); } @Override public void onPreExecute() { showDialog(Constants.DIALOG_COMPOSING); } @Override public void onPostExecute(Boolean success) { removeDialog(Constants.DIALOG_COMPOSING); if (success) { Toast.makeText(ProfileActivity.this, "Message sent.", Toast.LENGTH_SHORT).show(); // TODO: add the reply beneath the original, OR redirect to sent messages page } else { Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, ProfileActivity.this); new MyCaptchaDownloadTask(_mDialog).execute(); } } } private class MyVoteTask extends VoteTask { private int _mPreviousScore; private Boolean _mPreviousLikes; private ThingInfo _mTargetThingInfo; public MyVoteTask(ThingInfo thingInfo, int direction, String subreddit) { super(thingInfo.getName(), direction, subreddit, getApplicationContext(), mSettings, mClient); _mTargetThingInfo = thingInfo; _mPreviousScore = thingInfo.getScore(); _mPreviousLikes = thingInfo.getLikes(); } @Override public void onPreExecute() { if (!_mSettings.isLoggedIn()) { Common.showErrorToast("You must be logged in to vote.", Toast.LENGTH_LONG, _mContext); cancel(true); return; } if (_mDirection < -1 || _mDirection > 1) { if (Constants.LOGGING) Log.e(TAG, "WTF: _mDirection = " + _mDirection); throw new RuntimeException("How the hell did you vote something besides -1, 0, or 1?"); } int newScore; Boolean newLikes; _mPreviousScore = Integer.valueOf(_mTargetThingInfo.getScore()); _mPreviousLikes = _mTargetThingInfo.getLikes(); if (_mPreviousLikes == null) { if (_mDirection == 1) { newScore = _mPreviousScore + 1; newLikes = true; } else if (_mDirection == -1) { newScore = _mPreviousScore - 1; newLikes = false; } else { cancel(true); return; } } else if (_mPreviousLikes == true) { if (_mDirection == 0) { newScore = _mPreviousScore - 1; newLikes = null; } else if (_mDirection == -1) { newScore = _mPreviousScore - 2; newLikes = false; } else { cancel(true); return; } } else { if (_mDirection == 1) { newScore = _mPreviousScore + 2; newLikes = true; } else if (_mDirection == 0) { newScore = _mPreviousScore + 1; newLikes = null; } else { cancel(true); return; } } _mTargetThingInfo.setLikes(newLikes); _mTargetThingInfo.setScore(newScore); mThingsAdapter.notifyDataSetChanged(); } @Override public void onPostExecute(Boolean success) { if (success) { CacheInfo.invalidateCachedSubreddit(_mContext); } else { // Vote failed. Undo the score. _mTargetThingInfo.setLikes(_mPreviousLikes); _mTargetThingInfo.setScore(_mPreviousScore); mThingsAdapter.notifyDataSetChanged(); Common.showErrorToast(_mUserError, Toast.LENGTH_LONG, _mContext); } } } private class MyCaptchaCheckRequiredTask extends CaptchaCheckRequiredTask { Dialog _mDialog; public MyCaptchaCheckRequiredTask(Dialog dialog) { super(Constants.REDDIT_BASE_URL + "/message/compose/", mClient); _mDialog = dialog; } @Override protected void saveState() { ProfileActivity.this.mCaptchaIden = _mCaptchaIden; ProfileActivity.this.mCaptchaUrl = _mCaptchaUrl; } @Override public void onPreExecute() { // Hide send button so user can't send until we know whether he needs captcha final Button sendButton = (Button) _mDialog.findViewById(R.id.compose_send_button); sendButton.setVisibility(View.INVISIBLE); // Show "loading captcha" label final TextView loadingCaptcha = (TextView) _mDialog.findViewById(R.id.compose_captcha_loading); loadingCaptcha.setVisibility(View.VISIBLE); } @Override public void onPostExecute(Boolean required) { final TextView captchaLabel = (TextView) _mDialog.findViewById(R.id.compose_captcha_textview); final ImageView captchaImage = (ImageView) _mDialog.findViewById(R.id.compose_captcha_image); final EditText captchaEdit = (EditText) _mDialog.findViewById(R.id.compose_captcha_input); final TextView loadingCaptcha = (TextView) _mDialog.findViewById(R.id.compose_captcha_loading); final Button sendButton = (Button) _mDialog.findViewById(R.id.compose_send_button); if (required == null) { Common.showErrorToast("Error retrieving captcha. Use the menu to try again.", Toast.LENGTH_LONG, ProfileActivity.this); return; } if (required) { captchaLabel.setVisibility(View.VISIBLE); captchaImage.setVisibility(View.VISIBLE); captchaEdit.setVisibility(View.VISIBLE); // Launch a task to download captcha and display it new MyCaptchaDownloadTask(_mDialog).execute(); } else { captchaLabel.setVisibility(View.GONE); captchaImage.setVisibility(View.GONE); captchaEdit.setVisibility(View.GONE); } loadingCaptcha.setVisibility(View.GONE); sendButton.setVisibility(View.VISIBLE); } } private class MyCaptchaDownloadTask extends CaptchaDownloadTask { Dialog _mDialog; public MyCaptchaDownloadTask(Dialog dialog) { super(mCaptchaUrl, mClient); _mDialog = dialog; } @Override public void onPostExecute(Drawable captcha) { if (captcha == null) { Common.showErrorToast("Error retrieving captcha. Use the menu to try again.", Toast.LENGTH_LONG, ProfileActivity.this); return; } final ImageView composeCaptchaView = (ImageView) _mDialog.findViewById(R.id.compose_captcha_image); composeCaptchaView.setVisibility(View.VISIBLE); composeCaptchaView.setImageDrawable(captcha); } } /** * Populates the menu. */ @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.profile, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.compose_message_menu_id: if (mSettings.isLoggedIn()) { showDialog(Constants.DIALOG_COMPOSE); } else { showDialog(Constants.DIALOG_LOGIN); } break; case R.id.refresh_menu_id: new DownloadProfileTask(mUsername).execute(Constants.DEFAULT_COMMENT_DOWNLOAD_LIMIT); break; case android.R.id.home: Common.goHome(this); break; } return true; } @Override protected Dialog onCreateDialog(int id) { Dialog dialog; ProgressDialog pdialog; AlertDialog.Builder builder; LayoutInflater inflater; View layout; // used for inflated views for AlertDialog.Builder.setView() switch (id) { case Constants.DIALOG_LOGIN: dialog = new LoginDialog(this, mSettings, false) { @Override public void onLoginChosen(String user, String password) { removeDialog(Constants.DIALOG_LOGIN); new MyLoginTask(user, password).execute(); } }; break; case Constants.DIALOG_COMPOSE: inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); builder = new AlertDialog.Builder(new ContextThemeWrapper(this, mSettings.getDialogTheme())); layout = inflater.inflate(R.layout.compose_dialog, null); Common.setTextColorFromTheme( mSettings.getTheme(), getResources(), (TextView) layout.findViewById(R.id.compose_destination_textview), (TextView) layout.findViewById(R.id.compose_subject_textview), (TextView) layout.findViewById(R.id.compose_message_textview), (TextView) layout.findViewById(R.id.compose_captcha_textview), (TextView) layout.findViewById(R.id.compose_captcha_loading) ); final EditText composeDestination = (EditText) layout.findViewById(R.id.compose_destination_input); final EditText composeSubject = (EditText) layout.findViewById(R.id.compose_subject_input); final EditText composeText = (EditText) layout.findViewById(R.id.compose_text_input); final Button composeSendButton = (Button) layout.findViewById(R.id.compose_send_button); final Button composeCancelButton = (Button) layout.findViewById(R.id.compose_cancel_button); final EditText composeCaptcha = (EditText) layout.findViewById(R.id.compose_captcha_input); composeDestination.setText(mUsername); dialog = builder.setView(layout).create(); final Dialog composeDialog = dialog; composeSendButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { ThingInfo hi = new ThingInfo(); if (!FormValidation.validateComposeMessageInputFields(ProfileActivity.this, composeDestination, composeSubject, composeText, composeCaptcha)) return; hi.setDest(composeDestination.getText().toString().trim()); hi.setSubject(composeSubject.getText().toString().trim()); new MyMessageComposeTask(composeDialog, hi, composeCaptcha.getText().toString().trim()) .execute(composeText.getText().toString().trim()); removeDialog(Constants.DIALOG_COMPOSE); } }); composeCancelButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { removeDialog(Constants.DIALOG_COMPOSE); } }); break; case Constants.DIALOG_THREAD_CLICK: dialog = new ThreadClickDialog(this, mSettings); break; // "Please wait" case Constants.DIALOG_LOGGING_IN: pdialog = new ProgressDialog(new ContextThemeWrapper(this, mSettings.getDialogTheme())); pdialog.setMessage("Logging in..."); pdialog.setIndeterminate(true); pdialog.setCancelable(true); dialog = pdialog; break; case Constants.DIALOG_REPLYING: pdialog = new ProgressDialog(new ContextThemeWrapper(this, mSettings.getDialogTheme())); pdialog.setMessage("Sending reply..."); pdialog.setIndeterminate(true); pdialog.setCancelable(true); dialog = pdialog; break; case Constants.DIALOG_COMPOSING: pdialog = new ProgressDialog(new ContextThemeWrapper(this, mSettings.getDialogTheme())); pdialog.setMessage("Composing message..."); pdialog.setIndeterminate(true); pdialog.setCancelable(true); dialog = pdialog; break; default: throw new IllegalArgumentException("Unexpected dialog id "+id); } return dialog; } @Override protected void onPrepareDialog(int id, Dialog dialog) { super.onPrepareDialog(id, dialog); switch (id) { // case Constants.DIALOG_LOGIN: // if (mSettings.username != null) { // final TextView loginUsernameInput = (TextView) dialog.findViewById(R.id.login_username_input); // loginUsernameInput.setText(mSettings.username); // } // final TextView loginPasswordInput = (TextView) dialog.findViewById(R.id.login_password_input); // loginPasswordInput.setText(""); // break; case Constants.DIALOG_COMPOSE: final EditText composeDestination = (EditText) dialog.findViewById(R.id.compose_destination_input); composeDestination.setText(mUsername); new MyCaptchaCheckRequiredTask(dialog).execute(); break; case Constants.DIALOG_THREAD_CLICK: ThreadsListActivity.fillThreadClickDialog(dialog, mVoteTargetThingInfo, mSettings, mThreadClickDialogOnClickListenerFactory); break; default: // No preparation based on app state is required. break; } } private void setLinkClicked(ThingInfo threadThingInfo) { threadThingInfo.setClicked(true); mThingsAdapter.notifyDataSetChanged(); } private final OnClickListener downloadAfterOnClickListener = new OnClickListener() { public void onClick(View v) { new DownloadProfileTask(mUsername, mAfter, null, mCount).execute(); } }; private final OnClickListener downloadBeforeOnClickListener = new OnClickListener() { public void onClick(View v) { new DownloadProfileTask(mUsername, null, mBefore, mCount).execute(); } }; private final ThumbnailOnClickListenerFactory mThumbnailOnClickListenerFactory = new ThumbnailOnClickListenerFactory() { @Override public OnClickListener getThumbnailOnClickListener(final ThingInfo threadThingInfo, final Activity activity) { return new OnClickListener() { public void onClick(View v) { // mJumpToThreadId = jumpToId; setLinkClicked(threadThingInfo); Common.launchBrowser( activity, threadThingInfo.getUrl(), Util.createThreadUri(threadThingInfo).toString(), false, false, mSettings.isUseExternalBrowser(), mSettings.isSaveHistory() ); } }; } }; private final ThreadClickDialogOnClickListenerFactory mThreadClickDialogOnClickListenerFactory = new ThreadClickDialogOnClickListenerFactory() { @Override public OnClickListener getLoginOnClickListener() { return new OnClickListener() { public void onClick(View v) { removeDialog(Constants.DIALOG_THREAD_CLICK); showDialog(Constants.DIALOG_LOGIN); } }; } @Override public OnClickListener getLinkOnClickListener(ThingInfo thingInfo, boolean useExternalBrowser) { final ThingInfo info = thingInfo; final boolean fUseExternalBrowser = useExternalBrowser; return new OnClickListener() { public void onClick(View v) { removeDialog(Constants.DIALOG_THREAD_CLICK); // Launch Intent to goto the URL Common.launchBrowser(ProfileActivity.this, info.getUrl(), Util.createThreadUri(info).toString(), false, false, fUseExternalBrowser, mSettings.isSaveHistory()); } }; } @Override public OnClickListener getCommentsOnClickListener(ThingInfo thingInfo) { final ThingInfo info = thingInfo; return new OnClickListener() { public void onClick(View v) { removeDialog(Constants.DIALOG_THREAD_CLICK); // Launch an Intent for CommentsListActivity CacheInfo.invalidateCachedThread(ProfileActivity.this); Intent i = new Intent(ProfileActivity.this, CommentsListActivity.class); i.setData(Util.createThreadUri(info)); i.putExtra(Constants.EXTRA_SUBREDDIT, info.getSubreddit()); i.putExtra(Constants.EXTRA_TITLE, info.getTitle()); i.putExtra(Constants.EXTRA_NUM_COMMENTS, Integer.valueOf(info.getNum_comments())); startActivity(i); } }; } @Override public CompoundButton.OnCheckedChangeListener getVoteUpOnCheckedChangeListener(ThingInfo thingInfo) { final ThingInfo info = thingInfo; return new CompoundButton.OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { removeDialog(Constants.DIALOG_THREAD_CLICK); if (isChecked) { new MyVoteTask(info, 1, info.getSubreddit()).execute(); } else { new MyVoteTask(info, 0, info.getSubreddit()).execute(); } } }; } @Override public CompoundButton.OnCheckedChangeListener getVoteDownOnCheckedChangeListener(ThingInfo thingInfo) { final ThingInfo info = thingInfo; return new CompoundButton.OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { removeDialog(Constants.DIALOG_THREAD_CLICK); if (isChecked) { new MyVoteTask(info, -1, info.getSubreddit()).execute(); } else { new MyVoteTask(info, 0, info.getSubreddit()).execute(); } } }; } }; @Override protected void onSaveInstanceState(Bundle state) { super.onSaveInstanceState(state); state.putString(Constants.USERNAME_KEY, mUsername); state.putString(Constants.CommentsSort.SORT_BY_KEY, mSortByUrl); state.putString(Constants.JUMP_TO_THREAD_ID_KEY, mJumpToThreadId); state.putString(Constants.AFTER_KEY, mAfter); state.putString(Constants.BEFORE_KEY, mBefore); state.putInt(Constants.THREAD_COUNT_KEY, mCount); state.putString(Constants.LAST_AFTER_KEY, mLastAfter); state.putString(Constants.LAST_BEFORE_KEY, mLastBefore); state.putInt(Constants.THREAD_LAST_COUNT_KEY, mLastCount); state.putIntArray(Constants.KARMA_KEY, mKarma); state.putParcelable(Constants.VOTE_TARGET_THING_INFO_KEY, mVoteTargetThingInfo); } /** * Called to "thaw" re-animate the app from a previous onSaveInstanceState(). * * @see android.app.Activity#onRestoreInstanceState */ @Override protected void onRestoreInstanceState(Bundle state) { super.onRestoreInstanceState(state); final int[] myDialogs = { Constants.DIALOG_COMMENT_CLICK, Constants.DIALOG_LOGGING_IN, Constants.DIALOG_LOGIN, Constants.DIALOG_MESSAGE_CLICK, Constants.DIALOG_REPLY, Constants.DIALOG_REPLYING, }; for (int dialog : myDialogs) { try { removeDialog(dialog); } catch (IllegalArgumentException e) { // Ignore. } } } }