diff options
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/reader/utils')
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&rel=1& + 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(".")); + } +} |