/** * One fragment to rule them all (Notes, that is) */ package org.wordpress.android.ui.notifications; import android.app.ListFragment; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.NonNull; import android.text.TextUtils; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.ListView; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.wordpress.android.R; import org.wordpress.android.datasets.NotificationsTable; import org.wordpress.android.datasets.ReaderCommentTable; import org.wordpress.android.datasets.ReaderPostTable; import org.wordpress.android.fluxc.model.CommentStatus; import org.wordpress.android.models.Note; import org.wordpress.android.ui.notifications.adapters.NoteBlockAdapter; import org.wordpress.android.ui.notifications.blocks.BlockType; import org.wordpress.android.ui.notifications.blocks.CommentUserNoteBlock; import org.wordpress.android.ui.notifications.blocks.FooterNoteBlock; import org.wordpress.android.ui.notifications.blocks.HeaderNoteBlock; import org.wordpress.android.ui.notifications.blocks.NoteBlock; import org.wordpress.android.ui.notifications.blocks.NoteBlockClickableSpan; import org.wordpress.android.ui.notifications.blocks.UserNoteBlock; import org.wordpress.android.ui.reader.ReaderActivityLauncher; import org.wordpress.android.ui.reader.actions.ReaderPostActions; import org.wordpress.android.ui.reader.services.ReaderCommentService; import org.wordpress.android.ui.reader.utils.ReaderUtils; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.JSONUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.widgets.WPNetworkImageView.ImageType; import java.util.ArrayList; import java.util.List; public class NotificationsDetailListFragment extends ListFragment implements NotificationFragment { private static final String KEY_NOTE_ID = "noteId"; private static final String KEY_LIST_POSITION = "listPosition"; private int mRestoredListPosition; private Note mNote; private LinearLayout mRootLayout; private ViewGroup mFooterView; private String mRestoredNoteId; private int mBackgroundColor; private int mCommentListPosition = ListView.INVALID_POSITION; private CommentUserNoteBlock.OnCommentStatusChangeListener mOnCommentStatusChangeListener; private NoteBlockAdapter mNoteBlockAdapter; public NotificationsDetailListFragment() { } public static NotificationsDetailListFragment newInstance(final String noteId) { NotificationsDetailListFragment fragment = new NotificationsDetailListFragment(); fragment.setNote(noteId); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null && savedInstanceState.containsKey(KEY_NOTE_ID)) { // The note will be set in onResume() // See WordPress.deferredInit() mRestoredNoteId = savedInstanceState.getString(KEY_NOTE_ID); mRestoredListPosition = savedInstanceState.getInt(KEY_LIST_POSITION, 0); } } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.notifications_fragment_detail_list, container, false); mRootLayout = (LinearLayout)view.findViewById(R.id.notifications_list_root); return view; } @Override public void onActivityCreated(Bundle bundle) { super.onActivityCreated(bundle); mBackgroundColor = getResources().getColor(R.color.white); ListView listView = getListView(); listView.setDivider(null); listView.setDividerHeight(0); listView.setHeaderDividersEnabled(false); if (mFooterView != null) { listView.addFooterView(mFooterView); } reloadNoteBlocks(); } @Override public void onResume() { super.onResume(); // Set the note if we retrieved the noteId from savedInstanceState if (!TextUtils.isEmpty(mRestoredNoteId)) { setNote(mRestoredNoteId); reloadNoteBlocks(); mRestoredNoteId = null; } if (getNote() == null) { showErrorToastAndFinish(); } } @Override public void onPause() { // Stop the reader comment service if it is running ReaderCommentService.stopService(getActivity()); super.onPause(); } @Override public Note getNote() { return mNote; } @Override public void setNote(String noteId) { if (noteId == null) { showErrorToastAndFinish(); return; } Note note = NotificationsTable.getNoteById(noteId); if (note == null) { showErrorToastAndFinish(); return; } mNote = note; } private void showErrorToastAndFinish() { AppLog.e(AppLog.T.NOTIFS, "Note could not be found."); if (getActivity() != null) { ToastUtils.showToast(getActivity(), R.string.error_notification_open); getActivity().finish(); } } @Override public void onSaveInstanceState(Bundle outState) { if (mNote != null) { outState.putString(KEY_NOTE_ID, mNote.getId()); outState.putInt(KEY_LIST_POSITION, getListView().getFirstVisiblePosition()); } super.onSaveInstanceState(outState); } private void reloadNoteBlocks() { new LoadNoteBlocksTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } public void setFooterView(ViewGroup footerView) { mFooterView = footerView; } private final NoteBlock.OnNoteBlockTextClickListener mOnNoteBlockTextClickListener = new NoteBlock.OnNoteBlockTextClickListener() { @Override public void onNoteBlockTextClicked(NoteBlockClickableSpan clickedSpan) { if (!isAdded() || !(getActivity() instanceof NotificationsDetailActivity)) return; handleNoteBlockSpanClick((NotificationsDetailActivity) getActivity(), clickedSpan); } @Override public void showDetailForNoteIds() { if (!isAdded() || mNote == null || !(getActivity() instanceof NotificationsDetailActivity)) { return; } NotificationsDetailActivity detailActivity = (NotificationsDetailActivity)getActivity(); if (mNote.isCommentReplyType() || (!mNote.isCommentType() && mNote.getCommentId() > 0)) { long commentId = mNote.isCommentReplyType() ? mNote.getParentCommentId() : mNote.getCommentId(); // show comments list if it exists in the reader if (ReaderUtils.postAndCommentExists(mNote.getSiteId(), mNote.getPostId(), commentId)) { detailActivity.showReaderCommentsList(mNote.getSiteId(), mNote.getPostId(), commentId); } else { detailActivity.showWebViewActivityForUrl(mNote.getUrl()); } } else if (mNote.isFollowType()) { detailActivity.showBlogPreviewActivity(mNote.getSiteId()); } else { // otherwise, load the post in the Reader detailActivity.showPostActivity(mNote.getSiteId(), mNote.getPostId()); } } @Override public void showReaderPostComments() { if (!isAdded() || mNote == null || mNote.getCommentId() == 0) return; ReaderActivityLauncher.showReaderComments(getActivity(), mNote.getSiteId(), mNote.getPostId(), mNote.getCommentId()); } @Override public void showSitePreview(long siteId, String siteUrl) { if (!isAdded() || mNote == null || !(getActivity() instanceof NotificationsDetailActivity)) { return; } NotificationsDetailActivity detailActivity = (NotificationsDetailActivity)getActivity(); if (siteId != 0) { detailActivity.showBlogPreviewActivity(siteId); } else if (!TextUtils.isEmpty(siteUrl)) { detailActivity.showWebViewActivityForUrl(siteUrl); } } public void handleNoteBlockSpanClick(NotificationsDetailActivity activity, NoteBlockClickableSpan clickedSpan) { switch (clickedSpan.getRangeType()) { case SITE: // Show blog preview activity.showBlogPreviewActivity(clickedSpan.getId()); break; case USER: // Show blog preview activity.showBlogPreviewActivity(clickedSpan.getSiteId()); break; case POST: // Show post detail activity.showPostActivity(clickedSpan.getSiteId(), clickedSpan.getId()); break; case COMMENT: // Load the comment in the reader list if it exists, otherwise show a webview if (ReaderUtils.postAndCommentExists(clickedSpan.getSiteId(), clickedSpan.getPostId(), clickedSpan.getId())) { activity.showReaderCommentsList(clickedSpan.getSiteId(), clickedSpan.getPostId(), clickedSpan.getId()); } else { activity.showWebViewActivityForUrl(clickedSpan.getUrl()); } break; case STAT: case FOLLOW: // We can open native stats if the site is a wpcom or Jetpack sites activity.showStatsActivityForSite(clickedSpan.getSiteId(), clickedSpan.getRangeType()); break; case LIKE: if (ReaderPostTable.postExists(clickedSpan.getSiteId(), clickedSpan.getId())) { activity.showReaderPostLikeUsers(clickedSpan.getSiteId(), clickedSpan.getId()); } else { activity.showPostActivity(clickedSpan.getSiteId(), clickedSpan.getId()); } break; default: // We don't know what type of id this is, let's see if it has a URL and push a webview if (!TextUtils.isEmpty(clickedSpan.getUrl())) { activity.showWebViewActivityForUrl(clickedSpan.getUrl()); } } } }; private final UserNoteBlock.OnGravatarClickedListener mOnGravatarClickedListener = new UserNoteBlock.OnGravatarClickedListener() { @Override public void onGravatarClicked(long siteId, long userId, String siteUrl) { if (!isAdded() || !(getActivity() instanceof NotificationsDetailActivity)) return; NotificationsDetailActivity detailActivity = (NotificationsDetailActivity)getActivity(); if (siteId == 0 && !TextUtils.isEmpty(siteUrl)) { detailActivity.showWebViewActivityForUrl(siteUrl); } else if (siteId != 0) { detailActivity.showBlogPreviewActivity(siteId); } } }; private boolean hasNoteBlockAdapter() { return mNoteBlockAdapter != null; } // Loop through the 'body' items in this note, and create blocks for each. private class LoadNoteBlocksTask extends AsyncTask<Void, Boolean, List<NoteBlock>> { private boolean mIsBadgeView; @Override protected List<NoteBlock> doInBackground(Void... params) { if (mNote == null) return null; requestReaderContentForNote(); JSONArray bodyArray = mNote.getBody(); final List<NoteBlock> noteList = new ArrayList<>(); // Add the note header if one was provided if (mNote.getHeader() != null) { ImageType imageType = mNote.isFollowType() ? ImageType.BLAVATAR : ImageType.AVATAR; HeaderNoteBlock headerNoteBlock = new HeaderNoteBlock( getActivity(), mNote.getHeader(), imageType, mOnNoteBlockTextClickListener, mOnGravatarClickedListener ); headerNoteBlock.setIsComment(mNote.isCommentType()); noteList.add(headerNoteBlock); } if (bodyArray != null && bodyArray.length() > 0) { for (int i=0; i < bodyArray.length(); i++) { try { JSONObject noteObject = bodyArray.getJSONObject(i); // Determine NoteBlock type and add it to the array NoteBlock noteBlock; String noteBlockTypeString = JSONUtils.queryJSON(noteObject, "type", ""); if (BlockType.fromString(noteBlockTypeString) == BlockType.USER) { if (mNote.isCommentType()) { // Set comment position so we can target it later // See refreshBlocksForCommentStatus() mCommentListPosition = i + noteList.size(); // We'll snag the next body array item for comment user blocks if (i + 1 < bodyArray.length()) { JSONObject commentTextBlock = bodyArray.getJSONObject(i + 1); noteObject.put("comment_text", commentTextBlock); i++; } // Add timestamp to block for display noteObject.put("timestamp", mNote.getTimestamp()); noteBlock = new CommentUserNoteBlock( getActivity(), noteObject, mOnNoteBlockTextClickListener, mOnGravatarClickedListener ); // Set listener for comment status changes, so we can update bg and text colors CommentUserNoteBlock commentUserNoteBlock = (CommentUserNoteBlock)noteBlock; mOnCommentStatusChangeListener = commentUserNoteBlock.getOnCommentChangeListener(); commentUserNoteBlock.setCommentStatus(mNote.getCommentStatus()); commentUserNoteBlock.configureResources(getActivity()); } else { noteBlock = new UserNoteBlock( getActivity(), noteObject, mOnNoteBlockTextClickListener, mOnGravatarClickedListener ); } } else if (isFooterBlock(noteObject)) { noteBlock = new FooterNoteBlock(noteObject, mOnNoteBlockTextClickListener); ((FooterNoteBlock)noteBlock).setClickableSpan( JSONUtils.queryJSON(noteObject, "ranges[last]", new JSONObject()), mNote.getType() ); } else { noteBlock = new NoteBlock(noteObject, mOnNoteBlockTextClickListener); } // Badge notifications apply different colors and formatting if (isAdded() && noteBlock.containsBadgeMediaType()) { mIsBadgeView = true; mBackgroundColor = getActivity().getResources().getColor(R.color.transparent); } if (mIsBadgeView) { noteBlock.setIsBadge(); } noteList.add(noteBlock); } catch (JSONException e) { AppLog.e(AppLog.T.NOTIFS, "Invalid note data, could not parse."); } } } return noteList; } @Override protected void onPostExecute(List<NoteBlock> noteList) { if (!isAdded() || noteList == null) return; if (mIsBadgeView) { mRootLayout.setGravity(Gravity.CENTER_VERTICAL); } if (!hasNoteBlockAdapter()) { mNoteBlockAdapter = new NoteBlockAdapter(getActivity(), noteList, mBackgroundColor); setListAdapter(mNoteBlockAdapter); } else { mNoteBlockAdapter.setNoteList(noteList); } if (mRestoredListPosition > 0) { getListView().setSelectionFromTop(mRestoredListPosition, 0); mRestoredListPosition = 0; } } } private boolean isFooterBlock(JSONObject blockObject) { if (mNote == null || blockObject == null) return false; if (mNote.isCommentType()) { // Check if this is a comment notification that has been replied to // The block will not have a type, and its id will match the comment reply id in the Note. return (JSONUtils.queryJSON(blockObject, "type", null) == null && mNote.getCommentReplyId() == JSONUtils.queryJSON(blockObject, "ranges[1].id", 0)); } else if (mNote.isFollowType() || mNote.isLikeType() || mNote.isCommentLikeType() || mNote.isReblogType()) { // User list notifications have a footer if they have 10 or more users in the body // The last block will not have a type, so we can use that to determine if it is the footer return JSONUtils.queryJSON(blockObject, "type", null) == null; } return false; } public void refreshBlocksForCommentStatus(CommentStatus newStatus) { if (mOnCommentStatusChangeListener != null) { mOnCommentStatusChangeListener.onCommentStatusChanged(newStatus); ListView listView = getListView(); if (listView == null || mCommentListPosition == ListView.INVALID_POSITION) { return; } // Redraw the comment row if it is visible so that the background and text colors update // See: http://stackoverflow.com/questions/4075975/redraw-a-single-row-in-a-listview/9987616#9987616 int firstPosition = listView.getFirstVisiblePosition(); int endPosition = listView.getLastVisiblePosition(); for (int i = firstPosition; i < endPosition; i++) { if (mCommentListPosition == i) { View view = listView.getChildAt(i - firstPosition); listView.getAdapter().getView(i, view, listView); break; } } } } // Requests Reader content for certain notification types private void requestReaderContentForNote() { if (mNote == null || !isAdded()) return; // Request the reader post so that loading reader activities will work. if (mNote.isUserList() && !ReaderPostTable.postExists(mNote.getSiteId(), mNote.getPostId())) { ReaderPostActions.requestBlogPost(mNote.getSiteId(), mNote.getPostId(), null); } // Request reader comments until we retrieve the comment for this note if ((mNote.isCommentLikeType() || mNote.isCommentReplyType() || mNote.isCommentWithUserReply()) && !ReaderCommentTable.commentExists(mNote.getSiteId(), mNote.getPostId(), mNote.getCommentId())) { ReaderCommentService.startServiceForComment(getActivity(), mNote.getSiteId(), mNote.getPostId(), mNote.getCommentId()); } } }