Add support for smarter story downloads.
This commit is contained in:
parent
c4bc2162f2
commit
17111abc72
13 changed files with 125 additions and 15 deletions
|
@ -195,7 +195,10 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
|||
public abstract @NonNull Cursor getStoryReplies(long parentStoryId);
|
||||
public abstract @Nullable Long getOldestStorySendTimestamp();
|
||||
public abstract int deleteStoriesOlderThan(long timestamp);
|
||||
public abstract @NonNull MessageDatabase.Reader getUnreadStories(@NonNull RecipientId recipientId, int limit);
|
||||
|
||||
public abstract @NonNull StoryViewState getStoryViewState(@NonNull RecipientId recipientId);
|
||||
public abstract void updateViewedStories(@NonNull Set<SyncMessageId> syncMessageIds);
|
||||
|
||||
final @NonNull String getOutgoingTypeClause() {
|
||||
List<String> segments = new ArrayList<>(Types.OUTGOING_MESSAGE_TYPES.length);
|
||||
|
|
|
@ -590,6 +590,17 @@ public class MmsDatabase extends MessageDatabase {
|
|||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageDatabase.Reader getUnreadStories(@NonNull RecipientId recipientId, int limit) {
|
||||
final String query = IS_STORY_CLAUSE +
|
||||
" AND NOT (" + getOutgoingTypeClause() + ") " +
|
||||
" AND " + RECIPIENT_ID + " = ?" +
|
||||
" AND " + VIEWED_RECEIPT_COUNT + " = ?";
|
||||
final String[] args = SqlUtil.buildArgs(recipientId, 0);
|
||||
|
||||
return new Reader(rawQuery(query, args, false, limit));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull StoryViewState getStoryViewState(@NonNull RecipientId recipientId) {
|
||||
if (!Stories.isFeatureEnabled()) {
|
||||
|
@ -601,6 +612,28 @@ public class MmsDatabase extends MessageDatabase {
|
|||
return getStoryViewState(threadId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes whether we've viewed a recipient's story based on incoming sync messages.
|
||||
*/
|
||||
public void updateViewedStories(@NonNull Set<SyncMessageId> syncMessageIds) {
|
||||
final String timestamps = Util.join(syncMessageIds.stream().map(SyncMessageId::getTimetamp).collect(java.util.stream.Collectors.toList()), ",");
|
||||
final String[] projection = SqlUtil.buildArgs(RECIPIENT_ID);
|
||||
final String where = IS_STORY_CLAUSE + " AND " + NORMALIZED_DATE_SENT + " IN (" + timestamps + ") AND NOT (" + getOutgoingTypeClause() + ") AND " + VIEWED_RECEIPT_COUNT + " > 0";
|
||||
|
||||
try {
|
||||
getWritableDatabase().beginTransaction();
|
||||
try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, projection, where, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
Recipient recipient = Recipient.resolved(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)));
|
||||
SignalDatabase.recipients().updateLastStoryViewTimestamp(recipient.getId());
|
||||
}
|
||||
}
|
||||
getWritableDatabase().setTransactionSuccessful();
|
||||
} finally {
|
||||
getWritableDatabase().endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@NonNull StoryViewState getStoryViewState(long threadId) {
|
||||
final String hasStoryQuery = "SELECT EXISTS(SELECT 1 FROM " + TABLE_NAME + " WHERE " + IS_STORY_CLAUSE + " AND " + THREAD_ID_WHERE + " LIMIT 1)";
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.signal.libsignal.protocol.util.Pair;
|
|||
import org.thoughtcrime.securesms.database.MessageDatabase.MessageUpdate;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -529,6 +530,9 @@ public class MmsSmsDatabase extends Database {
|
|||
return SignalDatabase.mms().incrementReceiptCount(syncMessageId, timestamp, receiptType, true);
|
||||
}
|
||||
|
||||
public void updateViewedStories(@NonNull Set<SyncMessageId> syncMessageIds) {
|
||||
SignalDatabase.mms().updateViewedStories(syncMessageIds);
|
||||
}
|
||||
|
||||
public void setTimestampRead(@NonNull Recipient senderRecipient, @NonNull List<ReadMessage> readMessages, long proposedExpireStarted, @NonNull Map<Long, Long> threadToLatestRead) {
|
||||
SQLiteDatabase db = getWritableDatabase();
|
||||
|
|
|
@ -1964,6 +1964,10 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
|||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
|
||||
fun updateLastStoryViewTimestamp(id: RecipientId) {
|
||||
updateExtras(id) { it.setLastStoryView(System.currentTimeMillis()) }
|
||||
}
|
||||
|
||||
fun clearUsernameIfExists(username: String) {
|
||||
val existingUsername = getByUsername(username)
|
||||
if (existingUsername.isPresent) {
|
||||
|
|
|
@ -1451,11 +1451,21 @@ public class SmsDatabase extends MessageDatabase {
|
|||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateViewedStories(@NonNull Set<SyncMessageId> syncMessageIds) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteStoriesOlderThan(long timestamp) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageDatabase.Reader getUnreadStories(@NonNull RecipientId recipientId, int limit) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException {
|
||||
return getSmsMessage(messageId);
|
||||
|
|
|
@ -177,12 +177,14 @@ import java.util.Arrays;
|
|||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
@ -1407,13 +1409,7 @@ public final class MessageContentProcessor {
|
|||
}
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
List<DatabaseAttachment> allAttachments = SignalDatabase.attachments().getAttachmentsForMessage(insertResult.get().getMessageId());
|
||||
List<DatabaseAttachment> attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList();
|
||||
|
||||
for (DatabaseAttachment attachment : attachments) {
|
||||
ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false));
|
||||
}
|
||||
|
||||
Stories.enqueueNextStoriesForDownload(threadRecipient.getId(), false);
|
||||
ApplicationDependencies.getExpireStoriesManager().scheduleIfNecessary();
|
||||
}
|
||||
}
|
||||
|
@ -2164,6 +2160,11 @@ public final class MessageContentProcessor {
|
|||
Collection<SyncMessageId> unhandled = shouldOnlyProcessStories ? SignalDatabase.mmsSms().incrementViewedStoryReceiptCounts(ids, content.getTimestamp())
|
||||
: SignalDatabase.mmsSms().incrementViewedReceiptCounts(ids, content.getTimestamp());
|
||||
|
||||
Set<SyncMessageId> handled = new HashSet<>(ids);
|
||||
handled.removeAll(unhandled);
|
||||
|
||||
SignalDatabase.mmsSms().updateViewedStories(handled);
|
||||
|
||||
for (SyncMessageId id : unhandled) {
|
||||
warn(String.valueOf(content.getTimestamp()), "[handleViewedReceipt] Could not find matching message! timestamp: " + id.getTimetamp() + " author: " + senderRecipient.getId());
|
||||
if (!processingEarlyContent) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import androidx.annotation.WorkerThread;
|
|||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
@ -44,7 +45,6 @@ import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
|||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||
import org.whispersystems.signalservice.api.push.PNI;
|
||||
|
@ -725,6 +725,10 @@ public class Recipient {
|
|||
return extras.map(Extras::hideStory).orElse(false);
|
||||
}
|
||||
|
||||
public boolean hasViewedStory() {
|
||||
return extras.map(Extras::hasViewedStory).orElse(false);
|
||||
}
|
||||
|
||||
public @NonNull GroupId requireGroupId() {
|
||||
GroupId resolved = resolving ? resolve().groupId : groupId;
|
||||
|
||||
|
@ -1218,17 +1222,21 @@ public class Recipient {
|
|||
return recipientExtras.getHideStory();
|
||||
}
|
||||
|
||||
public boolean hasViewedStory() {
|
||||
return recipientExtras.getLastStoryView() > 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final Extras that = (Extras) o;
|
||||
return manuallyShownAvatar() == that.manuallyShownAvatar() && hideStory() == that.hideStory();
|
||||
return manuallyShownAvatar() == that.manuallyShownAvatar() && hideStory() == that.hideStory() && hasViewedStory() == that.hasViewedStory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(manuallyShownAvatar(), hideStory());
|
||||
return Objects.hash(manuallyShownAvatar(), hideStory(), hasViewedStory());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,9 +5,12 @@ import androidx.fragment.app.FragmentManager
|
|||
import io.reactivex.rxjava3.core.Completable
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.HeaderAction
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseStoryTypeBottomSheet
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage
|
||||
|
@ -72,4 +75,24 @@ object Stories {
|
|||
SignalDatabase.recipients.markNeedsSync(storyRecipientId)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun enqueueNextStoriesForDownload(recipientId: RecipientId, ignoreAutoDownloadConstraints: Boolean = false) {
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
if (!recipient.isSelf && (recipient.shouldHideStory() || !recipient.hasViewedStory())) {
|
||||
return
|
||||
}
|
||||
|
||||
val unreadStoriesReader = SignalDatabase.mms.getUnreadStories(recipientId, FeatureFlags.storiesAutoDownloadMaximum())
|
||||
while (unreadStoriesReader.next != null) {
|
||||
val record = unreadStoriesReader.current as MmsMessageRecord
|
||||
SignalDatabase.attachments.getAttachmentsForMessage(record.id).filterNot { it.isSticker }.forEach {
|
||||
if (it.transferState == AttachmentDatabase.TRANSFER_PROGRESS_PENDING) {
|
||||
val job = AttachmentDownloadJob(record.id, it.attachmentId, ignoreAutoDownloadConstraints)
|
||||
ApplicationDependencies.getJobManager().add(job)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -547,6 +547,10 @@ class StoryViewerPageFragment :
|
|||
AttachmentDatabase.TRANSFER_PROGRESS_DONE -> {
|
||||
storySlate.moveToState(StorySlateView.State.HIDDEN, post.id)
|
||||
viewModel.setIsDisplayingSlate(false)
|
||||
|
||||
if (post.content.transferState == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
|
||||
viewModel.markViewed(post)
|
||||
}
|
||||
}
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_PENDING -> {
|
||||
storySlate.moveToState(StorySlateView.State.LOADING, post.id)
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob
|
|||
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
|
||||
/**
|
||||
|
@ -173,6 +174,10 @@ open class StoryViewerPageRepository(context: Context) {
|
|||
)
|
||||
)
|
||||
MultiDeviceViewedUpdateJob.enqueue(listOf(markedMessageInfo.syncMessageId))
|
||||
|
||||
val recipientId = storyPost.group?.id ?: storyPost.sender.id
|
||||
SignalDatabase.recipients.updateLastStoryViewTimestamp(recipientId)
|
||||
Stories.enqueueNextStoriesForDownload(recipientId, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
|||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
@ -76,11 +77,15 @@ class StoryViewerPageViewModel(
|
|||
return repository.hideStory(recipientId)
|
||||
}
|
||||
|
||||
fun markViewed(storyPost: StoryPost) {
|
||||
repository.markViewed(storyPost)
|
||||
}
|
||||
|
||||
fun setSelectedPostIndex(index: Int) {
|
||||
val selectedPost = getPostAt(index)
|
||||
|
||||
if (selectedPost != null) {
|
||||
repository.markViewed(selectedPost)
|
||||
if (selectedPost != null && selectedPost.content.transferState != AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
|
||||
repository.forceDownload(selectedPost)
|
||||
}
|
||||
|
||||
store.update {
|
||||
|
|
|
@ -97,6 +97,7 @@ public final class FeatureFlags {
|
|||
private static final String PAYMENTS_COUNTRY_BLOCKLIST = "android.payments.blocklist";
|
||||
private static final String PNP_CDS = "android.pnp.cds";
|
||||
private static final String USE_FCM_FOREGROUND_SERVICE = "android.useFcmForegroundService";
|
||||
private static final String STORIES_AUTO_DOWNLOAD_MAXIMUM = "android.stories.autoDownloadMaximum";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
|
@ -145,7 +146,8 @@ public final class FeatureFlags {
|
|||
USE_HARDWARE_AEC_IF_OLD,
|
||||
USE_AEC3,
|
||||
PAYMENTS_COUNTRY_BLOCKLIST,
|
||||
USE_FCM_FOREGROUND_SERVICE
|
||||
USE_FCM_FOREGROUND_SERVICE,
|
||||
STORIES_AUTO_DOWNLOAD_MAXIMUM
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -512,6 +514,13 @@ public final class FeatureFlags {
|
|||
return getBoolean(USE_FCM_FOREGROUND_SERVICE, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch count for stories from a given user.
|
||||
*/
|
||||
public static int storiesAutoDownloadMaximum() {
|
||||
return getInteger(STORIES_AUTO_DOWNLOAD_MAXIMUM, 2);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
|
@ -178,8 +178,9 @@ message ChatColor {
|
|||
}
|
||||
|
||||
message RecipientExtras {
|
||||
bool manuallyShownAvatar = 1;
|
||||
bool hideStory = 2;
|
||||
bool manuallyShownAvatar = 1;
|
||||
bool hideStory = 2;
|
||||
int64 lastStoryView = 3;
|
||||
}
|
||||
|
||||
message CustomAvatar {
|
||||
|
|
Loading…
Add table
Reference in a new issue