diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 5c0555c1da..9f017f1c19 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -2927,7 +2927,7 @@ public class ConversationActivity extends PassphraseRequiredActivity long id = fragment.stageOutgoingMessage(secureMessage); SimpleTask.run(() -> { - long resultId = MessageSender.sendPushWithPreUploadedMedia(this, secureMessage, result.getPreUploadResults(), thread, () -> fragment.releaseOutgoingMessage(id)); + long resultId = MessageSender.sendPushWithPreUploadedMedia(this, secureMessage, result.getPreUploadResults(), thread, null); int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments(); Log.i(TAG, "Deleted " + deleted + " abandoned attachments."); @@ -3017,7 +3017,7 @@ public class ConversationActivity extends PassphraseRequiredActivity final long id = fragment.stageOutgoingMessage(outgoingMessage); SimpleTask.run(() -> { - return MessageSender.send(context, outgoingMessage, thread, forceSms, metricId, () -> fragment.releaseOutgoingMessage(id)); + return MessageSender.send(context, outgoingMessage, thread, forceSms, metricId, null); }, result -> { sendComplete(result); future.set(null); @@ -3058,7 +3058,7 @@ public class ConversationActivity extends PassphraseRequiredActivity .onAllGranted(() -> { final long id = new SecureRandom().nextLong(); SimpleTask.run(() -> { - return MessageSender.send(context, message, thread, forceSms, metricId, () -> fragment.releaseOutgoingMessage(id)); + return MessageSender.send(context, message, thread, forceSms, metricId, null); }, this::sendComplete); silentlySetComposeText(""); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java index f6f7d51497..961d6443a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java @@ -13,16 +13,21 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.conversation.ConversationData.MessageRequestData; import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MentionDatabase; +import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.Mention; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.Util; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -32,7 +37,7 @@ import java.util.stream.Collectors; /** * Core data source for loading an individual conversation. */ -class ConversationDataSource implements PagedDataSource { +class ConversationDataSource implements PagedDataSource { private static final String TAG = Log.tag(ConversationDataSource.class); @@ -109,6 +114,48 @@ class ConversationDataSource implements PagedDataSource { return messages; } + @Override + public @Nullable ConversationMessage load(@NonNull MessageId messageId) { + Stopwatch stopwatch = new Stopwatch("load(" + messageId + "), thread " + threadId); + MessageDatabase database = messageId.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context); + MessageRecord record = database.getMessageRecordOrNull(messageId.getId()); + + stopwatch.split("message"); + + try { + if (record != null) { + List mentions; + if (messageId.isMms()) { + mentions = DatabaseFactory.getMentionDatabase(context).getMentionsForMessage(messageId.getId()); + } else { + mentions = Collections.emptyList(); + } + + stopwatch.split("mentions"); + + if (messageId.isMms()) { + List attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageId.getId()); + if (attachments.size() > 0) { + record = ((MediaMmsMessageRecord) record).withAttachments(context, attachments); + } + } + + stopwatch.split("attachments"); + + return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(), record, mentions); + } else { + return null; + } + } finally { + stopwatch.stop(TAG); + } + } + + @Override + public @NonNull MessageId getKey(@NonNull ConversationMessage conversationMessage) { + return new MessageId(conversationMessage.getMessageRecord().getId(), conversationMessage.getMessageRecord().isMms()); + } + private static class MentionHelper { private Collection messageIds = new LinkedList<>(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 3029ec9b2a..0fef0c5bdb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -1055,7 +1055,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect if (getListAdapter() != null) { clearHeaderIfNotTyping(getListAdapter()); setLastSeen(0); - getListAdapter().addFastRecord(ConversationMessageFactory.createWithResolvedData(messageRecord, messageRecord.getDisplayBody(requireContext()), message.getMentions())); list.post(() -> list.scrollToPosition(0)); } @@ -1068,19 +1067,12 @@ public class ConversationFragment extends LoggingFragment implements Multiselect if (getListAdapter() != null) { clearHeaderIfNotTyping(getListAdapter()); setLastSeen(0); - getListAdapter().addFastRecord(ConversationMessageFactory.createWithResolvedData(messageRecord)); list.post(() -> list.scrollToPosition(0)); } return messageRecord.getId(); } - public void releaseOutgoingMessage(long id) { - if (getListAdapter() != null) { - getListAdapter().releaseFastRecord(id); - } - } - private void presentConversationMetadata(@NonNull ConversationData conversation) { ConversationAdapter adapter = getListAdapter(); if (adapter == null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java index 81063e22fe..0e906931b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java @@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors; import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette; import org.thoughtcrime.securesms.conversation.colors.NameColor; import org.thoughtcrime.securesms.database.DatabaseObserver; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.LiveGroup; @@ -64,8 +65,10 @@ public class ConversationViewModel extends ViewModel { private final MutableLiveData showScrollButtons; private final MutableLiveData hasUnreadMentions; private final LiveData canShowAsBubble; - private final ProxyPagingController pagingController; - private final DatabaseObserver.Observer messageObserver; + private final ProxyPagingController pagingController; + private final DatabaseObserver.Observer conversationObserver; + private final DatabaseObserver.MessageObserver messageUpdateObserver; + private final DatabaseObserver.MessageObserver messageInsertObserver; private final MutableLiveData recipientId; private final LiveData wallpaper; private final SingleLiveEvent events; @@ -89,8 +92,10 @@ public class ConversationViewModel extends ViewModel { this.hasUnreadMentions = new MutableLiveData<>(false); this.recipientId = new MutableLiveData<>(); this.events = new SingleLiveEvent<>(); - this.pagingController = new ProxyPagingController(); - this.messageObserver = pagingController::onDataInvalidated; + this.pagingController = new ProxyPagingController<>(); + this.conversationObserver = pagingController::onDataInvalidated; + this.messageUpdateObserver = pagingController::onDataItemChanged; + this.messageInsertObserver = messageId -> pagingController.onDataItemInserted(messageId, 0); this.toolbarBottom = new MutableLiveData<>(); this.inlinePlayerHeight = new MutableLiveData<>(); this.scrollDateTopMargin = Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(toolbarBottom, inlinePlayerHeight, Integer::sum)); @@ -106,7 +111,9 @@ public class ConversationViewModel extends ViewModel { return conversationData; }); - LiveData>> pagedDataForThreadId = Transformations.map(metadata, data -> { + ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageUpdateObserver); + + LiveData>> pagedDataForThreadId = Transformations.map(metadata, data -> { int startPosition; ConversationData.MessageRequestData messageRequestData = data.getMessageRequestData(); @@ -120,8 +127,10 @@ public class ConversationViewModel extends ViewModel { startPosition = data.getThreadSize(); } - ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver); - ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), messageObserver); + ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver); + ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver); + ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), conversationObserver); + ApplicationDependencies.getDatabaseObserver().registerMessageInsertObserver(data.getThreadId(), messageInsertObserver); ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId(), messageRequestData, data.showUniversalExpireTimerMessage()); PagingConfig config = new PagingConfig.Builder().setPageSize(25) @@ -292,7 +301,9 @@ public class ConversationViewModel extends ViewModel { @Override protected void onCleared() { super.onCleared(); - ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver); + ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver); + ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver); + ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver); EventBus.getDefault().unregister(this); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java index cdcb981aa1..d93f241348 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java @@ -6,6 +6,7 @@ import android.database.MatrixCursor; import android.database.MergeCursor; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import org.signal.core.util.logging.Log; @@ -23,7 +24,7 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; -abstract class ConversationListDataSource implements PagedDataSource { +abstract class ConversationListDataSource implements PagedDataSource { private static final String TAG = Log.tag(ConversationListDataSource.class); @@ -73,6 +74,16 @@ abstract class ConversationListDataSource implements PagedDataSource megaphone; private final MutableLiveData searchResult; - private final PagedData pagedData; + private final PagedData pagedData; private final LiveData hasNoConversations; private final SearchRepository searchRepository; private final MegaphoneRepository megaphoneRepository; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseObserver.java b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseObserver.java index a98a60d4d4..ad989fb78e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseObserver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseObserver.java @@ -5,6 +5,7 @@ import android.app.Application; import androidx.annotation.NonNull; import org.signal.core.util.concurrent.SignalExecutors; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.util.concurrent.SerialExecutor; import java.util.HashMap; @@ -24,13 +25,15 @@ public final class DatabaseObserver { private final Application application; private final Executor executor; - private final Set conversationListObservers; - private final Map> conversationObservers; - private final Map> verboseConversationObservers; - private final Map> paymentObservers; - private final Set allPaymentsObservers; - private final Set chatColorsObservers; - private final Set stickerPackObservers; + private final Set conversationListObservers; + private final Map> conversationObservers; + private final Map> verboseConversationObservers; + private final Map> paymentObservers; + private final Set allPaymentsObservers; + private final Set chatColorsObservers; + private final Set stickerPackObservers; + private final Set messageUpdateObservers; + private final Map> messageInsertObservers; public DatabaseObserver(Application application) { this.application = application; @@ -42,6 +45,8 @@ public final class DatabaseObserver { this.allPaymentsObservers = new HashSet<>(); this.chatColorsObservers = new HashSet<>(); this.stickerPackObservers = new HashSet<>(); + this.messageUpdateObservers = new HashSet<>(); + this.messageInsertObservers = new HashMap<>(); } public void registerConversationListObserver(@NonNull Observer listener) { @@ -86,6 +91,18 @@ public final class DatabaseObserver { }); } + public void registerMessageUpdateObserver(@NonNull MessageObserver listener) { + executor.execute(() -> { + messageUpdateObservers.add(listener); + }); + } + + public void registerMessageInsertObserver(long threadId, @NonNull MessageObserver listener) { + executor.execute(() -> { + registerMapped(messageInsertObservers, threadId, listener); + }); + } + public void unregisterObserver(@NonNull Observer listener) { executor.execute(() -> { conversationListObservers.remove(listener); @@ -97,6 +114,12 @@ public final class DatabaseObserver { }); } + public void unregisterObserver(@NonNull MessageObserver listener) { + executor.execute(() -> { + messageUpdateObservers.remove(listener); + }); + } + public void notifyConversationListeners(Set threadIds) { executor.execute(() -> { for (long threadId : threadIds) { @@ -177,8 +200,24 @@ public final class DatabaseObserver { }); } - private void registerMapped(@NonNull Map> map, @NonNull K key, @NonNull Observer listener) { - Set listeners = map.get(key); + public void notifyMessageUpdateObservers(@NonNull MessageId messageId) { + executor.execute(() -> { + messageUpdateObservers.stream().forEach(l -> l.onMessageChanged(messageId)); + }); + } + + public void notifyMessageInsertObservers(long threadId, @NonNull MessageId messageId) { + executor.execute(() -> { + Set listeners = messageInsertObservers.get(threadId); + + if (listeners != null) { + listeners.stream().forEach(l -> l.onMessageChanged(messageId)); + } + }); + } + + private void registerMapped(@NonNull Map> map, @NonNull K key, @NonNull V listener) { + Set listeners = map.get(key); if (listeners == null) { listeners = new HashSet<>(); @@ -217,4 +256,8 @@ public final class DatabaseObserver { */ void onChanged(); } + + public interface MessageObserver { + void onMessageChanged(@NonNull MessageId messageId); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index bcbeed5068..c7efc9bea3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -769,14 +769,14 @@ public class MmsDatabase extends MessageDatabase { public void markAsForcedSms(long messageId) { long threadId = getThreadIdForMessage(messageId); updateMailboxBitmask(messageId, Types.PUSH_MESSAGE_BIT, Types.MESSAGE_FORCE_SMS_BIT, Optional.of(threadId)); - notifyConversationListeners(threadId); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); } @Override public void markAsRateLimited(long messageId) { long threadId = getThreadIdForMessage(messageId); updateMailboxBitmask(messageId, 0, Types.MESSAGE_RATE_LIMITED_BIT, Optional.of(threadId)); - notifyConversationListeners(threadId); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); } @Override @@ -800,28 +800,28 @@ public class MmsDatabase extends MessageDatabase { public void markAsPendingInsecureSmsFallback(long messageId) { long threadId = getThreadIdForMessage(messageId); updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_PENDING_INSECURE_SMS_FALLBACK, Optional.of(threadId)); - notifyConversationListeners(threadId); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); } @Override public void markAsSending(long messageId) { long threadId = getThreadIdForMessage(messageId); updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE, Optional.of(threadId)); - notifyConversationListeners(threadId); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); } @Override public void markAsSentFailed(long messageId) { long threadId = getThreadIdForMessage(messageId); updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENT_FAILED_TYPE, Optional.of(threadId)); - notifyConversationListeners(threadId); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); } @Override public void markAsSent(long messageId, boolean secure) { long threadId = getThreadIdForMessage(messageId); updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENT_TYPE | (secure ? Types.PUSH_MESSAGE_BIT | Types.SECURE_MESSAGE_BIT : 0), Optional.of(threadId)); - notifyConversationListeners(threadId); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); } @Override @@ -854,7 +854,7 @@ public class MmsDatabase extends MessageDatabase { } finally { db.endTransaction(); } - notifyConversationListeners(threadId); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); } @Override @@ -864,7 +864,7 @@ public class MmsDatabase extends MessageDatabase { contentValues.put(STATUS, state); database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId + ""}); - notifyConversationListeners(getThreadIdForMessage(messageId)); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId, true)); } @Override @@ -1559,7 +1559,7 @@ public class MmsDatabase extends MessageDatabase { DatabaseFactory.getThreadDatabase(context).setLastSeenSilently(threadId); DatabaseFactory.getThreadDatabase(context).setHasSentSilently(threadId, true); - notifyConversationListeners(threadId); + ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, new MessageId(messageId, true)); notifyConversationListListeners(); TrimThreadJob.enqueueAsync(threadId); @@ -1651,7 +1651,6 @@ public class MmsDatabase extends MessageDatabase { long contentValuesThreadId = contentValues.getAsLong(THREAD_ID); - notifyConversationListeners(contentValuesThreadId); DatabaseFactory.getThreadDatabase(context).setLastScrolled(contentValuesThreadId, 0); ThreadUpdateJob.enqueue(contentValuesThreadId); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index df0a9e71a2..aae4f85a77 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -59,7 +59,6 @@ import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.CursorUtil; import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.SqlUtil; -import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.Pair; @@ -198,7 +197,7 @@ public class SmsDatabase extends MessageDatabase { db.endTransaction(); } - notifyConversationListeners(threadId); + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(id, false)); } @Override @@ -1235,7 +1234,7 @@ public class SmsDatabase extends MessageDatabase { DatabaseFactory.getThreadDatabase(context).setHasSentSilently(threadId, true); - notifyConversationListeners(threadId); + ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, new MessageId(messageId, false)); if (!message.isIdentityVerified() && !message.isIdentityDefault()) { TrimThreadJob.enqueueAsync(threadId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PagedDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PagedDataSource.java index c6b126a0cd..2f8e669f9d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PagedDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4PagedDataSource.java @@ -27,7 +27,7 @@ import okhttp3.Response; /** * Data source for GiphyImages. */ -final class GiphyMp4PagedDataSource implements PagedDataSource { +final class GiphyMp4PagedDataSource implements PagedDataSource { private static final Uri BASE_GIPHY_URI = Uri.parse("https://api.giphy.com/v1/gifs/") .buildUpon() @@ -76,6 +76,16 @@ final class GiphyMp4PagedDataSource implements PagedDataSource { } } + @Override + public String getKey(@NonNull GiphyImage giphyImage) { + return giphyImage.getGifUrl(); + } + + @Override + public @Nullable GiphyImage load(String url) { + throw new UnsupportedOperationException("Not implemented!"); + } + private @NonNull GiphyResponse performFetch(int start, int length) throws IOException { String url; diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewModel.java index 879ef3173d..8492860d7d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewModel.java @@ -28,12 +28,12 @@ import java.util.Objects; */ public final class GiphyMp4ViewModel extends ViewModel { - private final GiphyMp4Repository repository; - private final MutableLiveData> pagedData; - private final LiveData> images; - private final LiveData pagingController; - private final SingleLiveEvent saveResultEvents; - private final boolean isForMms; + private final GiphyMp4Repository repository; + private final MutableLiveData> pagedData; + private final LiveData> images; + private final LiveData> pagingController; + private final SingleLiveEvent saveResultEvents; + private final boolean isForMms; private String query; @@ -52,7 +52,7 @@ public final class GiphyMp4ViewModel extends ViewModel { .toList())); } - LiveData> getPagedData() { + LiveData> getPagedData() { return pagedData; } @@ -77,11 +77,11 @@ public final class GiphyMp4ViewModel extends ViewModel { return images; } - public @NonNull LiveData getPagingController() { + public @NonNull LiveData> getPagingController() { return pagingController; } - private PagedData getGiphyImagePagedData(@Nullable String query) { + private PagedData getGiphyImagePagedData(@Nullable String query) { return PagedData.create(new GiphyMp4PagedDataSource(query), new PagingConfig.Builder().setPageSize(20) .setBufferPages(1) diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogDataSource.kt index eb20533462..43bb00768e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogDataSource.kt @@ -4,6 +4,7 @@ import android.app.Application import org.signal.paging.PagedDataSource import org.thoughtcrime.securesms.database.LogDatabase import org.thoughtcrime.securesms.logsubmit.util.Scrubber +import java.lang.UnsupportedOperationException /** * Retrieves logs to show in the [SubmitDebugLogActivity]. @@ -16,7 +17,7 @@ class LogDataSource( private val prefixLines: List, private val untilTime: Long ) : - PagedDataSource { + PagedDataSource { val logDatabase = LogDatabase.getInstance(application) @@ -35,6 +36,14 @@ class LogDataSource( } } + override fun load(key: Long?): LogLine? { + throw UnsupportedOperationException("Not implemented!") + } + + override fun getKey(data: LogLine): Long { + return data.id + } + private fun convertToLogLine(raw: String): LogLine { val scrubbed: String = Scrubber.scrub(raw).toString() return SimpleLogLine(scrubbed, LogStyleParser.parseStyle(scrubbed), LogLine.Placeholder.NONE) diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java index 0988400aea..f2d8183abb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java @@ -24,7 +24,7 @@ public class SubmitDebugLogViewModel extends ViewModel { private final SubmitDebugLogRepository repo; private final MutableLiveData mode; - private final ProxyPagingController pagingController; + private final ProxyPagingController pagingController; private final List staticLines; private final MediatorLiveData> lines; private final long firstViewTime; @@ -35,7 +35,7 @@ public class SubmitDebugLogViewModel extends ViewModel { this.repo = new SubmitDebugLogRepository(); this.mode = new MutableLiveData<>(); this.trace = Tracer.getInstance().serialize(); - this.pagingController = new ProxyPagingController(); + this.pagingController = new ProxyPagingController<>(); this.firstViewTime = System.currentTimeMillis(); this.staticLines = new ArrayList<>(); this.lines = new MediatorLiveData<>(); @@ -51,7 +51,7 @@ public class SubmitDebugLogViewModel extends ViewModel { .setStartIndex(0) .build(); - PagedData pagedData = PagedData.create(dataSource, config); + PagedData pagedData = PagedData.create(dataSource, config); ThreadUtil.runOnMain(() -> { pagingController.set(pagedData.getController()); diff --git a/paging/app/build.gradle b/paging/app/build.gradle index d557e2a719..026cabe184 100644 --- a/paging/app/build.gradle +++ b/paging/app/build.gradle @@ -11,15 +11,19 @@ android { minSdkVersion MINIMUM_SDK targetSdkVersion TARGET_SDK + multiDexEnabled true } compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JAVA_VERSION targetCompatibility JAVA_VERSION } } dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.2.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' diff --git a/paging/app/src/main/java/org/signal/pagingtest/EventListener.java b/paging/app/src/main/java/org/signal/pagingtest/EventListener.java new file mode 100644 index 0000000000..0f84b167af --- /dev/null +++ b/paging/app/src/main/java/org/signal/pagingtest/EventListener.java @@ -0,0 +1,5 @@ +package org.signal.pagingtest; + +interface EventListener { + void onItemClicked(String key); +} diff --git a/paging/app/src/main/java/org/signal/pagingtest/Item.java b/paging/app/src/main/java/org/signal/pagingtest/Item.java new file mode 100644 index 0000000000..630d1423e1 --- /dev/null +++ b/paging/app/src/main/java/org/signal/pagingtest/Item.java @@ -0,0 +1,11 @@ +package org.signal.pagingtest; + +public final class Item { + final String key; + final long timestamp; + + public Item(String key, long timestamp) { + this.key = key; + this.timestamp = timestamp; + } +} diff --git a/paging/app/src/main/java/org/signal/pagingtest/MainActivity.java b/paging/app/src/main/java/org/signal/pagingtest/MainActivity.java index 04b39087ca..1944268198 100644 --- a/paging/app/src/main/java/org/signal/pagingtest/MainActivity.java +++ b/paging/app/src/main/java/org/signal/pagingtest/MainActivity.java @@ -1,8 +1,10 @@ package org.signal.pagingtest; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -16,22 +18,26 @@ import org.signal.paging.PagingController; import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.Optional; -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity implements EventListener { + + private MainViewModel viewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - MyAdapter adapter = new MyAdapter(); + MyAdapter adapter = new MyAdapter(this); RecyclerView list = findViewById(R.id.list); LinearLayoutManager layoutManager = new LinearLayoutManager(this); list.setAdapter(adapter); list.setLayoutManager(layoutManager); - MainViewModel viewModel = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(MainViewModel.class); + viewModel = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(MainViewModel.class); adapter.setPagingController(viewModel.getPagingController()); viewModel.getList().observe(this, newList -> { adapter.submitList(newList); @@ -51,21 +57,30 @@ public class MainActivity extends AppCompatActivity { layoutManager.scrollToPosition(target); }); - findViewById(R.id.append_btn).setOnClickListener(v -> { - viewModel.appendItems(); + findViewById(R.id.prepend_btn).setOnClickListener(v -> { + viewModel.prependItems(); }); } + @Override + public void onItemClicked(String key) { + viewModel.onItemClicked(key); + } + static class MyAdapter extends RecyclerView.Adapter { private final static int TYPE_NORMAL = 1; private final static int TYPE_PLACEHOLDER = -1; - private PagingController controller; + private final EventListener listener; + private final List data; - private final List data = new ArrayList<>(); + private PagingController controller; + + public MyAdapter(@NonNull EventListener listener) { + this.listener = listener; + this.data = new ArrayList<>(); - public MyAdapter() { setHasStableIds(true); } @@ -81,14 +96,18 @@ public class MainActivity extends AppCompatActivity { @Override public long getItemId(int position) { - return position; + Item item = getItem(position); + if (item != null) { + return item.key.hashCode(); + } else { + return 0; + } } @Override public @NonNull MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { switch (viewType) { case TYPE_NORMAL: - return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false)); case TYPE_PLACEHOLDER: return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false)); default: @@ -98,24 +117,53 @@ public class MainActivity extends AppCompatActivity { @Override public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { - holder.bind(getItem(position)); + holder.bind(getItem(position), position, listener); } - private String getItem(int index) { + private Item getItem(int index) { if (controller != null) { controller.onDataNeededAroundIndex(index); } return data.get(index); } - void setPagingController(PagingController pagingController) { + void setPagingController(PagingController pagingController) { this.controller = pagingController; } - void submitList(List list) { + void submitList(List list) { + DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() { + @Override + public int getOldListSize() { + return data.size(); + } + + @Override + public int getNewListSize() { + return list.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + String oldKey = Optional.ofNullable(data.get(oldItemPosition)).map(item -> item.key).orElse(null); + String newKey = Optional.ofNullable(list.get(newItemPosition)).map(item -> item.key).orElse(null); + + return Objects.equals(oldKey, newKey); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + Long oldKey = Optional.ofNullable(data.get(oldItemPosition)).map(item -> item.timestamp).orElse(null); + Long newKey = Optional.ofNullable(list.get(newItemPosition)).map(item -> item.timestamp).orElse(null); + + return Objects.equals(oldKey, newKey); + } + }, false); + + result.dispatchUpdatesTo(this); + data.clear(); data.addAll(list); - notifyDataSetChanged(); } } @@ -128,8 +176,16 @@ public class MainActivity extends AppCompatActivity { textView = itemView.findViewById(R.id.text); } - void bind(@NonNull String s) { - textView.setText(s == null ? "PLACEHOLDER" : s); + void bind(@Nullable Item item, int position, @NonNull EventListener listener) { + if (item != null) { + textView.setText(position + " | " + item.key.substring(0, 13) + " | " + System.currentTimeMillis()); + textView.setOnClickListener(v -> { + listener.onItemClicked(item.key); + }); + } else { + textView.setText(position + " | PLACEHOLDER"); + textView.setOnClickListener(null); + } } } } \ No newline at end of file diff --git a/paging/app/src/main/java/org/signal/pagingtest/MainDataSource.java b/paging/app/src/main/java/org/signal/pagingtest/MainDataSource.java new file mode 100644 index 0000000000..3cbb1137fe --- /dev/null +++ b/paging/app/src/main/java/org/signal/pagingtest/MainDataSource.java @@ -0,0 +1,71 @@ +package org.signal.pagingtest; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.signal.paging.PagedDataSource; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.UUID; + +class MainDataSource implements PagedDataSource { + + private final List items = new ArrayList<>(); + + MainDataSource(int size) { + buildItems(size); + } + + @Override + public int size() { + return items.size(); + } + + @Override + public @NonNull List load(int start, int length, @NonNull CancellationSignal cancellationSignal) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + return items.subList(start, start + length); + } + + @Override + public @Nullable Item load(String key) { + return items.stream().filter(item -> item.key.equals(key)).findFirst().orElse(null); + } + + @Override + public @NonNull String getKey(@NonNull Item item) { + return item.key; + } + + public void updateItem(@NonNull String key) { + ListIterator iter = items.listIterator(); + while (iter.hasNext()) { + if (iter.next().key.equals(key)) { + iter.set(new Item(key, System.currentTimeMillis())); + break; + } + } + } + + public @NonNull String prepend() { + Item item = new Item(UUID.randomUUID().toString(), System.currentTimeMillis()); + items.add(0, item); + return item.key; + } + + private void buildItems(int size) { + items.clear(); + + for (int i = 0; i < size; i++) { + items.add(new Item(UUID.randomUUID().toString(), System.currentTimeMillis())); + } + } +} diff --git a/paging/app/src/main/java/org/signal/pagingtest/MainViewModel.java b/paging/app/src/main/java/org/signal/pagingtest/MainViewModel.java index df232323ba..5a671d0b8a 100644 --- a/paging/app/src/main/java/org/signal/pagingtest/MainViewModel.java +++ b/paging/app/src/main/java/org/signal/pagingtest/MainViewModel.java @@ -4,71 +4,40 @@ import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.ViewModel; -import org.signal.paging.PagedDataSource; import org.signal.paging.PagingController; import org.signal.paging.PagingConfig; import org.signal.paging.PagedData; -import java.util.ArrayList; import java.util.List; public class MainViewModel extends ViewModel { - private final PagedData pagedData; - private final MyDataSource dataSource; + private final PagedData pagedData; + private final MainDataSource dataSource; public MainViewModel() { - this.dataSource = new MyDataSource(1000); + this.dataSource = new MainDataSource(1000); this.pagedData = PagedData.create(dataSource, new PagingConfig.Builder().setBufferPages(3) .setPageSize(25) .build()); } - public @NonNull LiveData> getList() { + public void onItemClicked(@NonNull String key) { + dataSource.updateItem(key); + pagedData.getController().onDataItemChanged(key); + } + + public @NonNull LiveData> getList() { return pagedData.getData(); } - public @NonNull PagingController getPagingController() { + public @NonNull PagingController getPagingController() { return pagedData.getController(); } - public void appendItems() { - dataSource.setSize(dataSource.size() + 1); - pagedData.getController().onDataInvalidated(); + public void prependItems() { + String key = dataSource.prepend(); + pagedData.getController().onDataItemInserted(key, 0); } - private static class MyDataSource implements PagedDataSource { - - private int size; - - MyDataSource(int size) { - this.size = size; - } - - public void setSize(int size) { - this.size = size; - } - - @Override - public int size() { - return size; - } - - @Override - public List load(int start, int length, @NonNull CancellationSignal cancellationSignal) { - try { - Thread.sleep(500); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - List data = new ArrayList<>(length); - - for (int i = 0; i < length; i++) { - data.add(String.valueOf(start + i) + " (" + System.currentTimeMillis() + ")"); - } - - return data; - } - } } diff --git a/paging/app/src/main/res/layout/activity_main.xml b/paging/app/src/main/res/layout/activity_main.xml index b9fd5fd99c..0baa1e7192 100644 --- a/paging/app/src/main/res/layout/activity_main.xml +++ b/paging/app/src/main/res/layout/activity_main.xml @@ -53,10 +53,10 @@ android:layout_height="wrap_content" /> + android:text="Prepend" /> diff --git a/paging/app/src/main/res/layout/item.xml b/paging/app/src/main/res/layout/item.xml index 6c82903b6f..a0cd66dbdd 100644 --- a/paging/app/src/main/res/layout/item.xml +++ b/paging/app/src/main/res/layout/item.xml @@ -22,6 +22,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="8dp" + android:fontFamily="monospace" tools:text="Spider-Man"/> diff --git a/paging/lib/src/main/java/org/signal/paging/BufferedPagingController.java b/paging/lib/src/main/java/org/signal/paging/BufferedPagingController.java index 6cb66efe3d..609b6d3ed8 100644 --- a/paging/lib/src/main/java/org/signal/paging/BufferedPagingController.java +++ b/paging/lib/src/main/java/org/signal/paging/BufferedPagingController.java @@ -20,17 +20,17 @@ import java.util.concurrent.Executors; * contains it. When invalidations come in, this class will just swap out the active controller with * a new one. */ -class BufferedPagingController implements PagingController { +class BufferedPagingController implements PagingController { - private final PagedDataSource dataSource; - private final PagingConfig config; - private final MutableLiveData> liveData; - private final Executor serializationExecutor; + private final PagedDataSource dataSource; + private final PagingConfig config; + private final MutableLiveData> liveData; + private final Executor serializationExecutor; - private PagingController activeController; - private int lastRequestedIndex; + private PagingController activeController; + private int lastRequestedIndex; - BufferedPagingController(PagedDataSource dataSource, PagingConfig config, @NonNull MutableLiveData> liveData) { + BufferedPagingController(PagedDataSource dataSource, PagingConfig config, @NonNull MutableLiveData> liveData) { this.dataSource = dataSource; this.config = config; this.liveData = liveData; @@ -61,4 +61,22 @@ class BufferedPagingController implements PagingController { activeController.onDataNeededAroundIndex(lastRequestedIndex); }); } + + @Override + public void onDataItemChanged(Key key) { + serializationExecutor.execute(() -> { + if (activeController != null) { + activeController.onDataItemChanged(key); + } + }); + } + + @Override + public void onDataItemInserted(Key key, int position) { + serializationExecutor.execute(() -> { + if (activeController != null) { + activeController.onDataItemInserted(key, position); + } + }); + } } diff --git a/paging/lib/src/main/java/org/signal/paging/CompressedList.java b/paging/lib/src/main/java/org/signal/paging/CompressedList.java index 7bfba4f841..6b9ae06c88 100644 --- a/paging/lib/src/main/java/org/signal/paging/CompressedList.java +++ b/paging/lib/src/main/java/org/signal/paging/CompressedList.java @@ -40,4 +40,9 @@ public class CompressedList extends AbstractList { public E set(int globalIndex, E element) { return wrapped.set(globalIndex, element); } + + @Override + public void add(int index, E element) { + wrapped.add(index, element); + } } diff --git a/paging/lib/src/main/java/org/signal/paging/FixedSizePagingController.java b/paging/lib/src/main/java/org/signal/paging/FixedSizePagingController.java index e907a99086..5de87cf84d 100644 --- a/paging/lib/src/main/java/org/signal/paging/FixedSizePagingController.java +++ b/paging/lib/src/main/java/org/signal/paging/FixedSizePagingController.java @@ -7,7 +7,9 @@ import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; /** @@ -18,32 +20,34 @@ import java.util.concurrent.Executor; * which allows it to keep track of pending requests in a thread-safe way, while spinning off * tasks to fetch data on its own executor. */ -class FixedSizePagingController implements PagingController { +class FixedSizePagingController implements PagingController { private static final String TAG = FixedSizePagingController.class.getSimpleName(); private static final Executor FETCH_EXECUTOR = SignalExecutors.newCachedSingleThreadExecutor("signal-FixedSizePagingController"); private static final boolean DEBUG = false; - private final PagedDataSource dataSource; - private final PagingConfig config; - private final MutableLiveData> liveData; - private final DataStatus loadState; + private final PagedDataSource dataSource; + private final PagingConfig config; + private final MutableLiveData> liveData; + private final DataStatus loadState; + private final Map keyToPosition; - private List data; + private List data; private volatile boolean invalidated; - FixedSizePagingController(@NonNull PagedDataSource dataSource, + FixedSizePagingController(@NonNull PagedDataSource dataSource, @NonNull PagingConfig config, - @NonNull MutableLiveData> liveData, + @NonNull MutableLiveData> liveData, int size) { - this.dataSource = dataSource; - this.config = config; - this.liveData = liveData; - this.loadState = DataStatus.obtain(size); - this.data = new CompressedList<>(loadState.size()); + this.dataSource = dataSource; + this.config = config; + this.liveData = liveData; + this.loadState = DataStatus.obtain(size); + this.data = new CompressedList<>(loadState.size()); + this.keyToPosition = new HashMap<>(); } /** @@ -96,17 +100,21 @@ class FixedSizePagingController implements PagingController { return; } - List loaded = dataSource.load(loadStart, loadEnd - loadStart, () -> invalidated); + List loaded = dataSource.load(loadStart, loadEnd - loadStart, () -> invalidated); if (invalidated) { Log.w(TAG, buildLog(aroundIndex, "Invalidated! Just after data was loaded.")); return; } - List updated = new CompressedList<>(data); + List updated = new CompressedList<>(data); for (int i = 0, len = Math.min(loaded.size(), data.size() - loadStart); i < len; i++) { - updated.set(loadStart + i, loaded.get(i)); + int position = loadStart + i; + Data item = loaded.get(i); + + updated.set(position, item); + keyToPosition.put(dataSource.getKey(item), position); } data = updated; @@ -124,6 +132,56 @@ class FixedSizePagingController implements PagingController { loadState.recycle(); } + @Override + public void onDataItemChanged(Key key) { + FETCH_EXECUTOR.execute(() -> { + Integer position = keyToPosition.get(key); + + if (position == null) { + Log.w(TAG, "Notified of key " + key + " but it wasn't in the cache!"); + return; + } + + Data item = dataSource.load(key); + + if (item == null) { + Log.w(TAG, "Notified of key " + key + " but the loaded item was null!"); + return; + } + + List updatedList = new CompressedList<>(data); + + updatedList.set(position, item); + data = updatedList; + liveData.postValue(updatedList); + }); + } + + @Override + public void onDataItemInserted(Key key, int position) { + FETCH_EXECUTOR.execute(() -> { + if (keyToPosition.containsKey(key)) { + Log.w(TAG, "Notified of key " + key + " being inserted at " + position + ", but the item already exists!"); + return; + } + + Data item = dataSource.load(key); + + if (item == null) { + Log.w(TAG, "Notified of key " + key + " being inserted at " + position + ", but the loaded item was null!"); + return; + } + + List updatedList = new CompressedList<>(data); + + updatedList.add(position, item); + keyToPosition.put(dataSource.getKey(item), position); + + data = updatedList; + liveData.postValue(updatedList); + }); + } + private static String buildLog(int aroundIndex, String message) { return "onDataNeededAroundIndex(" + aroundIndex + ") " + message; } diff --git a/paging/lib/src/main/java/org/signal/paging/PagedData.java b/paging/lib/src/main/java/org/signal/paging/PagedData.java index 92eb926049..970be9fd3d 100644 --- a/paging/lib/src/main/java/org/signal/paging/PagedData.java +++ b/paging/lib/src/main/java/org/signal/paging/PagedData.java @@ -10,31 +10,31 @@ import java.util.List; /** * The primary entry point for creating paged data. */ -public final class PagedData { +public final class PagedData { - private final LiveData> data; - private final PagingController controller; + private final LiveData> data; + private final PagingController controller; @AnyThread - public static PagedData create(@NonNull PagedDataSource dataSource, @NonNull PagingConfig config) { - MutableLiveData> liveData = new MutableLiveData<>(); - PagingController controller = new BufferedPagingController<>(dataSource, config, liveData); + public static PagedData create(@NonNull PagedDataSource dataSource, @NonNull PagingConfig config) { + MutableLiveData> liveData = new MutableLiveData<>(); + PagingController controller = new BufferedPagingController<>(dataSource, config, liveData); return new PagedData<>(liveData, controller); } - private PagedData(@NonNull LiveData> data, @NonNull PagingController controller) { + private PagedData(@NonNull LiveData> data, @NonNull PagingController controller) { this.data = data; this.controller = controller; } @AnyThread - public @NonNull LiveData> getData() { + public @NonNull LiveData> getData() { return data; } @AnyThread - public @NonNull PagingController getController() { + public @NonNull PagingController getController() { return controller; } } diff --git a/paging/lib/src/main/java/org/signal/paging/PagedDataSource.java b/paging/lib/src/main/java/org/signal/paging/PagedDataSource.java index 22efa43823..55b1d31557 100644 --- a/paging/lib/src/main/java/org/signal/paging/PagedDataSource.java +++ b/paging/lib/src/main/java/org/signal/paging/PagedDataSource.java @@ -1,6 +1,7 @@ package org.signal.paging; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import java.util.List; @@ -8,7 +9,7 @@ import java.util.List; /** * Represents a source of data that can be queried. */ -public interface PagedDataSource { +public interface PagedDataSource { /** * @return The total size of the data set. */ @@ -24,7 +25,13 @@ public interface PagedDataSource { * If you don't have the full range, just populate what you can. */ @WorkerThread - @NonNull List load(int start, int length, @NonNull CancellationSignal cancellationSignal); + @NonNull List load(int start, int length, @NonNull CancellationSignal cancellationSignal); + + @WorkerThread + @Nullable Data load(Key key); + + @WorkerThread + @NonNull Key getKey(@NonNull Data data); interface CancellationSignal { /** diff --git a/paging/lib/src/main/java/org/signal/paging/PagingController.java b/paging/lib/src/main/java/org/signal/paging/PagingController.java index db2399fa8f..e11020e60e 100644 --- a/paging/lib/src/main/java/org/signal/paging/PagingController.java +++ b/paging/lib/src/main/java/org/signal/paging/PagingController.java @@ -1,7 +1,9 @@ package org.signal.paging; -public interface PagingController { +public interface PagingController { void onDataNeededAroundIndex(int aroundIndex); void onDataInvalidated(); + void onDataItemChanged(Key key); + void onDataItemInserted(Key key, int position); } diff --git a/paging/lib/src/main/java/org/signal/paging/ProxyPagingController.java b/paging/lib/src/main/java/org/signal/paging/ProxyPagingController.java index 16b9edccb8..3f88758bec 100644 --- a/paging/lib/src/main/java/org/signal/paging/ProxyPagingController.java +++ b/paging/lib/src/main/java/org/signal/paging/ProxyPagingController.java @@ -1,7 +1,5 @@ package org.signal.paging; -import android.util.Log; - import androidx.annotation.Nullable; /** @@ -9,9 +7,9 @@ import androidx.annotation.Nullable; * to keep a single, static controller, even when the true controller may be changing due to data * source changes. */ -public class ProxyPagingController implements PagingController { +public class ProxyPagingController implements PagingController { - private PagingController proxied; + private PagingController proxied; @Override public synchronized void onDataNeededAroundIndex(int aroundIndex) { @@ -27,10 +25,24 @@ public class ProxyPagingController implements PagingController { } } + @Override + public void onDataItemChanged(Key key) { + if (proxied != null) { + proxied.onDataItemChanged(key); + } + } + + @Override + public void onDataItemInserted(Key key, int position) { + if (proxied != null) { + proxied.onDataItemInserted(key, position); + } + } + /** * Updates the underlying controller to the one specified. */ - public synchronized void set(@Nullable PagingController bound) { + public synchronized void set(@Nullable PagingController bound) { this.proxied = bound; } }