Add support for smarter story downloads.

This commit is contained in:
Alex Hart 2022-04-21 17:29:02 -03:00 committed by Cody Henthorne
parent c4bc2162f2
commit 17111abc72
13 changed files with 125 additions and 15 deletions

View file

@ -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);

View file

@ -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)";

View file

@ -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();

View file

@ -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) {

View file

@ -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);

View file

@ -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) {

View file

@ -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());
}
}

View file

@ -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)
}
}
}
}
}

View file

@ -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)

View file

@ -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)
}
}
}

View file

@ -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 {

View file

@ -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);

View file

@ -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 {