package kr.kdev.dg1s.biowiki.ui.comments; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ListView; import android.widget.ProgressBar; import com.actionbarsherlock.view.ActionMode; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; import org.xmlrpc.android.ApiHelper; import java.util.HashMap; import java.util.Map; import kr.kdev.dg1s.biowiki.BioWiki; import kr.kdev.dg1s.biowiki.R; import kr.kdev.dg1s.biowiki.models.Blog; import kr.kdev.dg1s.biowiki.models.Comment; import kr.kdev.dg1s.biowiki.models.CommentList; import kr.kdev.dg1s.biowiki.models.CommentStatus; import kr.kdev.dg1s.biowiki.ui.BWActionBarActivity; import kr.kdev.dg1s.biowiki.ui.PullToRefreshHelper; import kr.kdev.dg1s.biowiki.ui.PullToRefreshHelper.RefreshListener; import kr.kdev.dg1s.biowiki.ui.comments.CommentActions.ChangeType; import kr.kdev.dg1s.biowiki.ui.comments.CommentActions.ChangedFrom; import kr.kdev.dg1s.biowiki.ui.comments.CommentActions.OnCommentChangeListener; import kr.kdev.dg1s.biowiki.util.AppLog; import kr.kdev.dg1s.biowiki.util.NetworkUtils; import kr.kdev.dg1s.biowiki.util.SysUtils; import kr.kdev.dg1s.biowiki.util.ToastUtils; import uk.co.senab.actionbarpulltorefresh.extras.actionbarsherlock.PullToRefreshLayout; public class CommentsListFragment extends Fragment { private static final int COMMENTS_PER_PAGE = 30; private static final String KEY_AUTO_REFRESHED = "has_auto_refreshed"; private static final String KEY_HAS_CHECKED_DELETED_COMMENTS = "has_checked_deleted_comments"; private boolean mIsUpdatingComments = false; private boolean mCanLoadMoreComments = true; private boolean mHasAutoRefreshedComments = false; private boolean mHasCheckedDeletedComments = false; private ProgressBar mProgressLoadMore; private PullToRefreshHelper mPullToRefreshHelper; private ListView mListView; private View mEmptyView; private CommentAdapter mCommentAdapter; private ActionMode mActionMode; private UpdateCommentsTask mUpdateCommentsTask; private OnCommentSelectedListener mOnCommentSelectedListener; private OnCommentChangeListener mOnCommentChangeListener; private ListView getListView() { return mListView; } private CommentAdapter getCommentAdapter() { if (mCommentAdapter == null) { /* * called after comments have been loaded */ CommentAdapter.DataLoadedListener dataLoadedListener = new CommentAdapter.DataLoadedListener() { @Override public void onDataLoaded(boolean isEmpty) { if (!hasActivity()) return; if (isEmpty) { showEmptyView(); } else { hideEmptyView(); } } }; // adapter calls this to request more comments from server when it reaches the end CommentAdapter.OnLoadMoreListener loadMoreListener = new CommentAdapter.OnLoadMoreListener() { @Override public void onLoadMore() { if (mCanLoadMoreComments && !mIsUpdatingComments) { updateComments(true); } } }; // adapter calls this when selected comments have changed (CAB) CommentAdapter.OnSelectedItemsChangeListener changeListener = new CommentAdapter.OnSelectedItemsChangeListener() { @Override public void onSelectedItemsChanged() { if (mActionMode != null) { if (getSelectedCommentCount() == 0) { mActionMode.finish(); } else { updateActionModeTitle(); // must invalidate to ensure onPrepareActionMode is called mActionMode.invalidate(); } } } }; mCommentAdapter = new CommentAdapter(getActivity(), dataLoadedListener, loadMoreListener, changeListener); } return mCommentAdapter; } private boolean hasCommentAdapter() { return (mCommentAdapter != null); } private int getSelectedCommentCount() { return getCommentAdapter().getSelectedCommentCount(); } void clear() { if (hasCommentAdapter()) { getCommentAdapter().clear(); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { mHasAutoRefreshedComments = savedInstanceState.getBoolean(KEY_AUTO_REFRESHED); } } @Override public void onActivityCreated(Bundle bundle) { super.onActivityCreated(bundle); setUpListView(); getCommentAdapter().loadComments(); if (!NetworkUtils.isNetworkAvailable(getActivity())) { return; } if (!mHasAutoRefreshedComments) { updateComments(false); mPullToRefreshHelper.setRefreshing(true); mHasAutoRefreshedComments = true; } } public void onAttach(Activity activity) { super.onAttach(activity); try { // check that the containing activity implements our callback mOnCommentSelectedListener = (OnCommentSelectedListener) activity; mOnCommentChangeListener = (OnCommentChangeListener) activity; } catch (ClassCastException e) { activity.finish(); throw new ClassCastException(activity.toString() + " must implement Callback"); } } public void onBlogChanged() { mHasCheckedDeletedComments = false; if (mUpdateCommentsTask != null) { mUpdateCommentsTask.setRetryOnCancelled(true); mUpdateCommentsTask.cancel(true); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.comment_list_fragment, container, false); mListView = (ListView) view.findViewById(android.R.id.list); mEmptyView = view.findViewById(android.R.id.empty); // progress bar that appears when loading more comments mProgressLoadMore = (ProgressBar) view.findViewById(R.id.progress_loading); mProgressLoadMore.setVisibility(View.GONE); // pull to refresh setup mPullToRefreshHelper = new PullToRefreshHelper(getActivity(), (PullToRefreshLayout) view.findViewById(R.id.ptr_layout), new RefreshListener() { @Override public void onRefreshStarted(View view) { if (getActivity() == null || !NetworkUtils.checkConnection(getActivity())) { mPullToRefreshHelper.setRefreshing(false); return; } updateComments(false); } } ); return view; } public void setRefreshing(boolean refreshing) { mPullToRefreshHelper.setRefreshing(refreshing); } private void dismissDialog(int id) { if (!hasActivity()) return; try { getActivity().dismissDialog(id); } catch (IllegalArgumentException e) { // raised when dialog wasn't created } } private void moderateSelectedComments(final CommentStatus newStatus) { final CommentList selectedComments = getCommentAdapter().getSelectedComments(); final CommentList updateComments = new CommentList(); // build list of comments whose status is different than passed for (Comment comment : selectedComments) { if (comment.getStatusEnum() != newStatus) updateComments.add(comment); } if (updateComments.size() == 0) return; if (!NetworkUtils.checkConnection(getActivity())) return; final int dlgId; switch (newStatus) { case APPROVED: dlgId = CommentDialogs.ID_COMMENT_DLG_APPROVING; break; case UNAPPROVED: dlgId = CommentDialogs.ID_COMMENT_DLG_UNAPPROVING; break; case SPAM: dlgId = CommentDialogs.ID_COMMENT_DLG_SPAMMING; break; case TRASH: dlgId = CommentDialogs.ID_COMMENT_DLG_TRASHING; break; default: return; } getActivity().showDialog(dlgId); CommentActions.OnCommentsModeratedListener listener = new CommentActions.OnCommentsModeratedListener() { @Override public void onCommentsModerated(final CommentList moderatedComments) { if (!hasActivity()) return; finishActionMode(); dismissDialog(dlgId); if (moderatedComments.size() > 0) { getCommentAdapter().clearSelectedComments(); getCommentAdapter().replaceComments(moderatedComments); if (mOnCommentChangeListener != null) { ChangeType changeType = (newStatus == CommentStatus.TRASH ? ChangeType.TRASHED : ChangeType.STATUS); mOnCommentChangeListener.onCommentChanged(ChangedFrom.COMMENT_LIST, changeType); } } else { ToastUtils.showToast(getActivity(), R.string.error_moderate_comment); } } }; CommentActions.moderateComments(BioWiki.getCurrentLocalTableBlogId(), updateComments, newStatus, listener); } private void confirmDeleteComments() { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setMessage(R.string.dlg_confirm_trash_comments); builder.setTitle(R.string.trash); builder.setCancelable(true); builder.setPositiveButton(R.string.trash_yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { deleteSelectedComments(); } }); builder.setNegativeButton(R.string.trash_no, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); AlertDialog alert = builder.create(); alert.show(); } private void deleteSelectedComments() { if (!NetworkUtils.checkConnection(getActivity())) return; final CommentList selectedComments = getCommentAdapter().getSelectedComments(); getActivity().showDialog(CommentDialogs.ID_COMMENT_DLG_TRASHING); CommentActions.OnCommentsModeratedListener listener = new CommentActions.OnCommentsModeratedListener() { @Override public void onCommentsModerated(final CommentList deletedComments) { if (!hasActivity()) return; finishActionMode(); dismissDialog(CommentDialogs.ID_COMMENT_DLG_TRASHING); if (deletedComments.size() > 0) { getCommentAdapter().clearSelectedComments(); getCommentAdapter().deleteComments(deletedComments); if (mOnCommentChangeListener != null) mOnCommentChangeListener.onCommentChanged(ChangedFrom.COMMENT_LIST, ChangeType.TRASHED); } else { ToastUtils.showToast(getActivity(), R.string.error_moderate_comment); } } }; CommentActions.moderateComments(BioWiki.getCurrentLocalTableBlogId(), selectedComments, CommentStatus.TRASH, listener); } long getHighlightedCommentId() { return (hasCommentAdapter() ? getCommentAdapter().getHighlightedCommentId() : 0); } void setHighlightedCommentId(long commentId) { getCommentAdapter().setHighlightedCommentId(commentId); } private void setUpListView() { ListView listView = this.getListView(); listView.setAdapter(getCommentAdapter()); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (mActionMode == null) { Comment comment = (Comment) getCommentAdapter().getItem(position); mOnCommentSelectedListener.onCommentSelected(comment); getListView().invalidateViews(); } else { getCommentAdapter().toggleItemSelected(position, view); } } }); listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { // enable CAB if it's not already enabled if (mActionMode == null) { if (getActivity() instanceof BWActionBarActivity) { ((BWActionBarActivity) getActivity()).startActionMode(new ActionModeCallback()); getCommentAdapter().setEnableSelection(true); getCommentAdapter().setItemSelected(position, true, view); } } else { getCommentAdapter().toggleItemSelected(position, view); } return true; } }); } void loadComments() { // this is called from CommentsActivity when a comment was changed in the detail view, // and the change will already be in SQLite so simply reload the comment adapter // to show the change getCommentAdapter().loadComments(); } /* * get latest comments from server, or pass loadMore=true to get comments beyond the * existing ones */ @SuppressLint("NewApi") void updateComments(boolean loadMore) { if (mIsUpdatingComments) { AppLog.w(AppLog.T.COMMENTS, "update comments task already running"); return; } mUpdateCommentsTask = new UpdateCommentsTask(loadMore); if (SysUtils.canUseExecuteOnExecutor()) { mUpdateCommentsTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } else { mUpdateCommentsTask.execute(); } } @Override public void onSaveInstanceState(Bundle outState) { if (outState.isEmpty()) { outState.putBoolean("bug_19917_fix", true); } outState.putBoolean(KEY_AUTO_REFRESHED, mHasAutoRefreshedComments); outState.putBoolean(KEY_HAS_CHECKED_DELETED_COMMENTS, mHasCheckedDeletedComments); super.onSaveInstanceState(outState); } private boolean hasActivity() { return (getActivity() != null && !isRemoving()); } private void showEmptyView() { if (mEmptyView != null) mEmptyView.setVisibility(View.VISIBLE); } private void hideEmptyView() { if (mEmptyView != null) mEmptyView.setVisibility(View.GONE); } /** * show/hide progress bar which appears at the bottom when loading more comments */ private void showLoadingProgress() { if (hasActivity() && mProgressLoadMore != null) { mProgressLoadMore.setVisibility(View.VISIBLE); } } private void hideLoadingProgress() { if (hasActivity() && mProgressLoadMore != null) { mProgressLoadMore.setVisibility(View.GONE); } } /** * * * Contextual ActionBar (CAB) routines * * */ private void updateActionModeTitle() { if (mActionMode == null) return; int numSelected = getSelectedCommentCount(); if (numSelected > 0) { mActionMode.setTitle(Integer.toString(numSelected)); } else { mActionMode.setTitle(""); } } private void finishActionMode() { if (mActionMode != null) { mActionMode.finish(); } } public interface OnCommentSelectedListener { public void onCommentSelected(Comment comment); } /* * task to retrieve latest comments from server */ private class UpdateCommentsTask extends AsyncTask<Void, Void, CommentList> { final boolean isLoadingMore; boolean isError; boolean mRetryOnCancelled; private UpdateCommentsTask(boolean loadMore) { isLoadingMore = loadMore; } public void setRetryOnCancelled(boolean retryOnCancelled) { mRetryOnCancelled = retryOnCancelled; } @Override protected void onPreExecute() { super.onPreExecute(); mIsUpdatingComments = true; if (isLoadingMore) { showLoadingProgress(); } } @Override protected void onCancelled() { super.onCancelled(); mIsUpdatingComments = false; mUpdateCommentsTask = null; if (mRetryOnCancelled) { mRetryOnCancelled = false; updateComments(false); } else { mPullToRefreshHelper.setRefreshing(false); } } @Override protected CommentList doInBackground(Void... args) { if (!hasActivity()) return null; Blog blog = BioWiki.getCurrentBlog(); if (blog == null) { isError = true; return null; } // the first time this is called, make sure comments deleted on server are removed // from the local database if (!mHasCheckedDeletedComments && !isLoadingMore) { mHasCheckedDeletedComments = true; ApiHelper.removeDeletedComments(blog); } Map<String, Object> hPost = new HashMap<String, Object>(); if (isLoadingMore) { int numExisting = getCommentAdapter().getCount(); hPost.put("offset", numExisting); hPost.put("number", COMMENTS_PER_PAGE); } else { hPost.put("number", COMMENTS_PER_PAGE); } Object[] params = {blog.getRemoteBlogId(), blog.getUsername(), blog.getPassword(), hPost}; try { return ApiHelper.refreshComments(getActivity(), blog, params); } catch (Exception e) { isError = true; return null; } } protected void onPostExecute(CommentList comments) { mIsUpdatingComments = false; mUpdateCommentsTask = null; if (!hasActivity()) { return; } if (isLoadingMore) { hideLoadingProgress(); } mPullToRefreshHelper.setRefreshing(false); if (isCancelled()) return; mCanLoadMoreComments = (comments != null && comments.size() > 0); // result will be null on error OR if no more comments exists if (comments == null) { if (isError && !getActivity().isFinishing()) { ToastUtils.showToast(getActivity(), getString(R.string.error_refresh_comments)); } return; } if (comments.size() > 0) { getCommentAdapter().loadComments(); } } } private final class ActionModeCallback implements ActionMode.Callback { @Override public boolean onCreateActionMode(ActionMode actionMode, com.actionbarsherlock.view.Menu menu) { mActionMode = actionMode; MenuInflater inflater = actionMode.getMenuInflater(); inflater.inflate(R.menu.menu_comments_cab, menu); mPullToRefreshHelper.setEnabled(false); return true; } private void setItemEnabled(com.actionbarsherlock.view.Menu menu, int menuId, boolean isEnabled) { final MenuItem item = menu.findItem(menuId); if (item == null || item.isEnabled() == isEnabled) return; item.setEnabled(isEnabled); if (item.getIcon() != null) { // must mutate the drawable to avoid affecting other instances of it Drawable icon = item.getIcon().mutate(); icon.setAlpha(isEnabled ? 255 : 128); item.setIcon(icon); } } @Override public boolean onPrepareActionMode(ActionMode actionMode, com.actionbarsherlock.view.Menu menu) { final CommentList selectedComments = getCommentAdapter().getSelectedComments(); boolean hasSelection = (selectedComments.size() > 0); boolean hasApproved = hasSelection && selectedComments.hasAnyWithStatus(CommentStatus.APPROVED); boolean hasUnapproved = hasSelection && selectedComments.hasAnyWithStatus(CommentStatus.UNAPPROVED); boolean hasSpam = hasSelection && selectedComments.hasAnyWithStatus(CommentStatus.SPAM); boolean hasAnyNonSpam = hasSelection && selectedComments.hasAnyWithoutStatus(CommentStatus.SPAM); setItemEnabled(menu, R.id.menu_approve, hasUnapproved || hasSpam); setItemEnabled(menu, R.id.menu_unapprove, hasApproved); setItemEnabled(menu, R.id.menu_spam, hasAnyNonSpam); setItemEnabled(menu, R.id.menu_trash, hasSelection); return true; } @Override public boolean onActionItemClicked(ActionMode actionMode, com.actionbarsherlock.view.MenuItem menuItem) { int numSelected = getSelectedCommentCount(); if (numSelected == 0) return false; switch (menuItem.getItemId()) { case R.id.menu_approve: moderateSelectedComments(CommentStatus.APPROVED); return true; case R.id.menu_unapprove: moderateSelectedComments(CommentStatus.UNAPPROVED); return true; case R.id.menu_spam: moderateSelectedComments(CommentStatus.SPAM); return true; case R.id.menu_trash: // unlike the other status changes, we ask the user to confirm trashing confirmDeleteComments(); return true; default: return false; } } @Override public void onDestroyActionMode(ActionMode mode) { getCommentAdapter().setEnableSelection(false); mPullToRefreshHelper.setEnabled(true); mActionMode = null; } } }