MarshallAsch/veil-droid

View on GitHub
app/src/main/java/ca/marshallasch/veil/PostListAdapter.java

Summary

Maintainability
C
7 hrs
Test Coverage
package ca.marshallasch.veil;

import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.UiThread;
import android.support.v7.widget.RecyclerView;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.ImageView;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import ca.marshallasch.veil.comparators.PostAgeComparator;
import ca.marshallasch.veil.comparators.PostAuthorComparator;
import ca.marshallasch.veil.comparators.PostTitleComparator;
import ca.marshallasch.veil.database.KnownPostsContract;
import ca.marshallasch.veil.proto.DhtProto;
import ca.marshallasch.veil.utilities.Util;


/**
 * This class serves as an adapter for {@link FragmentDiscoverForums}. This class will hold the list
 * of posts to display to the user in the list.
 *
 * @author  Weihan Li
 * @version 1.0
 * @since 2018-06-04
 */
public class PostListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements Filterable{

    private List<DhtProto.Post> posts;
    private List<DhtProto.Post> postsFiltered;

    private final Activity activity;

    private CharSequence lastFilter = "";

    /**
     * Sort options.
     */
    public enum SortOption {
        ALPHA_TITLE_ASC,
        ALPHA_TITLE_DESC,
        ALPHA_AUTH_ASC,
        ALPHA_AUTH_DESC,
        AGE_ASC,
        AGE_DESC
    }

    /**
     * This is the cell for if there are no posts in the list
     */
     private static class ViewHolder0 extends RecyclerView.ViewHolder {

        /**
         * constructor for the ViewHolder class
         * @param itemsView the XML layout for the cell. Currently is a post_list_cell.xml
         */
        ViewHolder0(View itemsView){
            super(itemsView);
        }
    }

    /**
     * This is the cell for each post in the list.
     */
    private static class ViewHolder1 extends RecyclerView.ViewHolder {
        private final TextView title;
        private final TextView contentPreview;
        private final TextView commentCount;
        private final TextView authorName;
        private final TextView timeStamp;
        private final ImageView readMarker;
        private final ImageView protectedMarker;

        /**
         * constructor for the ViewHolder class
         * @param itemsView the XML layout for the cell. Currently is a post_list_cell.xml
         */
        ViewHolder1(View itemsView){
            super(itemsView);
            title = itemsView.findViewById(R.id.title);
            contentPreview = itemsView.findViewById(R.id.content_preview);
            commentCount = itemsView.findViewById(R.id.comments);
            authorName = itemsView.findViewById(R.id.author_name);
            timeStamp = itemsView.findViewById(R.id.time_stamp);
            readMarker = itemsView.findViewById(R.id.unread_marker);
            protectedMarker = itemsView.findViewById(R.id.protected_image);
        }
    }

    /**
     * Constructor for this current class. Will set the list content for the cells.
     *
     * @param posts The list of posts to display
     */
    public PostListAdapter(List<DhtProto.Post> posts, Activity activity) {
        this.activity = activity;
        this.posts = posts;
        this.postsFiltered = posts;
    }

    /**
     * This will refresh the posts that are in the list. This will also re-filter the results.
     * It will eventually call {@link #notifyDataSetChanged()} so must be called from the UI thread.
     * @param posts the new list of posts to display.
     */
    @UiThread
    public void update(List<DhtProto.Post> posts) {
        this.posts = posts;
        getFilter().filter(lastFilter);
    }

    /**
     * This will check what type of view to generate depending on the number of items in the list.
     * @param position the position of the cell to check the type for
     * @return 0 for the no post type, 1 otherwise
     */
    @Override
    public int getItemViewType(int position)
    {
        return postsFiltered.size() == 0 ? 0 : 1;
    }

    /**
     * Creates the cell view if there is no existing cells available for recycler view can reuse.
     * @param parent the list that it belongs to
     * @param viewType the type of view that is needed, in case there is more then one type in the list
     * @return viewHolder
     */
    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view;

        if (viewType == 0) {
            view = LayoutInflater.from(parent.getContext()).inflate(R.layout.no_posts_cell, parent, false);

            return  new ViewHolder0(view);
        } else {
            view = LayoutInflater.from(parent.getContext()).inflate(R.layout.post_list_cell, parent, false);
            return  new ViewHolder1(view);
        }
    }

    /**
     * Binds new information to the cell of the list based on position.
     * @param holder the UI cell item that is going to hold the data
     * @param position the position in the list that the cell is for.
     */
    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, final int position) {

        if (holder.getItemViewType() == 0) {
            return;
        }

        ViewHolder1 viewHolder = (ViewHolder1) holder;

        // Going to need to trim the content before it gets added here.
        DhtProto.Post post = postsFiltered.get(position);

        int numComments = DataStore.getInstance(activity).getNumCommentsFor(post.getUuid());
        boolean read = DataStore.getInstance(activity).isRead(post.getUuid());
        String authorName = post.getAnonymous() ? activity.getString(R.string.anonymous) : post.getAuthorName();

        viewHolder.title.setText(post.getTitle());
        viewHolder.contentPreview.setText(post.getMessage());
        viewHolder.commentCount.setText(activity.getString(R.string.num_comments, numComments));
        viewHolder.authorName.setText(authorName);

        //set or unset the protected marker
        updatePostStatus(viewHolder, post);


        // show or hide the read marker
        viewHolder.readMarker.setVisibility(read ? View.INVISIBLE : View.VISIBLE);

        // generate a string that describes the age of the post, 1s, 4 min, 5 hours, etc.
        CharSequence dateString = DateUtils.getRelativeTimeSpanString(
                Util.timestampToDate(post.getTimestamp()).getTime(),
                System.currentTimeMillis(),
                0,
                DateUtils.FORMAT_ABBREV_RELATIVE);

        viewHolder.timeStamp.setText(dateString);

        viewHolder.itemView.findViewById(R.id.view_btn).setOnClickListener(view -> {
            FragmentViewPost fragViewPost = new FragmentViewPost();
            //create a bundle to pass data to the next fragment for viewing
            Bundle bundle = new Bundle();

            bundle.putByteArray(activity.getString(R.string.post_object_key), post.toByteArray());

            fragViewPost.setArguments(bundle);
            ((MainActivity) activity).navigateTo(fragViewPost, true);
        });

        viewHolder.itemView.findViewById(R.id.protect_button).setOnClickListener(view -> {
            int postStatus = DataStore.getInstance(activity).getPostStatus(post.getUuid());

            if(postStatus == KnownPostsContract.POST_PROTECTED){
                DataStore.getInstance(activity).setPostStatus(post.getUuid(), KnownPostsContract.POST_NORMAL);
            }
            else if(postStatus == KnownPostsContract.POST_NORMAL){
                DataStore.getInstance(activity).setPostStatus(post.getUuid(), KnownPostsContract.POST_PROTECTED);
            }

            updatePostStatus(viewHolder, post);
        });

        viewHolder.itemView.findViewById(R.id.delete_button).setOnClickListener(view -> {
            DataStore.getInstance(activity).setPostStatus(post.getUuid(), KnownPostsContract.POST_DEAD);
            updatePostStatus(viewHolder, post);
        });
    }

    /**
     * This function will sort the list of posts and will update the UI. Because this will update
     * the UI it <b>MUST</b> be run on the UI thread
     * @param option the way that the list should be sorted
     */
    @UiThread
    public void sort(SortOption option) {

        switch (option) {
            case ALPHA_TITLE_ASC:
                Collections.sort(postsFiltered, new PostTitleComparator());
                break;
            case ALPHA_TITLE_DESC:
                Collections.sort(postsFiltered, new PostTitleComparator());
                Collections.reverse(postsFiltered);
                break;
            case ALPHA_AUTH_ASC:
                Collections.sort(postsFiltered, new PostAuthorComparator());
                break;
            case ALPHA_AUTH_DESC:
                Collections.sort(postsFiltered, new PostAuthorComparator());
                Collections.reverse(postsFiltered);
                break;
            case AGE_ASC:
                Collections.sort(postsFiltered, new PostAgeComparator());
                break;
            case AGE_DESC:
                Collections.sort(postsFiltered, new PostAgeComparator());
                Collections.reverse(postsFiltered);
                break;
            default:
        }

        notifyDataSetChanged();
    }

    /**
     * Returns the total number of items in the data set held by this adapter.
     * @return posts.size()
     */
    @Override
    public int getItemCount() {
        return postsFiltered.size() == 0 ? 1 : postsFiltered.size();
    }

    @Override
    public Filter getFilter() {
        /*
         * This class is the tag filter. The CharSequence must be a list of tags that are
         * denominated with a ':' character.
         */
        return new Filter() {
            @Override
            protected FilterResults performFiltering(CharSequence charSequence) {

                lastFilter = charSequence;
                List<DhtProto.Post> filteredList = new ArrayList<>();

                if (charSequence.length() == 0) {
                    filteredList = posts;
                } else {
                    String[] tokens = charSequence.toString().split(":");

                    List<String> tagList = new ArrayList<>();

                    for (String tag: tokens){
                        tagList.add(tag.trim());
                    }

                    // Check if post contains all specified tags
                    for (DhtProto.Post post: posts) {
                        if (post.getTagsList().containsAll(tagList)) {
                            filteredList.add(post);
                        }
                    }
                }

                FilterResults filterResults = new FilterResults();
                filterResults.values = filteredList;
                return filterResults;
            }

            @Override
            protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
                postsFiltered = (ArrayList<DhtProto.Post>) filterResults.values;
                notifyDataSetChanged();
            }
        };
    }

    /**
     * This function will update the protected marker for a post to make sure that it shows properly.
     * @param viewHolder the cell for the post
     * @param post the post that is contained within the cell.
     */
    private void updatePostStatus(ViewHolder1 viewHolder, DhtProto.Post post){
        int postStatus = DataStore.getInstance(activity).getPostStatus(post.getUuid());

        //sets the drawable
        int drawable = (postStatus == KnownPostsContract.POST_PROTECTED) ?
                R.drawable.ic_protected : R.drawable.ic_unprotected;
        viewHolder.protectedMarker.setImageResource(drawable);
    }
}