package org.thoughtcrime.securesms.database.loaders; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import androidx.annotation.NonNull; import androidx.loader.content.AsyncTaskLoader; import com.annimon.stream.Stream; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.recipients.Recipient; import org.thoughtcrime.securesms.database.MediaDatabase; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import network.loki.messenger.R; public class BucketedThreadMediaLoader extends AsyncTaskLoader { @SuppressWarnings("unused") private static final String TAG = BucketedThreadMediaLoader.class.getSimpleName(); private final Address address; private final ContentObserver observer; public BucketedThreadMediaLoader(@NonNull Context context, @NonNull Address address) { super(context); this.address = address; this.observer = new ForceLoadContentObserver(); onContentChanged(); } @Override protected void onStartLoading() { if (takeContentChanged()) { forceLoad(); } } @Override protected void onStopLoading() { cancelLoad(); } @Override protected void onAbandon() { DatabaseComponent.get(getContext()).mediaDatabase().unsubscribeToMediaChanges(observer); } @Override public BucketedThreadMedia loadInBackground() { BucketedThreadMedia result = new BucketedThreadMedia(getContext()); long threadId = DatabaseComponent.get(getContext()).threadDatabase().getOrCreateThreadIdFor(Recipient.from(getContext(), address, true)); MediaDatabase mediaDatabase = DatabaseComponent.get(getContext()).mediaDatabase(); mediaDatabase.subscribeToMediaChanges(observer); try (Cursor cursor = mediaDatabase.getGalleryMediaForThread(threadId)) { while (cursor != null && cursor.moveToNext()) { result.add(MediaDatabase.MediaRecord.from(getContext(), cursor)); } } return result; } public static class BucketedThreadMedia { private final TimeBucket TODAY; private final TimeBucket YESTERDAY; private final TimeBucket THIS_WEEK; private final TimeBucket THIS_MONTH; private final MonthBuckets OLDER; private final TimeBucket[] TIME_SECTIONS; public BucketedThreadMedia(@NonNull Context context) { this.TODAY = new TimeBucket(context.getString(R.string.BucketedThreadMedia_Today), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, 1000)); this.YESTERDAY = new TimeBucket(context.getString(R.string.BucketedThreadMedia_Yesterday), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1)); this.THIS_WEEK = new TimeBucket(context.getString(R.string.BucketedThreadMedia_This_week), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2)); this.THIS_MONTH = new TimeBucket(context.getString(R.string.BucketedThreadMedia_This_month), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -30), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7)); this.TIME_SECTIONS = new TimeBucket[]{TODAY, YESTERDAY, THIS_WEEK, THIS_MONTH}; this.OLDER = new MonthBuckets(); } public void add(MediaDatabase.MediaRecord mediaRecord) { for (TimeBucket timeSection : TIME_SECTIONS) { if (timeSection.inRange(mediaRecord.getDate())) { timeSection.add(mediaRecord); return; } } OLDER.add(mediaRecord); } public int getSectionCount() { return (int)Stream.of(TIME_SECTIONS) .filter(timeBucket -> !timeBucket.isEmpty()) .count() + OLDER.getSectionCount(); } public int getSectionItemCount(int section) { List activeTimeBuckets = Stream.of(TIME_SECTIONS).filter(timeBucket -> !timeBucket.isEmpty()).toList(); if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getItemCount(); else return OLDER.getSectionItemCount(section - activeTimeBuckets.size()); } public MediaDatabase.MediaRecord get(int section, int item) { List activeTimeBuckets = Stream.of(TIME_SECTIONS).filter(timeBucket -> !timeBucket.isEmpty()).toList(); if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getItem(item); else return OLDER.getItem(section - activeTimeBuckets.size(), item); } public String getName(int section, Locale locale) { List activeTimeBuckets = Stream.of(TIME_SECTIONS).filter(timeBucket -> !timeBucket.isEmpty()).toList(); if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getName(); else return OLDER.getName(section - activeTimeBuckets.size(), locale); } private static class TimeBucket { private final List records = new LinkedList<>(); private final long startTime; private final long endtime; private final String name; TimeBucket(String name, long startTime, long endtime) { this.name = name; this.startTime = startTime; this.endtime = endtime; } void add(MediaDatabase.MediaRecord record) { this.records.add(record); } boolean inRange(long timestamp) { return timestamp > startTime && timestamp <= endtime; } boolean isEmpty() { return records.isEmpty(); } int getItemCount() { return records.size(); } MediaDatabase.MediaRecord getItem(int position) { return records.get(position); } String getName() { return name; } static long addToCalendar(int field, int amount) { Calendar calendar = Calendar.getInstance(); calendar.add(field, amount); return calendar.getTimeInMillis(); } } private static class MonthBuckets { private final Map> months = new HashMap<>(); void add(MediaDatabase.MediaRecord record) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(record.getDate()); int year = calendar.get(Calendar.YEAR) - 1900; int month = calendar.get(Calendar.MONTH); Date date = new Date(year, month, 1); if (months.containsKey(date)) { months.get(date).add(record); } else { List list = new LinkedList<>(); list.add(record); months.put(date, list); } } int getSectionCount() { return months.size(); } int getSectionItemCount(int section) { return months.get(getSection(section)).size(); } MediaDatabase.MediaRecord getItem(int section, int position) { return months.get(getSection(section)).get(position); } Date getSection(int section) { ArrayList keys = new ArrayList<>(months.keySet()); Collections.sort(keys, Collections.reverseOrder()); return keys.get(section); } String getName(int section, Locale locale) { Date sectionDate = getSection(section); return new SimpleDateFormat("MMMM, yyyy", locale).format(sectionDate); } } } }