kopia lustrzana https://github.com/ryukoposting/Signal-Android
369 wiersze
19 KiB
Java
369 wiersze
19 KiB
Java
package org.thoughtcrime.securesms.sharing;
|
|
|
|
import android.content.Context;
|
|
import android.net.Uri;
|
|
|
|
import androidx.annotation.MainThread;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.WorkerThread;
|
|
import androidx.core.util.Consumer;
|
|
|
|
import com.annimon.stream.Stream;
|
|
|
|
import org.signal.core.util.ThreadUtil;
|
|
import org.signal.core.util.logging.Log;
|
|
import org.thoughtcrime.securesms.TransportOption;
|
|
import org.thoughtcrime.securesms.TransportOptions;
|
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
|
import org.thoughtcrime.securesms.database.model.Mention;
|
|
import org.thoughtcrime.securesms.database.model.StoryType;
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
import org.thoughtcrime.securesms.mediasend.Media;
|
|
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
|
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
|
import org.thoughtcrime.securesms.mms.Slide;
|
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
|
import org.thoughtcrime.securesms.mms.SlideFactory;
|
|
import org.thoughtcrime.securesms.mms.StickerSlide;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
|
import org.thoughtcrime.securesms.sms.MessageSender;
|
|
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
|
|
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
|
import org.thoughtcrime.securesms.util.MessageUtil;
|
|
import org.thoughtcrime.securesms.util.Util;
|
|
import org.signal.core.util.concurrent.SimpleTask;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.stream.Collectors;
|
|
|
|
/**
|
|
* MultiShareSender encapsulates send logic (stolen from {@link org.thoughtcrime.securesms.conversation.ConversationActivity}
|
|
* and provides a means to:
|
|
*
|
|
* 1. Send messages based off a {@link MultiShareArgs} object and
|
|
* 1. Parse through the result of the send via a {@link MultiShareSendResultCollection}
|
|
*/
|
|
public final class MultiShareSender {
|
|
|
|
private static final String TAG = Log.tag(MultiShareSender.class);
|
|
|
|
private MultiShareSender() {
|
|
}
|
|
|
|
@MainThread
|
|
public static void send(@NonNull MultiShareArgs multiShareArgs, @NonNull Consumer<MultiShareSendResultCollection> results) {
|
|
SimpleTask.run(() -> sendSync(multiShareArgs), results::accept);
|
|
}
|
|
|
|
@WorkerThread
|
|
public static MultiShareSendResultCollection sendSync(@NonNull MultiShareArgs multiShareArgs) {
|
|
List<MultiShareSendResult> results = new ArrayList<>(multiShareArgs.getShareContactAndThreads().size());
|
|
Context context = ApplicationDependencies.getApplication();
|
|
boolean isMmsEnabled = Util.isMmsCapable(context);
|
|
String message = multiShareArgs.getDraftText();
|
|
SlideDeck slideDeck;
|
|
|
|
try {
|
|
slideDeck = buildSlideDeck(context, multiShareArgs);
|
|
} catch (SlideNotFoundException e) {
|
|
Log.w(TAG, "Could not create slide for media message");
|
|
for (ShareContactAndThread shareContactAndThread : multiShareArgs.getShareContactAndThreads()) {
|
|
results.add(new MultiShareSendResult(shareContactAndThread, MultiShareSendResult.Type.GENERIC_ERROR));
|
|
}
|
|
|
|
return new MultiShareSendResultCollection(results);
|
|
}
|
|
|
|
|
|
for (ShareContactAndThread shareContactAndThread : multiShareArgs.getShareContactAndThreads()) {
|
|
Recipient recipient = Recipient.resolved(shareContactAndThread.getRecipientId());
|
|
|
|
List<Mention> mentions = getValidMentionsForRecipient(recipient, multiShareArgs.getMentions());
|
|
TransportOption transport = resolveTransportOption(context, recipient);
|
|
boolean forceSms = recipient.isForceSmsSelection() && transport.isSms();
|
|
int subscriptionId = transport.getSimSubscriptionId().orElse(-1);
|
|
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds());
|
|
boolean needsSplit = !transport.isSms() &&
|
|
message != null &&
|
|
message.length() > transport.calculateCharacters(message).maxPrimaryMessageSize;
|
|
boolean hasMmsMedia = !multiShareArgs.getMedia().isEmpty() ||
|
|
(multiShareArgs.getDataUri() != null && multiShareArgs.getDataUri() != Uri.EMPTY) ||
|
|
multiShareArgs.getStickerLocator() != null ||
|
|
recipient.isGroup() ||
|
|
recipient.getEmail().isPresent();
|
|
boolean hasPushMedia = hasMmsMedia ||
|
|
multiShareArgs.getLinkPreview() != null ||
|
|
!mentions.isEmpty() ||
|
|
needsSplit;
|
|
|
|
if ((recipient.isMmsGroup() || recipient.getEmail().isPresent()) && !isMmsEnabled) {
|
|
results.add(new MultiShareSendResult(shareContactAndThread, MultiShareSendResult.Type.MMS_NOT_ENABLED));
|
|
} else if (hasMmsMedia && transport.isSms() || hasPushMedia && !transport.isSms() || multiShareArgs.isTextStory()) {
|
|
sendMediaMessage(context, multiShareArgs, recipient, slideDeck, transport, shareContactAndThread.getThreadId(), forceSms, expiresIn, multiShareArgs.isViewOnce(), subscriptionId, mentions, shareContactAndThread.isStory());
|
|
results.add(new MultiShareSendResult(shareContactAndThread, MultiShareSendResult.Type.SUCCESS));
|
|
} else if (shareContactAndThread.isStory()) {
|
|
results.add(new MultiShareSendResult(shareContactAndThread, MultiShareSendResult.Type.INVALID_SHARE_TO_STORY));
|
|
} else {
|
|
sendTextMessage(context, multiShareArgs, recipient, shareContactAndThread.getThreadId(), forceSms, expiresIn, subscriptionId);
|
|
results.add(new MultiShareSendResult(shareContactAndThread, MultiShareSendResult.Type.SUCCESS));
|
|
}
|
|
|
|
// XXX We must do this to avoid sending out messages to the same recipient with the same
|
|
// sentTimestamp. If we do this, they'll be considered dupes by the receiver.
|
|
ThreadUtil.sleep(5);
|
|
}
|
|
|
|
return new MultiShareSendResultCollection(results);
|
|
}
|
|
|
|
public static @NonNull TransportOption getWorstTransportOption(@NonNull Context context, @NonNull Set<ShareContactAndThread> shareContactAndThreads) {
|
|
for (ShareContactAndThread shareContactAndThread : shareContactAndThreads) {
|
|
TransportOption option = resolveTransportOption(context, shareContactAndThread.isForceSms() && !shareContactAndThread.isStory());
|
|
if (option.isSms()) {
|
|
return option;
|
|
}
|
|
}
|
|
|
|
return TransportOptions.getPushTransportOption(context);
|
|
}
|
|
|
|
private static @NonNull TransportOption resolveTransportOption(@NonNull Context context, @NonNull Recipient recipient) {
|
|
return resolveTransportOption(context, !recipient.isDistributionList() && (recipient.isForceSmsSelection() || !recipient.isRegistered()));
|
|
}
|
|
|
|
public static @NonNull TransportOption resolveTransportOption(@NonNull Context context, boolean forceSms) {
|
|
if (forceSms) {
|
|
TransportOptions options = new TransportOptions(context, false);
|
|
options.setDefaultTransport(TransportOption.Type.SMS);
|
|
return options.getSelectedTransport();
|
|
} else {
|
|
return TransportOptions.getPushTransportOption(context);
|
|
}
|
|
}
|
|
|
|
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<Mention> validatedMentions,
|
|
boolean isStory)
|
|
{
|
|
String body = multiShareArgs.getDraftText();
|
|
if (transportOption.isType(TransportOption.Type.TEXTSECURE) && !forceSms && body != null) {
|
|
MessageUtil.SplitResult splitMessage = MessageUtil.getSplitMessage(context, body, transportOption.calculateCharacters(body).maxPrimaryMessageSize);
|
|
body = splitMessage.getBody();
|
|
|
|
if (splitMessage.getTextSlide().isPresent()) {
|
|
slideDeck.addSlide(splitMessage.getTextSlide().get());
|
|
}
|
|
}
|
|
|
|
List<OutgoingMediaMessage> outgoingMessages = new ArrayList<>();
|
|
|
|
if (isStory) {
|
|
final StoryType storyType;
|
|
if (recipient.isDistributionList()) {
|
|
storyType = SignalDatabase.distributionLists().getStoryType(recipient.requireDistributionListId());
|
|
} else {
|
|
storyType = StoryType.STORY_WITH_REPLIES;
|
|
}
|
|
|
|
if (recipient.isActiveGroup()) {
|
|
SignalDatabase.groups().markDisplayAsStory(recipient.requireGroupId());
|
|
}
|
|
|
|
if (multiShareArgs.isTextStory()) {
|
|
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient,
|
|
new SlideDeck(),
|
|
body,
|
|
System.currentTimeMillis(),
|
|
subscriptionId,
|
|
0L,
|
|
false,
|
|
ThreadDatabase.DistributionTypes.DEFAULT,
|
|
storyType.toTextStoryType(),
|
|
null,
|
|
false,
|
|
null,
|
|
Collections.emptyList(),
|
|
multiShareArgs.getLinkPreview() != null ? Collections.singletonList(multiShareArgs.getLinkPreview())
|
|
: Collections.emptyList(),
|
|
Collections.emptyList());
|
|
|
|
outgoingMessages.add(outgoingMediaMessage);
|
|
} else {
|
|
for (final Slide slide : slideDeck.getSlides()) {
|
|
SlideDeck singletonDeck = new SlideDeck();
|
|
singletonDeck.addSlide(slide);
|
|
|
|
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient,
|
|
singletonDeck,
|
|
body,
|
|
System.currentTimeMillis(),
|
|
subscriptionId,
|
|
0L,
|
|
false,
|
|
ThreadDatabase.DistributionTypes.DEFAULT,
|
|
storyType,
|
|
null,
|
|
false,
|
|
null,
|
|
Collections.emptyList(),
|
|
Collections.emptyList(),
|
|
validatedMentions);
|
|
|
|
outgoingMessages.add(outgoingMediaMessage);
|
|
|
|
// XXX We must do this to avoid sending out messages to the same recipient with the same
|
|
// sentTimestamp. If we do this, they'll be considered dupes by the receiver.
|
|
ThreadUtil.sleep(5);
|
|
}
|
|
}
|
|
} else {
|
|
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient,
|
|
slideDeck,
|
|
body,
|
|
System.currentTimeMillis(),
|
|
subscriptionId,
|
|
expiresIn,
|
|
isViewOnce,
|
|
ThreadDatabase.DistributionTypes.DEFAULT,
|
|
StoryType.NONE,
|
|
null,
|
|
false,
|
|
null,
|
|
Collections.emptyList(),
|
|
multiShareArgs.getLinkPreview() != null ? Collections.singletonList(multiShareArgs.getLinkPreview())
|
|
: Collections.emptyList(),
|
|
validatedMentions);
|
|
|
|
outgoingMessages.add(outgoingMediaMessage);
|
|
}
|
|
|
|
if (recipient.isRegistered() && !forceSms) {
|
|
for (final OutgoingMediaMessage outgoingMessage : outgoingMessages) {
|
|
MessageSender.send(context, new OutgoingSecureMediaMessage(outgoingMessage), threadId, false, null, null);
|
|
}
|
|
} else {
|
|
for (final OutgoingMediaMessage outgoingMessage : outgoingMessages) {
|
|
MessageSender.send(context, new OutgoingSecureMediaMessage(outgoingMessage), threadId, forceSms, null, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void sendTextMessage(@NonNull Context context,
|
|
@NonNull MultiShareArgs multiShareArgs,
|
|
@NonNull Recipient recipient,
|
|
long threadId,
|
|
boolean forceSms,
|
|
long expiresIn,
|
|
int subscriptionId)
|
|
{
|
|
|
|
final OutgoingTextMessage outgoingTextMessage;
|
|
if (recipient.isRegistered() && !forceSms) {
|
|
outgoingTextMessage = new OutgoingEncryptedMessage(recipient, multiShareArgs.getDraftText(), expiresIn);
|
|
} else {
|
|
outgoingTextMessage = new OutgoingTextMessage(recipient, multiShareArgs.getDraftText(), expiresIn, subscriptionId);
|
|
}
|
|
|
|
MessageSender.send(context, outgoingTextMessage, threadId, forceSms, null, null);
|
|
}
|
|
|
|
private static @NonNull SlideDeck buildSlideDeck(@NonNull Context context, @NonNull MultiShareArgs multiShareArgs) throws SlideNotFoundException {
|
|
SlideDeck slideDeck = new SlideDeck();
|
|
if (multiShareArgs.getStickerLocator() != null) {
|
|
slideDeck.addSlide(new StickerSlide(context, multiShareArgs.getDataUri(), 0, multiShareArgs.getStickerLocator(), multiShareArgs.getDataType()));
|
|
} else if (!multiShareArgs.getMedia().isEmpty()) {
|
|
for (Media media : multiShareArgs.getMedia()) {
|
|
Slide slide = SlideFactory.getSlide(context, media.getMimeType(), media.getUri(), media.getWidth(), media.getHeight());
|
|
if (slide != null) {
|
|
slideDeck.addSlide(slide);
|
|
} else {
|
|
throw new SlideNotFoundException();
|
|
}
|
|
}
|
|
} else if (multiShareArgs.getDataUri() != null) {
|
|
Slide slide = SlideFactory.getSlide(context, multiShareArgs.getDataType(), multiShareArgs.getDataUri(), 0, 0);
|
|
if (slide != null) {
|
|
slideDeck.addSlide(slide);
|
|
} else {
|
|
throw new SlideNotFoundException();
|
|
}
|
|
}
|
|
|
|
return slideDeck;
|
|
}
|
|
|
|
private static @NonNull List<Mention> getValidMentionsForRecipient(@NonNull Recipient recipient, @NonNull List<Mention> mentions) {
|
|
if (mentions.isEmpty() || !recipient.isPushV2Group() || !recipient.isActiveGroup()) {
|
|
return Collections.emptyList();
|
|
} else {
|
|
Set<RecipientId> validRecipientIds = recipient.getParticipants()
|
|
.stream()
|
|
.map(Recipient::getId)
|
|
.collect(Collectors.toSet());
|
|
|
|
return mentions.stream()
|
|
.filter(mention -> validRecipientIds.contains(mention.getRecipientId()))
|
|
.collect(Collectors.toList());
|
|
}
|
|
}
|
|
|
|
public static final class MultiShareSendResultCollection {
|
|
private final List<MultiShareSendResult> results;
|
|
|
|
private MultiShareSendResultCollection(List<MultiShareSendResult> results) {
|
|
this.results = results;
|
|
}
|
|
|
|
public boolean containsFailures() {
|
|
return Stream.of(results).anyMatch(result -> result.type != MultiShareSendResult.Type.SUCCESS);
|
|
}
|
|
|
|
public boolean containsOnlyFailures() {
|
|
return Stream.of(results).allMatch(result -> result.type != MultiShareSendResult.Type.SUCCESS);
|
|
}
|
|
}
|
|
|
|
private static final class MultiShareSendResult {
|
|
private final ShareContactAndThread contactAndThread;
|
|
private final Type type;
|
|
|
|
private MultiShareSendResult(ShareContactAndThread contactAndThread, Type type) {
|
|
this.contactAndThread = contactAndThread;
|
|
this.type = type;
|
|
}
|
|
|
|
public ShareContactAndThread getContactAndThread() {
|
|
return contactAndThread;
|
|
}
|
|
|
|
public Type getType() {
|
|
return type;
|
|
}
|
|
|
|
private enum Type {
|
|
GENERIC_ERROR,
|
|
INVALID_SHARE_TO_STORY,
|
|
MMS_NOT_ENABLED,
|
|
SUCCESS
|
|
}
|
|
}
|
|
|
|
private static final class SlideNotFoundException extends Exception {
|
|
}
|
|
}
|