Commit 9a665f3c authored by Thomas's avatar Thomas
Browse files

cache for notifications

parent 62b0caec
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -35,6 +35,12 @@ public class Notification {
    public Account account;
    @SerializedName("status")
    public Status status;
    public PositionFetchMore positionFetchMore = PositionFetchMore.BOTTOM;

    public enum PositionFetchMore {
        TOP,
        BOTTOM
    }

    public transient List<Notification> relatedNotifications;
    public boolean isFetchMore;
+147 −2
Original line number Diff line number Diff line
@@ -28,6 +28,8 @@ import java.util.Date;
import java.util.List;

import app.fedilab.android.activities.MainActivity;
import app.fedilab.android.client.entities.api.Notification;
import app.fedilab.android.client.entities.api.Notifications;
import app.fedilab.android.client.entities.api.Pagination;
import app.fedilab.android.client.entities.api.Status;
import app.fedilab.android.client.entities.api.Statuses;
@@ -53,6 +55,8 @@ public class StatusCache {
    public String status_id;
    @SerializedName("status")
    public Status status;
    @SerializedName("notification")
    public Notification notification;
    @SerializedName("created_at")
    public Date created_at;
    @SerializedName("updated_at")
@@ -84,6 +88,21 @@ public class StatusCache {
        }
    }

    /**
     * Serialized a Notification class
     *
     * @param mastodon_notification {@link Notification} to serialize
     * @return String serialized status
     */
    public static String mastodonNotificationToStringStorage(Notification mastodon_notification) {
        Gson gson = new Gson();
        try {
            return gson.toJson(mastodon_notification);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Unserialized a Mastodon Status
     *
@@ -100,6 +119,22 @@ public class StatusCache {
        }
    }

    /**
     * Unserialized a Mastodon Notification
     *
     * @param serializedNotification String serialized status
     * @return {@link Notification}
     */
    public static Notification restoreNotificationFromString(String serializedNotification) {
        Gson gson = new Gson();
        try {
            return gson.fromJson(serializedNotification, Notification.class);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Insert or update a status
     *
@@ -197,7 +232,12 @@ public class StatusCache {
        values.put(Sqlite.COL_SLUG, slug);
        values.put(Sqlite.COL_STATUS_ID, statusCache.status_id);
        values.put(Sqlite.COL_TYPE, statusCache.type.getValue());
        if (statusCache.status != null) {
            values.put(Sqlite.COL_STATUS, mastodonStatusToStringStorage(statusCache.status));
        }
        if (statusCache.notification != null) {
            values.put(Sqlite.COL_STATUS, mastodonNotificationToStringStorage(statusCache.notification));
        }
        values.put(Sqlite.COL_CREATED_AT, Helper.dateToString(new Date()));
        //Inserts token
        try {
@@ -222,7 +262,12 @@ public class StatusCache {
        ContentValues values = new ContentValues();
        values.put(Sqlite.COL_USER_ID, statusCache.user_id);
        values.put(Sqlite.COL_STATUS_ID, statusCache.status_id);
        if (statusCache.status != null) {
            values.put(Sqlite.COL_STATUS, mastodonStatusToStringStorage(statusCache.status));
        }
        if (statusCache.notification != null) {
            values.put(Sqlite.COL_STATUS, mastodonNotificationToStringStorage(statusCache.notification));
        }
        values.put(Sqlite.COL_UPDATED_AT, Helper.dateToString(new Date()));
        //Inserts token
        try {
@@ -298,6 +343,48 @@ public class StatusCache {
    }


    /**
     * Get paginated notifications from db
     *
     * @param instance String - instance
     * @param user_id  String - us
     * @param max_id   String - status having max id
     * @param min_id   String - status having min id
     * @return Statuses
     * @throws DBException - throws a db exception
     */
    public Notifications getNotifications(List<String> exclude_type, String instance, String user_id, String max_id, String min_id, String since_id) throws DBException {
        if (db == null) {
            throw new DBException("db is null. Wrong initialization.");
        }
        String order = " DESC";
        String selection = Sqlite.COL_INSTANCE + "='" + instance + "' AND " + Sqlite.COL_USER_ID + "= '" + user_id + "' AND " + Sqlite.COL_SLUG + "= '" + Timeline.TimeLineEnum.NOTIFICATION.getValue() + "' ";
        String limit = String.valueOf(MastodonHelper.statusesPerCall(context));
        if (min_id != null) {
            selection += "AND " + Sqlite.COL_STATUS_ID + " > '" + min_id + "' ";
            order = " ASC";
        } else if (max_id != null) {
            selection += "AND " + Sqlite.COL_STATUS_ID + " < '" + max_id + "' ";
        } else if (since_id != null) {
            selection += "AND " + Sqlite.COL_STATUS_ID + " > '" + since_id + "' ";
            limit = null;
        }
        if (exclude_type != null && exclude_type.size() > 0) {
            StringBuilder exclude = new StringBuilder();
            for (String excluded : exclude_type) {
                exclude.append("'").append(excluded).append("'").append(",");
            }
            exclude = new StringBuilder(exclude.substring(0, exclude.length() - 1));
            selection += "AND " + Sqlite.COL_SLUG + " NOT IN (" + exclude + ") ";
        }
        try {
            Cursor c = db.query(Sqlite.TABLE_STATUS_CACHE, null, selection, null, null, null, Sqlite.COL_STATUS_ID + order, limit);
            return createNotificationReply(cursorToListOfNotifications(c));
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
@@ -403,6 +490,52 @@ public class StatusCache {
        return statusList;
    }

    /**
     * Convert a cursor to list of notifications
     *
     * @param c Cursor
     * @return List<Status>
     */
    private List<Notification> cursorToListOfNotifications(Cursor c) {
        //No element found
        if (c.getCount() == 0) {
            c.close();
            return null;
        }
        List<Notification> notificationList = new ArrayList<>();
        while (c.moveToNext()) {
            Notification notification = convertCursorToNotification(c);
            notificationList.add(notification);
        }
        //Close the cursor
        c.close();
        return notificationList;
    }


    /**
     * Create a reply from db in the same way than API call
     *
     * @param notificationList List<Notification>
     * @return Notifications (with pagination)
     */
    private Notifications createNotificationReply(List<Notification> notificationList) {
        Notifications notifications = new Notifications();
        notifications.notifications = notificationList;
        Pagination pagination = new Pagination();
        if (notificationList != null && notificationList.size() > 0) {
            //Status list is inverted, it happens for min_id due to ASC ordering
            if (notificationList.get(0).id.compareTo(notificationList.get(notificationList.size() - 1).id) < 0) {
                Collections.reverse(notificationList);
                notifications.notifications = notificationList;
            }
            pagination.max_id = notificationList.get(0).id;
            pagination.min_id = notificationList.get(notificationList.size() - 1).id;
        }
        notifications.pagination = pagination;
        return notifications;
    }

    /**
     * Create a reply from db in the same way than API call
     *
@@ -437,6 +570,18 @@ public class StatusCache {
        return restoreStatusFromString(serializedStatus);
    }

    /**
     * Read cursor and hydrate without closing it
     *
     * @param c - Cursor
     * @return Notification
     */
    private Notification convertCursorToNotification(Cursor c) {
        String serializedNotification = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_STATUS));
        return restoreNotificationFromString(serializedNotification);
    }


    public enum order {
        @SerializedName("ASC")
        ASC("ASC"),
+26 −6
Original line number Diff line number Diff line
@@ -62,7 +62,7 @@ public class NotificationAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
    private final int TYPE_POLL = 5;
    private final int TYPE_STATUS = 6;
    private final int TYPE_REACTION = 8;
    public StatusAdapter.FetchMoreCallBack fetchMoreCallBack;
    public FetchMoreCallBack fetchMoreCallBack;
    private Context context;

    public NotificationAdapter(List<Notification> notificationList) {
@@ -114,7 +114,6 @@ public class NotificationAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
        }
    }


    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
        Notification notification = notificationList.get(position);
@@ -160,14 +159,29 @@ public class NotificationAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
                holderFollow.binding.layoutFetchMore.fetchMoreContainer.setVisibility(View.VISIBLE);
                holderFollow.binding.layoutFetchMore.fetchMoreMin.setOnClickListener(v -> {
                    notification.isFetchMore = false;
                    if (holderFollow.getBindingAdapterPosition() < notificationList.size() - 1) {
                        String fromId;
                        if (notification.positionFetchMore == Notification.PositionFetchMore.TOP) {
                            fromId = notificationList.get(position + 1).id;
                        } else {
                            fromId = notification.id;
                        }
                        fetchMoreCallBack.onClickMinId(fromId, notification);
                        notifyItemChanged(position);
                    fetchMoreCallBack.onClickMinId(notification.id);
                    }

                });
                holderFollow.binding.layoutFetchMore.fetchMoreMax.setOnClickListener(v -> {
                    //We hide the button
                    notification.isFetchMore = false;
                    String fromId;
                    if (notification.positionFetchMore == Notification.PositionFetchMore.TOP) {
                        fromId = notificationList.get(position).id;
                    } else {
                        fromId = notificationList.get(position - 1).id;
                    }
                    notifyItemChanged(position);
                    fetchMoreCallBack.onClickMaxId(notification.id);
                    fetchMoreCallBack.onClickMaxId(fromId, notification);
                });
            } else {
                holderFollow.binding.layoutFetchMore.fetchMoreContainer.setVisibility(View.GONE);
@@ -191,7 +205,7 @@ public class NotificationAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
            }
            StatusesVM statusesVM = new ViewModelProvider((ViewModelStoreOwner) context).get(StatusesVM.class);
            SearchVM searchVM = new ViewModelProvider((ViewModelStoreOwner) context).get(SearchVM.class);
            statusManagement(context, statusesVM, searchVM, holderStatus, this, null, notification.status, Timeline.TimeLineEnum.NOTIFICATION, false, true, fetchMoreCallBack);
            statusManagement(context, statusesVM, searchVM, holderStatus, this, null, notification.status, Timeline.TimeLineEnum.NOTIFICATION, false, true, null);
            holderStatus.bindingNotification.status.dateShort.setText(Helper.dateDiff(context, notification.created_at));
            holderStatus.bindingNotification.containerTransparent.setAlpha(.3f);
            if (getItemViewType(position) == TYPE_MENTION || getItemViewType(position) == TYPE_STATUS || getItemViewType(position) == TYPE_REACTION) {
@@ -300,6 +314,12 @@ public class NotificationAdapter extends RecyclerView.Adapter<RecyclerView.ViewH
        }
    }

    public interface FetchMoreCallBack {
        void onClickMinId(String min_id, Notification notificationToUpdate);

        void onClickMaxId(String max_id, Notification notificationToUpdate);
    }


    public long getItemId(int position) {
        return position;
+4 −32
Original line number Diff line number Diff line
@@ -1878,21 +1878,7 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
                    } else {
                        fromId = status.id;
                    }
                    fetchMoreCallBack.onClickMinId(fromId);
                    if (!remote) {
                        new Thread(() -> {
                            StatusCache statusCache = new StatusCache();
                            statusCache.instance = BaseMainActivity.currentInstance;
                            statusCache.user_id = BaseMainActivity.currentUserID;
                            statusCache.status = status;
                            statusCache.status_id = status.id;
                            try {
                                new StatusCache(context).updateIfExists(statusCache);
                            } catch (DBException e) {
                                e.printStackTrace();
                            }
                        }).start();
                    }
                    fetchMoreCallBack.onClickMinId(fromId, status);
                }
            });
            holder.binding.layoutFetchMore.fetchMoreMax.setOnClickListener(v -> {
@@ -1904,22 +1890,8 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
                } else {
                    fromId = statusList.get(holder.getBindingAdapterPosition() - 1).id;
                }
                fetchMoreCallBack.onClickMaxId(fromId);
                fetchMoreCallBack.onClickMaxId(fromId, status);
                adapter.notifyItemChanged(holder.getBindingAdapterPosition());
                if (!remote) {
                    new Thread(() -> {
                        StatusCache statusCache = new StatusCache();
                        statusCache.instance = BaseMainActivity.currentInstance;
                        statusCache.user_id = BaseMainActivity.currentUserID;
                        statusCache.status = status;
                        statusCache.status_id = status.id;
                        try {
                            new StatusCache(context).updateIfExists(statusCache);
                        } catch (DBException e) {
                            e.printStackTrace();
                        }
                    }).start();
                }
            });
        } else {
            holder.binding.layoutFetchMore.fetchMoreContainer.setVisibility(View.GONE);
@@ -2073,9 +2045,9 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
    }

    public interface FetchMoreCallBack {
        void onClickMinId(String min_id);
        void onClickMinId(String min_id, Status statusToUpdate);

        void onClickMaxId(String max_id);
        void onClickMaxId(String max_id, Status statusToUpdate);
    }

    public static class StatusViewHolder extends RecyclerView.ViewHolder {
+97 −14
Original line number Diff line number Diff line
@@ -43,16 +43,17 @@ import app.fedilab.android.R;
import app.fedilab.android.client.entities.api.Notification;
import app.fedilab.android.client.entities.api.Notifications;
import app.fedilab.android.client.entities.api.Status;
import app.fedilab.android.client.entities.app.Timeline;
import app.fedilab.android.databinding.FragmentPaginationBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.MastodonHelper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.drawer.NotificationAdapter;
import app.fedilab.android.ui.drawer.StatusAdapter;
import app.fedilab.android.viewmodel.mastodon.NotificationsVM;
import app.fedilab.android.viewmodel.mastodon.TimelinesVM;


public class FragmentMastodonNotification extends Fragment implements StatusAdapter.FetchMoreCallBack {
public class FragmentMastodonNotification extends Fragment implements NotificationAdapter.FetchMoreCallBack {


    private static final int NOTIFICATION_PRESENT = -1;
@@ -270,26 +271,108 @@ public class FragmentMastodonNotification extends Fragment implements StatusAdap
     * @param direction - DIRECTION null if first call, then is set to TOP or BOTTOM depending of scroll
     */
    private void route(FragmentMastodonTimeline.DIRECTION direction, boolean fetchingMissing) {
        route(direction, fetchingMissing, null);
    }

    /**
     * Router for timelines
     *
     * @param direction - DIRECTION null if first call, then is set to TOP or BOTTOM depending of scroll
     */
    private void route(FragmentMastodonTimeline.DIRECTION direction, boolean fetchingMissing, Notification notificationToUpdate) {
        if (binding == null || !isAdded() || getActivity() == null) {
            return;
        }
        if (!isAdded()) {
            return;
        }

        SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity());
        boolean useCache = sharedpreferences.getBoolean(getString(R.string.SET_USE_CACHE), true);

        TimelinesVM.TimelineParams timelineParams = new TimelinesVM.TimelineParams(Timeline.TimeLineEnum.NOTIFICATION, direction, null);
        timelineParams.limit = MastodonHelper.notificationsPerCall(requireActivity());
        if (direction == FragmentMastodonTimeline.DIRECTION.REFRESH || direction == FragmentMastodonTimeline.DIRECTION.SCROLL_TOP) {
            timelineParams.maxId = null;
            timelineParams.minId = null;
        } else if (direction == FragmentMastodonTimeline.DIRECTION.BOTTOM) {
            timelineParams.maxId = fetchingMissing ? max_id_fetch_more : max_id;
            timelineParams.minId = null;
        } else if (direction == FragmentMastodonTimeline.DIRECTION.TOP) {
            timelineParams.minId = fetchingMissing ? min_id_fetch_more : min_id;
            timelineParams.maxId = null;
        } else {
            timelineParams.maxId = max_id;
        }
        timelineParams.excludeType = excludeType;
        timelineParams.fetchingMissing = fetchingMissing;

        if (useCache) {
            getCachedNotifications(direction, fetchingMissing, timelineParams);
        } else {
            getLiveNotifications(direction, fetchingMissing, timelineParams, notificationToUpdate);
        }


    }

    private void getCachedNotifications(FragmentMastodonTimeline.DIRECTION direction, boolean fetchingMissing, TimelinesVM.TimelineParams timelineParams) {

        if (direction == null) {
            notificationsVM.getNotificationCache(notificationList, timelineParams)
                    .observe(getViewLifecycleOwner(), notificationsCached -> {
                        if (notificationsCached == null || notificationsCached.notifications == null || notificationsCached.notifications.size() == 0) {
                            getLiveNotifications(null, fetchingMissing, timelineParams, null);
                        } else {
                            initializeNotificationView(notificationsCached);
                        }
                    });
        } else if (direction == FragmentMastodonTimeline.DIRECTION.BOTTOM) {
            notificationsVM.getNotificationCache(notificationList, timelineParams)
                    .observe(getViewLifecycleOwner(), notificationsBottom -> {
                        if (notificationsBottom == null || notificationsBottom.notifications == null || notificationsBottom.notifications.size() == 0) {
                            getLiveNotifications(FragmentMastodonTimeline.DIRECTION.BOTTOM, fetchingMissing, timelineParams, null);
                        } else {
                            dealWithPagination(notificationsBottom, FragmentMastodonTimeline.DIRECTION.BOTTOM, fetchingMissing, null);
                        }

                    });
        } else if (direction == FragmentMastodonTimeline.DIRECTION.TOP) {
            notificationsVM.getNotificationCache(notificationList, timelineParams)
                    .observe(getViewLifecycleOwner(), notificationsTop -> {
                        if (notificationsTop == null || notificationsTop.notifications == null || notificationsTop.notifications.size() == 0) {
                            getLiveNotifications(FragmentMastodonTimeline.DIRECTION.BOTTOM, fetchingMissing, timelineParams, null);
                        } else {
                            dealWithPagination(notificationsTop, FragmentMastodonTimeline.DIRECTION.TOP, fetchingMissing, null);
                        }
                    });
        } else if (direction == FragmentMastodonTimeline.DIRECTION.REFRESH) {
            notificationsVM.getNotifications(notificationList, timelineParams)
                    .observe(getViewLifecycleOwner(), notificationsRefresh -> {
                        if (notificationAdapter != null) {
                            dealWithPagination(notificationsRefresh, FragmentMastodonTimeline.DIRECTION.REFRESH, true, null);
                        } else {
                            initializeNotificationView(notificationsRefresh);
                        }
                    });
        }
    }

    private void getLiveNotifications(FragmentMastodonTimeline.DIRECTION direction, boolean fetchingMissing, TimelinesVM.TimelineParams timelineParams, Notification notificationToUpdate) {
        if (direction == null) {
            notificationsVM.getNotifications(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, null, MastodonHelper.statusesPerCall(requireActivity()), excludeType, null)
            notificationsVM.getNotifications(notificationList, timelineParams)
                    .observe(getViewLifecycleOwner(), this::initializeNotificationView);
        } else if (direction == FragmentMastodonTimeline.DIRECTION.BOTTOM) {
            notificationsVM.getNotifications(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, fetchingMissing ? max_id_fetch_more : max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()), excludeType, null)
                    .observe(getViewLifecycleOwner(), notificationsBottom -> dealWithPagination(notificationsBottom, FragmentMastodonTimeline.DIRECTION.BOTTOM, fetchingMissing));
            notificationsVM.getNotifications(notificationList, timelineParams)
                    .observe(getViewLifecycleOwner(), notificationsBottom -> dealWithPagination(notificationsBottom, FragmentMastodonTimeline.DIRECTION.BOTTOM, fetchingMissing, notificationToUpdate));
        } else if (direction == FragmentMastodonTimeline.DIRECTION.TOP) {
            notificationsVM.getNotifications(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity()), excludeType, null)
                    .observe(getViewLifecycleOwner(), notificationsTop -> dealWithPagination(notificationsTop, FragmentMastodonTimeline.DIRECTION.TOP, fetchingMissing));
            notificationsVM.getNotifications(notificationList, timelineParams)
                    .observe(getViewLifecycleOwner(), notificationsTop -> dealWithPagination(notificationsTop, FragmentMastodonTimeline.DIRECTION.TOP, fetchingMissing, notificationToUpdate));
        } else if (direction == FragmentMastodonTimeline.DIRECTION.REFRESH) {
            notificationsVM.getNotifications(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, null, MastodonHelper.statusesPerCall(requireActivity()), excludeType, null)
            notificationsVM.getNotifications(notificationList, timelineParams)
                    .observe(getViewLifecycleOwner(), notificationsRefresh -> {
                        if (notificationAdapter != null) {
                            dealWithPagination(notificationsRefresh, FragmentMastodonTimeline.DIRECTION.REFRESH, true);
                            dealWithPagination(notificationsRefresh, FragmentMastodonTimeline.DIRECTION.REFRESH, true, notificationToUpdate);
                        } else {
                            initializeNotificationView(notificationsRefresh);
                        }
@@ -333,7 +416,7 @@ public class FragmentMastodonNotification extends Fragment implements StatusAdap
     *
     * @param fetched_notifications Notifications
     */
    private synchronized void dealWithPagination(Notifications fetched_notifications, FragmentMastodonTimeline.DIRECTION direction, boolean fetchingMissing) {
    private synchronized void dealWithPagination(Notifications fetched_notifications, FragmentMastodonTimeline.DIRECTION direction, boolean fetchingMissing, Notification notificationToUpdate) {
        if (binding == null || !isAdded() || getActivity() == null) {
            return;
        }
@@ -456,17 +539,17 @@ public class FragmentMastodonNotification extends Fragment implements StatusAdap


    @Override
    public void onClickMinId(String min_id) {
    public void onClickMinId(String min_id, Notification notificationToUpdate) {
        //Fetch more has been pressed
        min_id_fetch_more = min_id;
        route(FragmentMastodonTimeline.DIRECTION.TOP, true);
        route(FragmentMastodonTimeline.DIRECTION.TOP, true, notificationToUpdate);
    }

    @Override
    public void onClickMaxId(String max_id) {
    public void onClickMaxId(String max_id, Notification notificationToUpdate) {
        //Fetch more has been pressed
        max_id_fetch_more = max_id;
        route(FragmentMastodonTimeline.DIRECTION.BOTTOM, true);
        route(FragmentMastodonTimeline.DIRECTION.BOTTOM, true, notificationToUpdate);
    }

    public enum NotificationTypeEnum {
Loading