/******************************************************************************* * 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 org.quantumbadger.redreader.reddit.prepared; import android.Manifest; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.net.Uri; import android.preference.PreferenceManager; import android.support.v7.app.AppCompatActivity; import android.text.ClipboardManager; import android.text.SpannableStringBuilder; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageButton; import android.widget.Toast; import org.apache.commons.lang3.StringEscapeUtils; import org.quantumbadger.redreader.R; import org.quantumbadger.redreader.account.RedditAccount; import org.quantumbadger.redreader.account.RedditAccountManager; import org.quantumbadger.redreader.activities.*; import org.quantumbadger.redreader.cache.CacheManager; import org.quantumbadger.redreader.cache.CacheRequest; import org.quantumbadger.redreader.cache.downloadstrategy.DownloadStrategyIfNotCached; import org.quantumbadger.redreader.common.*; import org.quantumbadger.redreader.fragments.PostPropertiesDialog; import org.quantumbadger.redreader.image.SaveImageCallback; import org.quantumbadger.redreader.image.ShareImageCallback; import org.quantumbadger.redreader.image.ThumbnailScaler; import org.quantumbadger.redreader.reddit.APIResponseHandler; import org.quantumbadger.redreader.reddit.RedditAPI; import org.quantumbadger.redreader.reddit.api.RedditSubredditSubscriptionManager; import org.quantumbadger.redreader.reddit.things.RedditSubreddit; import org.quantumbadger.redreader.reddit.url.SubredditPostListURL; import org.quantumbadger.redreader.reddit.url.UserProfileURL; import org.quantumbadger.redreader.views.RedditPostView; import org.quantumbadger.redreader.views.bezelmenu.SideToolbarOverlay; import org.quantumbadger.redreader.views.bezelmenu.VerticalToolbar; import java.net.URI; import java.util.*; public final class RedditPreparedPost { public final RedditParsedPost src; private final RedditChangeDataManager mChangeDataManager; public SpannableStringBuilder postListDescription; public final boolean isArchived; public final boolean hasThumbnail; public final boolean mIsProbablyAnImage; // TODO make it possible to turn off in-memory caching when out of memory private volatile Bitmap thumbnailCache = null; private static final Object singleImageDecodeLock = new Object(); private ThumbnailLoadedCallback thumbnailCallback; private int usageId = -1; public long lastChange = Long.MIN_VALUE; private final boolean showSubreddit; private RedditPostView boundView = null; public enum Action { UPVOTE(R.string.action_upvote), UNVOTE(R.string.action_vote_remove), DOWNVOTE(R.string.action_downvote), SAVE(R.string.action_save), HIDE(R.string.action_hide), UNSAVE(R.string.action_unsave), UNHIDE(R.string.action_unhide), EDIT(R.string.action_edit), DELETE(R.string.action_delete), REPORT(R.string.action_report), SHARE(R.string.action_share), REPLY(R.string.action_reply), USER_PROFILE(R.string.action_user_profile), EXTERNAL(R.string.action_external), PROPERTIES(R.string.action_properties), COMMENTS(R.string.action_comments), LINK(R.string.action_link), COMMENTS_SWITCH(R.string.action_comments_switch), LINK_SWITCH(R.string.action_link_switch), SHARE_COMMENTS(R.string.action_share_comments), SHARE_IMAGE(R.string.action_share_image), GOTO_SUBREDDIT(R.string.action_gotosubreddit), ACTION_MENU(R.string.action_actionmenu), SAVE_IMAGE(R.string.action_save_image), COPY(R.string.action_copy), SELFTEXT_LINKS(R.string.action_selftext_links), BACK(R.string.action_back), BLOCK(R.string.action_block_subreddit), UNBLOCK(R.string.action_unblock_subreddit), PIN(R.string.action_pin_subreddit), UNPIN(R.string.action_unpin_subreddit), SUBSCRIBE(R.string.action_subscribe_subreddit), UNSUBSCRIBE(R.string.action_unsubscribe_subreddit); public final int descriptionResId; Action(final int descriptionResId) { this.descriptionResId = descriptionResId; } } // TODO too many parameters public RedditPreparedPost( final Context context, final CacheManager cm, final int listId, final RedditParsedPost post, final long timestamp, final boolean showSubreddit, final boolean showThumbnails) { this.src = post; this.showSubreddit = showSubreddit; final RedditAccount user = RedditAccountManager.getInstance(context).getDefaultAccount(); mChangeDataManager = RedditChangeDataManager.getInstance(user); isArchived = post.isArchived(); mIsProbablyAnImage = LinkHandler.isProbablyAnImage(post.getUrl()); hasThumbnail = showThumbnails && hasThumbnail(post); // TODO parameterise final int thumbnailWidth = General.dpToPixels(context, 64); if(hasThumbnail && hasThumbnail(post)) { downloadThumbnail(context, thumbnailWidth, cm, listId); } lastChange = timestamp; mChangeDataManager.update(timestamp, post.getSrc()); rebuildSubtitle(context); } public static void showActionMenu( final AppCompatActivity activity, final RedditPreparedPost post) { final EnumSet<Action> itemPref = PrefsUtility.pref_menus_post_context_items(activity, PreferenceManager.getDefaultSharedPreferences(activity)); if(itemPref.isEmpty()) return; final RedditAccount user = RedditAccountManager.getInstance(activity).getDefaultAccount(); final ArrayList<RPVMenuItem> menu = new ArrayList<>(); if(!RedditAccountManager.getInstance(activity).getDefaultAccount().isAnonymous()) { if(itemPref.contains(Action.UPVOTE)) { if(!post.isUpvoted()) { menu.add(new RPVMenuItem(activity, R.string.action_upvote, Action.UPVOTE)); } else { menu.add(new RPVMenuItem(activity, R.string.action_upvote_remove, Action.UNVOTE)); } } if(itemPref.contains(Action.DOWNVOTE)) { if(!post.isDownvoted()) { menu.add(new RPVMenuItem(activity, R.string.action_downvote, Action.DOWNVOTE)); } else { menu.add(new RPVMenuItem(activity, R.string.action_downvote_remove, Action.UNVOTE)); } } if(itemPref.contains(Action.SAVE)) { if(!post.isSaved()) { menu.add(new RPVMenuItem(activity, R.string.action_save, Action.SAVE)); } else { menu.add(new RPVMenuItem(activity, R.string.action_unsave, Action.UNSAVE)); } } if(itemPref.contains(Action.HIDE)) { if(!post.isHidden()) { menu.add(new RPVMenuItem(activity, R.string.action_hide, Action.HIDE)); } else { menu.add(new RPVMenuItem(activity, R.string.action_unhide, Action.UNHIDE)); } } if(itemPref.contains(Action.EDIT) && post.isSelf() && user.username.equalsIgnoreCase(post.src.getAuthor())){ menu.add(new RPVMenuItem(activity, R.string.action_edit, Action.EDIT)); } if(itemPref.contains(Action.DELETE) && user.username.equalsIgnoreCase(post.src.getAuthor())) { menu.add(new RPVMenuItem(activity, R.string.action_delete, Action.DELETE)); } if(itemPref.contains(Action.REPORT)) menu.add(new RPVMenuItem(activity, R.string.action_report, Action.REPORT)); } if(itemPref.contains(Action.EXTERNAL)) menu.add(new RPVMenuItem(activity, R.string.action_external, Action.EXTERNAL)); if(itemPref.contains(Action.SELFTEXT_LINKS) && post.src.getRawSelfText() != null && post.src.getRawSelfText().length() > 1) menu.add(new RPVMenuItem(activity, R.string.action_selftext_links, Action.SELFTEXT_LINKS)); if(itemPref.contains(Action.SAVE_IMAGE) && post.mIsProbablyAnImage) menu.add(new RPVMenuItem(activity, R.string.action_save_image, Action.SAVE_IMAGE)); if(itemPref.contains(Action.GOTO_SUBREDDIT)) menu.add(new RPVMenuItem(activity, R.string.action_gotosubreddit, Action.GOTO_SUBREDDIT)); if (post.showSubreddit){ try { String subredditCanonicalName = RedditSubreddit.getCanonicalName(post.src.getSubreddit()); if (itemPref.contains(Action.BLOCK) && post.showSubreddit) { final List<String> blockedSubreddits = PrefsUtility.pref_blocked_subreddits(activity, PreferenceManager.getDefaultSharedPreferences(activity)); if (blockedSubreddits.contains(subredditCanonicalName)) { menu.add(new RPVMenuItem(activity, R.string.action_unblock_subreddit, Action.UNBLOCK)); } else { menu.add(new RPVMenuItem(activity, R.string.action_block_subreddit, Action.BLOCK)); } } if (itemPref.contains(Action.PIN) && post.showSubreddit) { List<String> pinnedSubreddits = PrefsUtility.pref_pinned_subreddits(activity, PreferenceManager.getDefaultSharedPreferences(activity)); if (pinnedSubreddits.contains(subredditCanonicalName)) { menu.add(new RPVMenuItem(activity, R.string.action_unpin_subreddit, Action.UNPIN)); } else { menu.add(new RPVMenuItem(activity, R.string.action_pin_subreddit, Action.PIN)); } } if (!RedditAccountManager.getInstance(activity).getDefaultAccount().isAnonymous()) { if (itemPref.contains(Action.SUBSCRIBE)) { if (RedditSubredditSubscriptionManager .getSingleton(activity, RedditAccountManager.getInstance(activity).getDefaultAccount()) .getSubscriptionState(subredditCanonicalName) == RedditSubredditSubscriptionManager.SubredditSubscriptionState.SUBSCRIBED) { menu.add(new RPVMenuItem(activity, R.string.action_unsubscribe_subreddit, Action.UNSUBSCRIBE)); } else { menu.add(new RPVMenuItem(activity, R.string.action_subscribe_subreddit, Action.SUBSCRIBE)); } } } } catch (RedditSubreddit.InvalidSubredditNameException ex){ throw new RuntimeException(ex); } } if(itemPref.contains(Action.SHARE)) menu.add(new RPVMenuItem(activity, R.string.action_share, Action.SHARE)); if(itemPref.contains(Action.SHARE_COMMENTS)) menu.add(new RPVMenuItem(activity, R.string.action_share_comments, Action.SHARE_COMMENTS)); if(itemPref.contains(Action.SHARE_IMAGE) && post.mIsProbablyAnImage) menu.add(new RPVMenuItem(activity, R.string.action_share_image, Action.SHARE_IMAGE)); if(itemPref.contains(Action.COPY)) menu.add(new RPVMenuItem(activity, R.string.action_copy, Action.COPY)); if(itemPref.contains(Action.USER_PROFILE)) menu.add(new RPVMenuItem(activity, R.string.action_user_profile, Action.USER_PROFILE)); if(itemPref.contains(Action.PROPERTIES)) menu.add(new RPVMenuItem(activity, R.string.action_properties, Action.PROPERTIES)); final String[] menuText = new String[menu.size()]; for(int i = 0; i < menuText.length; i++) { menuText[i] = menu.get(i).title; } final AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setItems(menuText, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { onActionMenuItemSelected(post, activity, menu.get(which).action); } }); //builder.setNeutralButton(R.string.dialog_cancel, null); final AlertDialog alert = builder.create(); alert.setCanceledOnTouchOutside(true); alert.show(); } public static void onActionMenuItemSelected(final RedditPreparedPost post, final AppCompatActivity activity, final Action action) { switch(action) { case UPVOTE: post.action(activity, RedditAPI.ACTION_UPVOTE); break; case DOWNVOTE: post.action(activity, RedditAPI.ACTION_DOWNVOTE); break; case UNVOTE: post.action(activity, RedditAPI.ACTION_UNVOTE); break; case SAVE: post.action(activity, RedditAPI.ACTION_SAVE); break; case UNSAVE: post.action(activity, RedditAPI.ACTION_UNSAVE); break; case HIDE: post.action(activity, RedditAPI.ACTION_HIDE); break; case UNHIDE: post.action(activity, RedditAPI.ACTION_UNHIDE); break; case EDIT: final Intent editIntent = new Intent(activity, CommentEditActivity.class); editIntent.putExtra("commentIdAndType", post.src.getIdAndType()); editIntent.putExtra("commentText", StringEscapeUtils.unescapeHtml4(post.src.getRawSelfText())); editIntent.putExtra("isSelfPost", true); activity.startActivity(editIntent); break; case DELETE: new AlertDialog.Builder(activity) .setTitle(R.string.accounts_delete) .setMessage(R.string.delete_confirm) .setPositiveButton(R.string.action_delete, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { post.action(activity, RedditAPI.ACTION_DELETE); } }) .setNegativeButton(R.string.dialog_cancel, null) .show(); break; case REPORT: new AlertDialog.Builder(activity) .setTitle(R.string.action_report) .setMessage(R.string.action_report_sure) .setPositiveButton(R.string.action_report, new DialogInterface.OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { post.action(activity, RedditAPI.ACTION_REPORT); // TODO update the view to show the result // TODO don't forget, this also hides } }) .setNegativeButton(R.string.dialog_cancel, null) .show(); break; case EXTERNAL: { final Intent intent = new Intent(Intent.ACTION_VIEW); String url = (activity instanceof WebViewActivity) ? ((WebViewActivity) activity).getCurrentUrl() : post.src.getUrl(); intent.setData(Uri.parse(url)); activity.startActivity(intent); break; } case SELFTEXT_LINKS: { final HashSet<String> linksInComment = LinkHandler.computeAllLinks(StringEscapeUtils.unescapeHtml4(post.src.getRawSelfText())); if(linksInComment.isEmpty()) { General.quickToast(activity, R.string.error_toast_no_urls_in_self); } else { final String[] linksArr = linksInComment.toArray(new String[linksInComment.size()]); final AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setItems(linksArr, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { LinkHandler.onLinkClicked(activity, linksArr[which], false, post.src.getSrc()); dialog.dismiss(); } }); final AlertDialog alert = builder.create(); alert.setTitle(R.string.action_selftext_links); alert.setCanceledOnTouchOutside(true); alert.show(); } break; } case SAVE_IMAGE: { ((BaseActivity)activity).requestPermissionWithCallback(Manifest.permission.WRITE_EXTERNAL_STORAGE, new SaveImageCallback(activity, post.src.getUrl())); break; } case SHARE: { final Intent mailer = new Intent(Intent.ACTION_SEND); mailer.setType("text/plain"); mailer.putExtra(Intent.EXTRA_SUBJECT, post.src.getTitle()); mailer.putExtra(Intent.EXTRA_TEXT, post.src.getUrl()); activity.startActivity(Intent.createChooser(mailer, activity.getString(R.string.action_share))); break; } case SHARE_COMMENTS: { final boolean shareAsPermalink = PrefsUtility.pref_behaviour_share_permalink(activity, PreferenceManager.getDefaultSharedPreferences(activity)); final Intent mailer = new Intent(Intent.ACTION_SEND); mailer.setType("text/plain"); mailer.putExtra(Intent.EXTRA_SUBJECT, "Comments for " + post.src.getTitle()); if (shareAsPermalink) { mailer.putExtra(Intent.EXTRA_TEXT, Constants.Reddit.getNonAPIUri(post.src.getPermalink()).toString()); } else { mailer.putExtra(Intent.EXTRA_TEXT, Constants.Reddit.getNonAPIUri(Constants.Reddit.PATH_COMMENTS + post.src.getIdAlone()).toString()); } activity.startActivity(Intent.createChooser(mailer, activity.getString(R.string.action_share_comments))); break; } case SHARE_IMAGE: { ((BaseActivity)activity).requestPermissionWithCallback(Manifest.permission.WRITE_EXTERNAL_STORAGE, new ShareImageCallback(activity, post.src.getUrl())); break; } case COPY: { ClipboardManager manager = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); manager.setText(post.src.getUrl()); break; } case GOTO_SUBREDDIT: { try { final Intent intent = new Intent(activity, PostListingActivity.class); intent.setData(SubredditPostListURL.getSubreddit(post.src.getSubreddit()).generateJsonUri()); activity.startActivityForResult(intent, 1); } catch(RedditSubreddit.InvalidSubredditNameException e) { Toast.makeText(activity, R.string.invalid_subreddit_name, Toast.LENGTH_LONG).show(); } break; } case USER_PROFILE: LinkHandler.onLinkClicked(activity, new UserProfileURL(post.src.getAuthor()).toString()); break; case PROPERTIES: PostPropertiesDialog.newInstance(post.src.getSrc()).show(activity.getSupportFragmentManager(), null); break; case COMMENTS: ((RedditPostView.PostSelectionListener)activity).onPostCommentsSelected(post); new Thread() { @Override public void run() { post.markAsRead(activity); } }.start(); break; case LINK: ((RedditPostView.PostSelectionListener)activity).onPostSelected(post); break; case COMMENTS_SWITCH: if(!(activity instanceof MainActivity)) activity.finish(); ((RedditPostView.PostSelectionListener)activity).onPostCommentsSelected(post); break; case LINK_SWITCH: if(!(activity instanceof MainActivity)) activity.finish(); ((RedditPostView.PostSelectionListener)activity).onPostSelected(post); break; case ACTION_MENU: showActionMenu(activity, post); break; case REPLY: final Intent intent = new Intent(activity, CommentReplyActivity.class); intent.putExtra(CommentReplyActivity.PARENT_ID_AND_TYPE_KEY, post.src.getIdAndType()); intent.putExtra(CommentReplyActivity.PARENT_MARKDOWN_KEY, post.src.getUnescapedSelfText()); activity.startActivity(intent); break; case BACK: activity.onBackPressed(); break; case PIN: try { String subredditCanonicalName = RedditSubreddit.getCanonicalName(post.src.getSubreddit()); List<String> pinnedSubreddits = PrefsUtility.pref_pinned_subreddits(activity, PreferenceManager.getDefaultSharedPreferences(activity)); if (!pinnedSubreddits.contains(subredditCanonicalName)){ PrefsUtility.pref_pinned_subreddits_add( activity, PreferenceManager.getDefaultSharedPreferences(activity), subredditCanonicalName); } else { Toast.makeText(activity, R.string.mainmenu_toast_pinned, Toast.LENGTH_SHORT).show(); } } catch (RedditSubreddit.InvalidSubredditNameException e) { throw new RuntimeException(e); } break; case UNPIN: try { String subredditCanonicalName = RedditSubreddit.getCanonicalName(post.src.getSubreddit()); List<String> pinnedSubreddits = PrefsUtility.pref_pinned_subreddits(activity, PreferenceManager.getDefaultSharedPreferences(activity)); if (pinnedSubreddits.contains(subredditCanonicalName)) { PrefsUtility.pref_pinned_subreddits_remove( activity, PreferenceManager.getDefaultSharedPreferences(activity), subredditCanonicalName); } else { Toast.makeText(activity, R.string.mainmenu_toast_not_pinned, Toast.LENGTH_SHORT).show(); } } catch (RedditSubreddit.InvalidSubredditNameException e){ throw new RuntimeException(e); } break; case BLOCK: try { String subredditCanonicalName = RedditSubreddit.getCanonicalName(post.src.getSubreddit()); List<String> blockedSubreddits = PrefsUtility.pref_blocked_subreddits(activity, PreferenceManager.getDefaultSharedPreferences(activity)); if (!blockedSubreddits.contains(subredditCanonicalName)) { PrefsUtility.pref_blocked_subreddits_add( activity, PreferenceManager.getDefaultSharedPreferences(activity), subredditCanonicalName); } else { Toast.makeText(activity, R.string.mainmenu_toast_blocked, Toast.LENGTH_SHORT).show(); } } catch (RedditSubreddit.InvalidSubredditNameException e){ throw new RuntimeException(e); } break; case UNBLOCK: try { String subredditCanonicalName = RedditSubreddit.getCanonicalName(post.src.getSubreddit()); List<String> blockedSubreddits = PrefsUtility.pref_blocked_subreddits(activity, PreferenceManager.getDefaultSharedPreferences(activity)); if (blockedSubreddits.contains(subredditCanonicalName)) { PrefsUtility.pref_blocked_subreddits_remove( activity, PreferenceManager.getDefaultSharedPreferences(activity), subredditCanonicalName); } else { Toast.makeText(activity, R.string.mainmenu_toast_not_blocked, Toast.LENGTH_SHORT).show(); } } catch (RedditSubreddit.InvalidSubredditNameException e){ throw new RuntimeException(e); } break; case SUBSCRIBE: try { String subredditCanonicalName = RedditSubreddit.getCanonicalName(post.src.getSubreddit()); RedditSubredditSubscriptionManager subMan = RedditSubredditSubscriptionManager .getSingleton(activity, RedditAccountManager.getInstance(activity).getDefaultAccount()); if (subMan.getSubscriptionState(subredditCanonicalName) == RedditSubredditSubscriptionManager.SubredditSubscriptionState.NOT_SUBSCRIBED) { subMan.subscribe(subredditCanonicalName, activity); Toast.makeText(activity, R.string.options_subscribing, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(activity, R.string.mainmenu_toast_subscribed, Toast.LENGTH_SHORT).show(); } } catch (RedditSubreddit.InvalidSubredditNameException e) { throw new RuntimeException(e); } break; case UNSUBSCRIBE: try { String subredditCanonicalName = RedditSubreddit.getCanonicalName(post.src.getSubreddit()); RedditSubredditSubscriptionManager subMan = RedditSubredditSubscriptionManager .getSingleton(activity, RedditAccountManager.getInstance(activity).getDefaultAccount()); if (subMan.getSubscriptionState(subredditCanonicalName) == RedditSubredditSubscriptionManager.SubredditSubscriptionState.SUBSCRIBED) { subMan.unsubscribe(subredditCanonicalName, activity); Toast.makeText(activity, R.string.options_unsubscribing, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(activity, R.string.mainmenu_toast_not_subscribed, Toast.LENGTH_SHORT).show(); } } catch (RedditSubreddit.InvalidSubredditNameException e) { throw new RuntimeException(e); } break; } } public int computeScore() { int score = src.getScoreExcludingOwnVote(); if(isUpvoted()) { score++; } else if(isDownvoted()) { score--; } return score; } private void rebuildSubtitle(Context context) { // TODO customise display // TODO preference for the X days, X hours thing final TypedArray appearance = context.obtainStyledAttributes(new int[]{ R.attr.rrPostSubtitleBoldCol, R.attr.rrPostSubtitleUpvoteCol, R.attr.rrPostSubtitleDownvoteCol, R.attr.rrFlairBackCol, R.attr.rrFlairTextCol }); final int boldCol = appearance.getColor(0, 255), rrPostSubtitleUpvoteCol = appearance.getColor(1, 255), rrPostSubtitleDownvoteCol = appearance.getColor(2, 255), rrFlairBackCol = appearance.getColor(3, 255), rrFlairTextCol = appearance.getColor(4, 255); appearance.recycle(); final BetterSSB postListDescSb = new BetterSSB(); final int pointsCol; final int score = computeScore(); if(isUpvoted()) { pointsCol = rrPostSubtitleUpvoteCol; } else if(isDownvoted()) { pointsCol = rrPostSubtitleDownvoteCol; } else { pointsCol = boldCol; } if(src.isSpoiler()) { postListDescSb.append(" SPOILER ", BetterSSB.BOLD | BetterSSB.FOREGROUND_COLOR | BetterSSB.BACKGROUND_COLOR, Color.WHITE, Color.rgb(50, 50, 50), 1f); postListDescSb.append(" ", 0); } if(src.isStickied()) { postListDescSb.append(" STICKY ", BetterSSB.BOLD | BetterSSB.FOREGROUND_COLOR | BetterSSB.BACKGROUND_COLOR, Color.WHITE, Color.rgb(0, 170, 0), 1f); // TODO color? postListDescSb.append(" ", 0); } if(src.isNsfw()) { postListDescSb.append(" NSFW ", BetterSSB.BOLD | BetterSSB.FOREGROUND_COLOR | BetterSSB.BACKGROUND_COLOR, Color.WHITE, Color.RED, 1f); // TODO color? postListDescSb.append(" ", 0); } if(src.getFlairText() != null) { postListDescSb.append(" " + src.getFlairText() + " ", BetterSSB.BOLD | BetterSSB.FOREGROUND_COLOR | BetterSSB.BACKGROUND_COLOR, rrFlairTextCol, rrFlairBackCol, 1f); postListDescSb.append(" ", 0); } postListDescSb.append(String.valueOf(score), BetterSSB.BOLD | BetterSSB.FOREGROUND_COLOR, pointsCol, 0, 1f); postListDescSb.append(" " + context.getString(R.string.subtitle_points) + " ", 0); postListDescSb.append(RRTime.formatDurationFrom(context, src.getCreatedTimeSecsUTC() * 1000), BetterSSB.BOLD | BetterSSB.FOREGROUND_COLOR, boldCol, 0, 1f); postListDescSb.append(" " + context.getString(R.string.subtitle_by) + " ", 0); postListDescSb.append(src.getAuthor(), BetterSSB.BOLD | BetterSSB.FOREGROUND_COLOR, boldCol, 0, 1f); if(showSubreddit) { postListDescSb.append(" " + context.getString(R.string.subtitle_to) + " ", 0); postListDescSb.append(src.getSubreddit(), BetterSSB.BOLD | BetterSSB.FOREGROUND_COLOR, boldCol, 0, 1f); } postListDescSb.append(" (" + src.getDomain() + ")", 0); postListDescription = postListDescSb.get(); } // lol, reddit api private static boolean hasThumbnail(final RedditParsedPost post) { final String url = post.getThumbnailUrl(); return url != null && url.length() != 0 && !url.equalsIgnoreCase("nsfw") && !url.equalsIgnoreCase("self") && !url.equalsIgnoreCase("default"); } private void downloadThumbnail(final Context context, final int widthPixels, final CacheManager cm, final int listId) { final String uriStr = src.getThumbnailUrl(); final URI uri = General.uriFromString(uriStr); final int priority = Constants.Priority.THUMBNAIL; final int fileType = Constants.FileType.THUMBNAIL; final RedditAccount anon = RedditAccountManager.getAnon(); cm.makeRequest(new CacheRequest(uri, anon, null, priority, listId, DownloadStrategyIfNotCached.INSTANCE, fileType, CacheRequest.DOWNLOAD_QUEUE_IMMEDIATE, false, false, context) { @Override protected void onDownloadNecessary() {} @Override protected void onDownloadStarted() {} @Override protected void onCallbackException(final Throwable t) { // TODO handle -- internal error throw new RuntimeException(t); } @Override protected void onFailure(final @CacheRequest.RequestFailureType int type, final Throwable t, final Integer status, final String readableMessage) {} @Override protected void onProgress(final boolean authorizationInProgress, 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) { try { synchronized(singleImageDecodeLock) { BitmapFactory.Options justDecodeBounds = new BitmapFactory.Options(); justDecodeBounds.inJustDecodeBounds = true; BitmapFactory.decodeStream(cacheFile.getInputStream(), null, justDecodeBounds); final int width = justDecodeBounds.outWidth; final int height = justDecodeBounds.outHeight; int factor = 1; while(width / (factor + 1) > widthPixels && height / (factor + 1) > widthPixels) factor *= 2; BitmapFactory.Options scaledOptions = new BitmapFactory.Options(); scaledOptions.inSampleSize = factor; final Bitmap data = BitmapFactory.decodeStream(cacheFile.getInputStream(), null, scaledOptions); if(data == null) return; thumbnailCache = ThumbnailScaler.scale(data, widthPixels); if(thumbnailCache != data) data.recycle(); } if(thumbnailCallback != null) thumbnailCallback.betterThumbnailAvailable(thumbnailCache, usageId); } catch (OutOfMemoryError e) { // TODO handle this better - disable caching of images Log.e("RedditPreparedPost", "Out of memory trying to download image"); e.printStackTrace(); } catch(Throwable t) { // Just ignore it. } } }); } // These operations are ordered so as to avoid race conditions public Bitmap getThumbnail(final ThumbnailLoadedCallback callback, final int usageId) { this.thumbnailCallback = callback; this.usageId = usageId; return thumbnailCache; } public boolean isSelf() { return src.isSelfPost(); } public boolean isRead() { return mChangeDataManager.isRead(src); } public void bind(RedditPostView boundView) { this.boundView = boundView; } public void unbind(RedditPostView boundView) { if(this.boundView == boundView) this.boundView = null; } // TODO handle download failure - show red "X" or something public interface ThumbnailLoadedCallback { void betterThumbnailAvailable(Bitmap thumbnail, int usageId); } public void markAsRead(final Context context) { final RedditAccount user = RedditAccountManager.getInstance(context).getDefaultAccount(); RedditChangeDataManager.getInstance(user).markRead(RRTime.utcCurrentTimeMillis(), src); refreshView(context); } public void refreshView(final Context context) { AndroidApi.UI_THREAD_HANDLER.post(new Runnable() { @Override public void run() { rebuildSubtitle(context); if(boundView != null) { boundView.updateAppearance(); } } }); } public void action(final AppCompatActivity activity, final @RedditAPI.RedditAction int action) { final RedditAccount user = RedditAccountManager.getInstance(activity).getDefaultAccount(); if(user.isAnonymous()) { AndroidApi.UI_THREAD_HANDLER.post(new Runnable() { @Override public void run() { Toast.makeText(activity, activity.getString(R.string.error_toast_notloggedin), Toast.LENGTH_SHORT).show(); } }); return; } final int lastVoteDirection = getVoteDirection(); final boolean archived = src.isArchived(); final long now = RRTime.utcCurrentTimeMillis(); switch(action) { case RedditAPI.ACTION_DOWNVOTE: if(!archived) { mChangeDataManager.markDownvoted(now, src); } break; case RedditAPI.ACTION_UNVOTE: if(!archived) { mChangeDataManager.markUnvoted(now, src); } break; case RedditAPI.ACTION_UPVOTE: if(!archived) { mChangeDataManager.markUpvoted(now, src); } break; case RedditAPI.ACTION_SAVE: mChangeDataManager.markSaved(now, src, true); break; case RedditAPI.ACTION_UNSAVE: mChangeDataManager.markSaved(now, src, false); break; case RedditAPI.ACTION_HIDE: mChangeDataManager.markHidden(now, src, true); break; case RedditAPI.ACTION_UNHIDE: mChangeDataManager.markHidden(now, src, false); break; case RedditAPI.ACTION_REPORT: break; case RedditAPI.ACTION_DELETE: break; default: throw new RuntimeException("Unknown post action"); } refreshView(activity); boolean vote = (action == RedditAPI.ACTION_DOWNVOTE | action == RedditAPI.ACTION_UPVOTE | action == RedditAPI.ACTION_UNVOTE); if(archived && vote){ Toast.makeText(activity, R.string.error_archived_vote, Toast.LENGTH_SHORT) .show(); return; } RedditAPI.action(CacheManager.getInstance(activity), new APIResponseHandler.ActionResponseHandler(activity) { @Override protected void onCallbackException(final Throwable t) { BugReportActivity.handleGlobalError(context, t); } @Override protected void onFailure(final @CacheRequest.RequestFailureType int type, final Throwable t, final Integer status, final String readableMessage) { revertOnFailure(); if(t != null) t.printStackTrace(); final RRError error = General.getGeneralErrorForFailure(context, type, t, status, "Reddit API action code: " + action + " " + src.getIdAndType()); AndroidApi.UI_THREAD_HANDLER.post(new Runnable() { @Override public void run() { General.showResultDialog(activity, error); } }); } @Override protected void onFailure(final APIFailureType type) { revertOnFailure(); final RRError error = General.getGeneralErrorForFailure(context, type); AndroidApi.UI_THREAD_HANDLER.post(new Runnable() { @Override public void run() { General.showResultDialog(activity, error); } }); } @Override protected void onSuccess() { final long now = RRTime.utcCurrentTimeMillis(); switch(action) { case RedditAPI.ACTION_DOWNVOTE: mChangeDataManager.markDownvoted(now, src); break; case RedditAPI.ACTION_UNVOTE: mChangeDataManager.markUnvoted(now, src); break; case RedditAPI.ACTION_UPVOTE: mChangeDataManager.markUpvoted(now, src); break; case RedditAPI.ACTION_SAVE: mChangeDataManager.markSaved(now, src, true); break; case RedditAPI.ACTION_UNSAVE: mChangeDataManager.markSaved(now, src, false); break; case RedditAPI.ACTION_HIDE: mChangeDataManager.markHidden(now, src, true); break; case RedditAPI.ACTION_UNHIDE: mChangeDataManager.markHidden(now, src, false); break; case RedditAPI.ACTION_REPORT: break; case RedditAPI.ACTION_DELETE: General.quickToast(activity, R.string.delete_success); break; default: throw new RuntimeException("Unknown post action"); } refreshView(context); } private void revertOnFailure() { final long now = RRTime.utcCurrentTimeMillis(); switch(action) { case RedditAPI.ACTION_DOWNVOTE: case RedditAPI.ACTION_UNVOTE: case RedditAPI.ACTION_UPVOTE: switch(lastVoteDirection) { case -1: mChangeDataManager.markDownvoted(now, src); break; case 0: mChangeDataManager.markUnvoted(now, src); break; case 1: mChangeDataManager.markUpvoted(now, src); break; } case RedditAPI.ACTION_SAVE: mChangeDataManager.markSaved(now, src, false); break; case RedditAPI.ACTION_UNSAVE: mChangeDataManager.markSaved(now, src, true); break; case RedditAPI.ACTION_HIDE: mChangeDataManager.markHidden(now, src, false); break; case RedditAPI.ACTION_UNHIDE: mChangeDataManager.markHidden(now, src, true); break; case RedditAPI.ACTION_REPORT: break; case RedditAPI.ACTION_DELETE: break; default: throw new RuntimeException("Unknown post action"); } refreshView(context); } }, user, src.getIdAndType(), action, activity); } public boolean isUpvoted() { return mChangeDataManager.isUpvoted(src); } public boolean isDownvoted() { return mChangeDataManager.isDownvoted(src); } public int getVoteDirection() { return isUpvoted() ? 1 : (isDownvoted() ? -1 : 0); } public boolean isSaved() { return mChangeDataManager.isSaved(src); } public boolean isHidden() { return Boolean.TRUE.equals(mChangeDataManager.isHidden(src)); } private static class RPVMenuItem { public final String title; public final Action action; private RPVMenuItem(Context context, int titleRes, Action action) { this.title = context.getString(titleRes); this.action = action; } } public VerticalToolbar generateToolbar( final AppCompatActivity activity, boolean isComments, final SideToolbarOverlay overlay) { final VerticalToolbar toolbar = new VerticalToolbar(activity); final EnumSet<Action> itemsPref = PrefsUtility.pref_menus_post_toolbar_items(activity, PreferenceManager.getDefaultSharedPreferences(activity)); final Action[] possibleItems = { Action.ACTION_MENU, isComments ? Action.LINK_SWITCH : Action.COMMENTS_SWITCH, Action.UPVOTE, Action.DOWNVOTE, Action.SAVE, Action.HIDE, Action.DELETE, Action.REPLY, Action.EXTERNAL, Action.SAVE_IMAGE, Action.SHARE, Action.COPY, Action.USER_PROFILE, Action.PROPERTIES }; // TODO make static final EnumMap<Action, Integer> iconsDark = new EnumMap<>(Action.class); iconsDark.put(Action.ACTION_MENU, R.drawable.ic_action_overflow); iconsDark.put(Action.COMMENTS_SWITCH, R.drawable.ic_action_comments_dark); iconsDark.put(Action.LINK_SWITCH, mIsProbablyAnImage ? R.drawable.ic_action_image_dark : R.drawable.ic_action_link_dark); iconsDark.put(Action.UPVOTE, R.drawable.action_upvote_dark); iconsDark.put(Action.DOWNVOTE, R.drawable.action_downvote_dark); iconsDark.put(Action.SAVE, R.drawable.ic_action_star_filled_dark); iconsDark.put(Action.HIDE, R.drawable.ic_action_cross_dark); iconsDark.put(Action.REPLY, R.drawable.ic_action_reply_dark); iconsDark.put(Action.EXTERNAL, R.drawable.ic_action_external_dark); iconsDark.put(Action.SAVE_IMAGE, R.drawable.ic_action_save_dark); iconsDark.put(Action.SHARE, R.drawable.ic_action_share_dark); iconsDark.put(Action.COPY, R.drawable.ic_action_copy_dark); iconsDark.put(Action.USER_PROFILE, R.drawable.ic_action_person_dark); iconsDark.put(Action.PROPERTIES, R.drawable.ic_action_info_dark); final EnumMap<Action, Integer> iconsLight = new EnumMap<>(Action.class); iconsLight.put(Action.ACTION_MENU, R.drawable.ic_action_overflow); iconsLight.put(Action.COMMENTS_SWITCH, R.drawable.ic_action_comments_light); iconsLight.put(Action.LINK_SWITCH, mIsProbablyAnImage ? R.drawable.ic_action_image_light : R.drawable.ic_action_link_light); iconsLight.put(Action.UPVOTE, R.drawable.action_upvote_light); iconsLight.put(Action.DOWNVOTE, R.drawable.action_downvote_light); iconsLight.put(Action.SAVE, R.drawable.ic_action_star_filled_light); iconsLight.put(Action.HIDE, R.drawable.ic_action_cross_light); iconsLight.put(Action.REPLY, R.drawable.ic_action_reply_light); iconsLight.put(Action.EXTERNAL, R.drawable.ic_action_external_light); iconsLight.put(Action.SAVE_IMAGE, R.drawable.ic_action_save_light); iconsLight.put(Action.SHARE, R.drawable.ic_action_share_light); iconsLight.put(Action.COPY, R.drawable.ic_action_copy_light); iconsLight.put(Action.USER_PROFILE, R.drawable.ic_action_person_light); iconsLight.put(Action.PROPERTIES, R.drawable.ic_action_info_light); for(final Action action : possibleItems) { if(action == Action.SAVE_IMAGE && !mIsProbablyAnImage) continue; if(itemsPref.contains(action)) { final ImageButton ib = (ImageButton) LayoutInflater.from(activity).inflate(R.layout.flat_image_button, toolbar, false); final int buttonPadding = General.dpToPixels(activity, 14); ib.setPadding(buttonPadding, buttonPadding, buttonPadding, buttonPadding); if(action == Action.UPVOTE && isUpvoted() || action == Action.DOWNVOTE && isDownvoted() || action == Action.SAVE && isSaved() || action == Action.HIDE && isHidden()) { ib.setBackgroundColor(Color.WHITE); ib.setImageResource(iconsLight.get(action)); } else { ib.setImageResource(iconsDark.get(action)); // TODO highlight on click } ib.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { final Action actionToTake; switch(action) { case UPVOTE: actionToTake = isUpvoted() ? Action.UNVOTE : Action.UPVOTE; break; case DOWNVOTE: actionToTake = isDownvoted() ? Action.UNVOTE : Action.DOWNVOTE; break; case SAVE: actionToTake = isSaved() ? Action.UNSAVE : Action.SAVE; break; case HIDE: actionToTake = isHidden() ? Action.UNHIDE : Action.HIDE; break; default: actionToTake = action; break; } onActionMenuItemSelected(RedditPreparedPost.this, activity, actionToTake); overlay.hide(); } }); Action accessibilityAction = action; if(accessibilityAction == Action.UPVOTE && isUpvoted() || accessibilityAction == Action.DOWNVOTE && isDownvoted()) { accessibilityAction = Action.UNVOTE; } if(accessibilityAction == Action.SAVE && isSaved()) { accessibilityAction = Action.UNSAVE; } if(accessibilityAction == Action.HIDE && isHidden()) { accessibilityAction = Action.UNHIDE; } final int textRes = accessibilityAction.descriptionResId; ib.setContentDescription(activity.getString(textRes)); ib.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(final View view) { General.quickToast(activity, textRes); return true; } }); toolbar.addItem(ib); } } return toolbar; } }