Java tutorial
/******************************************************************************* * This file is part of RedReader. * * RedReader 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. * * RedReader 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 RedReader. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package com.ryan.ryanreader.fragments; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.text.ClipboardManager; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import com.actionbarsherlock.view.ContextMenu; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; import com.laurencedawson.activetextview.ActiveTextView; import com.ryan.ryanreader.account.RedditAccount; import com.ryan.ryanreader.account.RedditAccountManager; import com.ryan.ryanreader.activities.BugReportActivity; import com.ryan.ryanreader.activities.CommentEditActivity; import com.ryan.ryanreader.activities.CommentReplyActivity; import com.ryan.ryanreader.activities.SessionChangeListener; import com.ryan.ryanreader.adapters.CommentListingAdapter; import com.ryan.ryanreader.cache.CacheManager; import com.ryan.ryanreader.cache.CacheRequest; import com.ryan.ryanreader.cache.RequestFailureType; import com.ryan.ryanreader.common.*; import com.ryan.ryanreader.jsonwrap.JsonBufferedArray; import com.ryan.ryanreader.jsonwrap.JsonBufferedObject; import com.ryan.ryanreader.jsonwrap.JsonValue; import com.ryan.ryanreader.reddit.RedditAPI; import com.ryan.ryanreader.reddit.prepared.RedditChangeDataManager; import com.ryan.ryanreader.reddit.prepared.RedditCommentTextParser; import com.ryan.ryanreader.reddit.prepared.RedditPreparedComment; import com.ryan.ryanreader.reddit.prepared.RedditPreparedPost; import com.ryan.ryanreader.reddit.things.RedditComment; import com.ryan.ryanreader.reddit.things.RedditPost; import com.ryan.ryanreader.reddit.things.RedditSubreddit; import com.ryan.ryanreader.reddit.things.RedditThing; import com.ryan.ryanreader.views.RedditCommentView; import com.ryan.ryanreader.views.RedditPostHeaderView; import com.ryan.ryanreader.views.RedditPostView; import com.ryan.ryanreader.views.bezelmenu.BezelSwipeOverlay; import com.ryan.ryanreader.views.bezelmenu.SideToolbarOverlay; import com.ryan.ryanreader.views.liststatus.ErrorView; import com.ryan.ryanreader.views.liststatus.LoadingView; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.http.StatusLine; import org.holoeverywhere.LayoutInflater; import org.holoeverywhere.app.Activity; import org.holoeverywhere.app.AlertDialog; import org.holoeverywhere.app.Fragment; import org.holoeverywhere.preference.PreferenceManager; import org.holoeverywhere.preference.SharedPreferences; import org.holoeverywhere.widget.FrameLayout; import org.holoeverywhere.widget.LinearLayout; import org.holoeverywhere.widget.ListView; import org.holoeverywhere.widget.TextView; import com.ryan.ryanreader.R; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashSet; import java.util.UUID; public class CommentListingFragment extends Fragment implements ActiveTextView.OnLinkClickedListener, RedditPostView.PostSelectionListener { private URI url; private UUID session = null; private CacheRequest.DownloadType downloadType; private CommentListingAdapter adapter; private LoadingView loadingView; private LinearLayout notifications, listHeaderNotifications, listHeaderPost, listHeaderSelftext, listFooter; private ListView lv; private String after = null; private String parentPostIdAndType; private CacheRequest request; private RedditPreparedPost post; private float commentFontScale = 1.0f; private EnumSet<PrefsUtility.AppearanceCommentHeaderItems> headerItems; private Context context; @Override public void onAttach(Activity activity) { super.onAttach(activity); context = activity.getApplicationContext(); } private final Handler commentHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(final Message msg) { if (isAdded()) { final ArrayList<RedditPreparedComment> comments = (ArrayList<RedditPreparedComment>) msg.obj; adapter.addComments(comments); } } }; // TODO load more on scroll to bottom? public static CommentListingFragment newInstance(final String parentPostIdAndType, final URI url, final UUID session, final CacheRequest.DownloadType downloadType) { final CommentListingFragment f = new CommentListingFragment(); final Bundle bundle = new Bundle(4); bundle.putString("parentPostIdAndType", parentPostIdAndType); bundle.putString("url", url.toString()); if (session != null) bundle.putString("session", session.toString()); bundle.putString("downloadType", downloadType.name()); f.setArguments(bundle); return f; } @Override public void onCreate(final Bundle savedInstanceState) { // TODO load position/etc? super.onCreate(savedInstanceState); setHasOptionsMenu(true); final Bundle arguments = getArguments(); parentPostIdAndType = arguments.getString("parentPostIdAndType"); url = General.uriFromString(arguments.getString("url")); if (arguments.containsKey("session")) { session = UUID.fromString(arguments.getString("session")); } downloadType = CacheRequest.DownloadType.valueOf(arguments.getString("downloadType")); } public void handleCommentVisibilityToggle(RedditCommentView view) { if (view.handleVisibilityToggle()) { // Comment is collapsed. final int position = adapter.findPositionOf(view.getComment()); if (lv.getFirstVisiblePosition() - 1 == position) { final RedditPreparedComment comment = adapter.getItem(position); adapter.notifyDataSetChanged(); lv.smoothScrollToPosition(adapter.findPositionOf(comment) + 1); return; } } adapter.notifyDataSetChanged(); } private LinearLayout createVerticalLinearLayout(Context context) { final LinearLayout result = new LinearLayout(context); result.setOrientation(LinearLayout.VERTICAL); return result; } @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); final Context context = container.getContext(); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); commentFontScale = PrefsUtility.appearance_fontscale_comments(context, prefs); headerItems = PrefsUtility.appearance_comment_header_items(context, prefs); final LinearLayout outer = new LinearLayout(context); outer.setOrientation(android.widget.LinearLayout.VERTICAL); loadingView = new LoadingView(context, context.getString(R.string.download_waiting), true, true); notifications = new LinearLayout(context); notifications.setOrientation(android.widget.LinearLayout.VERTICAL); lv = new ListView(context); lv.setSmoothScrollbarEnabled(false); lv.setVerticalFadingEdgeEnabled(false); final LinearLayout listHeader = createVerticalLinearLayout(context); this.listHeaderPost = createVerticalLinearLayout(context); this.listHeaderNotifications = createVerticalLinearLayout(context); this.listHeaderSelftext = createVerticalLinearLayout(context); listHeader.addView(listHeaderPost); listHeader.addView(listHeaderNotifications); listHeader.addView(listHeaderSelftext); listFooter = createVerticalLinearLayout(context); lv.addHeaderView(listHeader); lv.addFooterView(listFooter, null, false); adapter = new CommentListingAdapter(context, this); lv.setAdapter(adapter); registerForContextMenu(lv); lv.setOnItemClickListener(new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (view instanceof RedditCommentView) { switch (PrefsUtility.pref_behaviour_actions_comment_tap(context, prefs)) { case COLLAPSE: handleCommentVisibilityToggle((RedditCommentView) view); break; case ACTION_MENU: openContextMenu(view); break; } } else if (position == 0 && post != null && !post.src.is_self) { LinkHandler.onLinkClicked(getSupportActivity(), post.url, false, post.src); } } }); outer.addView(notifications); outer.addView(lv); final FrameLayout outerFrame = new FrameLayout(context); outerFrame.addView(outer); final SideToolbarOverlay toolbarOverlay = new SideToolbarOverlay(context); final BezelSwipeOverlay bezelOverlay = new BezelSwipeOverlay(context, new BezelSwipeOverlay.BezelSwipeListener() { public boolean onSwipe(BezelSwipeOverlay.SwipeEdge edge) { if (post == null) return false; toolbarOverlay.setContents( post.generateToolbar(context, CommentListingFragment.this, toolbarOverlay)); toolbarOverlay.show(edge == BezelSwipeOverlay.SwipeEdge.LEFT ? SideToolbarOverlay.SideToolbarPosition.LEFT : SideToolbarOverlay.SideToolbarPosition.RIGHT); return true; } public boolean onTap() { if (toolbarOverlay.isShown()) { toolbarOverlay.hide(); return true; } return false; } }); outerFrame.addView(bezelOverlay); outerFrame.addView(toolbarOverlay); bezelOverlay.getLayoutParams().width = android.widget.FrameLayout.LayoutParams.MATCH_PARENT; bezelOverlay.getLayoutParams().height = android.widget.FrameLayout.LayoutParams.MATCH_PARENT; toolbarOverlay.getLayoutParams().width = android.widget.FrameLayout.LayoutParams.MATCH_PARENT; toolbarOverlay.getLayoutParams().height = android.widget.FrameLayout.LayoutParams.MATCH_PARENT; makeFirstRequest(context); return outerFrame; } public void cancel() { if (request != null) request.cancel(); } private void makeFirstRequest(final Context context) { final RedditAccount user = RedditAccountManager.getInstance(context).getDefaultAccount(); final CacheManager cm = CacheManager.getInstance(context); // TODO parameterise limit request = new CacheRequest(url, user, session, Constants.Priority.API_COMMENT_LIST, 0, downloadType, Constants.FileType.COMMENT_LIST, true, true, false, context) { @Override protected void onDownloadNecessary() { new Handler(Looper.getMainLooper()).post(new Runnable() { public void run() { listFooter.addView(loadingView); adapter.notifyDataSetChanged(); } }); } @Override protected void onDownloadStarted() { loadingView.setIndeterminate(context.getString(R.string.download_connecting)); } @Override protected void onCallbackException(final Throwable t) { request = null; BugReportActivity.handleGlobalError(context, t); } @Override protected void onFailure(final RequestFailureType type, final Throwable t, final StatusLine status, final String readableMessage) { request = null; if (!isAdded()) return; if (loadingView != null) loadingView.setDoneNoAnim(R.string.download_failed); final RRError error = General.getGeneralErrorForFailure(context, type, t, status); new Handler(Looper.getMainLooper()).post(new Runnable() { public void run() { notifications.addView(new ErrorView(getSupportActivity(), error)); } }); } @Override protected void onProgress(final long bytesRead, final long totalBytes) { } @Override protected void onSuccess(final CacheManager.ReadableCacheFile cacheFile, final long timestamp, final UUID session, final boolean fromCache, final String mimetype) { request = null; } @Override public void onJsonParseStarted(final JsonValue value, final long timestamp, final UUID session, final boolean fromCache) { if (isAdded() && loadingView != null) loadingView.setIndeterminate("Downloading..."); // TODO pref (currently 10 mins) // TODO xml if (fromCache && RRTime.since(timestamp) > 10 * 60 * 1000) { new Handler(Looper.getMainLooper()).post(new Runnable() { public void run() { if (isDetached()) return; final TextView cacheNotif = new TextView(context); cacheNotif.setText(context.getString(R.string.listing_cached) + " " + RRTime.formatDateTime(timestamp, context)); final int paddingPx = General.dpToPixels(context, 6); final int sidePaddingPx = General.dpToPixels(context, 10); cacheNotif.setPadding(sidePaddingPx, paddingPx, sidePaddingPx, paddingPx); cacheNotif.setTextSize(13f); listHeaderNotifications.addView(cacheNotif); adapter.notifyDataSetChanged(); } }); } ((SessionChangeListener) getSupportActivity()).onSessionChanged(session, SessionChangeListener.SessionChangeType.COMMENTS, timestamp); // TODO {"error": 403} is received for unauthorized subreddits try { // Download main post if (value.getType() == JsonValue.Type.ARRAY) { // lol, reddit api final JsonBufferedArray root = value.asArray(); final JsonBufferedObject thing = root.get(0).asObject(); final JsonBufferedObject listing = thing.getObject("data"); final JsonBufferedArray postContainer = listing.getArray("children"); final RedditThing postThing = postContainer.getObject(0, RedditThing.class); final RedditPost post = postThing.asPost(); // TODO show upvote/downvote/etc buttons final RedditSubreddit parentSubreddit = new RedditSubreddit("/r/" + post.subreddit, post.subreddit, false); CommentListingFragment.this.post = new RedditPreparedPost(context, cm, 0, post, timestamp, true, parentSubreddit, false, false, false, user); final ViewGroup selfText; if (post.is_self && post.selftext != null && post.selftext.trim().length() > 0) { selfText = RedditCommentTextParser.parse(StringEscapeUtils.unescapeHtml4(post.selftext)) .generate(context, 14f * commentFontScale, null, new ActiveTextView.OnLinkClickedListener() { public void onClickUrl(String url) { if (url != null) LinkHandler.onLinkClicked(getSupportActivity(), url, false); } public void onClickText(Object attachment) { } }, CommentListingFragment.this.post); } else { selfText = null; } new Handler(Looper.getMainLooper()).post(new Runnable() { public void run() { final RedditPostHeaderView postHeader = new RedditPostHeaderView( getSupportActivity(), CommentListingFragment.this.post, CommentListingFragment.this); listHeaderPost.addView(postHeader); if (selfText != null) { selfText.setFocusable(false); selfText.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); final int paddingPx = General.dpToPixels(context, 10); listHeaderSelftext.addView(selfText); listHeaderSelftext.setPadding(paddingPx, paddingPx, paddingPx, paddingPx); listHeaderNotifications.setBackgroundColor(Color.argb(35, 128, 128, 128)); } if (!General.isTablet(context, PreferenceManager.getDefaultSharedPreferences(context))) { getSupportActivity().getSupportActionBar().setTitle(post.title); } } }); } // Download comments final JsonBufferedObject thing; if (value.getType() == JsonValue.Type.ARRAY) { thing = value.asArray().get(1).asObject(); } else { thing = value.asObject(); } final JsonBufferedObject listing = thing.getObject("data"); final JsonBufferedArray topLevelComments = listing.getArray("children"); final HashSet<String> needsChanging = RedditChangeDataManager.getInstance(context) .getChangedForParent(parentPostIdAndType, user); for (final JsonValue commentThingValue : topLevelComments) { buildComments(commentThingValue, null, timestamp, needsChanging); } commentHandler.sendMessage(General.handlerMessage(0, buffer)); } catch (Throwable t) { notifyFailure(RequestFailureType.PARSE, t, null, "Parse failure"); return; } if (isAdded() && loadingView != null) loadingView.setDoneNoAnim(R.string.download_done); } private ArrayList<RedditPreparedComment> buffer = new ArrayList<RedditPreparedComment>(); private void buildComments(final JsonValue value, final RedditPreparedComment parent, final long timestamp, final HashSet<String> needsChanging) throws IOException, InterruptedException, IllegalAccessException, java.lang.InstantiationException, NoSuchMethodException, InvocationTargetException { final RedditThing commentThing = value.asObject(RedditThing.class); if (commentThing.getKind() != RedditThing.Kind.COMMENT) return; final RedditComment comment = commentThing.asComment(); final RedditPreparedComment preparedComment = new RedditPreparedComment(context, comment, parent, timestamp, needsChanging.contains(comment.name), post, user, headerItems); after = preparedComment.idAndType; buffer.add(preparedComment); if (buffer.size() >= 40) { commentHandler.sendMessage(General.handlerMessage(0, buffer)); buffer = new ArrayList<RedditPreparedComment>(); } if (comment.replies.getType() == JsonValue.Type.OBJECT) { final JsonBufferedObject replies = comment.replies.asObject(); final JsonBufferedArray children = replies.getObject("data").getArray("children"); for (final JsonValue v : children) { buildComments(v, preparedComment, timestamp, needsChanging); } } } }; cm.makeRequest(request); } @Override public void onCreateContextMenu(ContextMenu menu, View v, android.view.ContextMenu.ContextMenuInfo menuInfo) { if (v instanceof ListView) { final AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; if (info.position > 0) { final RedditPreparedComment comment = (RedditPreparedComment) lv.getAdapter() .getItem(info.position); final RedditAccount user = RedditAccountManager.getInstance(getSupportActivity()) .getDefaultAccount(); if (!user.isAnonymous()) { if (!comment.isUpvoted()) { menu.add(Menu.NONE, Action.UPVOTE.ordinal(), 0, R.string.action_upvote); } else { menu.add(Menu.NONE, Action.UNVOTE.ordinal(), 0, R.string.action_upvote_remove); } if (!comment.isDownvoted()) { menu.add(Menu.NONE, Action.DOWNVOTE.ordinal(), 0, R.string.action_downvote); } else { menu.add(Menu.NONE, Action.UNVOTE.ordinal(), 0, R.string.action_downvote_remove); } menu.add(Menu.NONE, Action.REPORT.ordinal(), 0, R.string.action_report); menu.add(Menu.NONE, Action.REPLY.ordinal(), 0, R.string.action_reply); if (user.username.equalsIgnoreCase(comment.src.author)) menu.add(Menu.NONE, Action.EDIT.ordinal(), 0, R.string.action_edit); } menu.add(Menu.NONE, Action.COMMENT_LINKS.ordinal(), 0, R.string.action_comment_links); menu.add(Menu.NONE, Action.COLLAPSE.ordinal(), 0, R.string.action_collapse); menu.add(Menu.NONE, Action.SHARE.ordinal(), 0, R.string.action_share); menu.add(Menu.NONE, Action.COPY.ordinal(), 0, R.string.action_copy); menu.add(Menu.NONE, Action.USER_PROFILE.ordinal(), 0, R.string.action_user_profile); menu.add(Menu.NONE, Action.PROPERTIES.ordinal(), 0, R.string.action_properties); } else { if (post == null) { General.quickToast(getSupportActivity(), R.string.error_toast_parent_post_not_downloaded); return; } RedditPreparedPost.showActionMenu(getSupportActivity(), CommentListingFragment.this, post); } } } public void onClickUrl(String url) { if (url != null) LinkHandler.onLinkClicked(getSupportActivity(), url, false, null); } public void onClickText(Object attachment) { } private static enum Action { UPVOTE, UNVOTE, DOWNVOTE, REPORT, SHARE, COPY, REPLY, USER_PROFILE, COMMENT_LINKS, COLLAPSE, EDIT, PROPERTIES } @Override public boolean onContextItemSelected(MenuItem item) { final AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); if (info.position <= 0) return false; final Action action = Action.values()[item.getItemId()]; final RedditPreparedComment comment = (RedditPreparedComment) lv.getAdapter().getItem(info.position); switch (action) { case UPVOTE: comment.action(getSupportActivity(), RedditAPI.RedditAction.UPVOTE); break; case DOWNVOTE: comment.action(getSupportActivity(), RedditAPI.RedditAction.DOWNVOTE); break; case UNVOTE: comment.action(getSupportActivity(), RedditAPI.RedditAction.UNVOTE); break; case REPORT: new AlertDialog.Builder(getSupportActivity()).setTitle(R.string.action_report) .setMessage(R.string.action_report_sure) .setPositiveButton(R.string.action_report, new DialogInterface.OnClickListener() { public void onClick(final DialogInterface dialog, final int which) { comment.action(getSupportActivity(), RedditAPI.RedditAction.REPORT); // TODO update the view to show the result } }).setNegativeButton(R.string.dialog_cancel, null).show(); break; case REPLY: { final Intent intent = new Intent(getSupportActivity(), CommentReplyActivity.class); intent.putExtra("parentIdAndType", comment.idAndType); startActivity(intent); break; } case EDIT: { final Intent intent = new Intent(getSupportActivity(), CommentEditActivity.class); intent.putExtra("commentIdAndType", comment.idAndType); intent.putExtra("commentText", comment.src.body); startActivity(intent); break; } case COMMENT_LINKS: final HashSet<String> linksInComment = comment.computeAllLinks(); if (linksInComment.isEmpty()) { General.quickToast(getSupportActivity(), R.string.error_toast_no_urls_in_comment); } else { final String[] linksArr = linksInComment.toArray(new String[linksInComment.size()]); final AlertDialog.Builder builder = new AlertDialog.Builder(getSupportActivity()); builder.setItems(linksArr, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { LinkHandler.onLinkClicked(getSupportActivity(), linksArr[which], false); dialog.dismiss(); } }); final AlertDialog alert = builder.create(); alert.setTitle(R.string.action_comment_links); alert.setCanceledOnTouchOutside(true); alert.show(); } break; case SHARE: final Intent mailer = new Intent(Intent.ACTION_SEND); mailer.setType("text/plain"); mailer.putExtra(Intent.EXTRA_SUBJECT, "Comment by " + comment.src.author + " on Reddit"); // TODO this currently just dumps the markdown mailer.putExtra(Intent.EXTRA_TEXT, StringEscapeUtils.unescapeHtml4(comment.src.body)); startActivityForResult(Intent.createChooser(mailer, context.getString(R.string.action_share)), 1); break; case COPY: ClipboardManager manager = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); // TODO this currently just dumps the markdown manager.setText(StringEscapeUtils.unescapeHtml4(comment.src.body)); break; case COLLAPSE: if (comment.getBoundView() != null) handleCommentVisibilityToggle(comment.getBoundView()); break; case USER_PROFILE: UserProfileDialog.newInstance(comment.src.author).show(getSupportActivity()); break; case PROPERTIES: CommentPropertiesDialog.newInstance(comment.src).show(getSupportActivity()); break; } return true; } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { menu.add(R.string.action_reply); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getTitle().equals(context.getString(R.string.action_reply))) { onParentReply(); return true; } return false; } private void onParentReply() { if (post != null) { final Intent intent = new Intent(getSupportActivity(), CommentReplyActivity.class); intent.putExtra("parentIdAndType", post.idAndType); startActivity(intent); } else { General.quickToast(getSupportActivity(), R.string.error_toast_parent_post_not_downloaded); } } public void onPostSelected(final RedditPreparedPost post) { ((RedditPostView.PostSelectionListener) getSupportActivity()).onPostSelected(post); } public void onPostCommentsSelected(final RedditPreparedPost post) { ((RedditPostView.PostSelectionListener) getSupportActivity()).onPostCommentsSelected(post); } }