blob: d6716a267bad9803da612fa39836dda4500ea34f [file] [log] [blame]
package org.wordpress.android.ui.posts;
import android.app.Fragment;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.wordpress.android.R;
import org.wordpress.android.WordPress;
import org.wordpress.android.models.Post;
import org.wordpress.android.models.PostStatus;
import org.wordpress.android.models.PostsListPost;
import org.wordpress.android.models.PostsListPostList;
import org.wordpress.android.ui.ActivityLauncher;
import org.wordpress.android.ui.EmptyViewMessageType;
import org.wordpress.android.ui.posts.adapters.PostsListAdapter;
import org.wordpress.android.ui.posts.services.PostEvents;
import org.wordpress.android.ui.posts.services.PostUpdateService;
import org.wordpress.android.ui.posts.services.PostUploadService;
import org.wordpress.android.util.AniUtils;
import org.wordpress.android.util.NetworkUtils;
import org.wordpress.android.util.ToastUtils;
import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
import org.wordpress.android.util.helpers.SwipeToRefreshHelper.RefreshListener;
import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
import org.wordpress.android.widgets.PostListButton;
import org.wordpress.android.widgets.RecyclerItemDecoration;
import org.xmlrpc.android.ApiHelper;
import org.xmlrpc.android.ApiHelper.ErrorType;
import de.greenrobot.event.EventBus;
public class PostsListFragment extends Fragment
implements PostsListAdapter.OnPostsLoadedListener,
PostsListAdapter.OnLoadMoreListener,
PostsListAdapter.OnPostSelectedListener,
PostsListAdapter.OnPostButtonClickListener {
public static final int POSTS_REQUEST_COUNT = 20;
private SwipeToRefreshHelper mSwipeToRefreshHelper;
private PostsListAdapter mPostsListAdapter;
private View mFabView;
private RecyclerView mRecyclerView;
private View mEmptyView;
private ProgressBar mProgressLoadMore;
private TextView mEmptyViewTitle;
private ImageView mEmptyViewImage;
private boolean mCanLoadMorePosts = true;
private boolean mIsPage;
private boolean mIsFetchingPosts;
private final PostsListPostList mTrashedPosts = new PostsListPostList();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
if (isAdded()) {
Bundle extras = getActivity().getIntent().getExtras();
if (extras != null) {
mIsPage = extras.getBoolean(PostsListActivity.EXTRA_VIEW_PAGES);
}
}
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.post_list_fragment, container, false);
mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
mProgressLoadMore = (ProgressBar) view.findViewById(R.id.progress);
mFabView = view.findViewById(R.id.fab_button);
mEmptyView = view.findViewById(R.id.empty_view);
mEmptyViewTitle = (TextView) mEmptyView.findViewById(R.id.title_empty);
mEmptyViewImage = (ImageView) mEmptyView.findViewById(R.id.image_empty);
Context context = getActivity();
mRecyclerView.setLayoutManager(new LinearLayoutManager(context));
int spacingVertical = mIsPage ? 0 : context.getResources().getDimensionPixelSize(R.dimen.reader_card_gutters);
int spacingHorizontal = context.getResources().getDimensionPixelSize(R.dimen.content_margin);
mRecyclerView.addItemDecoration(new RecyclerItemDecoration(spacingHorizontal, spacingVertical));
// hide the fab so we can animate it in - note that we only do this on Lollipop and higher
// due to a bug in the current implementation which prevents it from being hidden
// correctly on pre-L devices (which makes animating it in/out ugly)
// https://code.google.com/p/android/issues/detail?id=175331
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mFabView.setVisibility(View.GONE);
}
mFabView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
newPost();
}
});
return view;
}
private void initSwipeToRefreshHelper() {
mSwipeToRefreshHelper = new SwipeToRefreshHelper(
getActivity(),
(CustomSwipeRefreshLayout) getView().findViewById(R.id.ptr_layout),
new RefreshListener() {
@Override
public void onRefreshStarted() {
if (!isAdded()) {
return;
}
if (!NetworkUtils.checkConnection(getActivity())) {
setRefreshing(false);
updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
return;
}
requestPosts(false);
}
});
}
public PostsListAdapter getPostListAdapter() {
if (mPostsListAdapter == null) {
mPostsListAdapter = new PostsListAdapter(getActivity(), WordPress.getCurrentBlog(), mIsPage);
mPostsListAdapter.setOnLoadMoreListener(this);
mPostsListAdapter.setOnPostsLoadedListener(this);
mPostsListAdapter.setOnPostSelectedListener(this);
mPostsListAdapter.setOnPostButtonClickListener(this);
}
return mPostsListAdapter;
}
private boolean isPostAdapterEmpty() {
return (mPostsListAdapter != null && mPostsListAdapter.getItemCount() == 0);
}
private void loadPosts() {
getPostListAdapter().loadPosts();
}
@Override
public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle);
initSwipeToRefreshHelper();
// since setRetainInstance(true) is used, we only need to request latest
// posts the first time this is called (ie: not after device rotation)
if (bundle == null && NetworkUtils.checkConnection(getActivity())) {
requestPosts(false);
}
}
private void newPost() {
if (!isAdded()) return;
if (WordPress.getCurrentBlog() != null) {
ActivityLauncher.addNewBlogPostOrPageForResult(getActivity(), WordPress.getCurrentBlog(), mIsPage);
} else {
ToastUtils.showToast(getActivity(), R.string.blog_not_found);
}
}
public void onResume() {
super.onResume();
if (WordPress.getCurrentBlog() != null && mRecyclerView.getAdapter() == null) {
mRecyclerView.setAdapter(getPostListAdapter());
}
if (WordPress.getCurrentBlog() != null) {
// always (re)load when resumed to reflect changes made elsewhere
loadPosts();
}
// scale in the fab after a brief delay if it's not already showing
if (mFabView.getVisibility() != View.VISIBLE) {
long delayMs = getResources().getInteger(R.integer.fab_animation_delay);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (isAdded()) {
AniUtils.scaleIn(mFabView, AniUtils.Duration.MEDIUM);
}
}
}, delayMs);
}
}
public boolean isRefreshing() {
return mSwipeToRefreshHelper.isRefreshing();
}
private void setRefreshing(boolean refreshing) {
mSwipeToRefreshHelper.setRefreshing(refreshing);
}
private void requestPosts(boolean loadMore) {
if (!isAdded() || mIsFetchingPosts) {
return;
}
if (!NetworkUtils.isNetworkAvailable(getActivity())) {
updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
return;
}
mIsFetchingPosts = true;
if (loadMore) {
showLoadMoreProgress();
}
PostUpdateService.startServiceForBlog(getActivity(), WordPress.getCurrentLocalTableBlogId(), mIsPage, loadMore);
}
private void showLoadMoreProgress() {
if (mProgressLoadMore != null) {
mProgressLoadMore.setVisibility(View.VISIBLE);
}
}
private void hideLoadMoreProgress() {
if (mProgressLoadMore != null) {
mProgressLoadMore.setVisibility(View.GONE);
}
}
/*
* PostMediaService has downloaded the media info for a post's featured image, tell
* the adapter so it can show the featured image now that we have its URL
*/
@SuppressWarnings("unused")
public void onEventMainThread(PostEvents.PostMediaInfoUpdated event) {
if (isAdded() && WordPress.getCurrentBlog() != null) {
getPostListAdapter().mediaUpdated(event.getMediaId(), event.getMediaUrl());
}
}
/*
* upload start, reload so correct status on uploading post appears
*/
@SuppressWarnings("unused")
public void onEventMainThread(PostEvents.PostUploadStarted event) {
if (isAdded() && WordPress.getCurrentLocalTableBlogId() == event.mLocalBlogId) {
loadPosts();
}
}
/*
* upload ended, reload regardless of success/fail so correct status of uploaded post appears
*/
@SuppressWarnings("unused")
public void onEventMainThread(PostEvents.PostUploadEnded event) {
if (isAdded() && WordPress.getCurrentLocalTableBlogId() == event.mLocalBlogId) {
loadPosts();
}
}
/*
* PostUpdateService finished a request to retrieve new posts
*/
@SuppressWarnings("unused")
public void onEventMainThread(PostEvents.RequestPosts event) {
mIsFetchingPosts = false;
if (isAdded() && event.getBlogId() == WordPress.getCurrentLocalTableBlogId()) {
setRefreshing(false);
hideLoadMoreProgress();
if (!event.getFailed()) {
mCanLoadMorePosts = event.canLoadMore();
loadPosts();
} else {
ApiHelper.ErrorType errorType = event.getErrorType();
if (errorType != null && errorType != ErrorType.TASK_CANCELLED && errorType != ErrorType.NO_ERROR) {
switch (errorType) {
case UNAUTHORIZED:
updateEmptyView(EmptyViewMessageType.PERMISSION_ERROR);
break;
default:
updateEmptyView(EmptyViewMessageType.GENERIC_ERROR);
break;
}
}
}
}
}
private void updateEmptyView(EmptyViewMessageType emptyViewMessageType) {
int stringId;
switch (emptyViewMessageType) {
case LOADING:
stringId = mIsPage ? R.string.pages_fetching : R.string.posts_fetching;
break;
case NO_CONTENT:
stringId = mIsPage ? R.string.pages_empty_list : R.string.posts_empty_list;
break;
case NETWORK_ERROR:
stringId = R.string.no_network_message;
break;
case PERMISSION_ERROR:
stringId = mIsPage ? R.string.error_refresh_unauthorized_pages :
R.string.error_refresh_unauthorized_posts;
break;
case GENERIC_ERROR:
stringId = mIsPage ? R.string.error_refresh_pages : R.string.error_refresh_posts;
break;
default:
return;
}
mEmptyViewTitle.setText(getText(stringId));
mEmptyViewImage.setVisibility(emptyViewMessageType == EmptyViewMessageType.NO_CONTENT ? View.VISIBLE : View.GONE);
mEmptyView.setVisibility(isPostAdapterEmpty() ? View.VISIBLE : View.GONE);
}
private void hideEmptyView() {
if (isAdded() && mEmptyView != null) {
mEmptyView.setVisibility(View.GONE);
}
}
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
public void onStop() {
EventBus.getDefault().unregister(this);
super.onStop();
}
/*
* called by the adapter after posts have been loaded
*/
@Override
public void onPostsLoaded(int postCount) {
if (!isAdded()) {
return;
}
if (postCount == 0 && !mIsFetchingPosts) {
if (NetworkUtils.isNetworkAvailable(getActivity())) {
updateEmptyView(EmptyViewMessageType.NO_CONTENT);
} else {
updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
}
} else if (postCount > 0) {
hideEmptyView();
}
}
/*
* called by the adapter to load more posts when the user scrolls towards the last post
*/
@Override
public void onLoadMore() {
if (mCanLoadMorePosts && !mIsFetchingPosts) {
requestPosts(true);
}
}
/*
* called by the adapter when the user clicks a post
*/
@Override
public void onPostSelected(PostsListPost post) {
onPostButtonClicked(PostListButton.BUTTON_PREVIEW, post);
}
/*
* called by the adapter when the user clicks the edit/view/stats/trash button for a post
*/
@Override
public void onPostButtonClicked(int buttonType, PostsListPost post) {
if (!isAdded()) return;
Post fullPost = WordPress.wpDB.getPostForLocalTablePostId(post.getPostId());
if (fullPost == null) {
ToastUtils.showToast(getActivity(), R.string.post_not_found);
return;
}
switch (buttonType) {
case PostListButton.BUTTON_EDIT:
ActivityLauncher.editBlogPostOrPageForResult(getActivity(), post.getPostId(), mIsPage);
break;
case PostListButton.BUTTON_PUBLISH:
publishPost(fullPost);
break;
case PostListButton.BUTTON_VIEW:
ActivityLauncher.browsePostOrPage(getActivity(), WordPress.getCurrentBlog(), fullPost);
break;
case PostListButton.BUTTON_PREVIEW:
ActivityLauncher.viewPostPreviewForResult(getActivity(), fullPost, mIsPage);
break;
case PostListButton.BUTTON_STATS:
ActivityLauncher.viewStatsSinglePostDetails(getActivity(), fullPost, mIsPage);
break;
case PostListButton.BUTTON_TRASH:
case PostListButton.BUTTON_DELETE:
// prevent deleting post while it's being uploaded
if (!post.isUploading()) {
trashPost(post);
}
break;
}
}
private void publishPost(final Post post) {
if (!NetworkUtils.isNetworkAvailable(getActivity())) {
ToastUtils.showToast(getActivity(), R.string.error_publish_no_network, ToastUtils.Duration.SHORT);
return;
}
// If the post is empty, don't publish
if (!post.isPublishable()) {
ToastUtils.showToast(getActivity(), R.string.error_publish_empty_post, ToastUtils.Duration.SHORT);
return;
}
post.setPostStatus(PostStatus.toString(PostStatus.PUBLISHED));
post.setChangedFromDraftToPublished(true);
PostUploadService.addPostToUpload(post);
getActivity().startService(new Intent(getActivity(), PostUploadService.class));
PostUtils.trackSavePostAnalytics(post);
}
/*
* send the passed post to the trash with undo
*/
private void trashPost(final PostsListPost post) {
//only check if network is available in case this is not a local draft - local drafts have not yet
//been posted to the server so they can be trashed w/o further care
if (!isAdded() || (!post.isLocalDraft() && !NetworkUtils.checkConnection(getActivity()))) {
return;
}
final Post fullPost = WordPress.wpDB.getPostForLocalTablePostId(post.getPostId());
if (fullPost == null) {
ToastUtils.showToast(getActivity(), R.string.post_not_found);
return;
}
// remove post from the list and add it to the list of trashed posts
getPostListAdapter().hidePost(post);
mTrashedPosts.add(post);
// make sure empty view shows if user deleted the only post
if (getPostListAdapter().getItemCount() == 0) {
updateEmptyView(EmptyViewMessageType.NO_CONTENT);
}
View.OnClickListener undoListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
// user undid the trash, so unhide the post and remove it from the list of trashed posts
mTrashedPosts.remove(post);
getPostListAdapter().unhidePost(post);
hideEmptyView();
}
};
// different undo text if this is a local draft since it will be deleted rather than trashed
String text;
if (post.isLocalDraft()) {
text = mIsPage ? getString(R.string.page_deleted) : getString(R.string.post_deleted);
} else {
text = mIsPage ? getString(R.string.page_trashed) : getString(R.string.post_trashed);
}
Snackbar snackbar = Snackbar.make(getView().findViewById(R.id.coordinator), text, Snackbar.LENGTH_LONG)
.setAction(R.string.undo, undoListener);
// wait for the undo snackbar to disappear before actually deleting the post
snackbar.setCallback(new Snackbar.Callback() {
@Override
public void onDismissed(Snackbar snackbar, int event) {
super.onDismissed(snackbar, event);
// if the post no longer exists in the list of trashed posts it's because the
// user undid the trash, so don't perform the deletion
if (!mTrashedPosts.contains(post)) {
return;
}
// remove from the list of trashed posts in case onDismissed is called multiple
// times - this way the above check prevents us making the call to delete it twice
// https://code.google.com/p/android/issues/detail?id=190529
mTrashedPosts.remove(post);
WordPress.wpDB.deletePost(fullPost);
if (!post.isLocalDraft()) {
new ApiHelper.DeleteSinglePostTask().execute(WordPress.getCurrentBlog(),
fullPost.getRemotePostId(), mIsPage);
}
}
});
snackbar.show();
}
}