diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java b/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java index 078845d355..23a56f2bd4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java @@ -47,6 +47,7 @@ import org.thoughtcrime.securesms.util.Util; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -78,6 +79,7 @@ public final class MultiShareSender { boolean isMmsEnabled = Util.isMmsCapable(context); String message = multiShareArgs.getDraftText(); SlideDeck slideDeck; + List storiesBatch = new LinkedList<>(); try { slideDeck = buildSlideDeck(context, multiShareArgs); @@ -118,7 +120,7 @@ public final class MultiShareSender { if ((recipient.isMmsGroup() || recipient.getEmail().isPresent()) && !isMmsEnabled) { results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.MMS_NOT_ENABLED)); } else if (hasMmsMedia && transport.isSms() || hasPushMedia && !transport.isSms() || canSendAsTextStory) { - sendMediaMessage(context, multiShareArgs, recipient, slideDeck, transport, threadId, forceSms, expiresIn, multiShareArgs.isViewOnce(), subscriptionId, mentions, recipientSearchKey.isStory(), sentTimestamp, canSendAsTextStory); + sendMediaMessageOrCollectStoryToBatch(context, multiShareArgs, recipient, slideDeck, transport, threadId, forceSms, expiresIn, multiShareArgs.isViewOnce(), subscriptionId, mentions, recipientSearchKey.isStory(), sentTimestamp, canSendAsTextStory, storiesBatch); results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.SUCCESS)); } else if (recipientSearchKey.isStory()) { results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.INVALID_SHARE_TO_STORY)); @@ -132,6 +134,15 @@ public final class MultiShareSender { ThreadUtil.sleep(5); } + if (!storiesBatch.isEmpty()) { + MessageSender.sendStories(context, + storiesBatch.stream() + .map(OutgoingSecureMediaMessage::new) + .collect(Collectors.toList()), + null, + null); + } + return new MultiShareSendResultCollection(results); } @@ -160,20 +171,21 @@ public final class MultiShareSender { } } - private static void sendMediaMessage(@NonNull Context context, - @NonNull MultiShareArgs multiShareArgs, - @NonNull Recipient recipient, - @NonNull SlideDeck slideDeck, - @NonNull TransportOption transportOption, - long threadId, - boolean forceSms, - long expiresIn, - boolean isViewOnce, - int subscriptionId, - @NonNull List validatedMentions, - boolean isStory, - long sentTimestamp, - boolean canSendAsTextStory) + private static void sendMediaMessageOrCollectStoryToBatch(@NonNull Context context, + @NonNull MultiShareArgs multiShareArgs, + @NonNull Recipient recipient, + @NonNull SlideDeck slideDeck, + @NonNull TransportOption transportOption, + long threadId, + boolean forceSms, + long expiresIn, + boolean isViewOnce, + int subscriptionId, + @NonNull List validatedMentions, + boolean isStory, + long sentTimestamp, + boolean canSendAsTextStory, + @NonNull List storiesToBatchSend) { String body = multiShareArgs.getDraftText(); if (transportOption.isType(TransportOption.Type.TEXTSECURE) && !forceSms && body != null) { @@ -270,8 +282,9 @@ public final class MultiShareSender { outgoingMessages.add(outgoingMediaMessage); } - if (shouldSendAsPush(recipient, forceSms)) - { + if (isStory) { + storiesToBatchSend.addAll(outgoingMessages); + } else if (shouldSendAsPush(recipient, forceSms)) { for (final OutgoingMediaMessage outgoingMessage : outgoingMessages) { MessageSender.send(context, new OutgoingSecureMediaMessage(outgoingMessage), threadId, false, null, null); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java index e6daceffbe..fa8e3e45c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java @@ -130,6 +130,67 @@ public class MessageSender { return allocatedThreadId; } + public static void sendStories(@NonNull final Context context, + @NonNull final List messages, + @Nullable final String metricId, + @Nullable final SmsDatabase.InsertListener insertListener) + { + Log.i(TAG, "Sending story messages to " + messages.size() + " targets."); + ThreadDatabase threadDatabase = SignalDatabase.threads(); + MessageDatabase database = SignalDatabase.mms(); + List messageIds = new ArrayList<>(messages.size()); + List threads = new ArrayList<>(messages.size()); + + try { + database.beginTransaction(); + for (OutgoingMediaMessage message : messages) { + long allocatedThreadId = threadDatabase.getOrCreateValidThreadId(message.getRecipient(), -1L, message.getDistributionType()); + Recipient recipient = message.getRecipient(); + long messageId = database.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId), allocatedThreadId, false, insertListener); + + messageIds.add(messageId); + threads.add(allocatedThreadId); + + if (message.getRecipient().isGroup() && message.getAttachments().isEmpty() && message.getLinkPreviews().isEmpty() && message.getSharedContacts().isEmpty()) { + SignalLocalMetrics.GroupMessageSend.onInsertedIntoDatabase(messageId, metricId); + } else { + SignalLocalMetrics.GroupMessageSend.cancel(metricId); + } + } + + for (int i = 0; i < messageIds.size(); i++) { + long messageId = messageIds.get(i); + OutgoingSecureMediaMessage message = messages.get(i); + Recipient recipient = message.getRecipient(); + + if (recipient.isDistributionList()) { + List members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId()); + SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies()); + } + } + + database.setTransactionSuccessful(); + } catch (MmsException e) { + Log.w(TAG, e); + } finally { + database.endTransaction(); + } + + for (int i = 0; i < messageIds.size(); i++) { + long messageId = messageIds.get(i); + OutgoingSecureMediaMessage message = messages.get(i); + Recipient recipient = message.getRecipient(); + + sendMediaMessage(context, recipient, false, messageId, Collections.emptyList()); + } + + onMessageSent(); + + for (long threadId : threads) { + threadDatabase.update(threadId, true); + } + } + public static long send(final Context context, final OutgoingMediaMessage message, final long threadId, diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/Stories.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/Stories.kt index b9cacf9224..120fcfbebe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/Stories.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/Stories.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.stories import androidx.annotation.WorkerThread import androidx.fragment.app.FragmentManager import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.schedulers.Schedulers import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.contacts.HeaderAction import org.thoughtcrime.securesms.database.AttachmentDatabase @@ -21,6 +22,7 @@ import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.util.BottomSheetUtil import org.thoughtcrime.securesms.util.FeatureFlags +import org.thoughtcrime.securesms.util.hasLinkPreview import java.util.concurrent.TimeUnit object Stories { @@ -49,10 +51,9 @@ object Stories { } } - @WorkerThread fun sendTextStories(messages: List): Completable { return Completable.create { emitter -> - MessageSender.sendMediaBroadcast(ApplicationDependencies.getApplication(), messages, listOf(), listOf()) + MessageSender.sendStories(ApplicationDependencies.getApplication(), messages, null, null) emitter.onComplete() } } @@ -87,12 +88,29 @@ object Stories { 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) - } + enqueueAttachmentsFromStoryForDownloadSync(record, ignoreAutoDownloadConstraints) + } + } + + fun enqueueAttachmentsFromStoryForDownload(record: MmsMessageRecord, ignoreAutoDownloadConstraints: Boolean): Completable { + return Completable.fromAction { + enqueueAttachmentsFromStoryForDownloadSync(record, ignoreAutoDownloadConstraints) + }.subscribeOn(Schedulers.io()) + } + + @WorkerThread + private fun enqueueAttachmentsFromStoryForDownloadSync(record: MmsMessageRecord, ignoreAutoDownloadConstraints: Boolean) { + 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) } } + + if (record.hasLinkPreview()) { + ApplicationDependencies.getJobManager().add( + AttachmentDownloadJob(record.id, record.linkPreviews[0].attachmentId, true) + ) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt index 8b97ff0ef5..6693b900a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt @@ -7,7 +7,6 @@ import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.schedulers.Schedulers import org.signal.core.util.BreakIteratorCompat import org.signal.core.util.concurrent.SignalExecutors -import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.database.DatabaseObserver import org.thoughtcrime.securesms.database.NoSuchMessageException @@ -17,7 +16,6 @@ import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost import org.thoughtcrime.securesms.dependencies.ApplicationDependencies -import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob import org.thoughtcrime.securesms.recipients.Recipient @@ -133,12 +131,8 @@ open class StoryViewerPageRepository(context: Context) { } } - fun forceDownload(post: StoryPost) { - if (post.content is StoryPost.Content.AttachmentContent) { - ApplicationDependencies.getJobManager().add( - AttachmentDownloadJob(post.id, (post.content.attachment as DatabaseAttachment).attachmentId, true) - ) - } + fun forceDownload(post: StoryPost): Completable { + return Stories.enqueueAttachmentsFromStoryForDownload(post.conversationMessage.messageRecord as MmsMessageRecord, true) } fun getStoryPostsFor(recipientId: RecipientId): Observable> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt index 21d5329a59..35b254aa36 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModel.kt @@ -85,7 +85,7 @@ class StoryViewerPageViewModel( val selectedPost = getPostAt(index) if (selectedPost != null && selectedPost.content.transferState != AttachmentDatabase.TRANSFER_PROGRESS_DONE) { - repository.forceDownload(selectedPost) + disposables += repository.forceDownload(selectedPost).subscribe() } store.update { @@ -136,7 +136,7 @@ class StoryViewerPageViewModel( } fun forceDownloadSelectedPost() { - repository.forceDownload(getPost()) + disposables += repository.forceDownload(getPost()).subscribe() } fun startDirectReply(storyId: Long, recipientId: RecipientId) { diff --git a/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModelTest.kt index bd20c6d975..65af310cac 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageViewModelTest.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.stories.viewer.page import android.app.Application +import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.plugins.RxJavaPlugins import io.reactivex.rxjava3.schedulers.TestScheduler @@ -28,6 +29,8 @@ class StoryViewerPageViewModelTest { fun setUp() { RxJavaPlugins.setInitComputationSchedulerHandler { testScheduler } RxJavaPlugins.setComputationSchedulerHandler { testScheduler } + + whenever(repository.forceDownload(any())).thenReturn(Completable.complete()) } @After