aboutsummaryrefslogtreecommitdiff
path: root/WordPress/src/main/java/org/wordpress/android/ui/reader/utils
diff options
context:
space:
mode:
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/reader/utils')
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ImageSizeMap.java86
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderHtmlUtils.java131
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderIframeScanner.java34
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderImageScanner.java117
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderLinkMovementMethod.java103
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderUtils.java221
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderVideoUtils.java163
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderXPostUtils.java74
8 files changed, 929 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ImageSizeMap.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ImageSizeMap.java
new file mode 100644
index 000000000..fdb883bb1
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ImageSizeMap.java
@@ -0,0 +1,86 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.text.TextUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.JSONUtils;
+import org.wordpress.android.util.UrlUtils;
+
+import java.util.HashMap;
+import java.util.Iterator;
+
+/**
+ * hash map of sizes of attachments in a reader post - created from the json "attachments" section
+ * of the post endpoints
+ */
+public class ImageSizeMap extends HashMap<String, ImageSizeMap.ImageSize> {
+ private static final String EMPTY_JSON = "{}";
+ public ImageSizeMap(String jsonString) {
+ if (TextUtils.isEmpty(jsonString) || jsonString.equals(EMPTY_JSON)) {
+ return;
+ }
+
+ try {
+ JSONObject json = new JSONObject(jsonString);
+ Iterator<String> it = json.keys();
+ if (!it.hasNext()) {
+ return;
+ }
+
+ while (it.hasNext()) {
+ JSONObject jsonAttach = json.optJSONObject(it.next());
+ if (jsonAttach != null && JSONUtils.getString(jsonAttach, "mime_type").startsWith("image")) {
+ String normUrl = UrlUtils.normalizeUrl(UrlUtils.removeQuery(JSONUtils.getString(jsonAttach, "URL")));
+ int width = jsonAttach.optInt("width");
+ int height = jsonAttach.optInt("height");
+
+ // chech if data-orig-size is present and use it
+ String originalSize = jsonAttach.optString("data-orig-size", null);
+ if (originalSize != null) {
+ String[] sizes = originalSize.split(",");
+ if (sizes != null && sizes.length == 2) {
+ width = Integer.parseInt(sizes[0]);
+ height = Integer.parseInt(sizes[1]);
+ }
+ }
+
+ this.put(normUrl, new ImageSize(width, height));
+ }
+ }
+ } catch (JSONException e) {
+ AppLog.e(AppLog.T.READER, e);
+ }
+ }
+
+ public ImageSize getImageSize(final String imageUrl) {
+ if (imageUrl == null) {
+ return null;
+ } else {
+ return super.get(UrlUtils.normalizeUrl(UrlUtils.removeQuery(imageUrl)));
+ }
+ }
+
+ public String getLargestImageUrl(int minImageWidth) {
+ String currentImageUrl = null;
+ int currentMaxWidth = minImageWidth;
+ for (Entry<String, ImageSize> attach: this.entrySet()) {
+ if (attach.getValue().width > currentMaxWidth) {
+ currentImageUrl = attach.getKey();
+ currentMaxWidth = attach.getValue().width;
+ }
+ }
+
+ return currentImageUrl;
+ }
+
+ public static class ImageSize {
+ public final int width;
+ public final int height;
+ public ImageSize(int width, int height) {
+ this.width = width;
+ this.height = height;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderHtmlUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderHtmlUtils.java
new file mode 100644
index 000000000..7f980b490
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderHtmlUtils.java
@@ -0,0 +1,131 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.net.Uri;
+
+import org.wordpress.android.util.StringUtils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ReaderHtmlUtils {
+
+ public interface HtmlScannerListener {
+ void onTagFound(String tag, String src);
+ }
+
+ // regex for matching oriwidth attributes in tags
+ private static final Pattern ORIGINAL_WIDTH_ATTR_PATTERN = Pattern.compile(
+ "data-orig-size\\s*=\\s*(?:'|\")(.*?),.*?(?:'|\")",
+ Pattern.DOTALL|Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern ORIGINAL_HEIGHT_ATTR_PATTERN = Pattern.compile(
+ "data-orig-size\\s*=\\s*(?:'|\").*?,(.*?)(?:'|\")",
+ Pattern.DOTALL|Pattern.CASE_INSENSITIVE);
+
+ // regex for matching width attributes in tags
+ private static final Pattern WIDTH_ATTR_PATTERN = Pattern.compile(
+ "width\\s*=\\s*(?:'|\")(.*?)(?:'|\")",
+ Pattern.DOTALL|Pattern.CASE_INSENSITIVE);
+
+ // regex for matching height attributes in tags
+ private static final Pattern HEIGHT_ATTR_PATTERN = Pattern.compile(
+ "height\\s*=\\s*(?:'|\")(.*?)(?:'|\")",
+ Pattern.DOTALL|Pattern.CASE_INSENSITIVE);
+
+ // regex for matching src attributes in tags
+ private static final Pattern SRC_ATTR_PATTERN = Pattern.compile(
+ "src\\s*=\\s*(?:'|\")(.*?)(?:'|\")",
+ Pattern.DOTALL|Pattern.CASE_INSENSITIVE);
+
+
+ /*
+ * returns the integer value from the data-orig-size attribute in the passed html tag
+ */
+ public static int getOriginalWidthAttrValue(final String tag) {
+ if (tag == null) {
+ return 0;
+ }
+
+ Matcher matcher = ORIGINAL_WIDTH_ATTR_PATTERN.matcher(tag);
+ if (matcher.find()) {
+ return StringUtils.stringToInt(matcher.group(1), 0);
+ } else {
+ return 0;
+ }
+ }
+
+ public static int getOriginalHeightAttrValue(final String tag) {
+ if (tag == null) {
+ return 0;
+ }
+
+ Matcher matcher = ORIGINAL_HEIGHT_ATTR_PATTERN.matcher(tag);
+ if (matcher.find()) {
+ return StringUtils.stringToInt(matcher.group(1), 0);
+ } else {
+ return 0;
+ }
+ }
+
+ /*
+ * returns the integer value from the width attribute in the passed html tag
+ */
+ public static int getWidthAttrValue(final String tag) {
+ if (tag == null) {
+ return 0;
+ }
+
+ Matcher matcher = WIDTH_ATTR_PATTERN.matcher(tag);
+ if (matcher.find()) {
+ // remove "width=" and quotes from the result
+ return StringUtils.stringToInt(tag.substring(matcher.start() + 7, matcher.end() - 1), 0);
+ } else {
+ return 0;
+ }
+ }
+
+ public static int getHeightAttrValue(final String tag) {
+ if (tag == null) {
+ return 0;
+ }
+ Matcher matcher = HEIGHT_ATTR_PATTERN.matcher(tag);
+ if (matcher.find()) {
+ return StringUtils.stringToInt(tag.substring(matcher.start() + 8, matcher.end() - 1), 0);
+ } else {
+ return 0;
+ }
+ }
+
+ /*
+ * returns the value from the src attribute in the passed html tag
+ */
+ public static String getSrcAttrValue(final String tag) {
+ if (tag == null) {
+ return null;
+ }
+
+ Matcher matcher = SRC_ATTR_PATTERN.matcher(tag);
+ if (matcher.find()) {
+ // remove "src=" and quotes from the result
+ return tag.substring(matcher.start() + 5, matcher.end() - 1);
+ } else {
+ return null;
+ }
+ }
+
+ /*
+ * returns the integer value of the passed query param in the passed url - returns zero
+ * if the url is invalid, or the param doesn't exist, or the param value could not be
+ * converted to an int
+ */
+ public static int getIntQueryParam(final String url,
+ @SuppressWarnings("SameParameterValue") final String param) {
+ if (url == null
+ || param == null
+ || !url.startsWith("http")
+ || !url.contains(param + "=")) {
+ return 0;
+ }
+ return StringUtils.stringToInt(Uri.parse(url).getQueryParameter(param));
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderIframeScanner.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderIframeScanner.java
new file mode 100644
index 000000000..4b9eb93fb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderIframeScanner.java
@@ -0,0 +1,34 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.text.TextUtils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ReaderIframeScanner {
+
+ private final String mContent;
+
+ private static final Pattern IFRAME_TAG_PATTERN = Pattern.compile(
+ "<iframe(\\s+.*?)(?:src\\s*=\\s*(?:'|\")(.*?)(?:'|\"))(.*?)>",
+ Pattern.DOTALL| Pattern.CASE_INSENSITIVE);
+
+ public ReaderIframeScanner(String contentOfPost) {
+ mContent = contentOfPost;
+ }
+
+ public void beginScan(ReaderHtmlUtils.HtmlScannerListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("HtmlScannerListener is required");
+ }
+
+ Matcher matcher = IFRAME_TAG_PATTERN.matcher(mContent);
+ while (matcher.find()) {
+ String tag = mContent.substring(matcher.start(), matcher.end());
+ String src = ReaderHtmlUtils.getSrcAttrValue(tag);
+ if (!TextUtils.isEmpty(src)) {
+ listener.onTagFound(tag, src);
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderImageScanner.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderImageScanner.java
new file mode 100644
index 000000000..d1708302f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderImageScanner.java
@@ -0,0 +1,117 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.text.TextUtils;
+
+import org.wordpress.android.ui.reader.ReaderConstants;
+import org.wordpress.android.ui.reader.models.ReaderImageList;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ReaderImageScanner {
+ private final String mContent;
+ private final boolean mIsPrivate;
+ private final boolean mContentContainsImages;
+
+ private static final Pattern IMG_TAG_PATTERN = Pattern.compile(
+ "<img(\\s+.*?)(?:src\\s*=\\s*(?:'|\")(.*?)(?:'|\"))(.*?)>",
+ Pattern.DOTALL| Pattern.CASE_INSENSITIVE);
+
+ public ReaderImageScanner(String contentOfPost, boolean isPrivate) {
+ mContent = contentOfPost;
+ mIsPrivate = isPrivate;
+ mContentContainsImages = mContent != null && mContent.contains("<img");
+ }
+
+ /*
+ * start scanning the content for images and notify the passed listener about each one
+ */
+ public void beginScan(ReaderHtmlUtils.HtmlScannerListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("HtmlScannerListener is required");
+ }
+
+ if (!mContentContainsImages) {
+ return;
+ }
+
+ Matcher imgMatcher = IMG_TAG_PATTERN.matcher(mContent);
+ while (imgMatcher.find()) {
+ String imageTag = mContent.substring(imgMatcher.start(), imgMatcher.end());
+ String imageUrl = ReaderHtmlUtils.getSrcAttrValue(imageTag);
+ if (!TextUtils.isEmpty(imageUrl)) {
+ listener.onTagFound(imageTag, imageUrl);
+ }
+ }
+ }
+
+ /*
+ * returns a list of all image URLs in the content above a certain width - pass zero
+ * for the min to include all images regardless of size
+ */
+ public ReaderImageList getImageList() {
+ return getImageList(0);
+ }
+ public ReaderImageList getGalleryImageList() {
+ return getImageList(ReaderConstants.MIN_GALLERY_IMAGE_WIDTH);
+ }
+ public ReaderImageList getImageList(int minImageWidth) {
+ ReaderImageList imageList = new ReaderImageList(mIsPrivate);
+
+ if (!mContentContainsImages) {
+ return imageList;
+ }
+
+ Matcher imgMatcher = IMG_TAG_PATTERN.matcher(mContent);
+ while (imgMatcher.find()) {
+ String imgTag = mContent.substring(imgMatcher.start(), imgMatcher.end());
+ String imageUrl = ReaderHtmlUtils.getSrcAttrValue(imgTag);
+
+ if (minImageWidth == 0) {
+ imageList.addImageUrl(imageUrl);
+ } else {
+ int width = Math.max(ReaderHtmlUtils.getWidthAttrValue(imgTag), ReaderHtmlUtils.getIntQueryParam(imageUrl, "w"));
+ if (width >= minImageWidth) {
+ imageList.addImageUrl(imageUrl);
+ }
+ }
+ }
+
+ return imageList;
+ }
+
+ /*
+ * used when a post doesn't have a featured image assigned, searches post's content
+ * for an image that may be large enough to be suitable as a featured image
+ */
+ public String getLargestImage(int minImageWidth) {
+ if (!mContentContainsImages) {
+ return null;
+ }
+
+ String currentImageUrl = null;
+ int currentMaxWidth = minImageWidth;
+
+ Matcher imgMatcher = IMG_TAG_PATTERN.matcher(mContent);
+ while (imgMatcher.find()) {
+ String imgTag = mContent.substring(imgMatcher.start(), imgMatcher.end());
+ String imageUrl = ReaderHtmlUtils.getSrcAttrValue(imgTag);
+
+ int width = Math.max(ReaderHtmlUtils.getWidthAttrValue(imgTag), ReaderHtmlUtils.getIntQueryParam(imageUrl, "w"));
+ if (width > currentMaxWidth) {
+ currentImageUrl = imageUrl;
+ currentMaxWidth = width;
+ }
+ }
+
+ return currentImageUrl;
+ }
+
+ /*
+ * same as above, but doesn't enforce the max width - will return the first image found if
+ * no images have their width set
+ */
+ public String getLargestImage() {
+ return getLargestImage(-1);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderLinkMovementMethod.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderLinkMovementMethod.java
new file mode 100644
index 000000000..f6dd7659c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderLinkMovementMethod.java
@@ -0,0 +1,103 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.content.ActivityNotFoundException;
+import android.support.annotation.NonNull;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.method.LinkMovementMethod;
+import android.text.style.ImageSpan;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher.PhotoViewerOption;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.StringUtils;
+
+import java.util.EnumSet;
+
+/*
+ * custom LinkMovementMethod which shows photo viewer when an image span is tapped
+ */
+public class ReaderLinkMovementMethod extends LinkMovementMethod {
+ private static ReaderLinkMovementMethod mMovementMethod;
+ private static ReaderLinkMovementMethod mMovementMethodPrivate;
+
+ private final boolean mIsPrivate;
+
+ /*
+ * note that separate instances are returned depending on whether we're showing
+ * content from a private blog
+ */
+ public static ReaderLinkMovementMethod getInstance(boolean isPrivate) {
+ if (isPrivate) {
+ if (mMovementMethodPrivate == null) {
+ mMovementMethodPrivate = new ReaderLinkMovementMethod(true);
+ }
+ return mMovementMethodPrivate;
+ } else {
+ if (mMovementMethod == null) {
+ mMovementMethod = new ReaderLinkMovementMethod(false);
+ }
+ return mMovementMethod;
+ }
+ }
+
+ /*
+ * override MovementMethod.getInstance() to ensure our getInstance(false) is used
+ */
+ @SuppressWarnings("unused")
+ public static ReaderLinkMovementMethod getInstance() {
+ return getInstance(false);
+ }
+
+ private ReaderLinkMovementMethod(boolean isPrivate) {
+ super();
+ mIsPrivate = isPrivate;
+ }
+
+ @Override
+ public boolean onTouchEvent(@NonNull TextView textView,
+ @NonNull Spannable buffer,
+ @NonNull MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ x -= textView.getTotalPaddingLeft();
+ y -= textView.getTotalPaddingTop();
+
+ x += textView.getScrollX();
+ y += textView.getScrollY();
+
+ Layout layout = textView.getLayout();
+ int line = layout.getLineForVertical(y);
+ int off = layout.getOffsetForHorizontal(line, x);
+
+ ImageSpan[] images = buffer.getSpans(off, off, ImageSpan.class);
+ if (images != null && images.length > 0) {
+ EnumSet<PhotoViewerOption> options = EnumSet.noneOf(PhotoViewerOption.class);
+ if (mIsPrivate) {
+ options.add(ReaderActivityLauncher.PhotoViewerOption.IS_PRIVATE_IMAGE);
+ }
+ String imageUrl = StringUtils.notNullStr(images[0].getSource());
+ ReaderActivityLauncher.showReaderPhotoViewer(
+ textView.getContext(),
+ imageUrl,
+ null,
+ textView,
+ options,
+ (int) event.getX(),
+ (int) event.getY());
+ return true;
+ }
+ }
+
+ try {
+ return super.onTouchEvent(textView, buffer, event);
+ } catch (ActivityNotFoundException e) {
+ AppLog.e(AppLog.T.UTILS, e);
+ return false;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderUtils.java
new file mode 100644
index 000000000..bdb79a907
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderUtils.java
@@ -0,0 +1,221 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.text.TextUtils;
+import android.view.View;
+
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.ReaderCommentTable;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.datasets.ReaderTagTable;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagType;
+import org.wordpress.android.util.FormatUtils;
+import org.wordpress.android.util.HtmlUtils;
+import org.wordpress.android.util.PhotonUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.UrlUtils;
+
+public class ReaderUtils {
+
+ public static String getResizedImageUrl(final String imageUrl, int width, int height, boolean isPrivate) {
+ return getResizedImageUrl(imageUrl, width, height, isPrivate, PhotonUtils.Quality.MEDIUM);
+ }
+ public static String getResizedImageUrl(final String imageUrl,
+ int width,
+ int height,
+ boolean isPrivate,
+ PhotonUtils.Quality quality) {
+
+ final String unescapedUrl = HtmlUtils.fastUnescapeHtml(imageUrl);
+ if (isPrivate) {
+ return getPrivateImageForDisplay(unescapedUrl, width, height);
+ } else {
+ return PhotonUtils.getPhotonImageUrl(unescapedUrl, width, height, quality);
+ }
+ }
+
+ /*
+ * use this to request a reduced size image from a private post - images in private posts can't
+ * use photon but these are usually wp images so they support the h= and w= query params
+ */
+ private static String getPrivateImageForDisplay(final String imageUrl, int width, int height) {
+ if (TextUtils.isEmpty(imageUrl)) {
+ return "";
+ }
+
+ final String query;
+ if (width > 0 && height > 0) {
+ query = "?w=" + width + "&h=" + height;
+ } else if (width > 0) {
+ query = "?w=" + width;
+ } else if (height > 0) {
+ query = "?h=" + height;
+ } else {
+ query = "";
+ }
+ // remove the existing query string, add the new one, and make sure the url is https:
+ return UrlUtils.removeQuery(UrlUtils.makeHttps(imageUrl)) + query;
+ }
+
+ /*
+ * returns the passed string formatted for use with our API - see sanitize_title_with_dashes
+ * https://github.com/WordPress/WordPress/blob/master/wp-includes/formatting.php#L1258
+ * http://stackoverflow.com/a/1612015/1673548
+ */
+ public static String sanitizeWithDashes(final String title) {
+ if (title == null) {
+ return "";
+ }
+
+ return title.trim()
+ .replaceAll("&[^\\s]*;", "") // remove html entities
+ .replaceAll("[\\.\\s]+", "-") // replace periods and whitespace with a dash
+ .replaceAll("[^\\p{L}\\p{Nd}\\-]+", "") // remove remaining non-alphanum/non-dash chars (Unicode aware)
+ .replaceAll("--", "-"); // reduce double dashes potentially added above
+ }
+
+ /*
+ * returns the long text to use for a like label ("Liked by 3 people", etc.)
+ */
+ public static String getLongLikeLabelText(Context context, int numLikes, boolean isLikedByCurrentUser) {
+ if (isLikedByCurrentUser) {
+ switch (numLikes) {
+ case 1:
+ return context.getString(R.string.reader_likes_only_you);
+ case 2:
+ return context.getString(R.string.reader_likes_you_and_one);
+ default:
+ String youAndMultiLikes = context.getString(R.string.reader_likes_you_and_multi);
+ return String.format(youAndMultiLikes, numLikes - 1);
+ }
+ } else {
+ if (numLikes == 1) {
+ return context.getString(R.string.reader_likes_one);
+ } else {
+ String likes = context.getString(R.string.reader_likes_multi);
+ return String.format(likes, numLikes);
+ }
+ }
+ }
+
+ /*
+ * short like text ("1 like," "5 likes," etc.)
+ */
+ public static String getShortLikeLabelText(Context context, int numLikes) {
+ switch (numLikes) {
+ case 0:
+ return context.getString(R.string.reader_short_like_count_none);
+ case 1:
+ return context.getString(R.string.reader_short_like_count_one);
+ default:
+ String count = FormatUtils.formatInt(numLikes);
+ return String.format(context.getString(R.string.reader_short_like_count_multi), count);
+ }
+ }
+
+ public static String getShortCommentLabelText(Context context, int numComments) {
+ switch (numComments) {
+ case 1:
+ return context.getString(R.string.reader_short_comment_count_one);
+ default:
+ String count = FormatUtils.formatInt(numComments);
+ return String.format(context.getString(R.string.reader_short_comment_count_multi), count);
+ }
+ }
+
+ /*
+ * returns true if the reader should provide a "logged out" experience - no likes,
+ * comments, or anything else that requires a wp.com account
+ */
+ public static boolean isLoggedOutReader() {
+ return !AccountHelper.isSignedInWordPressDotCom();
+ }
+
+ /*
+ * returns true if a ReaderPost and ReaderComment exist for the passed Ids
+ */
+ public static boolean postAndCommentExists(long blogId, long postId, long commentId) {
+ return ReaderPostTable.postExists(blogId, postId) &&
+ ReaderCommentTable.commentExists(blogId, postId, commentId);
+ }
+
+ /*
+ * used by Discover site picks to add a "Visit [BlogName]" link which shows the
+ * native blog preview for that blog
+ */
+ public static String makeBlogPreviewUrl(long blogId) {
+ return "wordpress://blogpreview?blogId=" + Long.toString(blogId);
+ }
+
+ public static boolean isBlogPreviewUrl(String url) {
+ return (url != null && url.startsWith("wordpress://blogpreview"));
+ }
+
+ public static long getBlogIdFromBlogPreviewUrl(String url) {
+ if (isBlogPreviewUrl(url)) {
+ String strBlogId = Uri.parse(url).getQueryParameter("blogId");
+ return StringUtils.stringToLong(strBlogId);
+ } else {
+ return 0;
+ }
+ }
+
+ /*
+ * returns the passed string prefixed with a "#" if it's non-empty and isn't already
+ * prefixed with a "#"
+ */
+ public static String makeHashTag(String tagName) {
+ if (TextUtils.isEmpty(tagName)) {
+ return "";
+ } else if (tagName.startsWith("#")) {
+ return tagName;
+ } else {
+ return "#" + tagName;
+ }
+ }
+
+ /*
+ * set the background of the passed view to the round ripple drawable - only works on
+ * Lollipop or later, does nothing on earlier Android versions
+ */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public static void setBackgroundToRoundRipple(View view) {
+ if (view != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ view.setBackgroundResource(R.drawable.ripple_oval);
+ }
+ }
+
+ /*
+ * returns a tag object from the passed tag name - first checks for it in the tag db
+ * (so we can also get its title & endpoint), returns a new tag if that fails
+ */
+ public static ReaderTag getTagFromTagName(String tagName, ReaderTagType tagType) {
+ ReaderTag tag = ReaderTagTable.getTag(tagName, tagType);
+ if (tag != null) {
+ return tag;
+ } else {
+ return createTagFromTagName(tagName, tagType);
+ }
+ }
+
+ public static ReaderTag createTagFromTagName(String tagName, ReaderTagType tagType) {
+ String tagSlug = sanitizeWithDashes(tagName).toLowerCase();
+ String tagDisplayName = tagType == ReaderTagType.DEFAULT ? tagName : tagSlug;
+ return new ReaderTag(tagSlug, tagDisplayName, tagName, null, tagType);
+ }
+
+ /*
+ * returns the default tag, which is the one selected by default in the reader when
+ * the user hasn't already chosen one
+ */
+ public static ReaderTag getDefaultTag() {
+ return getTagFromTagName(ReaderTag.TAG_TITLE_DEFAULT, ReaderTagType.DEFAULT);
+ }
+
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderVideoUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderVideoUtils.java
new file mode 100644
index 000000000..94ad8ece0
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderVideoUtils.java
@@ -0,0 +1,163 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+import com.android.volley.Response;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.JsonArrayRequest;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.JSONUtils;
+
+public class ReaderVideoUtils {
+ private ReaderVideoUtils() {
+ throw new AssertionError();
+ }
+
+ /*
+ * returns the url to get the full-size (480x360) thumbnail url for the passed video
+ * see http://www.reelseo.com/youtube-thumbnail-image/ for other sizes
+ */
+ public static String getYouTubeThumbnailUrl(final String videoUrl) {
+ String videoId = getYouTubeVideoId(videoUrl);
+ if (TextUtils.isEmpty(videoId))
+ return "";
+ // note that this *must* use https rather than http - ex: https://img.youtube.com/vi/ClbE019cLNI/0.jpg
+ return "https://img.youtube.com/vi/" + videoId + "/0.jpg";
+ }
+
+ /*
+ * returns true if the passed url is a link to a YouTube video
+ */
+ public static boolean isYouTubeVideoLink(final String link) {
+ return (!TextUtils.isEmpty(getYouTubeVideoId(link)));
+ }
+
+ /*
+ * extract the video id from the passed YouTube url
+ */
+ private static String getYouTubeVideoId(final String link) {
+ if (link==null)
+ return "";
+
+ Uri uri = Uri.parse(link);
+ try {
+ String host = uri.getHost();
+ if (host==null)
+ return "";
+
+ // youtube.com links
+ if (host.equals("youtube.com") || host.equals("www.youtube.com")) {
+ // if link contains "watch" in the path, then the id is in the "v=" query param
+ if (link.contains("watch"))
+ return uri.getQueryParameter("v");
+ // if the link contains "embed" in the path, then the id is the last path segment
+ // ex: https://www.youtube.com/embed/fw3w68YrKwc?version=3&#038;rel=1&#038;
+ if (link.contains("/embed/"))
+ return uri.getLastPathSegment();
+ return "";
+ }
+
+ // youtu.be urls have the videoId as the path - ex: http://youtu.be/pEnXclbO9jg
+ if (host.equals("youtu.be")) {
+ String path = uri.getPath();
+ if (path==null)
+ return "";
+ // remove the leading slash
+ return path.replace("/", "");
+ }
+
+ // YouTube mobile urls include video id in fragment, ex: http://m.youtube.com/?dc=organic&source=mog#/watch?v=t77Vlme_pf8
+ if (host.equals("m.youtube.com")) {
+ String fragment = uri.getFragment();
+ if (fragment==null)
+ return "";
+ int index = fragment.lastIndexOf("v=");
+ if (index!=-1)
+ return fragment.substring(index+2, fragment.length());
+ }
+
+ return "";
+ } catch (UnsupportedOperationException e) {
+ AppLog.e(T.READER, e);
+ return "";
+ } catch (IndexOutOfBoundsException e) {
+ // thrown by substring
+ AppLog.e(T.READER, e);
+ return "";
+ }
+ }
+
+ /*
+ * returns true if the passed url is a link to a Vimeo video
+ */
+ public static boolean isVimeoLink(final String link) {
+ return (!TextUtils.isEmpty(getVimeoVideoId(link)));
+ }
+
+ /*
+ * extract the video id from the passed Vimeo url
+ * ex: http://player.vimeo.com/video/72386905 -> 72386905
+ */
+ private static String getVimeoVideoId(final String link) {
+ if (link==null)
+ return "";
+ if (!link.contains("player.vimeo.com"))
+ return "";
+
+ Uri uri = Uri.parse(link);
+ return uri.getLastPathSegment();
+ }
+
+ /*
+ * unlike YouTube thumbnails, Vimeo thumbnails require network request
+ */
+ public static void requestVimeoThumbnail(final String videoUrl, final VideoThumbnailListener thumbListener) {
+ // useless without a listener
+ if (thumbListener==null)
+ return;
+
+ String id = getVimeoVideoId(videoUrl);
+ if (TextUtils.isEmpty(id)) {
+ thumbListener.onResponse(false, null);
+ return;
+ }
+
+ Response.Listener<JSONArray> listener = new Response.Listener<JSONArray>() {
+ public void onResponse(JSONArray response) {
+ String thumbnailUrl = null;
+ if (response!=null && response.length() > 0) {
+ JSONObject json = response.optJSONObject(0);
+ if (json!=null && json.has("thumbnail_large"))
+ thumbnailUrl = JSONUtils.getString(json, "thumbnail_large");
+ }
+ if (TextUtils.isEmpty(thumbnailUrl)) {
+ thumbListener.onResponse(false, null);
+ } else {
+ thumbListener.onResponse(true, thumbnailUrl);
+ }
+ }
+ };
+ Response.ErrorListener errorListener = new Response.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.READER, volleyError);
+ thumbListener.onResponse(false, null);
+ }
+ };
+
+ String url = "http://vimeo.com/api/v2/video/" + id + ".json";
+ JsonArrayRequest request = new JsonArrayRequest(url, listener, errorListener);
+
+ WordPress.requestQueue.add(request);
+ }
+
+ public interface VideoThumbnailListener {
+ void onResponse(boolean successful, String thumbnailUrl);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderXPostUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderXPostUtils.java
new file mode 100644
index 000000000..75e6908b8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderXPostUtils.java
@@ -0,0 +1,74 @@
+package org.wordpress.android.ui.reader.utils;
+
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.text.Html;
+import android.text.Spanned;
+
+import org.wordpress.android.models.ReaderPost;
+
+/**
+ * Reader cross-post utility routines
+ */
+
+public class ReaderXPostUtils {
+
+ // note that these strings don't need to be localized due to the intended audience
+ private static final String UNKNOWN_SITE = "(unknown)";
+ private static final String FMT_SITE_XPOST = "%1$s cross-posted from %2$s to %3$s";
+ private static final String FMT_COMMENT_XPOST = "%1$s commented on %2$s, cross-posted to %3$s";
+
+ /*
+ * returns the title to display for this xpost, which is simply the post's title
+ * without the "X-post: " prefix
+ */
+ public static String getXPostTitle(@NonNull ReaderPost post) {
+ if (post.getTitle().startsWith("X-post: ")) {
+ return post.getTitle().substring(8);
+ } else {
+ return post.getTitle();
+ }
+ }
+
+ /*
+ * returns the html subtitle to display for this xpost
+ * ex: "Nick cross-posted from +blog1 to +blog2"
+ * ex: "Nick commented on +blog1, cross-posted to +blog2"
+ */
+ public static Spanned getXPostSubtitleHtml(@NonNull ReaderPost post) {
+ boolean isCommentXPost = post.getExcerpt().startsWith("X-comment");
+
+ String name = post.hasAuthorFirstName() ? post.getAuthorFirstName() : post.getAuthorName();
+ String subtitle = String.format(
+ isCommentXPost ? FMT_COMMENT_XPOST : FMT_SITE_XPOST,
+ "<strong>" + name + "</strong>",
+ getFromSiteName(post),
+ getToSiteName(post));
+
+ return Html.fromHtml(subtitle);
+ }
+
+ // origin site name can be extracted from the excerpt,
+ // example excerpt: "<p>X-post from +blog2: I have a request..."
+ private static String getFromSiteName(@NonNull ReaderPost post) {
+ String excerpt = post.getExcerpt();
+ int plusPos = excerpt.indexOf("+");
+ int colonPos = excerpt.indexOf(":", plusPos);
+ if (plusPos > 0 && colonPos > 0) {
+ return excerpt.substring(plusPos, colonPos);
+ } else {
+ return UNKNOWN_SITE;
+ }
+ }
+
+ // destination site name is the subdomain of the blog url
+ private static String getToSiteName(@NonNull ReaderPost post) {
+ Uri uri = Uri.parse(post.getBlogUrl());
+ String domain = uri.getHost();
+ if (domain == null || !domain.contains(".")) {
+ return "+" + UNKNOWN_SITE;
+ }
+
+ return "+" + domain.substring(0, domain.indexOf("."));
+ }
+}