| /** |
| * 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 com.simperium.client.Bucket; |
| import com.simperium.client.BucketObjectMissingException; |
| |
| import org.json.JSONArray; |
| import org.json.JSONException; |
| import org.json.JSONObject; |
| import org.wordpress.android.R; |
| import org.wordpress.android.datasets.ReaderCommentTable; |
| import org.wordpress.android.datasets.ReaderPostTable; |
| import org.wordpress.android.models.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.notifications.utils.NotificationsUtils; |
| import org.wordpress.android.ui.notifications.utils.SimperiumUtils; |
| 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.widgets.WPNetworkImageView.ImageType; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import de.greenrobot.event.EventBus; |
| |
| public class NotificationsDetailListFragment extends ListFragment implements NotificationFragment, Bucket.Listener<Note> { |
| private static final String KEY_NOTE_ID = "noteId"; |
| private static final String KEY_LIST_POSITION = "listPosition"; |
| |
| private int mRestoredListPosition; |
| |
| public interface OnNoteChangeListener { |
| void onNoteChanged(Note note); |
| } |
| |
| private Note mNote; |
| private LinearLayout mRootLayout; |
| private ViewGroup mFooterView; |
| |
| private String mRestoredNoteId; |
| private int mBackgroundColor; |
| private int mCommentListPosition = ListView.INVALID_POSITION; |
| private boolean mIsUnread; |
| |
| private CommentUserNoteBlock.OnCommentStatusChangeListener mOnCommentStatusChangeListener; |
| private OnNoteChangeListener mOnNoteChangeListener; |
| private NoteBlockAdapter mNoteBlockAdapter; |
| |
| public NotificationsDetailListFragment() { |
| } |
| |
| public static NotificationsDetailListFragment newInstance(final String noteId) { |
| NotificationsDetailListFragment fragment = new NotificationsDetailListFragment(); |
| fragment.setNoteWithNoteId(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() because Simperium will be running there |
| // 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(); |
| |
| // Start listening to bucket change events |
| if (SimperiumUtils.getNotesBucket() != null) { |
| SimperiumUtils.getNotesBucket().addListener(this); |
| } |
| |
| // Set the note if we retrieved the noteId from savedInstanceState |
| if (!TextUtils.isEmpty(mRestoredNoteId)) { |
| setNoteWithNoteId(mRestoredNoteId); |
| reloadNoteBlocks(); |
| mRestoredNoteId = null; |
| } |
| } |
| |
| @Override |
| public void onPause() { |
| // Remove the simperium bucket listener |
| if (SimperiumUtils.getNotesBucket() != null) { |
| SimperiumUtils.getNotesBucket().removeListener(this); |
| } |
| |
| // Stop the reader comment service if it is running |
| ReaderCommentService.stopService(getActivity()); |
| |
| super.onPause(); |
| } |
| |
| @Override |
| public Note getNote() { |
| return mNote; |
| } |
| |
| @Override |
| public void setNote(Note note) { |
| mNote = note; |
| } |
| |
| private void setNoteWithNoteId(String noteId) { |
| if (noteId == null) return; |
| |
| if (SimperiumUtils.getNotesBucket() != null) { |
| try { |
| Note note = SimperiumUtils.getNotesBucket().get(noteId); |
| mIsUnread = note.isUnread(); |
| setNote(note); |
| } catch (BucketObjectMissingException e) { |
| e.printStackTrace(); |
| } |
| } |
| } |
| |
| @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); |
| } |
| |
| public void setOnNoteChangeListener(OnNoteChangeListener listener) { |
| mOnNoteChangeListener = listener; |
| } |
| |
| 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; |
| |
| NotificationsUtils.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); |
| } |
| } |
| }; |
| |
| 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.requestPost(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()); |
| } |
| } |
| |
| // Simperium bucket listener |
| @Override |
| public void onBeforeUpdateObject(Bucket<Note> noteBucket, Note note) { |
| // noop |
| } |
| |
| @Override |
| public void onDeleteObject(Bucket<Note> noteBucket, Note note) { |
| // noop |
| } |
| |
| @Override |
| public void onNetworkChange(Bucket<Note> noteBucket, Bucket.ChangeType changeType, String noteId) { |
| // We're not interested in INDEX events here |
| if (changeType == Bucket.ChangeType.INDEX) return; |
| |
| // Refresh content if we receive a change for the Note |
| if (mNote != null && mNote.getId().equals(noteId)) { |
| // If the note was removed, pop the back stack to return to the notes list |
| if (changeType == Bucket.ChangeType.REMOVE) { |
| getFragmentManager().popBackStack(); |
| return; |
| } |
| |
| try { |
| mNote = noteBucket.get(noteId); |
| |
| // Don't refresh if the note was just marked as read |
| if (!mNote.isUnread() && mIsUnread) { |
| mIsUnread = false; |
| return; |
| } |
| |
| // Mark note as read since we are looking at it already |
| if (mNote.isUnread()) { |
| mNote.markAsRead(); |
| EventBus.getDefault().post(new NotificationEvents.NotificationsChanged()); |
| } |
| |
| if (getActivity() != null) { |
| getActivity().runOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| reloadNoteBlocks(); |
| if (mOnNoteChangeListener != null) { |
| mOnNoteChangeListener.onNoteChanged(mNote); |
| } |
| } |
| }); |
| } |
| } catch (BucketObjectMissingException e) { |
| AppLog.e(AppLog.T.NOTIFS, "Couldn't load note after receiving change."); |
| } |
| } |
| } |
| |
| @Override |
| public void onSaveObject(Bucket<Note> noteBucket, Note note) { |
| // noop |
| } |
| } |