package org.wordpress.android.ui.reader;
import android.app.Activity;
import android.app.Fragment;
import android.content.Intent;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.wordpress.android.R;
import org.wordpress.android.WordPress;
import org.wordpress.android.analytics.AnalyticsTracker;
import org.wordpress.android.datasets.ReaderLikeTable;
import org.wordpress.android.datasets.ReaderPostTable;
import org.wordpress.android.fluxc.store.AccountStore;
import org.wordpress.android.fluxc.store.SiteStore;
import org.wordpress.android.models.ReaderPost;
import org.wordpress.android.models.ReaderPostDiscoverData;
import org.wordpress.android.ui.main.WPMainActivity;
import org.wordpress.android.ui.reader.ReaderActivityLauncher.OpenUrlType;
import org.wordpress.android.ui.reader.ReaderActivityLauncher.PhotoViewerOption;
import org.wordpress.android.ui.reader.ReaderInterfaces.AutoHideToolbarListener;
import org.wordpress.android.ui.reader.ReaderPostPagerActivity.DirectOperation;
import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType;
import org.wordpress.android.ui.reader.actions.ReaderActions;
import org.wordpress.android.ui.reader.actions.ReaderPostActions;
import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId;
import org.wordpress.android.ui.reader.models.ReaderSimplePostList;
import org.wordpress.android.ui.reader.utils.ReaderUtils;
import org.wordpress.android.ui.reader.utils.ReaderVideoUtils;
import org.wordpress.android.ui.reader.views.ReaderIconCountView;
import org.wordpress.android.ui.reader.views.ReaderLikingUsersView;
import org.wordpress.android.ui.reader.views.ReaderPostDetailHeaderView;
import org.wordpress.android.ui.reader.views.ReaderSimplePostContainerView;
import org.wordpress.android.ui.reader.views.ReaderSimplePostView;
import org.wordpress.android.ui.reader.views.ReaderTagStrip;
import org.wordpress.android.ui.reader.views.ReaderWebView;
import org.wordpress.android.ui.reader.views.ReaderWebView.ReaderCustomViewListener;
import org.wordpress.android.ui.reader.views.ReaderWebView.ReaderWebViewPageFinishedListener;
import org.wordpress.android.ui.reader.views.ReaderWebView.ReaderWebViewUrlClickListener;
import org.wordpress.android.util.AnalyticsUtils;
import org.wordpress.android.util.AniUtils;
import org.wordpress.android.util.AppLog;
import org.wordpress.android.util.AppLog.T;
import org.wordpress.android.util.DateTimeUtils;
import org.wordpress.android.util.NetworkUtils;
import org.wordpress.android.util.ToastUtils;
import org.wordpress.android.util.UrlUtils;
import org.wordpress.android.util.WPUrlUtils;
import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
import org.wordpress.android.widgets.WPScrollView;
import org.wordpress.android.widgets.WPScrollView.ScrollDirectionListener;
import org.wordpress.android.widgets.WPTextView;
import org.wordpress.passcodelock.AppLockManager;
import java.util.EnumSet;
import javax.inject.Inject;
import de.greenrobot.event.EventBus;
public class ReaderPostDetailFragment extends Fragment
implements WPMainActivity.OnActivityBackPressedListener,
ScrollDirectionListener,
ReaderCustomViewListener,
ReaderWebViewPageFinishedListener,
ReaderWebViewUrlClickListener {
private long mPostId;
private long mBlogId;
private DirectOperation mDirectOperation;
private int mCommentId;
private boolean mIsFeed;
private String mInterceptedUri;
private ReaderPost mPost;
private ReaderPostRenderer mRenderer;
private ReaderPostListType mPostListType;
private final ReaderPostHistory mPostHistory = new ReaderPostHistory();
private SwipeToRefreshHelper mSwipeToRefreshHelper;
private WPScrollView mScrollView;
private ViewGroup mLayoutFooter;
private ReaderWebView mReaderWebView;
private ReaderLikingUsersView mLikingUsersView;
private View mLikingUsersDivider;
private View mLikingUsersLabel;
private WPTextView mSignInButton;
private ReaderSimplePostContainerView mGlobalRelatedPostsView;
private ReaderSimplePostContainerView mLocalRelatedPostsView;
private boolean mPostSlugsResolutionUnderway;
private boolean mHasAlreadyUpdatedPost;
private boolean mHasAlreadyRequestedPost;
private boolean mIsWebViewPaused;
private boolean mIsRelatedPost;
private boolean mHasTrackedGlobalRelatedPosts;
private boolean mHasTrackedLocalRelatedPosts;
private int mToolbarHeight;
private String mErrorMessage;
private boolean mIsToolbarShowing = true;
private AutoHideToolbarListener mAutoHideToolbarListener;
// min scroll distance before toggling toolbar
private static final float MIN_SCROLL_DISTANCE_Y = 10;
@Inject AccountStore mAccountStore;
@Inject SiteStore mSiteStore;
public static ReaderPostDetailFragment newInstance(long blogId, long postId) {
return newInstance(false, blogId, postId, null, 0, false, null, null, false);
}
public static ReaderPostDetailFragment newInstance(boolean isFeed,
long blogId,
long postId,
DirectOperation directOperation,
int commentId,
boolean isRelatedPost,
String interceptedUri,
ReaderPostListType postListType,
boolean postSlugsResolutionUnderway) {
AppLog.d(T.READER, "reader post detail > newInstance");
Bundle args = new Bundle();
args.putBoolean(ReaderConstants.ARG_IS_FEED, isFeed);
args.putLong(ReaderConstants.ARG_BLOG_ID, blogId);
args.putLong(ReaderConstants.ARG_POST_ID, postId);
args.putBoolean(ReaderConstants.ARG_IS_RELATED_POST, isRelatedPost);
args.putSerializable(ReaderConstants.ARG_DIRECT_OPERATION, directOperation);
args.putInt(ReaderConstants.ARG_COMMENT_ID, commentId);
args.putString(ReaderConstants.ARG_INTERCEPTED_URI, interceptedUri);
if (postListType != null) {
args.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, postListType);
}
args.putBoolean(ReaderConstants.KEY_POST_SLUGS_RESOLUTION_UNDERWAY, postSlugsResolutionUnderway);
ReaderPostDetailFragment fragment = new ReaderPostDetailFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((WordPress) getActivity().getApplication()).component().inject(this);
if (savedInstanceState != null) {
mPostHistory.restoreInstance(savedInstanceState);
}
}
@Override
public void setArguments(Bundle args) {
super.setArguments(args);
if (args != null) {
mIsFeed = args.getBoolean(ReaderConstants.ARG_IS_FEED);
mBlogId = args.getLong(ReaderConstants.ARG_BLOG_ID);
mPostId = args.getLong(ReaderConstants.ARG_POST_ID);
mDirectOperation = (DirectOperation) args.getSerializable(ReaderConstants.ARG_DIRECT_OPERATION);
mCommentId = args.getInt(ReaderConstants.ARG_COMMENT_ID);
mIsRelatedPost = args.getBoolean(ReaderConstants.ARG_IS_RELATED_POST);
mInterceptedUri = args.getString(ReaderConstants.ARG_INTERCEPTED_URI);
if (args.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) {
mPostListType = (ReaderPostListType) args.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE);
}
mPostSlugsResolutionUnderway = args.getBoolean(ReaderConstants.KEY_POST_SLUGS_RESOLUTION_UNDERWAY);
}
}
@SuppressWarnings("deprecation")
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (activity instanceof AutoHideToolbarListener) {
mAutoHideToolbarListener = (AutoHideToolbarListener) activity;
}
mToolbarHeight = activity.getResources().getDimensionPixelSize(R.dimen.toolbar_height);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.reader_fragment_post_detail, container, false);
CustomSwipeRefreshLayout swipeRefreshLayout = (CustomSwipeRefreshLayout) view.findViewById(R.id.swipe_to_refresh);
//this fragment hides/shows toolbar with scrolling, which messes up ptr animation position
//so we have to set it manually
int swipeToRefreshOffset = getResources().getDimensionPixelSize(R.dimen.toolbar_content_offset);
swipeRefreshLayout.setProgressViewOffset(false, 0, swipeToRefreshOffset);
mSwipeToRefreshHelper = new SwipeToRefreshHelper(getActivity(), swipeRefreshLayout, new SwipeToRefreshHelper.RefreshListener() {
@Override
public void onRefreshStarted() {
if (!isAdded()) {
return;
}
updatePost();
}
});
mScrollView = (WPScrollView) view.findViewById(R.id.scroll_view_reader);
mScrollView.setScrollDirectionListener(this);
mLayoutFooter = (ViewGroup) view.findViewById(R.id.layout_post_detail_footer);
mLikingUsersView = (ReaderLikingUsersView) view.findViewById(R.id.layout_liking_users_view);
mLikingUsersDivider = view.findViewById(R.id.layout_liking_users_divider);
mLikingUsersLabel = view.findViewById(R.id.text_liking_users_label);
// setup the ReaderWebView
mReaderWebView = (ReaderWebView) view.findViewById(R.id.reader_webview);
mReaderWebView.setCustomViewListener(this);
mReaderWebView.setUrlClickListener(this);
mReaderWebView.setPageFinishedListener(this);
// hide footer and scrollView until the post is loaded
mLayoutFooter.setVisibility(View.INVISIBLE);
mScrollView.setVisibility(View.INVISIBLE);
View relatedPostsContainer = view.findViewById(R.id.container_related_posts);
mGlobalRelatedPostsView = (ReaderSimplePostContainerView) relatedPostsContainer.findViewById(R.id.related_posts_view_global);
mLocalRelatedPostsView = (ReaderSimplePostContainerView) relatedPostsContainer.findViewById(R.id.related_posts_view_local);
mSignInButton = (WPTextView) view.findViewById(R.id.nux_sign_in_button);
mSignInButton.setOnClickListener(mSignInClickListener);
final ProgressBar progress = (ProgressBar) view.findViewById(R.id.progress_loading);
if (mPostSlugsResolutionUnderway) {
progress.setVisibility(View.VISIBLE);
}
showPost();
return view;
}
private final View.OnClickListener mSignInClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
EventBus.getDefault().post(new ReaderEvents.DoSignIn());
}
};
@Override
public void onDestroy() {
super.onDestroy();
if (mReaderWebView != null) {
mReaderWebView.destroy();
}
}
private boolean hasPost() {
return (mPost != null);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
menu.clear();
inflater.inflate(R.menu.reader_detail, menu);
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
// browse & share require the post to have a URL (some feed-based posts don't have one)
boolean postHasUrl = hasPost() && mPost.hasUrl();
MenuItem mnuBrowse = menu.findItem(R.id.menu_browse);
if (mnuBrowse != null) {
mnuBrowse.setVisible(postHasUrl || (mInterceptedUri != null));
}
MenuItem mnuShare = menu.findItem(R.id.menu_share);
if (mnuShare != null) {
mnuShare.setVisible(postHasUrl);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int i = item.getItemId();
if (i == R.id.menu_browse) {
if (hasPost()) {
ReaderActivityLauncher.openUrl(getActivity(), mPost.getUrl(), OpenUrlType.EXTERNAL);
} else if (mInterceptedUri != null) {
AnalyticsUtils.trackWithInterceptedUri(AnalyticsTracker.Stat.DEEP_LINKED_FALLBACK, mInterceptedUri);
ReaderActivityLauncher.openUrl(getActivity(), mInterceptedUri, OpenUrlType.EXTERNAL);
getActivity().finish();
}
return true;
} else if (i == R.id.menu_share) {
AnalyticsTracker.track(AnalyticsTracker.Stat.SHARED_ITEM);
sharePage();
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
private ReaderPostListType getPostListType() {
return (mPostListType != null ? mPostListType : ReaderTypes.DEFAULT_POST_LIST_TYPE);
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putBoolean(ReaderConstants.ARG_IS_FEED, mIsFeed);
outState.putLong(ReaderConstants.ARG_BLOG_ID, mBlogId);
outState.putLong(ReaderConstants.ARG_POST_ID, mPostId);
outState.putSerializable(ReaderConstants.ARG_DIRECT_OPERATION, mDirectOperation);
outState.putInt(ReaderConstants.ARG_COMMENT_ID, mCommentId);
outState.putBoolean(ReaderConstants.ARG_IS_RELATED_POST, mIsRelatedPost);
outState.putString(ReaderConstants.ARG_INTERCEPTED_URI, mInterceptedUri);
outState.putBoolean(ReaderConstants.KEY_POST_SLUGS_RESOLUTION_UNDERWAY, mPostSlugsResolutionUnderway);
outState.putBoolean(ReaderConstants.KEY_ALREADY_UPDATED, mHasAlreadyUpdatedPost);
outState.putBoolean(ReaderConstants.KEY_ALREADY_REQUESTED, mHasAlreadyRequestedPost);
outState.putBoolean(ReaderConstants.KEY_ALREADY_TRACKED_GLOBAL_RELATED_POSTS, mHasTrackedGlobalRelatedPosts);
outState.putBoolean(ReaderConstants.KEY_ALREADY_TRACKED_LOCAL_RELATED_POSTS, mHasTrackedLocalRelatedPosts);
outState.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, getPostListType());
mPostHistory.saveInstance(outState);
if (!TextUtils.isEmpty(mErrorMessage)) {
outState.putString(ReaderConstants.KEY_ERROR_MESSAGE, mErrorMessage);
}
super.onSaveInstanceState(outState);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(true);
restoreState(savedInstanceState);
}
private void restoreState(Bundle savedInstanceState) {
if (savedInstanceState != null) {
mIsFeed = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_FEED);
mBlogId = savedInstanceState.getLong(ReaderConstants.ARG_BLOG_ID);
mPostId = savedInstanceState.getLong(ReaderConstants.ARG_POST_ID);
mDirectOperation = (DirectOperation) savedInstanceState
.getSerializable(ReaderConstants.ARG_DIRECT_OPERATION);
mCommentId = savedInstanceState.getInt(ReaderConstants.ARG_COMMENT_ID);
mIsRelatedPost = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_RELATED_POST);
mInterceptedUri = savedInstanceState.getString(ReaderConstants.ARG_INTERCEPTED_URI);
mPostSlugsResolutionUnderway = savedInstanceState.getBoolean(ReaderConstants.KEY_POST_SLUGS_RESOLUTION_UNDERWAY);
mHasAlreadyUpdatedPost = savedInstanceState.getBoolean(ReaderConstants.KEY_ALREADY_UPDATED);
mHasAlreadyRequestedPost = savedInstanceState.getBoolean(ReaderConstants.KEY_ALREADY_REQUESTED);
mHasTrackedGlobalRelatedPosts = savedInstanceState.getBoolean(ReaderConstants.KEY_ALREADY_TRACKED_GLOBAL_RELATED_POSTS);
mHasTrackedLocalRelatedPosts = savedInstanceState.getBoolean(ReaderConstants.KEY_ALREADY_TRACKED_LOCAL_RELATED_POSTS);
if (savedInstanceState.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) {
mPostListType = (ReaderPostListType) savedInstanceState.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE);
}
if (savedInstanceState.containsKey(ReaderConstants.KEY_ERROR_MESSAGE)) {
mErrorMessage = savedInstanceState.getString(ReaderConstants.KEY_ERROR_MESSAGE);
}
}
}
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
/*
* called by the activity when user hits the back button - returns true if the back button
* is handled here and should be ignored by the activity
*/
@Override
public boolean onActivityBackPressed() {
return goBackInPostHistory();
}
/*
* changes the like on the passed post
*/
private void togglePostLike() {
if (hasPost()) {
setPostLike(!mPost.isLikedByCurrentUser);
}
}
/*
* changes the like on the passed post
*/
private void setPostLike(boolean isAskingToLike) {
if (!isAdded() || !hasPost() || !NetworkUtils.checkConnection(getActivity())) {
return;
}
if (isAskingToLike != ReaderPostTable.isPostLikedByCurrentUser(mPost)) {
ReaderIconCountView likeCount = (ReaderIconCountView) getView().findViewById(R.id.count_likes);
likeCount.setSelected(isAskingToLike);
ReaderAnim.animateLikeButton(likeCount.getImageView(), isAskingToLike);
boolean success = ReaderPostActions.performLikeAction(mPost, isAskingToLike,
mAccountStore.getAccount().getUserId());
if (!success) {
likeCount.setSelected(!isAskingToLike);
return;
}
// get the post again since it has changed, then refresh to show changes
mPost = ReaderPostTable.getBlogPost(mPost.blogId, mPost.postId, false);
refreshLikes();
refreshIconCounts();
}
if (isAskingToLike) {
AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_ARTICLE_LIKED, mPost);
} else {
AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_ARTICLE_UNLIKED, mPost);
}
}
/**
* display the standard Android share chooser to share this post
*/
private void sharePage() {
if (!isAdded() || !hasPost()) {
return;
}
String url = (mPost.hasShortUrl() ? mPost.getShortUrl() : mPost.getUrl());
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, url);
intent.putExtra(Intent.EXTRA_SUBJECT, mPost.getTitle());
try {
startActivity(Intent.createChooser(intent, getString(R.string.share_link)));
} catch (android.content.ActivityNotFoundException ex) {
ToastUtils.showToast(getActivity(), R.string.reader_toast_err_share_intent);
}
}
/*
* replace the current post with the passed one
*/
private void replacePost(long blogId, long postId, boolean clearCommentOperation) {
mIsFeed = false;
mBlogId = blogId;
mPostId = postId;
if (clearCommentOperation) {
mDirectOperation = null;
mCommentId = 0;
}
mHasAlreadyRequestedPost = false;
mHasAlreadyUpdatedPost = false;
mHasTrackedGlobalRelatedPosts = false;
mHasTrackedLocalRelatedPosts = false;
// hide views that would show info for the previous post - these will be re-displayed
// with the correct info once the new post loads
mGlobalRelatedPostsView.setVisibility(View.GONE);
mLocalRelatedPostsView.setVisibility(View.GONE);
mLikingUsersView.setVisibility(View.GONE);
mLikingUsersDivider.setVisibility(View.GONE);
mLikingUsersLabel.setVisibility(View.GONE);
// clear the webView - otherwise it will remain scrolled to where the user scrolled to
mReaderWebView.clearContent();
// make sure the toolbar and footer are showing
showToolbar(true);
showFooter(true);
// now show the passed post
showPost();
}
/*
* request posts related to the current one - only available for wp.com
*/
private void requestRelatedPosts() {
if (hasPost() && mPost.isWP()) {
ReaderPostActions.requestRelatedPosts(mPost);
}
}
/*
* related posts were retrieved
*/
@SuppressWarnings("unused")
public void onEventMainThread(ReaderEvents.RelatedPostsUpdated event) {
if (!isAdded() || !hasPost()) return;
// make sure this event is for the current post
if (event.getSourcePostId() == mPost.postId && event.getSourceSiteId() == mPost.blogId) {
if (event.hasLocalRelatedPosts()) {
showRelatedPosts(event.getLocalRelatedPosts(), false);
}
if (event.hasGlobalRelatedPosts()) {
showRelatedPosts(event.getGlobalRelatedPosts(), true);
}
}
}
/*
* show the passed list of related posts - can be either global (related posts from
* across wp.com) or local (related posts from the same site as the current post)
*/
private void showRelatedPosts(@NonNull ReaderSimplePostList relatedPosts, final boolean isGlobal) {
// tapping a related post should open the related post detail
ReaderSimplePostView.OnSimplePostClickListener listener = new ReaderSimplePostView.OnSimplePostClickListener() {
@Override
public void onSimplePostClick(View v, long siteId, long postId) {
showRelatedPostDetail(siteId, postId, isGlobal);
}
};
// different container views for global/local related posts
ReaderSimplePostContainerView relatedPostsView = isGlobal ? mGlobalRelatedPostsView : mLocalRelatedPostsView;
relatedPostsView.showPosts(relatedPosts, mPost.getBlogName(), isGlobal, listener);
// fade in this related posts view
if (relatedPostsView.getVisibility() != View.VISIBLE) {
AniUtils.fadeIn(relatedPostsView, AniUtils.Duration.MEDIUM);
}
trackRelatedPostsIfShowing();
}
/*
* track that related posts have loaded and are scrolled into view if we haven't
* already tracked it
*/
private void trackRelatedPostsIfShowing() {
if (!mHasTrackedGlobalRelatedPosts && isVisibleAndScrolledIntoView(mGlobalRelatedPostsView)) {
mHasTrackedGlobalRelatedPosts = true;
AppLog.d(T.READER, "reader post detail > global related posts rendered");
mGlobalRelatedPostsView.trackRailcarRender();
}
if (!mHasTrackedLocalRelatedPosts && isVisibleAndScrolledIntoView(mLocalRelatedPostsView)) {
mHasTrackedLocalRelatedPosts = true;
AppLog.d(T.READER, "reader post detail > local related posts rendered");
mLocalRelatedPostsView.trackRailcarRender();
}
}
/*
* returns True if the passed view is visible and has been scrolled into view - assumes
* that the view is a child of mScrollView
*/
private boolean isVisibleAndScrolledIntoView(View view) {
if (view != null && view.getVisibility() == View.VISIBLE) {
Rect scrollBounds = new Rect();
mScrollView.getHitRect(scrollBounds);
return view.getLocalVisibleRect(scrollBounds);
}
return false;
}
/*
* user clicked a single related post - if we're already viewing a related post, add it to the
* history stack so the user can back-button through the history - otherwise start a new detail
* activity for this related post
*/
private void showRelatedPostDetail(long blogId, long postId, boolean isGlobal) {
AnalyticsTracker.Stat stat = isGlobal
? AnalyticsTracker.Stat.READER_GLOBAL_RELATED_POST_CLICKED
: AnalyticsTracker.Stat.READER_LOCAL_RELATED_POST_CLICKED;
AnalyticsUtils.trackWithReaderPostDetails(stat, blogId, postId);
if (mIsRelatedPost) {
mPostHistory.push(new ReaderBlogIdPostId(mPost.blogId, mPost.postId));
replacePost(blogId, postId, true);
} else {
ReaderActivityLauncher.showReaderPostDetail(
getActivity(),
false,
blogId,
postId,
null,
0,
true,
null);
}
}
/*
* if the fragment is maintaining a backstack of posts, navigate to the previous one
*/
protected boolean goBackInPostHistory() {
if (!mPostHistory.isEmpty()) {
ReaderBlogIdPostId ids = mPostHistory.pop();
replacePost(ids.getBlogId(), ids.getPostId(), true);
return true;
} else {
return false;
}
}
/*
* get the latest version of this post
*/
private void updatePost() {
if (!hasPost() || !mPost.isWP()) {
setRefreshing(false);
return;
}
final int numLikesBefore = ReaderLikeTable.getNumLikesForPost(mPost);
ReaderActions.UpdateResultListener resultListener = new ReaderActions.UpdateResultListener() {
@Override
public void onUpdateResult(ReaderActions.UpdateResult result) {
if (!isAdded()) {
return;
}
// if the post has changed, reload it from the db and update the like/comment counts
if (result.isNewOrChanged()) {
mPost = ReaderPostTable.getBlogPost(mPost.blogId, mPost.postId, false);
refreshIconCounts();
}
// refresh likes if necessary - done regardless of whether the post has changed
// since it's possible likes weren't stored until the post was updated
if (result != ReaderActions.UpdateResult.FAILED
&& numLikesBefore != ReaderLikeTable.getNumLikesForPost(mPost)) {
refreshLikes();
}
setRefreshing(false);
if (mDirectOperation != null && mDirectOperation == DirectOperation.POST_LIKE) {
doLikePost();
}
}
};
ReaderPostActions.updatePost(mPost, resultListener);
}
private void refreshIconCounts() {
if (!isAdded() || !hasPost() || !canShowFooter()) {
return;
}
final ReaderIconCountView countLikes = (ReaderIconCountView) getView().findViewById(R.id.count_likes);
final ReaderIconCountView countComments = (ReaderIconCountView) getView().findViewById(R.id.count_comments);
if (canShowCommentCount()) {
countComments.setCount(mPost.numReplies);
countComments.setVisibility(View.VISIBLE);
countComments.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ReaderActivityLauncher.showReaderComments(getActivity(), mPost.blogId, mPost.postId);
}
});
} else {
countComments.setVisibility(View.INVISIBLE);
countComments.setOnClickListener(null);
}
if (canShowLikeCount()) {
countLikes.setCount(mPost.numLikes);
countLikes.setVisibility(View.VISIBLE);
countLikes.setSelected(mPost.isLikedByCurrentUser);
if (!mAccountStore.hasAccessToken()) {
countLikes.setEnabled(false);
} else if (mPost.canLikePost()) {
countLikes.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
togglePostLike();
}
});
}
// if we know refreshLikes() is going to show the liking users, force liking user
// views to take up space right now
if (mPost.numLikes > 0 && mLikingUsersView.getVisibility() == View.GONE) {
mLikingUsersView.setVisibility(View.INVISIBLE);
mLikingUsersDivider.setVisibility(View.INVISIBLE);
mLikingUsersLabel.setVisibility(View.INVISIBLE);
}
} else {
countLikes.setVisibility(View.INVISIBLE);
countLikes.setOnClickListener(null);
}
}
private void doLikePost() {
if (!isAdded()) {
return;
}
if (!mAccountStore.hasAccessToken()) {
Snackbar.make(getView(), R.string.reader_snackbar_err_cannot_like_post_logged_out, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.sign_in, mSignInClickListener).show();
return;
}
if (!mPost.canLikePost()) {
ToastUtils.showToast(getActivity(), R.string.reader_toast_err_cannot_like_post);
return;
}
setPostLike(true);
}
/*
* show latest likes for this post
*/
private void refreshLikes() {
if (!isAdded() || !hasPost() || !mPost.canLikePost()) {
return;
}
// nothing more to do if no likes
if (mPost.numLikes == 0) {
mLikingUsersView.setVisibility(View.GONE);
mLikingUsersDivider.setVisibility(View.GONE);
mLikingUsersLabel.setVisibility(View.GONE);
return;
}
// clicking likes view shows activity displaying all liking users
mLikingUsersView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ReaderActivityLauncher.showReaderLikingUsers(getActivity(), mPost.blogId, mPost.postId);
}
});
mLikingUsersDivider.setVisibility(View.VISIBLE);
mLikingUsersLabel.setVisibility(View.VISIBLE);
mLikingUsersView.setVisibility(View.VISIBLE);
mLikingUsersView.showLikingUsers(mPost);
}
private boolean showPhotoViewer(String imageUrl, View sourceView, int startX, int startY) {
if (!isAdded() || TextUtils.isEmpty(imageUrl)) {
return false;
}
// make sure this is a valid web image (could be file: or data:)
if (!imageUrl.startsWith("http")) {
return false;
}
String postContent = (mRenderer != null ? mRenderer.getRenderedHtml() : null);
boolean isPrivatePost = (mPost != null && mPost.isPrivate);
EnumSet<PhotoViewerOption> options = EnumSet.noneOf(PhotoViewerOption.class);
if (isPrivatePost) {
options.add(ReaderActivityLauncher.PhotoViewerOption.IS_PRIVATE_IMAGE);
}
ReaderActivityLauncher.showReaderPhotoViewer(
getActivity(),
imageUrl,
postContent,
sourceView,
options,
startX,
startY);
return true;
}
/*
* called when the post doesn't exist in local db, need to get it from server
*/
private void requestPost() {
final ProgressBar progress = (ProgressBar) getView().findViewById(R.id.progress_loading);
progress.setVisibility(View.VISIBLE);
progress.bringToFront();
ReaderActions.OnRequestListener listener = new ReaderActions.OnRequestListener() {
@Override
public void onSuccess() {
mHasAlreadyRequestedPost = true;
if (isAdded()) {
progress.setVisibility(View.GONE);
showPost();
EventBus.getDefault().post(new ReaderEvents.SinglePostDownloaded());
}
}
@Override
public void onFailure(int statusCode) {
mHasAlreadyRequestedPost = true;
if (isAdded()) {
progress.setVisibility(View.GONE);
onRequestFailure(statusCode);
}
}
};
if (mIsFeed) {
ReaderPostActions.requestFeedPost(mBlogId, mPostId, listener);
} else {
ReaderPostActions.requestBlogPost(mBlogId, mPostId, listener);
}
}
/*
* post slugs resolution to IDs has completed
*/
@SuppressWarnings("unused")
public void onEventMainThread(ReaderEvents.PostSlugsRequestCompleted event) {
mPostSlugsResolutionUnderway = false;
if (!isAdded()) return;
final ProgressBar progress = (ProgressBar) getView().findViewById(R.id.progress_loading);
progress.setVisibility(View.GONE);
if (event.getStatusCode() == 200) {
replacePost(event.getBlogId(), event.getPostId(), false);
} else {
onRequestFailure(event.getStatusCode());
}
}
private void onRequestFailure(int statusCode) {
int errMsgResId;
if (!NetworkUtils.isNetworkAvailable(getActivity())) {
errMsgResId = R.string.no_network_message;
} else {
switch (statusCode) {
case 401:
case 403:
final boolean offerSignIn = WPUrlUtils.isWordPressCom(mInterceptedUri)
&& !mAccountStore.hasAccessToken();
if (!offerSignIn) {
errMsgResId = (mInterceptedUri == null)
? R.string.reader_err_get_post_not_authorized
: R.string.reader_err_get_post_not_authorized_fallback;
mSignInButton.setVisibility(View.GONE);
} else {
errMsgResId = (mInterceptedUri == null)
? R.string.reader_err_get_post_not_authorized_signin
: R.string.reader_err_get_post_not_authorized_signin_fallback;
mSignInButton.setVisibility(View.VISIBLE);
AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_WPCOM_SIGN_IN_NEEDED,
mPost);
}
AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_USER_UNAUTHORIZED, mPost);
break;
case 404:
errMsgResId = R.string.reader_err_get_post_not_found;
break;
default:
errMsgResId = R.string.reader_err_get_post_generic;
break;
}
}
showError(getString(errMsgResId));
}
/*
* shows an error message in the middle of the screen - used when requesting post fails
*/
private void showError(String errorMessage) {
if (!isAdded()) return;
TextView txtError = (TextView) getView().findViewById(R.id.text_error);
txtError.setText(errorMessage);
if (errorMessage == null) {
txtError.setVisibility(View.GONE);
} else if (txtError.getVisibility() != View.VISIBLE) {
AniUtils.fadeIn(txtError, AniUtils.Duration.MEDIUM);
}
mErrorMessage = errorMessage;
}
private void showPost() {
if (mPostSlugsResolutionUnderway) {
AppLog.w(T.READER, "reader post detail > post request already running");
return;
}
if (mIsPostTaskRunning) {
AppLog.w(T.READER, "reader post detail > show post task already running");
return;
}
new ShowPostTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/*
* AsyncTask to retrieve this post from SQLite and display it
*/
private boolean mIsPostTaskRunning = false;
private class ShowPostTask extends AsyncTask<Void, Void, Boolean> {
@Override
protected void onPreExecute() {
mIsPostTaskRunning = true;
}
@Override
protected void onCancelled() {
mIsPostTaskRunning = false;
}
@Override
protected Boolean doInBackground(Void... params) {
mPost = mIsFeed ? ReaderPostTable.getFeedPost(mBlogId, mPostId, false)
: ReaderPostTable.getBlogPost(mBlogId, mPostId, false);
if (mPost == null) {
return false;
}
// "discover" Editor Pick posts should open the original (source) post
if (mPost.isDiscoverPost()) {
ReaderPostDiscoverData discoverData = mPost.getDiscoverData();
if (discoverData != null
&& discoverData.getDiscoverType() == ReaderPostDiscoverData.DiscoverType.EDITOR_PICK
&& discoverData.getBlogId() != 0
&& discoverData.getPostId() != 0) {
mIsFeed = false;
mBlogId = discoverData.getBlogId();
mPostId = discoverData.getPostId();
mPost = ReaderPostTable.getBlogPost(mBlogId, mPostId, false);
if (mPost == null) {
return false;
}
}
}
return true;
}
@Override
protected void onPostExecute(Boolean result) {
mIsPostTaskRunning = false;
if (!isAdded()) return;
// make sure options menu reflects whether we now have a post
getActivity().invalidateOptionsMenu();
if (!result) {
// post couldn't be loaded which means it doesn't exist in db, so request it from
// the server if it hasn't already been requested
if (!mHasAlreadyRequestedPost) {
AppLog.i(T.READER, "reader post detail > post not found, requesting it");
requestPost();
} else if (!TextUtils.isEmpty(mErrorMessage)) {
// post has already been requested and failed, so restore previous error message
showError(mErrorMessage);
}
return;
}
if (mDirectOperation != null) {
switch (mDirectOperation) {
case COMMENT_JUMP:
case COMMENT_REPLY:
case COMMENT_LIKE:
if (AppLockManager.getInstance().isAppLockFeatureEnabled()) {
// passcode screen was launched already (when ReaderPostPagerActivity got resumed) so reset
// the timeout to let the passcode screen come up for the ReaderCommentListActivity.
// See https://github.com/wordpress-mobile/WordPress-Android/issues/4887
AppLockManager.getInstance().getAppLock().forcePasswordLock();
}
ReaderActivityLauncher.showReaderComments(getActivity(), mPost.blogId, mPost.postId,
mDirectOperation, mCommentId, mInterceptedUri);
getActivity().finish();
getActivity().overridePendingTransition(0, 0);
return;
case POST_LIKE:
// Liking needs to be handled "later" after the post has been updated from the server so,
// nothing special to do here
break;
}
}
AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_ARTICLE_RENDERED, mPost);
mReaderWebView.setIsPrivatePost(mPost.isPrivate);
mReaderWebView.setBlogSchemeIsHttps(UrlUtils.isHttps(mPost.getBlogUrl()));
TextView txtTitle = (TextView) getView().findViewById(R.id.text_title);
TextView txtDateline = (TextView) getView().findViewById(R.id.text_dateline);
ReaderTagStrip tagStrip = (ReaderTagStrip) getView().findViewById(R.id.tag_strip);
ReaderPostDetailHeaderView headerView = (ReaderPostDetailHeaderView) getView().findViewById(R.id.header_view);
if (!canShowFooter()) {
mLayoutFooter.setVisibility(View.GONE);
}
// add padding to the scrollView to make room for the top and bottom toolbars - this also
// ensures the scrollbar matches the content so it doesn't disappear behind the toolbars
int topPadding = (mAutoHideToolbarListener != null ? mToolbarHeight : 0);
int bottomPadding = (canShowFooter() ? mLayoutFooter.getHeight() : 0);
mScrollView.setPadding(0, topPadding, 0, bottomPadding);
// scrollView was hidden in onCreateView, show it now that we have the post
mScrollView.setVisibility(View.VISIBLE);
// 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));
String timestamp = DateTimeUtils.javaDateToTimeSpan(mPost.getDisplayDate(), WordPress.getContext());
txtDateline.setText(timestamp);
headerView.setPost(mPost, mAccountStore.hasAccessToken());
tagStrip.setPost(mPost);
if (canShowFooter() && mLayoutFooter.getVisibility() != View.VISIBLE) {
AniUtils.fadeIn(mLayoutFooter, AniUtils.Duration.LONG);
}
refreshIconCounts();
}
}
/*
* called by the web view when the content finishes loading - likes aren't displayed
* until this is triggered, to avoid having them appear before the webView content
*/
@Override
public void onPageFinished(WebView view, String url) {
if (!isAdded()) {
return;
}
if (url != null && url.equals("about:blank")) {
// brief delay before showing comments/likes to give page time to render
view.postDelayed(new Runnable() {
@Override
public void run() {
if (!isAdded()) {
return;
}
refreshLikes();
if (!mHasAlreadyUpdatedPost) {
mHasAlreadyUpdatedPost = true;
updatePost();
}
requestRelatedPosts();
}
}, 300);
} else {
AppLog.w(T.READER, "reader post detail > page finished - " + url);
}
}
/*
* return the container view that should host the full screen video
*/
@Override
public ViewGroup onRequestCustomView() {
if (isAdded()) {
return (ViewGroup) getView().findViewById(R.id.layout_custom_view_container);
} else {
return null;
}
}
/*
* return the container view that should be hidden when full screen video is shown
*/
@Override
public ViewGroup onRequestContentView() {
if (isAdded()) {
return (ViewGroup) getView().findViewById(R.id.layout_post_detail_container);
} else {
return null;
}
}
@Override
public void onCustomViewShown() {
// full screen video has just been shown so hide the ActionBar
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.hide();
}
}
@Override
public void onCustomViewHidden() {
// user returned from full screen video so re-display the ActionBar
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.show();
}
}
boolean isCustomViewShowing() {
return mReaderWebView != null && mReaderWebView.isCustomViewShowing();
}
void hideCustomView() {
if (mReaderWebView != null) {
mReaderWebView.hideCustomView();
}
}
@Override
public boolean onUrlClick(String url) {
// if this is a "wordpress://blogpreview?" link, show blog preview for the blog - this is
// used for Discover posts that highlight a blog
if (ReaderUtils.isBlogPreviewUrl(url)) {
long siteId = ReaderUtils.getBlogIdFromBlogPreviewUrl(url);
if (siteId != 0) {
ReaderActivityLauncher.showReaderBlogPreview(getActivity(), siteId);
}
return true;
}
OpenUrlType openUrlType = shouldOpenExternal(url) ? OpenUrlType.EXTERNAL : OpenUrlType.INTERNAL;
ReaderActivityLauncher.openUrl(getActivity(), url, openUrlType);
return true;
}
/*
* returns True if the passed URL should be opened in the external browser app
*/
private boolean shouldOpenExternal(String url) {
// open YouTube videos in external app so they launch the YouTube player
if (ReaderVideoUtils.isYouTubeVideoLink(url)) {
return true;
}
// if the mime type starts with "application" open it externally - this will either
// open it in the associated app or the default browser (which will enable the user
// to download it)
String mimeType = UrlUtils.getUrlMimeType(url);
if (mimeType != null && mimeType.startsWith("application")) {
return true;
}
// open all other urls using an AuthenticatedWebViewActivity
return false;
}
@Override
public boolean onImageUrlClick(String imageUrl, View view, int x, int y) {
return showPhotoViewer(imageUrl, view, x, y);
}
private ActionBar getActionBar() {
if (isAdded() && getActivity() instanceof AppCompatActivity) {
return ((AppCompatActivity) getActivity()).getSupportActionBar();
} else {
AppLog.w(T.READER, "reader post detail > getActionBar returned null");
return null;
}
}
void pauseWebView() {
if (mReaderWebView == null) {
AppLog.w(T.READER, "reader post detail > attempt to pause null webView");
} else if (!mIsWebViewPaused) {
AppLog.d(T.READER, "reader post detail > pausing webView");
mReaderWebView.hideCustomView();
mReaderWebView.onPause();
mIsWebViewPaused = true;
}
}
void resumeWebViewIfPaused() {
if (mReaderWebView == null) {
AppLog.w(T.READER, "reader post detail > attempt to resume null webView");
} else if (mIsWebViewPaused) {
AppLog.d(T.READER, "reader post detail > resuming paused webView");
mReaderWebView.onResume();
mIsWebViewPaused = false;
}
}
@Override
public void onScrollUp(float distanceY) {
if (!mIsToolbarShowing
&& -distanceY >= MIN_SCROLL_DISTANCE_Y) {
showToolbar(true);
showFooter(true);
}
}
@Override
public void onScrollDown(float distanceY) {
if (mIsToolbarShowing
&& distanceY >= MIN_SCROLL_DISTANCE_Y
&& mScrollView.canScrollDown()
&& mScrollView.canScrollUp()
&& mScrollView.getScrollY() > mToolbarHeight) {
showToolbar(false);
showFooter(false);
}
}
@Override
public void onScrollCompleted() {
if (!mIsToolbarShowing
&& (!mScrollView.canScrollDown() || !mScrollView.canScrollUp())) {
showToolbar(true);
showFooter(true);
}
trackRelatedPostsIfShowing();
}
private void showToolbar(boolean show) {
mIsToolbarShowing = show;
if (mAutoHideToolbarListener != null) {
mAutoHideToolbarListener.onShowHideToolbar(show);
}
}
private void showFooter(boolean show) {
if (isAdded() && canShowFooter()) {
AniUtils.animateBottomBar(mLayoutFooter, show);
}
}
/*
* can we show the footer bar which contains the like & comment counts?
*/
private boolean canShowFooter() {
return canShowLikeCount() || canShowCommentCount();
}
private boolean canShowCommentCount() {
if (mPost == null) {
return false;
}
if (!mAccountStore.hasAccessToken()) {
return mPost.numReplies > 0;
}
return mPost.isWP()
&& !mPost.isJetpack
&& !mPost.isDiscoverPost()
&& (mPost.isCommentsOpen || mPost.numReplies > 0);
}
private boolean canShowLikeCount() {
if (mPost == null) {
return false;
}
if (!mAccountStore.hasAccessToken()) {
return mPost.numLikes > 0;
}
return mPost.canLikePost() || mPost.numLikes > 0;
}
private void setRefreshing(boolean refreshing) {
mSwipeToRefreshHelper.setRefreshing(refreshing);
}
}