Configure ItemTouchHelper in RecyclerView to work for Snackbar being dismissed by another Snackbar being displayed

Viewed 92

I have a RecyclerView in my app that contains messages. On swiping right, I've configured it to delete the messages and on swiping left, to archive the messages. Here's the code for the ItemTouchHelper part related to deleting and archiving:

if (direction == ItemTouchHelper.LEFT) {
    donorMessagesAdapter.deleteMessages(position);
} else if (direction == ItemTouchHelper.RIGHT) {
    donorMessagesAdapter.archiveMessages(position);
}

Here are the deleteMessages() and archiveMessages() functions (they're almost the same):

public void deleteMessages(final int position) {
    recentlyDeletedPeer = sortedPeers.get(position);
    recentlyDeletedPeerPosition = position;

    sortedPeers.remove(position);
    notifyItemRemoved(position);

    Snackbar snackbar = Snackbar.make(activity.findViewById(R.id.constraint_layout),
            "Messages Deleted", Snackbar.LENGTH_LONG);
    snackbar.setAction("UNDO", new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            sortedPeers.add(recentlyDeletedPeerPosition, recentlyDeletedPeer);
            notifyItemInserted(recentlyDeletedPeerPosition);
        }
    });
    snackbar.setActionTextColor(0xff0099ff);

    snackbar.addCallback(new Snackbar.Callback() {
        @Override
        public void onDismissed(Snackbar snackbar, int event) {
            if (event == Snackbar.Callback.DISMISS_EVENT_TIMEOUT || event == Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE) {
                MainActivity.database.receivedMessagesDao().deleteAllReceivedMessagesFromSender(email, recentlyDeletedPeer);
                MainActivity.database.sentMessagesDao().deleteAllSentMessagesFromReceiver(email, recentlyDeletedPeer);
            }
        }
    });

    snackbar.show();
}

public void archiveMessages(final int position) {
    recentlyArchivedPeer = sortedPeers.get(position);
    recentlyArchivedPeerPosition = position;

    sortedPeers.remove(position);
    notifyItemRemoved(position);

    Snackbar snackbar = Snackbar.make(activity.findViewById(R.id.constraint_layout),
            "Messages archived", Snackbar.LENGTH_LONG);
    snackbar.setAction("UNDO", new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            sortedPeers.add(recentlyArchivedPeerPosition, recentlyArchivedPeer);
            notifyItemInserted(recentlyArchivedPeerPosition);
        }
    });
    snackbar.setActionTextColor(0xff0099ff);

    snackbar.addCallback(new Snackbar.Callback() {
        @Override
        public void onDismissed(Snackbar snackbar, int event) {
            if (event == Snackbar.Callback.DISMISS_EVENT_TIMEOUT || event == Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE) {
                MainActivity.database.receivedMessagesDao().archive(email, recentlyArchivedPeer);
                MainActivity.database.sentMessagesDao().archive(email, recentlyArchivedPeer);
            }
        }
    });

    snackbar.show();
}

Everything works perfectly well when deleting/archiving one message. Swiping deletes or archives the message after the Snackbar disappears, and when I click undo, the deletion/archival is reversed. Problems arise when deleting/archiving multiple messages.

Problem 1: When I delete/archive multiple messages without waiting for the Snackbar to disappear for each message, all except the first message is deleted/archived. What it makes it stranger for me is that it's not that only the last message is deleted/archived, all except the first are deleted/archived.

Problem 2: When I delete/archive multiple messages without waiting for the Snackbar to disappear for each message, if I press UNDO, that is, on the Snackbar for the last message that was deleted/archived, the app crashes.

By the way,I'm using Room Library to store the messages in a database (referenced by MainActivity.database).

I think the problem is in the addCallback() function for the Snackbar. I'm supposed to do something differently for event == Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE from event == Snackbar.Callback.DISMISS_EVENT_TIMEOUT, but I can't figure out exactly what I'm supposed to do differently, and what exactly causes the problem.

Here is my entire adapter class for reference (error message lines marked with comment next to them):

package com.example.treeapp;

import android.app.Activity;
import android.content.Intent;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.google.android.material.snackbar.Snackbar;
import com.mikhaellopez.circularimageview.CircularImageView;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

public class DonorMessagesAdapter extends RecyclerView.Adapter<DonorMessagesAdapter.DonorMessagesViewHolder> { // line 24 in error message
    private Activity activity;
    public String email;
    public List<String> sortedPeers;
    public static CircularImageView imageView;
    private String recentlyDeletedPeer;
    private int recentlyDeletedPeerPosition;
    private String recentlyArchivedPeer;
    private int recentlyArchivedPeerPosition;

    public static class DonorMessagesViewHolder extends RecyclerView.ViewHolder {
        public Button middleButton;
        public TextView nameTextView;
        public TextView messageTextView;

        DonorMessagesViewHolder(View view) {
            super(view);

            middleButton = view.findViewById(R.id.middle_button);
            imageView = view.findViewById(R.id.image_view);
            nameTextView = view.findViewById(R.id.text_view_name);
            messageTextView = view.findViewById(R.id.text_view_message);
        }
    }

    DonorMessagesAdapter(Activity activity, String email) {
        this.activity = activity;
        this.email = email;
        this.sortedPeers = getSortedPeers(email);
    }

    @NonNull
    @Override
    public DonorMessagesViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.message, parent, false);

        return new DonorMessagesViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull DonorMessagesViewHolder holder, int position) {
        final String current = sortedPeers.get(position);

        // Start of getting last message
        String lastMessage;
        String lastMessageType;
        List<Integer> receivedMessages = MainActivity.database.receivedMessagesDao().getMessagesToReceiverFromSender(email, current, 0);
        List<Integer> sentMessages = MainActivity.database.sentMessagesDao().getMessagesBySenderToReceiver(email, current, 0);

        if (sentMessages.size() == 0) {
            lastMessage = MainActivity.database.receivedMessagesDao().getMessage(receivedMessages.get(receivedMessages.size() - 1));  // line 74 in error message
            lastMessageType = "received";
            if (MainActivity.database.receivedMessagesDao().getImage(receivedMessages.get(receivedMessages.size() - 1)) == 1) {
                lastMessageType = "receivedImage";
            }
        } else if (receivedMessages.size() == 0) {
            lastMessage = MainActivity.database.sentMessagesDao().getMessage(sentMessages.get(sentMessages.size() - 1));
            lastMessageType = "sent";
            if (MainActivity.database.sentMessagesDao().getImage(sentMessages.get(sentMessages.size() - 1)) == 1) {
                lastMessageType = "sentImage";
            }
        } else {
            LocalDateTime lastReceivedMessageTime = LocalDateTime.parse(MainActivity.database.receivedMessagesDao().getTime(receivedMessages.get(receivedMessages.size() - 1)));
            LocalDateTime lastSentMessageTime = LocalDateTime.parse(MainActivity.database.sentMessagesDao().getTime(sentMessages.get(sentMessages.size() - 1)));

            if (lastReceivedMessageTime.isAfter(lastSentMessageTime)) {
                lastMessage = MainActivity.database.receivedMessagesDao().getMessage(receivedMessages.get(receivedMessages.size() - 1));
                lastMessageType = "received";
                if (MainActivity.database.receivedMessagesDao().getImage(receivedMessages.get(receivedMessages.size() - 1)) == 1) {
                    lastMessageType = "receivedImage";
                }
            } else {
                lastMessage = MainActivity.database.sentMessagesDao().getMessage(sentMessages.get(sentMessages.size() - 1));
                lastMessageType = "sent";
                if (MainActivity.database.sentMessagesDao().getImage(sentMessages.get(sentMessages.size() - 1)) == 1) {
                    lastMessageType = "sentImage";
                }
            }
        }
        // End of getting last message

        if (MainActivity.database.plantersDao().checkEmail(current) == 1) {
            if (MainActivity.database.plantersDao().getImageUrl(current) != null) {
                new DownloadImageTask(imageView).execute(MainActivity.database.plantersDao().getImageUrl(current));
            }
        } else if (MainActivity.database.donorsDao().checkEmail(current) == 1) {
            if (MainActivity.database.donorsDao().getImageUrl(current) != null) {
                new DownloadImageTask(imageView).execute(MainActivity.database.donorsDao().getImageUrl(current));
            }
        }

        if (MainActivity.database.plantersDao().checkEmail(current) == 1) {
            holder.nameTextView.setText(MainActivity.database.plantersDao().getName(current));
        } else if (MainActivity.database.donorsDao().checkEmail(current) == 1) {
            holder.nameTextView.setText(MainActivity.database.donorsDao().getName(current));
        }

        if (lastMessageType.equals("received")) {
            if (lastMessage.length() > 17) {
                holder.messageTextView.setText(lastMessage.substring(0, 16) + "...");
            } else {
                holder.messageTextView.setText(lastMessage);
            }
        } else if (lastMessageType.equals("sent")) {
            if (lastMessage.length() > 12) {
                holder.messageTextView.setText("You: " + lastMessage.substring(0, 11) + "...");
            } else {
                holder.messageTextView.setText("You: " + lastMessage);
            }
        } else if (lastMessageType.equals("receivedImage")) {
            holder.messageTextView.setText("Image");
        } else if (lastMessageType.equals("sentImage")) {
            holder.messageTextView.setText("You: " + "Image");
        }

        holder.middleButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(view.getContext(), MessageActivity.class);
                intent.putExtra("email", email);
                intent.putExtra("peerEmail", current);
                intent.putExtra("archived", 0);

                view.getContext().startActivity(intent);
            }
        });
    }

    @Override
    public int getItemCount() {
        return sortedPeers.size();
    }

    public void reload() {
        sortedPeers = getSortedPeers(email);
        notifyDataSetChanged();
    }

    private List<String> getSortedPeers(String email) {
        List<String> senders = MainActivity.database.receivedMessagesDao().getSendersForReceiver(email, 0);
        List<String> receivers = MainActivity.database.sentMessagesDao().getReceiversForSender(email, 0);

        List<String> peers = new ArrayList<>();
        peers.addAll(senders);
        peers.addAll(receivers);
        peers = new ArrayList<>(new HashSet<>(peers));

        List<String> sortedPeers = new ArrayList<>();
        while (peers.size() > 0) {
            LocalDateTime latestMessageTime = null;
            String latestPeer = null;

            for (String peer : peers) {
                List<Integer> receivedMessages = MainActivity.database.receivedMessagesDao().getMessagesToReceiverFromSender(email, peer, 0);
                List<Integer> sentMessages = MainActivity.database.sentMessagesDao().getMessagesBySenderToReceiver(email, peer, 0);

                LocalDateTime lastMessageTime;
                if (sentMessages.size() == 0) {
                    lastMessageTime = LocalDateTime.parse(MainActivity.database.receivedMessagesDao().getTime(receivedMessages.get(receivedMessages.size() - 1)));
                } else if (receivedMessages.size() == 0) {
                    lastMessageTime = LocalDateTime.parse(MainActivity.database.sentMessagesDao().getTime(sentMessages.get(sentMessages.size() - 1)));
                } else {
                    LocalDateTime lastReceivedMessageTime = LocalDateTime.parse(MainActivity.database.receivedMessagesDao().getTime(receivedMessages.get(receivedMessages.size() - 1)));
                    LocalDateTime lastSentMessageTime = LocalDateTime.parse(MainActivity.database.sentMessagesDao().getTime(sentMessages.get(sentMessages.size() - 1)));

                    if (lastReceivedMessageTime.isAfter(lastSentMessageTime)) {
                        lastMessageTime = lastReceivedMessageTime;
                    } else {
                        lastMessageTime = lastSentMessageTime;
                    }
                }

                if (latestMessageTime != null) {
                    if (lastMessageTime.isAfter(latestMessageTime)) {
                        latestMessageTime = lastMessageTime;
                        latestPeer = peer;
                    }
                } else {
                    latestMessageTime = lastMessageTime;
                    latestPeer = peer;
                }
            }

            sortedPeers.add(latestPeer);
            peers.remove(latestPeer);
        }

        return sortedPeers;
    }

    public void deleteMessages(final int position) {
        recentlyDeletedPeer = sortedPeers.get(position);
        recentlyDeletedPeerPosition = position;

        sortedPeers.remove(position);
        notifyItemRemoved(position);

        Snackbar snackbar = Snackbar.make(activity.findViewById(R.id.constraint_layout),
                "Messages Deleted", Snackbar.LENGTH_LONG);
        snackbar.setAction("UNDO", new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                sortedPeers.add(recentlyDeletedPeerPosition, recentlyDeletedPeer);
                notifyItemInserted(recentlyDeletedPeerPosition);
            }
        });
        snackbar.setActionTextColor(0xff0099ff);

        snackbar.addCallback(new Snackbar.Callback() {
            @Override
            public void onDismissed(Snackbar snackbar, int event) {
                if (event == Snackbar.Callback.DISMISS_EVENT_TIMEOUT || event == Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE) {
                    MainActivity.database.receivedMessagesDao().deleteAllReceivedMessagesFromSender(email, recentlyDeletedPeer);
                    MainActivity.database.sentMessagesDao().deleteAllSentMessagesFromReceiver(email, recentlyDeletedPeer);
                }
            }
        });

        snackbar.show();
    }

    public void archiveMessages(final int position) {
        recentlyArchivedPeer = sortedPeers.get(position);
        recentlyArchivedPeerPosition = position;

        sortedPeers.remove(position);
        notifyItemRemoved(position);

        Snackbar snackbar = Snackbar.make(activity.findViewById(R.id.constraint_layout),
                "Messages archived", Snackbar.LENGTH_LONG);
        snackbar.setAction("UNDO", new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                sortedPeers.add(recentlyArchivedPeerPosition, recentlyArchivedPeer);
                notifyItemInserted(recentlyArchivedPeerPosition);
            }
        });
        snackbar.setActionTextColor(0xff0099ff);

        snackbar.addCallback(new Snackbar.Callback() {
            @Override
            public void onDismissed(Snackbar snackbar, int event) {
                if (event == Snackbar.Callback.DISMISS_EVENT_TIMEOUT || event == Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE) {
                    MainActivity.database.receivedMessagesDao().archive(email, recentlyArchivedPeer);
                    MainActivity.database.sentMessagesDao().archive(email, recentlyArchivedPeer);
                    reload();
                }
            }
        });

        snackbar.show();
    }
}

And here is the error I get (my classes marked with **):

2020-09-12 19:24:11.547 689-689/com.example.treeapp E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.treeapp, PID: 689
    java.lang.ArrayIndexOutOfBoundsException: length=0; index=-1
        at java.util.ArrayList.get(ArrayList.java:439)
        **at com.example.treeapp.DonorMessagesAdapter.onBindViewHolder(DonorMessagesAdapter.java:74)
        at com.example.treeapp.DonorMessagesAdapter.onBindViewHolder(DonorMessagesAdapter.java:24)**
        at androidx.recyclerview.widget.RecyclerView$Adapter.onBindViewHolder(RecyclerView.java:7065)
        at androidx.recyclerview.widget.RecyclerView$Adapter.bindViewHolder(RecyclerView.java:7107)
        at androidx.recyclerview.widget.RecyclerView$Recycler.tryBindViewHolderByDeadline(RecyclerView.java:6012)
        at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6279)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6118)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6114)
        at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2303)
        at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1627)
        at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1587)
        at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:665)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4134)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3851)
        at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4404)
        at android.view.View.layout(View.java:20967)
        at android.view.ViewGroup.layout(ViewGroup.java:6440)
        at androidx.constraintlayout.widget.ConstraintLayout.onLayout(ConstraintLayout.java:1762)
        at android.view.View.layout(View.java:20967)
        at android.view.ViewGroup.layout(ViewGroup.java:6440)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
        at android.view.View.layout(View.java:20967)
        at android.view.ViewGroup.layout(ViewGroup.java:6440)
        at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1791)
        at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1635)
        at android.widget.LinearLayout.onLayout(LinearLayout.java:1544)
        at android.view.View.layout(View.java:20967)
        at android.view.ViewGroup.layout(ViewGroup.java:6440)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
        at android.view.View.layout(View.java:20967)
        at android.view.ViewGroup.layout(ViewGroup.java:6440)
        at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1791)
        at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1635)
        at android.widget.LinearLayout.onLayout(LinearLayout.java:1544)
        at android.view.View.layout(View.java:20967)
        at android.view.ViewGroup.layout(ViewGroup.java:6440)
        at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
        at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
        at com.android.internal.policy.DecorView.onLayout(DecorView.java:955)
        at android.view.View.layout(View.java:20967)
        at android.view.ViewGroup.layout(ViewGroup.java:6440)
        at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:3092)
        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2779)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1863)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8072)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:911)
        at android.view.Choreographer.doCallbacks(Choreographer.java:723)
        at android.view.Choreographer.doFrame(Choreographer.java:658)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:897)
        at android.os.Handler.handleCallback(Handler.java:790)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:164)
1 Answers

To solve your problem no.1 you could try to only check if the dismiss was caused by a click e.g.:

snackbar.addCallback(new Snackbar.Callback() {
    @Override
    public void onDismissed(Snackbar snackbar, int event) {
        if (event != Snackbar.Callback.DISMISS_EVENT_ACTION) {
            MainActivity.database.receivedMessagesDao().deleteAllReceivedMessagesFromSender(email, recentlyDeletedPeer);
            MainActivity.database.sentMessagesDao().deleteAllSentMessagesFromReceiver(email, recentlyDeletedPeer);
        }
    }
});

As for your second problem, the crash happens because you only check if the sentMessages list is empty

if (sentMessages.size() == 0) {
        lastMessage = MainActivity.database.receivedMessagesDao().getMessage(receivedMessages.get(receivedMessages.size() - 1));  // line 74 in error message
        lastMessageType = "received";
        if (MainActivity.database.receivedMessagesDao().getImage(receivedMessages.get(receivedMessages.size() - 1)) == 1) {
            lastMessageType = "receivedImage";
        }
    }

What you forget to do here is to check whether reveivedMessages is also empty. For example:

if (sentMessages.size() == 0) {
    if(receivedMessages.size() != 0) {
        lastMessage = MainActivity.database.receivedMessagesDao().getMessage(receivedMessages.get(receivedMessages.size() - 1));  // line 74 in error message
        lastMessageType = "received";
        if (MainActivity.database.receivedMessagesDao().getImage(receivedMessages.get(receivedMessages.size() - 1)) == 1) {
            lastMessageType = "receivedImage";
        }
    }
}

What happens is your receivedMessages list is also empty which means you try to get the item at position -1, which obviously wont work.

Even if these tips solve your problem you should seriously think about how you access your data in the database, because you seem to execute all of the calls on the UI Thread, which is not advisable since it will block the UI Thread.

Related