diff --git a/WordPress/src/main/assets/ic_reader_video_overlay.png b/WordPress/src/main/assets/ic_reader_video_overlay.png deleted file mode 120000 index a58cc199483f..000000000000 --- a/WordPress/src/main/assets/ic_reader_video_overlay.png +++ /dev/null @@ -1 +0,0 @@ -../res/drawable-xhdpi/ic_reader_video_overlay.png \ No newline at end of file diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderDatabase.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderDatabase.java index abe69084ec24..e94203ba982d 100644 --- a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderDatabase.java +++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderDatabase.java @@ -19,7 +19,7 @@ */ public class ReaderDatabase extends SQLiteOpenHelper { protected static final String DB_NAME = "wpreader.db"; - private static final int DB_VERSION = 83; + private static final int DB_VERSION = 85; /* * version history @@ -40,6 +40,8 @@ public class ReaderDatabase extends SQLiteOpenHelper { * 81 - added image_url to tbl_blog_info * 82 - added idx_posts_timestamp to tbl_posts * 83 - removed tag_list from tbl_posts + * 84 - added tbl_attachments + * 85 - removed tbl_attachments, added attachments_json to tbl_posts */ /* diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java index 6bc66eafdee9..c1d9bc01414e 100644 --- a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java +++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderPostTable.java @@ -47,7 +47,8 @@ public class ReaderPostTable { + "primary_tag," // 26 + "secondary_tag," // 27 + "is_likes_enabled," // 28 - + "is_sharing_enabled"; // 29 + + "is_sharing_enabled," // 29 + + "attachments_json"; // 30 protected static void createTables(SQLiteDatabase db) { @@ -81,6 +82,7 @@ protected static void createTables(SQLiteDatabase db) { + " secondary_tag TEXT," + " is_likes_enabled INTEGER DEFAULT 0," + " is_sharing_enabled INTEGER DEFAULT 0," + + " attachments_json TEXT," + " PRIMARY KEY (post_id, blog_id)" + ")"); db.execSQL("CREATE INDEX idx_posts_timestamp ON tbl_posts(timestamp)"); @@ -373,7 +375,7 @@ public static void addOrUpdatePosts(final ReaderTag tag, ReaderPostList posts) { SQLiteStatement stmtPosts = db.compileStatement( "INSERT OR REPLACE INTO tbl_posts (" + COLUMN_NAMES - + ") VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22,?23,?24,?25,?26,?27,?28,?29)"); + + ") VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22,?23,?24,?25,?26,?27,?28,?29,?30)"); SQLiteStatement stmtTags = db.compileStatement( "INSERT OR REPLACE INTO tbl_post_tags (post_id, blog_id, pseudo_id, tag_name, tag_type) VALUES (?1,?2,?3,?4,?5)"); @@ -410,6 +412,7 @@ public static void addOrUpdatePosts(final ReaderTag tag, ReaderPostList posts) { stmtPosts.bindString(27, post.getSecondaryTag()); stmtPosts.bindLong (28, SqlUtils.boolToSql(post.isLikesEnabled)); stmtPosts.bindLong (29, SqlUtils.boolToSql(post.isSharingEnabled)); + stmtPosts.bindString(30, post.getAttachmentsJson()); stmtPosts.execute(); } @@ -560,6 +563,8 @@ private static class PostColumnIndexes { private final int idx_is_likes_enabled; private final int idx_is_sharing_enabled; + private final int idx_attachments_json; + private PostColumnIndexes(Cursor c) { if (c == null) throw new IllegalArgumentException("PostColumnIndexes > null cursor"); @@ -600,6 +605,8 @@ private PostColumnIndexes(Cursor c) { idx_is_likes_enabled = c.getColumnIndex("is_likes_enabled"); idx_is_sharing_enabled = c.getColumnIndex("is_sharing_enabled"); + + idx_attachments_json = c.getColumnIndex("attachments_json"); } } @@ -652,6 +659,8 @@ private static ReaderPost getPostFromCursor(Cursor c, PostColumnIndexes cols) { post.isLikesEnabled = SqlUtils.sqlToBool(c.getInt(cols.idx_is_likes_enabled)); post.isSharingEnabled = SqlUtils.sqlToBool(c.getInt(cols.idx_is_sharing_enabled)); + post.setAttachmentsJson(c.getString(cols.idx_attachments_json)); + return post; } } diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java index 0290b69484ce..313298d7ff28 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java +++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java @@ -52,6 +52,8 @@ public class ReaderPost { public boolean isLikesEnabled; public boolean isSharingEnabled; // currently unused + private String attachmentsJson; + public static ReaderPost fromJson(JSONObject json) { if (json == null) { throw new IllegalArgumentException("null json post"); @@ -155,6 +157,12 @@ public static ReaderPost fromJson(JSONObject json) { // parse the tags section assignTagsFromJson(post, json.optJSONObject("tags")); + // parse the attachments + JSONObject jsonAttachments = json.optJSONObject("attachments"); + if (jsonAttachments != null) { + post.attachmentsJson = jsonAttachments.toString(); + } + // the single-post sites/$site/posts/$post endpoint returns all site metadata // under meta/data/site (assuming ?meta=site was added to the request) JSONObject jsonSite = JSONUtil.getJSONChild(json, "meta/data/site"); @@ -343,8 +351,6 @@ public void setPublished(String published) { this.published = StringUtils.notNullStr(published); } - // -------------------------------------------------------------------------------------------- - public String getPrimaryTag() { return StringUtils.notNullStr(primaryTag); } @@ -368,7 +374,17 @@ public void setSecondaryTag(String tagName) { } } - // -------------------------------------------------------------------------------------------- + /* + * attachments are stored as the actual JSON to avoid having a separate table for + * them, may need to revisit this if/when attachments become more important + */ + public String getAttachmentsJson() { + return StringUtils.notNullStr(attachmentsJson); + } + public void setAttachmentsJson(String json) { + attachmentsJson = StringUtils.notNullStr(json); + } + public boolean hasText() { return !TextUtils.isEmpty(text); @@ -434,12 +450,8 @@ public String getFeaturedImageForDisplay(int width, int height) { if (featuredImageForDisplay == null) { if (!hasFeaturedImage()) { featuredImageForDisplay = ""; - } else if (isPrivate) { - // images in private posts can't use photon, so handle separately - featuredImageForDisplay = ReaderUtils.getPrivateImageForDisplay(featuredImage, width, height); } else { - // not private, so set to correctly sized photon url - featuredImageForDisplay = PhotonUtils.getPhotonImageUrl(featuredImage, width, height); + featuredImageForDisplay = ReaderUtils.getResizedImageUrl(featuredImage, width, height, isPrivate); } } return featuredImageForDisplay; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoView.java index e50c12ea19f5..34a4437b1585 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoView.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoView.java @@ -21,7 +21,6 @@ import org.wordpress.android.ui.reader.utils.ReaderUtils; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.DisplayUtils; -import org.wordpress.android.util.PhotonUtils; import uk.co.senab.photoview.PhotoViewAttacher; @@ -77,13 +76,8 @@ public void setImageUrl(String imageUrl, boolean isPrivate, PhotoViewListener listener) { int loResWidth = (int) (hiResWidth * 0.10f); - if (isPrivate) { - mLoResImageUrl = ReaderUtils.getPrivateImageForDisplay(imageUrl, loResWidth, 0); - mHiResImageUrl = ReaderUtils.getPrivateImageForDisplay(imageUrl, hiResWidth, 0); - } else { - mLoResImageUrl = PhotonUtils.getPhotonImageUrl(imageUrl, loResWidth, 0); - mHiResImageUrl = PhotonUtils.getPhotonImageUrl(imageUrl, hiResWidth, 0); - } + mLoResImageUrl = ReaderUtils.getResizedImageUrl(imageUrl, loResWidth, 0, isPrivate); + mHiResImageUrl = ReaderUtils.getResizedImageUrl(imageUrl, hiResWidth, 0, isPrivate); mPhotoViewListener = listener; loadLoResImage(); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerActivity.java index 6451112c7c36..814c195a77ef 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerActivity.java @@ -16,6 +16,7 @@ import org.wordpress.android.ui.reader.ReaderViewPagerTransformer.TransformType; import org.wordpress.android.ui.reader.models.ReaderImageList; import org.wordpress.android.ui.reader.utils.ReaderImageScanner; +import org.wordpress.android.util.AppLog; import javax.annotation.Nonnull; @@ -68,26 +69,35 @@ public void onPageSelected(int position) { } private void loadImageList() { - new Thread() { - @Override - public void run() { - // parse list of images from content that was (optionally) passed to - // this activity, and make sure the list includes the passed url - ReaderImageScanner scanner = new ReaderImageScanner(mContent, mIsPrivate); - final ReaderImageList imageList = scanner.getImageList(); - if (!TextUtils.isEmpty(mInitialImageUrl) && !imageList.hasImageUrl(mInitialImageUrl)) { - imageList.addImageUrl(0, mInitialImageUrl); - } - runOnUiThread(new Runnable() { - @Override - public void run() { - if (!isFinishing()) { - setImageList(imageList, mInitialImageUrl); - } - } - }); + // content will be empty unless this was called by ReaderPostDetailFragment to show + // a list of images in a post (in which case, it's the content of the post) + if (TextUtils.isEmpty(mContent)) { + final ReaderImageList imageList = new ReaderImageList(mIsPrivate); + if (!TextUtils.isEmpty(mInitialImageUrl)) { + imageList.add(mInitialImageUrl); } - }.start(); + setImageList(imageList, mInitialImageUrl); + } else { + // parse images from content and make sure the list includes the passed url + new Thread() { + @Override + public void run() { + final ReaderImageList imageList = new ReaderImageScanner(mContent, mIsPrivate).getImageList(); + if (!TextUtils.isEmpty(mInitialImageUrl) && !imageList.hasImageUrl(mInitialImageUrl)) { + AppLog.w(AppLog.T.READER, "reader photo viewer > initial image not in list"); + imageList.addImageUrl(0, mInitialImageUrl); + } + runOnUiThread(new Runnable() { + @Override + public void run() { + if (!isFinishing()) { + setImageList(imageList, mInitialImageUrl); + } + } + }); + } + }.start(); + } } private void setImageList(ReaderImageList imageList, String initialImageUrl) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.java index c7e44f8742e7..0d77842d9dc4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.java @@ -3,10 +3,7 @@ import android.app.ActionBar; import android.app.Activity; import android.app.Fragment; -import android.content.Context; import android.content.Intent; -import android.content.res.Resources; -import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; @@ -36,7 +33,6 @@ import org.wordpress.android.datasets.ReaderPostTable; import org.wordpress.android.models.ReaderComment; import org.wordpress.android.models.ReaderPost; -import org.wordpress.android.ui.WPActionBarActivity; import org.wordpress.android.ui.reader.ReaderActivityLauncher.OpenUrlType; import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType; import org.wordpress.android.ui.reader.ReaderWebView.ReaderCustomViewListener; @@ -53,11 +49,7 @@ import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.DateTimeUtils; -import org.wordpress.android.util.DisplayUtils; import org.wordpress.android.util.EditTextUtils; -import org.wordpress.android.util.HtmlUtils; -import org.wordpress.android.util.PhotonUtils; -import org.wordpress.android.util.StringUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.UrlUtils; import org.wordpress.android.widgets.WPListView; @@ -77,7 +69,7 @@ public class ReaderPostDetailFragment extends Fragment private long mPostId; private long mBlogId; private ReaderPost mPost; - + private ReaderPostRenderer mRenderer; private ReaderPostListType mPostListType; private ViewGroup mLayoutIcons; @@ -102,7 +94,7 @@ public class ReaderPostDetailFragment extends Fragment private int mPrevScrollState = SCROLL_STATE_IDLE; private Parcelable mListState; - private ResourceVars mResourceVars; + private ReaderResourceVars mResourceVars; private ReaderUtils.FullScreenListener mFullScreenListener; @@ -130,60 +122,6 @@ public static ReaderPostDetailFragment newInstance(long blogId, return fragment; } - /* - * class which holds all resource-based variables used by this fragment - */ - private static class ResourceVars { - final int displayWidth; - final int actionBarHeight; - final int likeAvatarSize; - final int videoOverlaySize; - - final int marginLarge; - final int marginSmall; - final int marginExtraSmall; - final int listMarginWidth; - final int fullSizeImageWidth; - - final int colorGreyExtraLight; - final int mediumAnimTime; - - final String linkColorStr; - final String greyLightStr; - final String greyExtraLightStr; - - private ResourceVars(Context context) { - Resources resources = context.getResources(); - - displayWidth = DisplayUtils.getDisplayPixelWidth(context); - actionBarHeight = DisplayUtils.getActionBarHeight(context); - likeAvatarSize = resources.getDimensionPixelSize(R.dimen.avatar_sz_small); - videoOverlaySize = resources.getDimensionPixelSize(R.dimen.reader_video_overlay_size); - - marginLarge = resources.getDimensionPixelSize(R.dimen.margin_large); - marginSmall = resources.getDimensionPixelSize(R.dimen.margin_small); - marginExtraSmall = resources.getDimensionPixelSize(R.dimen.margin_extra_small); - listMarginWidth = resources.getDimensionPixelOffset(R.dimen.reader_list_margin); - - colorGreyExtraLight = resources.getColor(R.color.grey_extra_light); - mediumAnimTime = resources.getInteger(android.R.integer.config_mediumAnimTime); - - linkColorStr = HtmlUtils.colorResToHtmlColor(context, R.color.reader_hyperlink); - greyLightStr = HtmlUtils.colorResToHtmlColor(context, R.color.grey_light); - greyExtraLightStr = HtmlUtils.colorResToHtmlColor(context, R.color.grey_extra_light); - - int imageWidth = displayWidth - (listMarginWidth * 2); - boolean hasStaticMenuDrawer = - (context instanceof WPActionBarActivity) - && (((WPActionBarActivity) context).isStaticMenuDrawer()); - if (hasStaticMenuDrawer) { - int drawerWidth = resources.getDimensionPixelOffset(R.dimen.menu_drawer_width); - imageWidth -= drawerWidth; - } - fullSizeImageWidth = imageWidth; - } - } - /* * adapter containing comments for this post */ @@ -286,7 +224,7 @@ public void setArguments(Bundle args) { public void onAttach(Activity activity) { super.onAttach(activity); - mResourceVars = new ResourceVars(activity); + mResourceVars = new ReaderResourceVars(activity); if (activity instanceof ReaderUtils.FullScreenListener) { mFullScreenListener = (ReaderUtils.FullScreenListener) activity; @@ -300,14 +238,13 @@ public void onAttach(Activity activity) { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.reader_fragment_post_detail, container, false); - final Context context = container.getContext(); // locate & init listView mListView = (WPListView) view.findViewById(android.R.id.list); if (isFullScreenSupported()) { mListView.setOnScrollDirectionListener(this); mListView.setOnScrollListener(this); - ReaderUtils.addListViewHeader(mListView, mResourceVars.actionBarHeight); + ReaderUtils.addListViewHeader(mListView, mResourceVars.actionBarHeightPx); } // add post detail as header to listView - must be done before setting adapter @@ -1006,33 +943,7 @@ private void refreshFollowed() { ReaderUtils.showFollowStatus(txtFollow, isFollowed); } - /* - * creates formatted div for passed video with passed (optional) thumbnail - */ - private static final String OVERLAY_IMG = "file:///android_asset/ic_reader_video_overlay.png"; - - private String makeVideoDiv(String videoUrl, String thumbnailUrl) { - if (TextUtils.isEmpty(videoUrl)) { - return ""; - } - - // sometimes we get src values like "//player.vimeo.com/video/70534716" - prefix these with http: - if (videoUrl.startsWith("//")) { - videoUrl = "http:" + videoUrl; - } - - int overlaySz = mResourceVars.videoOverlaySize / 2; - if (TextUtils.isEmpty(thumbnailUrl)) { - return String.format("
", videoUrl, overlaySz, overlaySz, OVERLAY_IMG); - } else { - return "
" - + String.format("", videoUrl, thumbnailUrl) - + String.format("", videoUrl, OVERLAY_IMG, overlaySz, overlaySz) - + "
"; - } - } - - private boolean showPhotoViewer(String imageUrl, View source, int startX, int startY) { + private boolean showPhotoViewer(String imageUrl, View sourceView, int startX, int startY) { if (!isAdded() || TextUtils.isEmpty(imageUrl)) { return false; } @@ -1042,13 +953,14 @@ private boolean showPhotoViewer(String imageUrl, View source, int startX, int st return false; } + String postContent = (mRenderer != null ? mRenderer.getRenderedHtml() : null); boolean isPrivatePost = (mPost != null && mPost.isPrivate); ReaderActivityLauncher.showReaderPhotoViewer( getActivity(), imageUrl, - getPostContent(), - source, + postContent, + sourceView, isPrivatePost, startX, startY); @@ -1056,134 +968,6 @@ private boolean showPhotoViewer(String imageUrl, View source, int startX, int st return true; } - /* - * returns the basic content of the post tweaked for use here - */ - private String getPostContent() { - if (mPost == null) { - return ""; - } else if (mPost.hasText()) { - // some content (such as Vimeo embeds) don't have "http:" before links, correct this here - String content = mPost.getText().replace("src=\"//", "src=\"http://"); - // insert video div before content if this is a VideoPress post (video otherwise won't appear) - if (mPost.isVideoPress) { - content = makeVideoDiv(mPost.getFeaturedVideo(), mPost.getFeaturedImage()) + content; - } else if (mPost.hasFeaturedImage() && !PhotonUtils.isMshotsUrl(mPost.getFeaturedImage())) { - // if the post has a featured image other than an mshot that's not in the content, - // add it to the content - Uri uri = Uri.parse(mPost.getFeaturedImage()); - String path = StringUtils.notNullStr(uri.getLastPathSegment()); - if (!content.contains(path)) { - AppLog.d(T.READER, "reader post detail > added featured image to content"); - content = String.format("

", mPost.getFeaturedImage()) - + content; - } - } - return content; - } else if (mPost.hasFeaturedImage()) { - // some photo blogs have posts with empty content but still have a featured image, so - // use the featured image as the content - return String.format("

", mPost.getFeaturedImage()); - } else { - return ""; - } - } - - /* - * returns the full content, including CSS, that will be shown in the WebView for this post - */ - private String getPostContentForWebView(Context context) { - if (mPost == null || context == null) { - return ""; - } - - String content = getPostContent(); - - StringBuilder sbHtml = new StringBuilder(""); - - // title isn't strictly necessary, but source is invalid html5 without one - sbHtml.append("Reader Post"); - - // https://developers.google.com/chrome/mobile/docs/webview/pixelperfect - sbHtml.append(""); - - // use "Open Sans" Google font - sbHtml.append(""); - - sbHtml.append("") - .append(content) - .append(""); - - return sbHtml.toString(); - } - /* * called when the post doesn't exist in local db, need to get it from server */ @@ -1217,16 +1001,10 @@ private void postFailed() { } } - /* - * javascript should only be enabled for wp blogs (not external feeds) - */ - private boolean canEnableJavaScript() { - return (mPost != null && mPost.isWP()); - } - private void showPost() { if (mIsPostTaskRunning) { AppLog.w(T.READER, "reader post detail > show post task already running"); + return; } new ShowPostTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); @@ -1250,8 +1028,6 @@ private class ShowPostTask extends AsyncTask { WPNetworkImageView imgAvatar; ViewGroup layoutDetailHeader; - String postHtml; - @Override protected void onPreExecute() { mIsPostTaskRunning = true; @@ -1288,8 +1064,6 @@ protected Boolean doInBackground(Void... params) { layoutDetailHeader = (ViewGroup) container.findViewById(R.id.layout_detail_header); - postHtml = getPostContentForWebView(container.getContext()); - return true; } @@ -1306,17 +1080,15 @@ protected void onPostExecute(Boolean result) { // the server if it hasn't already been requested if (!mHasAlreadyRequestedPost) { mHasAlreadyRequestedPost = true; + AppLog.i(T.READER, "reader post detail > post not found, requesting it"); requestPost(); } return; } - // enable JavaScript in the webView if it's safe to do so - mReaderWebView.getSettings().setJavaScriptEnabled(canEnableJavaScript()); - - // IMPORTANT: use loadDataWithBaseURL() since loadData() may fail - // https://code.google.com/p/android/issues/detail?id=4401 - mReaderWebView.loadDataWithBaseURL(null, postHtml, "text/html", "UTF-8", null); + // render the post in the webView + mRenderer = new ReaderPostRenderer(mReaderWebView, mPost); + mRenderer.beginRender(); txtTitle.setText(mPost.hasTitle() ? mPost.getTitle() : getString(R.string.reader_untitled_post)); @@ -1349,13 +1121,13 @@ public void onClick(View view) { if (mPost.hasPostAvatar()) { imgAvatar.setImageUrl(mPost.getPostAvatarForDisplay( - mResourceVars.likeAvatarSize), WPNetworkImageView.ImageType.AVATAR); + mResourceVars.likeAvatarSizePx), WPNetworkImageView.ImageType.AVATAR); imgAvatar.setVisibility(View.VISIBLE); } else { imgAvatar.setVisibility(View.GONE); } - // hide blog name, avatar & follow button if this fragment was shown from blog preview + // hide header if this fragment was shown from blog preview if (isBlogPreview()) { layoutDetailHeader.setVisibility(View.GONE); } @@ -1432,7 +1204,6 @@ public void onClick(View view) { } } - /* * called by the web view when the content finishes loading - likes & comments aren't displayed * until this is triggered, to avoid having them appear before the webView content diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java index 641140877d03..2d0884d392c6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java @@ -17,7 +17,6 @@ import org.wordpress.android.datasets.ReaderTagTable; import org.wordpress.android.models.ReaderTag; import org.wordpress.android.models.ReaderTagType; -import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.ui.WPActionBarActivity; import org.wordpress.android.ui.accounts.WPComLoginActivity; import org.wordpress.android.ui.prefs.AppPrefs; @@ -30,6 +29,7 @@ import org.wordpress.android.ui.reader.services.ReaderUpdateService.UpdateTask; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; +import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.StringUtils; import java.util.EnumSet; @@ -412,6 +412,11 @@ public void onReceive(Context context, Intent intent) { } else if (listFragment.getPostListType() == ReaderTypes.ReaderPostListType.TAG_FOLLOWED) { // list fragment is viewing followed tags, tell it to refresh the list of tags listFragment.refreshTags(); + // update the current tag if the list fragment is empty - this will happen if + // the tag table was previously empty (ie: first run) + if (listFragment.isPostAdapterEmpty()) { + listFragment.updateCurrentTag(); + } } } else if (action.equals(ReaderUpdateService.ACTION_FOLLOWED_BLOGS_CHANGED)) { // followed blogs have changed, so remove posts in blogs that are diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java index d85df3f9af96..832605d939c3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java @@ -872,6 +872,10 @@ public void onActionResult(boolean succeeded) { ReaderPostActions.requestPostsForBlog(mCurrentBlogId, mCurrentBlogUrl, updateAction, listener); } + void updateCurrentTag() { + updatePostsWithTag(getCurrentTag(), RequestDataAction.LOAD_NEWER, ReaderTypes.RefreshType.AUTOMATIC); + } + /* * get latest posts for this tag from the server */ diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.java index 6d883f454118..7d2f500dfa7a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.java @@ -25,7 +25,6 @@ import org.wordpress.android.models.ReaderPost; import org.wordpress.android.models.ReaderPostList; import org.wordpress.android.models.ReaderTag; -import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.ui.reader.ReaderAnim.AnimationEndListener; import org.wordpress.android.ui.reader.ReaderAnim.Duration; import org.wordpress.android.ui.reader.ReaderPostPagerEndFragment.EndFragmentType; @@ -40,6 +39,7 @@ import org.wordpress.android.ui.reader.models.ReaderBlogIdPostIdList; import org.wordpress.android.ui.reader.utils.ReaderUtils; import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; import javax.annotation.Nonnull; @@ -232,15 +232,12 @@ private void loadPosts(final long blogId, new Thread() { @Override public void run() { - final ReaderPostList postList; + final ReaderBlogIdPostIdList idList; if (mIsSinglePostView) { - ReaderPost post = ReaderPostTable.getPost(blogId, postId); - if (post == null) { - return; - } - postList = new ReaderPostList(); - postList.add(post); + idList = new ReaderBlogIdPostIdList(); + idList.add(new ReaderBlogIdPostId(blogId, postId)); } else { + final ReaderPostList postList; int maxPosts = ReaderConstants.READER_MAX_POSTS_TO_DISPLAY; switch (getPostListType()) { case TAG_FOLLOWED: @@ -253,22 +250,22 @@ public void run() { default: return; } + idList = postList.getBlogIdPostIdList(); } - final ReaderBlogIdPostIdList ids = postList.getBlogIdPostIdList(); final int currentPosition = mViewPager.getCurrentItem(); final int newPosition; if (gotoNext) { - newPosition = ids.indexOf(blogId, postId) + 1; + newPosition = idList.indexOf(blogId, postId) + 1; } else { - newPosition = ids.indexOf(blogId, postId); + newPosition = idList.indexOf(blogId, postId); } runOnUiThread(new Runnable() { @Override public void run() { mPagerAdapter = new PostPagerAdapter(getFragmentManager()); - mPagerAdapter.showPosts(ids); + mPagerAdapter.showPosts(idList); mViewPager.setAdapter(mPagerAdapter); if (mPagerAdapter.isValidPosition(newPosition)) { mViewPager.setCurrentItem(newPosition); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java new file mode 100644 index 000000000000..f7c41302226e --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java @@ -0,0 +1,427 @@ +package org.wordpress.android.ui.reader; + +import android.annotation.SuppressLint; +import android.net.Uri; +import android.os.Handler; +import android.text.TextUtils; + +import org.json.JSONException; +import org.json.JSONObject; +import org.wordpress.android.WordPress; +import org.wordpress.android.models.ReaderPost; +import org.wordpress.android.ui.reader.utils.ReaderImageScanner; +import org.wordpress.android.ui.reader.utils.ReaderUtils; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.DisplayUtils; +import org.wordpress.android.util.JSONUtil; +import org.wordpress.android.util.PhotonUtils; +import org.wordpress.android.util.StringUtils; +import org.wordpress.android.util.UrlUtils; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Iterator; + +/** + * generates and displays the HTML for post detail content - main purpose is to assign the + * height/width attributes on image tags to (1) avoid the webView resizing as images are + * loaded, and (2) avoid requesting images at a size larger than the display + * + * important to note that displayed images rely on dp rather than px sizes due to the + * fact that WebView "converts CSS pixel values to density-independent pixel values" + * http://developer.android.com/guide/webapps/targeting.html + */ +class ReaderPostRenderer { + + private final ReaderResourceVars mResourceVars; + private final ReaderPost mPost; + private final int mMinFullSizeWidthDp; + private final int mMinMidSizeWidthDp; + private final WeakReference mWeakWebView; + + private StringBuilder mRenderBuilder; + private String mRenderedHtml; + private ImageSizeMap mAttachmentSizes; + + @SuppressLint("SetJavaScriptEnabled") + ReaderPostRenderer(ReaderWebView webView, ReaderPost post) { + if (webView == null) { + throw new IllegalArgumentException("ReaderPostRenderer requires a webView"); + } + if (post == null) { + throw new IllegalArgumentException("ReaderPostRenderer requires a post"); + } + + mPost = post; + mWeakWebView = new WeakReference(webView); + mResourceVars = new ReaderResourceVars(webView.getContext()); + + mMinFullSizeWidthDp = pxToDp(mResourceVars.fullSizeImageWidthPx / 3); + mMinMidSizeWidthDp = mMinFullSizeWidthDp / 2; + + // enable JavaScript in the webView if it's safe to do so, otherwise videos + // and other embedded content won't work + webView.getSettings().setJavaScriptEnabled(canEnableJavaScript()); + } + + void beginRender() { + mRenderBuilder = new StringBuilder(getPostContent()); + + // start image scanner to find images so we can replace them with ones that have h/w set + final Handler handler = new Handler(); + new Thread() { + @Override + public void run() { + ReaderImageScanner.ImageScanListener imageListener = new ReaderImageScanner.ImageScanListener() { + @Override + public void onImageFound(String imageTag, String imageUrl, int start, int end) { + replaceImageTag(imageTag, imageUrl); + } + + @Override + public void onScanCompleted() { + final String htmlContent = formatPostContentForWebView(mRenderBuilder.toString()); + mRenderBuilder = null; + handler.post(new Runnable() { + @Override + public void run() { + renderHtmlContent(htmlContent); + } + }); + } + }; + ReaderImageScanner scanner = new ReaderImageScanner(mRenderBuilder.toString(), mPost.isPrivate); + scanner.beginScan(imageListener); + } + }.start(); + } + + /* + * called once the content is ready to be rendered in the webView + */ + private void renderHtmlContent(final String htmlContent) { + mRenderedHtml = htmlContent; + + // make sure webView is still valid (containing fragment may have been detached) + ReaderWebView webView = mWeakWebView.get(); + if (webView == null) { + AppLog.w(AppLog.T.READER, "reader renderer > null webView"); + return; + } + + // IMPORTANT: use loadDataWithBaseURL() since loadData() may fail + // https://code.google.com/p/android/issues/detail?id=4401 + // also important to use null as the baseUrl since onPageFinished + // doesn't appear to fire when it's set to an actual url + webView.loadDataWithBaseURL(null, htmlContent, "text/html", "UTF-8", null); + } + + /* + * called when image scanner finds an image, tries to replace the image tag with one that + * has height & width attributes set correctly for the current display, if that fails + * replaces it with one that has our 'size-none' class + */ + private void replaceImageTag(final String imageTag, final String imageUrl) { + ImageSize origSize = getImageSize(imageTag, imageUrl); + boolean hasWidth = (origSize != null && origSize.width > 0); + boolean isFullSize = hasWidth && (origSize.width >= mMinFullSizeWidthDp); + boolean isMidSize = hasWidth + && (origSize.width >= mMinMidSizeWidthDp) + && (origSize.width < mMinFullSizeWidthDp); + + final String newImageTag; + if (isFullSize) { + newImageTag = makeFullSizeImageTag(imageUrl, origSize.width, origSize.height); + } else if (isMidSize) { + newImageTag = makeImageTag(imageUrl, origSize.width, origSize.height, "size-medium"); + } else if (hasWidth) { + newImageTag = makeImageTag(imageUrl, origSize.width, origSize.height, "size-none"); + } else { + newImageTag = ""; + } + + int start = mRenderBuilder.indexOf(imageTag); + if (start == -1) { + AppLog.w(AppLog.T.READER, "reader renderer > image not found in builder"); + return; + } + + mRenderBuilder.replace(start, start + imageTag.length(), newImageTag); + } + + private String makeImageTag(final String imageUrl, int width, int height, final String imageClass) { + String newImageUrl = ReaderUtils.getResizedImageUrl(imageUrl, width, height, mPost.isPrivate); + if (height > 0) { + return new StringBuilder("") + .toString(); + } else { + return new StringBuilder("") + .toString(); + } + } + + private String makeFullSizeImageTag(final String imageUrl, int width, int height) { + int newWidth; + int newHeight; + if (width > 0 && height > 0) { + if (height > width) { + newHeight = mResourceVars.fullSizeImageWidthPx; + float ratio = ((float) width / (float) height); + newWidth = (int) (newHeight * ratio); + } else { + float ratio = ((float) height / (float) width); + newWidth = mResourceVars.fullSizeImageWidthPx; + newHeight = (int) (newWidth * ratio); + } + } else { + newWidth = mResourceVars.fullSizeImageWidthPx; + newHeight = 0; + } + + return makeImageTag(imageUrl, newWidth, newHeight, "size-full"); + } + + /* + * returns true if the post has a featured image and there are no images in the + * post's content - when this is the case, the featured image is inserted at + * the top of the content + */ + private boolean shouldAddFeaturedImage() { + return mPost.hasFeaturedImage() + && !mPost.getText().contains(" added featured image"); + content = getFeaturedImageHtml() + content; + } + return content; + } + + /* + * returns the HTML that was last rendered, will be null prior to rendering + */ + String getRenderedHtml() { + return mRenderedHtml; + } + + /* + * returns the HTML to use when inserting a featured image into the rendered content + */ + private String getFeaturedImageHtml() { + String imageUrl = ReaderUtils.getResizedImageUrl( + mPost.getFeaturedImage(), + mResourceVars.fullSizeImageWidthPx, + mResourceVars.featuredImageHeightPx, + mPost.isPrivate); + + return ""; + } + + /* + * returns the full content, including CSS, that will be shown in the WebView for this post + */ + private String formatPostContentForWebView(final String content) { + StringBuilder sbHtml = new StringBuilder(""); + + // title isn't necessary, but it's invalid html5 without one + sbHtml.append("Reader Post") + + // https://developers.google.com/chrome/mobile/docs/webview/pixelperfect + .append("") + + // use "Open Sans" Google font + .append("") + + .append("") + .append("") + .append(content) + .append(""); + + return sbHtml.toString(); + } + + private ImageSize getImageSize(final String imageTag, final String imageUrl) { + ImageSize size = getImageSizeFromAttachments(imageUrl); + if (size == null && imageUrl.contains("?")) { + size = getImageSizeFromQueryParams(imageUrl); + } + if (size == null && imageTag.contains("width=")) { + size = getImageSizeFromAttributes(imageTag); + } + return size; + } + + private ImageSize getImageSizeFromAttachments(final String imageUrl) { + if (mAttachmentSizes == null) { + mAttachmentSizes = new ImageSizeMap(mPost.getAttachmentsJson()); + } + return mAttachmentSizes.getAttachmentSize(imageUrl); + } + + private ImageSize getImageSizeFromQueryParams(final String imageUrl) { + if (imageUrl.contains("w=")) { + Uri uri = Uri.parse(imageUrl.replace("&", "&")); + return new ImageSize( + StringUtils.stringToInt(uri.getQueryParameter("w")), + StringUtils.stringToInt(uri.getQueryParameter("h"))); + } else if (imageUrl.contains("resize=")) { + Uri uri = Uri.parse(imageUrl.replace("&", "&")); + String param = uri.getQueryParameter("resize"); + if (param != null) { + String[] sizes = param.split(","); + if (sizes.length == 2) { + return new ImageSize( + StringUtils.stringToInt(sizes[0]), + StringUtils.stringToInt(sizes[1])); + } + } + } + + return null; + } + + private ImageSize getImageSizeFromAttributes(final String imageTag) { + return new ImageSize( + ReaderImageScanner.getWidthAttrValue(imageTag), + ReaderImageScanner.getHeightAttrValue(imageTag)); + } + + private int pxToDp(int px) { + if (px == 0) { + return 0; + } + return DisplayUtils.pxToDp(WordPress.getContext(), px); + } + + /* + * javascript should only be enabled for wp blogs (not external feeds) + */ + private boolean canEnableJavaScript() { + return mPost.isWP(); + } + + /* + * hash map of sizes of attachments in the current post for quick lookup - created from + * the json "attachments" section of the post endpoints + */ + class ImageSizeMap extends HashMap { + ImageSizeMap(String jsonString) { + if (TextUtils.isEmpty(jsonString)) { + return; + } + + try { + JSONObject json = new JSONObject(jsonString); + Iterator it = json.keys(); + if (!it.hasNext()) { + return; + } + + while (it.hasNext()) { + JSONObject jsonAttach = json.optJSONObject(it.next()); + if (jsonAttach != null && JSONUtil.getString(jsonAttach, "mime_type").startsWith("image")) { + String normUrl = UrlUtils.normalizeUrl(UrlUtils.removeQuery(JSONUtil.getString(json, "URL"))); + int width = jsonAttach.optInt("width"); + int height = jsonAttach.optInt("height"); + this.put(normUrl, new ImageSize(width, height)); + } + } + } catch (JSONException e) { + AppLog.e(AppLog.T.READER, e); + } + } + + ImageSize getAttachmentSize(final String imageUrl) { + if (imageUrl == null) { + return null; + } else { + return super.get(UrlUtils.normalizeUrl(UrlUtils.removeQuery(imageUrl))); + } + } + } + + static class ImageSize { + final int width; + final int height; + ImageSize(int width, int height) { + this.width = width; + this.height = height; + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderResourceVars.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderResourceVars.java new file mode 100644 index 000000000000..03ac68be85b4 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderResourceVars.java @@ -0,0 +1,75 @@ +package org.wordpress.android.ui.reader; + +import android.content.Context; +import android.content.res.Resources; + +import org.wordpress.android.R; +import org.wordpress.android.ui.WPActionBarActivity; +import org.wordpress.android.util.DisplayUtils; +import org.wordpress.android.util.HtmlUtils; + +/* + * class which holds all resource-based variables used when rendering post detail + */ +class ReaderResourceVars { + final int displayWidthPx; + final int actionBarHeightPx; + final int likeAvatarSizePx; + + final int marginLargePx; + final int marginSmallPx; + final int marginExtraSmallPx; + final int listMarginWidthPx; + + final int fullSizeImageWidthPx; + final int featuredImageHeightPx; + + final int videoWidthPx; + final int videoHeightPx; + + final int colorGreyExtraLight; + final int mediumAnimTime; + + final String linkColorStr; + final String greyMediumDarkStr; + final String greyLightStr; + final String greyExtraLightStr; + + ReaderResourceVars(Context context) { + Resources resources = context.getResources(); + + displayWidthPx = DisplayUtils.getDisplayPixelWidth(context); + actionBarHeightPx = DisplayUtils.getActionBarHeight(context); + likeAvatarSizePx = resources.getDimensionPixelSize(R.dimen.avatar_sz_small); + featuredImageHeightPx = resources.getDimensionPixelSize(R.dimen.reader_featured_image_height); + + marginLargePx = resources.getDimensionPixelSize(R.dimen.margin_large); + marginSmallPx = resources.getDimensionPixelSize(R.dimen.margin_small); + marginExtraSmallPx = resources.getDimensionPixelSize(R.dimen.margin_extra_small); + listMarginWidthPx = resources.getDimensionPixelOffset(R.dimen.reader_list_margin); + + colorGreyExtraLight = resources.getColor(R.color.grey_extra_light); + mediumAnimTime = resources.getInteger(android.R.integer.config_mediumAnimTime); + + linkColorStr = HtmlUtils.colorResToHtmlColor(context, R.color.reader_hyperlink); + greyMediumDarkStr = HtmlUtils.colorResToHtmlColor(context, R.color.grey_medium_dark); + greyLightStr = HtmlUtils.colorResToHtmlColor(context, R.color.grey_light); + greyExtraLightStr = HtmlUtils.colorResToHtmlColor(context, R.color.grey_extra_light); + + // full-size image width must take list margin and padding into account + int listPadding = resources.getDimensionPixelOffset(R.dimen.margin_large); + int imageWidth = displayWidthPx - (listMarginWidthPx * 2) - (listPadding * 2); + boolean hasStaticMenuDrawer = + (context instanceof WPActionBarActivity) + && (((WPActionBarActivity) context).isStaticMenuDrawer()); + if (hasStaticMenuDrawer) { + int drawerWidth = resources.getDimensionPixelOffset(R.dimen.menu_drawer_width); + imageWidth -= drawerWidth; + } + fullSizeImageWidthPx = imageWidth; + + // 16:9 ratio (YouTube standard) + videoWidthPx = fullSizeImageWidthPx - (marginLargePx * 2); + videoHeightPx = (int) (videoWidthPx * 0.5625f); + } +} 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 index 3549e6f2930d..08173de03623 100644 --- 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 @@ -1,6 +1,7 @@ package org.wordpress.android.ui.reader.utils; import android.net.Uri; +import android.text.TextUtils; import org.wordpress.android.ui.reader.models.ReaderImageList; import org.wordpress.android.util.PhotonUtils; @@ -12,13 +13,18 @@ public class ReaderImageScanner { + public static interface ImageScanListener { + public void onImageFound(String imageTag, String imageUrl, int start, int end); + public void onScanCompleted(); + } private final String mContent; private final boolean mIsPrivate; + private final boolean mContentContainsImages; private static final int MIN_FEATURED_IMAGE_WIDTH = 500; // regex for matching img tags in html content private static final Pattern IMG_TAG_PATTERN = Pattern.compile( - "", + "", Pattern.DOTALL| Pattern.CASE_INSENSITIVE); // regex for matching width attributes in tags @@ -26,6 +32,11 @@ public class ReaderImageScanner { "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*(?:'|\")(.*?)(?:'|\")", @@ -34,15 +45,41 @@ public class ReaderImageScanner { public ReaderImageScanner(String contentOfPost, boolean isPrivate) { mContent = contentOfPost; mIsPrivate = isPrivate; + mContentContainsImages = mContent != null && mContent.contains(" 0 && height > 0) { - query = String.format("?w=%d&h=%d", width, height); + query = "?w=" + width + "&h=" + height; } else if (width > 0) { - query = String.format("?w=%d", width); + query = "?w=" + width; } else if (height > 0) { - query = String.format("?h=%d", height); + query = "?h=" + height; } else { query = ""; } diff --git a/WordPress/src/main/res/layout/reader_listitem_post_detail.xml b/WordPress/src/main/res/layout/reader_listitem_post_detail.xml index 3642f6314220..e346246e84d9 100644 --- a/WordPress/src/main/res/layout/reader_listitem_post_detail.xml +++ b/WordPress/src/main/res/layout/reader_listitem_post_detail.xml @@ -40,7 +40,7 @@ android:layout_marginBottom="@dimen/margin_small" android:ellipsize="end" android:maxLines="2" - android:textColor="@color/reader_hyperlink" + android:textColor="@color/grey_dark" android:textSize="@dimen/text_sz_medium" tools:text="text_blog_name" /> diff --git a/WordPress/src/main/res/values/dimens.xml b/WordPress/src/main/res/values/dimens.xml index 943037c2ca23..7fee4b237e47 100644 --- a/WordPress/src/main/res/values/dimens.xml +++ b/WordPress/src/main/res/values/dimens.xml @@ -68,7 +68,6 @@ 340dp @dimen/reader_featured_image_height_default - 48dp 12dp @dimen/margin_large diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java index 497d756ee377..bf275e56abdc 100644 --- a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java +++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/PhotonUtils.java @@ -24,7 +24,7 @@ public static String fixAvatar(final String imageUrl, int avatarSz) { return getPhotonImageUrl(imageUrl, avatarSz, avatarSz); // remove all other params, then add query string for size and "mystery man" default - return UrlUtils.removeQuery(imageUrl) + String.format("?s=%d&d=mm", avatarSz); + return UrlUtils.removeQuery(imageUrl) + "?s=" + avatarSz + "&d=mm"; } /* @@ -64,18 +64,18 @@ public static String getPhotonImageUrl(String imageUrl, int width, int height) { // see http://wp.tutsplus.com/tutorials/how-to-generate-website-screenshots-for-your-wordpress-site/ // ex: http://s.wordpress.com/mshots/v1/http%3A%2F%2Fnickbradbury.com?w=600 if (isMshotsUrl(imageUrl)) { - return imageUrl + String.format("?w=%d&h=%d", width, height); + return imageUrl + "?w=" + width + "&h=" + height; } // if both width & height are passed use the "resize" param, use only "w" or "h" if just // one of them is set, otherwise no query string final String query; if (width > 0 && height > 0) { - query = String.format("?resize=%d,%d", width, height); + query = "?resize=" + width + "," + height; } else if (width > 0) { - query = String.format("?w=%d", width); + query = "?w=" + width; } else if (height > 0) { - query = String.format("?h=%d", height); + query = "?h=" + height; } else { query = ""; }