mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 00:29:11 +01:00
Enable sharing to stories and refactor share activity.
This commit is contained in:
committed by
Greyson Parrelli
parent
fd4543ffe0
commit
523537cf05
@@ -10,65 +10,73 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.BreakIteratorCompat;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.stories.Stories;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class MultiShareArgs implements Parcelable {
|
||||
|
||||
private final Set<ShareContactAndThread> shareContactAndThreads;
|
||||
private final List<Media> media;
|
||||
private final String draftText;
|
||||
private final StickerLocator stickerLocator;
|
||||
private final boolean borderless;
|
||||
private final Uri dataUri;
|
||||
private final String dataType;
|
||||
private final boolean viewOnce;
|
||||
private final LinkPreview linkPreview;
|
||||
private final List<Mention> mentions;
|
||||
private final long timestamp;
|
||||
private final long expiresAt;
|
||||
private final boolean isTextStory;
|
||||
private final Set<ContactSearchKey> contactSearchKeys;
|
||||
private final List<Media> media;
|
||||
private final String draftText;
|
||||
private final StickerLocator stickerLocator;
|
||||
private final boolean borderless;
|
||||
private final Uri dataUri;
|
||||
private final String dataType;
|
||||
private final boolean viewOnce;
|
||||
private final LinkPreview linkPreview;
|
||||
private final List<Mention> mentions;
|
||||
private final long timestamp;
|
||||
private final long expiresAt;
|
||||
private final boolean isTextStory;
|
||||
|
||||
private MultiShareArgs(@NonNull Builder builder) {
|
||||
shareContactAndThreads = builder.shareContactAndThreads;
|
||||
media = builder.media == null ? new ArrayList<>() : new ArrayList<>(builder.media);
|
||||
draftText = builder.draftText;
|
||||
stickerLocator = builder.stickerLocator;
|
||||
borderless = builder.borderless;
|
||||
dataUri = builder.dataUri;
|
||||
dataType = builder.dataType;
|
||||
viewOnce = builder.viewOnce;
|
||||
linkPreview = builder.linkPreview;
|
||||
mentions = builder.mentions == null ? new ArrayList<>() : new ArrayList<>(builder.mentions);
|
||||
timestamp = builder.timestamp;
|
||||
expiresAt = builder.expiresAt;
|
||||
isTextStory = builder.isTextStory;
|
||||
contactSearchKeys = builder.contactSearchKeys;
|
||||
media = builder.media == null ? new ArrayList<>() : new ArrayList<>(builder.media);
|
||||
draftText = builder.draftText;
|
||||
stickerLocator = builder.stickerLocator;
|
||||
borderless = builder.borderless;
|
||||
dataUri = builder.dataUri;
|
||||
dataType = builder.dataType;
|
||||
viewOnce = builder.viewOnce;
|
||||
linkPreview = builder.linkPreview;
|
||||
mentions = builder.mentions == null ? new ArrayList<>() : new ArrayList<>(builder.mentions);
|
||||
timestamp = builder.timestamp;
|
||||
expiresAt = builder.expiresAt;
|
||||
isTextStory = builder.isTextStory;
|
||||
}
|
||||
|
||||
protected MultiShareArgs(Parcel in) {
|
||||
shareContactAndThreads = new HashSet<>(Objects.requireNonNull(in.createTypedArrayList(ShareContactAndThread.CREATOR)));
|
||||
media = in.createTypedArrayList(Media.CREATOR);
|
||||
draftText = in.readString();
|
||||
stickerLocator = in.readParcelable(StickerLocator.class.getClassLoader());
|
||||
borderless = in.readByte() != 0;
|
||||
dataUri = in.readParcelable(Uri.class.getClassLoader());
|
||||
dataType = in.readString();
|
||||
viewOnce = in.readByte() != 0;
|
||||
mentions = in.createTypedArrayList(Mention.CREATOR);
|
||||
timestamp = in.readLong();
|
||||
expiresAt = in.readLong();
|
||||
isTextStory = ParcelUtil.readBoolean(in);
|
||||
List<ContactSearchKey.ParcelableRecipientSearchKey> parcelableRecipientSearchKeys = in.createTypedArrayList(ContactSearchKey.ParcelableRecipientSearchKey.CREATOR);
|
||||
|
||||
contactSearchKeys = parcelableRecipientSearchKeys.stream()
|
||||
.map(ContactSearchKey.ParcelableRecipientSearchKey::asContactSearchKey)
|
||||
.collect(Collectors.toSet());
|
||||
media = in.createTypedArrayList(Media.CREATOR);
|
||||
draftText = in.readString();
|
||||
stickerLocator = in.readParcelable(StickerLocator.class.getClassLoader());
|
||||
borderless = in.readByte() != 0;
|
||||
dataUri = in.readParcelable(Uri.class.getClassLoader());
|
||||
dataType = in.readString();
|
||||
viewOnce = in.readByte() != 0;
|
||||
mentions = in.createTypedArrayList(Mention.CREATOR);
|
||||
timestamp = in.readLong();
|
||||
expiresAt = in.readLong();
|
||||
isTextStory = ParcelUtil.readBoolean(in);
|
||||
|
||||
String linkedPreviewString = in.readString();
|
||||
LinkPreview preview;
|
||||
@@ -81,8 +89,15 @@ public final class MultiShareArgs implements Parcelable {
|
||||
linkPreview = preview;
|
||||
}
|
||||
|
||||
public Set<ShareContactAndThread> getShareContactAndThreads() {
|
||||
return shareContactAndThreads;
|
||||
public Set<ContactSearchKey> getContactSearchKeys() {
|
||||
return contactSearchKeys;
|
||||
}
|
||||
|
||||
public Set<ContactSearchKey.RecipientSearchKey> getRecipientSearchKeys() {
|
||||
return contactSearchKeys.stream()
|
||||
.filter(key -> key instanceof ContactSearchKey.RecipientSearchKey)
|
||||
.map(key -> (ContactSearchKey.RecipientSearchKey) key)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public @NonNull List<Media> getMedia() {
|
||||
@@ -134,13 +149,34 @@ public final class MultiShareArgs implements Parcelable {
|
||||
}
|
||||
|
||||
public boolean isValidForStories() {
|
||||
return isTextStory || !media.isEmpty() && media.stream().allMatch(m -> MediaUtil.isImageOrVideoType(m.getMimeType()) && !MediaUtil.isGif(m.getMimeType()));
|
||||
return isTextStory ||
|
||||
!media.isEmpty() && media.stream().allMatch(m -> MediaUtil.isImageOrVideoType(m.getMimeType()) && !MediaUtil.isGif(m.getMimeType())) ||
|
||||
MediaUtil.isImageType(dataType) ||
|
||||
MediaUtil.isVideoType(dataType) ||
|
||||
isValidForTextStoryGeneration();
|
||||
}
|
||||
|
||||
public boolean isValidForNonStories() {
|
||||
return !isTextStory;
|
||||
}
|
||||
|
||||
public boolean isValidForTextStoryGeneration() {
|
||||
if (isTextStory || !media.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Util.isEmpty(getDraftText())) {
|
||||
BreakIteratorCompat breakIteratorCompat = BreakIteratorCompat.getInstance();
|
||||
breakIteratorCompat.setText(getDraftText());
|
||||
|
||||
if (breakIteratorCompat.countBreaks() > Stories.MAX_BODY_SIZE) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return linkPreview != null || !Util.isEmpty(draftText);
|
||||
}
|
||||
|
||||
public @NonNull InterstitialContentType getInterstitialContentType() {
|
||||
if (!requiresInterstitial()) {
|
||||
return InterstitialContentType.NONE;
|
||||
@@ -148,6 +184,8 @@ public final class MultiShareArgs implements Parcelable {
|
||||
(this.getDataUri() != null && this.getDataUri() != Uri.EMPTY && this.getDataType() != null && MediaUtil.isImageOrVideoType(this.getDataType())))
|
||||
{
|
||||
return InterstitialContentType.MEDIA;
|
||||
} else if (!TextUtils.isEmpty(this.getDraftText()) && allRecipientsAreStories()) {
|
||||
return InterstitialContentType.MEDIA;
|
||||
} else if (!TextUtils.isEmpty(this.getDraftText())) {
|
||||
return InterstitialContentType.TEXT;
|
||||
} else {
|
||||
@@ -155,6 +193,9 @@ public final class MultiShareArgs implements Parcelable {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean allRecipientsAreStories() {
|
||||
return !contactSearchKeys.isEmpty() && contactSearchKeys.stream().allMatch(key -> key instanceof ContactSearchKey.RecipientSearchKey.Story);
|
||||
}
|
||||
|
||||
public static final Creator<MultiShareArgs> CREATOR = new Creator<MultiShareArgs>() {
|
||||
@Override
|
||||
@@ -175,7 +216,7 @@ public final class MultiShareArgs implements Parcelable {
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeTypedList(Stream.of(shareContactAndThreads).toList());
|
||||
dest.writeTypedList(Stream.of(contactSearchKeys).map(ContactSearchKey::requireParcelable).toList());
|
||||
dest.writeTypedList(media);
|
||||
dest.writeString(draftText);
|
||||
dest.writeParcelable(stickerLocator, flags);
|
||||
@@ -200,32 +241,35 @@ public final class MultiShareArgs implements Parcelable {
|
||||
}
|
||||
|
||||
public Builder buildUpon() {
|
||||
return buildUpon(shareContactAndThreads);
|
||||
return buildUpon(contactSearchKeys);
|
||||
}
|
||||
|
||||
public Builder buildUpon(@NonNull Set<ShareContactAndThread> shareContactAndThreads) {
|
||||
return new Builder(shareContactAndThreads).asBorderless(borderless)
|
||||
.asViewOnce(viewOnce)
|
||||
.withDataType(dataType)
|
||||
.withDataUri(dataUri)
|
||||
.withDraftText(draftText)
|
||||
.withLinkPreview(linkPreview)
|
||||
.withMedia(media)
|
||||
.withStickerLocator(stickerLocator)
|
||||
.withMentions(mentions)
|
||||
.withTimestamp(timestamp)
|
||||
.withExpiration(expiresAt)
|
||||
.asTextStory(isTextStory);
|
||||
public Builder buildUpon(@NonNull Set<ContactSearchKey> recipientSearchKeys) {
|
||||
return new Builder(recipientSearchKeys).asBorderless(borderless)
|
||||
.asViewOnce(viewOnce)
|
||||
.withDataType(dataType)
|
||||
.withDataUri(dataUri)
|
||||
.withDraftText(draftText)
|
||||
.withLinkPreview(linkPreview)
|
||||
.withMedia(media)
|
||||
.withStickerLocator(stickerLocator)
|
||||
.withMentions(mentions)
|
||||
.withTimestamp(timestamp)
|
||||
.withExpiration(expiresAt)
|
||||
.asTextStory(isTextStory);
|
||||
}
|
||||
|
||||
private boolean requiresInterstitial() {
|
||||
return stickerLocator == null &&
|
||||
(!media.isEmpty() || !TextUtils.isEmpty(draftText) || MediaUtil.isImageOrVideoType(dataType));
|
||||
(!media.isEmpty() ||
|
||||
!TextUtils.isEmpty(draftText) ||
|
||||
MediaUtil.isImageOrVideoType(dataType) ||
|
||||
(!contactSearchKeys.isEmpty() && contactSearchKeys.stream().anyMatch(key -> key instanceof ContactSearchKey.RecipientSearchKey.Story)));
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private final Set<ShareContactAndThread> shareContactAndThreads;
|
||||
private final Set<ContactSearchKey> contactSearchKeys;
|
||||
|
||||
private List<Media> media;
|
||||
private String draftText;
|
||||
@@ -240,8 +284,12 @@ public final class MultiShareArgs implements Parcelable {
|
||||
private long expiresAt;
|
||||
private boolean isTextStory;
|
||||
|
||||
public Builder(@NonNull Set<ShareContactAndThread> shareContactAndThreads) {
|
||||
this.shareContactAndThreads = shareContactAndThreads;
|
||||
public Builder() {
|
||||
this(Collections.emptySet());
|
||||
}
|
||||
|
||||
public Builder(@NonNull Set<ContactSearchKey> contactSearchKeys) {
|
||||
this.contactSearchKeys = contactSearchKeys;
|
||||
}
|
||||
|
||||
public @NonNull Builder withMedia(@Nullable List<Media> media) {
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.BreakIteratorCompat;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.TransportOptions;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
|
||||
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.database.model.databaseprotos.StoryTextPost;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryBackgroundColors;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
@@ -32,6 +39,8 @@ 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.stories.Stories;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.MessageUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
@@ -63,7 +72,7 @@ public final class MultiShareSender {
|
||||
|
||||
@WorkerThread
|
||||
public static MultiShareSendResultCollection sendSync(@NonNull MultiShareArgs multiShareArgs) {
|
||||
List<MultiShareSendResult> results = new ArrayList<>(multiShareArgs.getShareContactAndThreads().size());
|
||||
List<MultiShareSendResult> results = new ArrayList<>(multiShareArgs.getContactSearchKeys().size());
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
boolean isMmsEnabled = Util.isMmsCapable(context);
|
||||
String message = multiShareArgs.getDraftText();
|
||||
@@ -73,45 +82,48 @@ public final class MultiShareSender {
|
||||
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));
|
||||
for (ContactSearchKey.RecipientSearchKey recipientSearchKey : multiShareArgs.getRecipientSearchKeys()) {
|
||||
results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.GENERIC_ERROR));
|
||||
}
|
||||
|
||||
return new MultiShareSendResultCollection(results);
|
||||
}
|
||||
|
||||
long distributionListSentTimestamp = System.currentTimeMillis();
|
||||
for (ContactSearchKey.RecipientSearchKey recipientSearchKey : multiShareArgs.getRecipientSearchKeys()) {
|
||||
Recipient recipient = Recipient.resolved(recipientSearchKey.getRecipientId());
|
||||
|
||||
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;
|
||||
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
|
||||
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;
|
||||
long sentTimestamp = recipient.isDistributionList() ? distributionListSentTimestamp : System.currentTimeMillis();
|
||||
boolean canSendAsTextStory = recipientSearchKey.isStory() && multiShareArgs.isValidForTextStoryGeneration();
|
||||
|
||||
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));
|
||||
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);
|
||||
results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.SUCCESS));
|
||||
} else if (recipientSearchKey.isStory()) {
|
||||
results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.INVALID_SHARE_TO_STORY));
|
||||
} else {
|
||||
sendTextMessage(context, multiShareArgs, recipient, shareContactAndThread.getThreadId(), forceSms, expiresIn, subscriptionId);
|
||||
results.add(new MultiShareSendResult(shareContactAndThread, MultiShareSendResult.Type.SUCCESS));
|
||||
sendTextMessage(context, multiShareArgs, recipient, threadId, forceSms, expiresIn, subscriptionId);
|
||||
results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.SUCCESS));
|
||||
}
|
||||
|
||||
// XXX We must do this to avoid sending out messages to the same recipient with the same
|
||||
@@ -122,9 +134,9 @@ public final class MultiShareSender {
|
||||
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());
|
||||
public static @NonNull TransportOption getWorstTransportOption(@NonNull Context context, @NonNull Set<ContactSearchKey.RecipientSearchKey> recipientSearchKeys) {
|
||||
for (ContactSearchKey.RecipientSearchKey recipientSearchKey : recipientSearchKeys) {
|
||||
TransportOption option = resolveTransportOption(context, Recipient.resolved(recipientSearchKey.getRecipientId()).isForceSmsSelection() && !recipientSearchKey.isStory());
|
||||
if (option.isSms()) {
|
||||
return option;
|
||||
}
|
||||
@@ -158,7 +170,9 @@ public final class MultiShareSender {
|
||||
boolean isViewOnce,
|
||||
int subscriptionId,
|
||||
@NonNull List<Mention> validatedMentions,
|
||||
boolean isStory)
|
||||
boolean isStory,
|
||||
long sentTimestamp,
|
||||
boolean canSendAsTextStory)
|
||||
{
|
||||
String body = multiShareArgs.getDraftText();
|
||||
if (transportOption.isType(TransportOption.Type.TEXTSECURE) && !forceSms && body != null) {
|
||||
@@ -188,7 +202,7 @@ public final class MultiShareSender {
|
||||
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient,
|
||||
new SlideDeck(),
|
||||
body,
|
||||
System.currentTimeMillis(),
|
||||
sentTimestamp,
|
||||
subscriptionId,
|
||||
0L,
|
||||
false,
|
||||
@@ -203,6 +217,8 @@ public final class MultiShareSender {
|
||||
Collections.emptyList());
|
||||
|
||||
outgoingMessages.add(outgoingMediaMessage);
|
||||
} else if (canSendAsTextStory) {
|
||||
outgoingMessages.add(generateTextStory(recipient, multiShareArgs, sentTimestamp, storyType));
|
||||
} else {
|
||||
for (final Slide slide : slideDeck.getSlides()) {
|
||||
SlideDeck singletonDeck = new SlideDeck();
|
||||
@@ -211,7 +227,7 @@ public final class MultiShareSender {
|
||||
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient,
|
||||
singletonDeck,
|
||||
body,
|
||||
System.currentTimeMillis(),
|
||||
sentTimestamp,
|
||||
subscriptionId,
|
||||
0L,
|
||||
false,
|
||||
@@ -225,17 +241,13 @@ public final class MultiShareSender {
|
||||
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(),
|
||||
sentTimestamp,
|
||||
subscriptionId,
|
||||
expiresIn,
|
||||
isViewOnce,
|
||||
@@ -283,6 +295,59 @@ public final class MultiShareSender {
|
||||
MessageSender.send(context, outgoingTextMessage, threadId, forceSms, null, null);
|
||||
}
|
||||
|
||||
private static @NonNull OutgoingMediaMessage generateTextStory(@NonNull Recipient recipient,
|
||||
@NonNull MultiShareArgs multiShareArgs,
|
||||
long sentTimestamp,
|
||||
@NonNull StoryType storyType)
|
||||
{
|
||||
return new OutgoingMediaMessage(
|
||||
recipient,
|
||||
Base64.encodeBytes(StoryTextPost.newBuilder()
|
||||
.setBody(getBodyForTextStory(multiShareArgs.getDraftText(), multiShareArgs.getLinkPreview()))
|
||||
.setStyle(StoryTextPost.Style.DEFAULT)
|
||||
.setBackground(TextStoryBackgroundColors.getRandomBackgroundColor().serialize())
|
||||
.setTextBackgroundColor(0)
|
||||
.setTextForegroundColor(Color.WHITE)
|
||||
.build()
|
||||
.toByteArray()),
|
||||
Collections.emptyList(),
|
||||
sentTimestamp,
|
||||
-1,
|
||||
0,
|
||||
false,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
storyType.toTextStoryType(),
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
multiShareArgs.getLinkPreview() != null ? Collections.singletonList(multiShareArgs.getLinkPreview())
|
||||
: Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptySet(),
|
||||
Collections.emptySet());
|
||||
}
|
||||
|
||||
private static @NonNull String getBodyForTextStory(@Nullable String draftText, @Nullable LinkPreview linkPreview) {
|
||||
if (Util.isEmpty(draftText)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
BreakIteratorCompat breakIteratorCompat = BreakIteratorCompat.getInstance();
|
||||
breakIteratorCompat.setText(draftText);
|
||||
|
||||
String trimmed = breakIteratorCompat.take(Stories.MAX_BODY_SIZE).toString();
|
||||
if (linkPreview == null) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
if (linkPreview.getUrl().equals(trimmed)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return trimmed.replace(linkPreview.getUrl(), "").trim();
|
||||
}
|
||||
|
||||
private static boolean shouldSendAsPush(@NonNull Recipient recipient, boolean forceSms) {
|
||||
return recipient.isDistributionList() ||
|
||||
recipient.isServiceIdOnly() ||
|
||||
@@ -346,16 +411,16 @@ public final class MultiShareSender {
|
||||
}
|
||||
|
||||
private static final class MultiShareSendResult {
|
||||
private final ShareContactAndThread contactAndThread;
|
||||
private final Type type;
|
||||
private final ContactSearchKey.RecipientSearchKey recipientSearchKey;
|
||||
private final Type type;
|
||||
|
||||
private MultiShareSendResult(ShareContactAndThread contactAndThread, Type type) {
|
||||
this.contactAndThread = contactAndThread;
|
||||
this.type = type;
|
||||
private MultiShareSendResult(ContactSearchKey.RecipientSearchKey contactSearchKey, Type type) {
|
||||
this.recipientSearchKey = contactSearchKey;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public ShareContactAndThread getContactAndThread() {
|
||||
return contactAndThread;
|
||||
public ContactSearchKey.RecipientSearchKey getContactSearchKey() {
|
||||
return recipientSearchKey;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
|
||||
@@ -1,745 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014-2017 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.SearchToolbar;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sharing.interstitial.ShareInterstitialActivity;
|
||||
import org.thoughtcrime.securesms.stories.Stories;
|
||||
import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs;
|
||||
import org.thoughtcrime.securesms.stories.settings.hide.HideStoryFromDialogFragment;
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import kotlin.Unit;
|
||||
|
||||
/**
|
||||
* Entry point for sharing content into the app.
|
||||
*
|
||||
* Handles contact selection when necessary, but also serves as an entry point for when the contact
|
||||
* is known (such as choosing someone in a direct share).
|
||||
*/
|
||||
public class ShareActivity extends PassphraseRequiredActivity
|
||||
implements ContactSelectionListFragment.OnContactSelectedListener,
|
||||
ContactSelectionListFragment.OnSelectionLimitReachedListener
|
||||
{
|
||||
private static final String TAG = Log.tag(ShareActivity.class);
|
||||
|
||||
private static final short RESULT_TEXT_CONFIRMATION = 1;
|
||||
private static final short RESULT_MEDIA_CONFIRMATION = 2;
|
||||
|
||||
public static final String EXTRA_THREAD_ID = "thread_id";
|
||||
public static final String EXTRA_RECIPIENT_ID = "recipient_id";
|
||||
public static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private ConstraintLayout shareContainer;
|
||||
private ContactSelectionListFragment contactsFragment;
|
||||
private SearchToolbar searchToolbar;
|
||||
private ImageView searchAction;
|
||||
private View shareConfirm;
|
||||
private RecyclerView contactsRecycler;
|
||||
private View contactsRecyclerDivider;
|
||||
private ShareSelectionAdapter adapter;
|
||||
private boolean disallowMultiShare;
|
||||
|
||||
private ShareIntents.Args args;
|
||||
private ShareViewModel viewModel;
|
||||
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
setContentView(R.layout.share_activity);
|
||||
|
||||
disposables.bindTo(getLifecycle());
|
||||
|
||||
initializeArgs();
|
||||
initializeViewModel();
|
||||
initializeMedia();
|
||||
initializeIntent();
|
||||
initializeToolbar();
|
||||
initializeResources();
|
||||
initializeSearch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
Log.i(TAG, "onResume()");
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.onResume(this);
|
||||
handleDirectShare();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
|
||||
if (!isFinishing() && !viewModel.isMultiShare()) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (searchToolbar.isVisible()) searchToolbar.collapse();
|
||||
else super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
switch (requestCode) {
|
||||
case RESULT_MEDIA_CONFIRMATION:
|
||||
case RESULT_TEXT_CONFIRMATION:
|
||||
viewModel.onSuccessfulShare();
|
||||
finish();
|
||||
break;
|
||||
default:
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
} else {
|
||||
shareConfirm.setClickable(true);
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull java.util.function.Consumer<Boolean> callback) {
|
||||
if (disallowMultiShare) {
|
||||
Toast.makeText(this, R.string.ShareActivity__sharing_to_multiple_chats_is, Toast.LENGTH_LONG).show();
|
||||
callback.accept(false);
|
||||
} else {
|
||||
disposables.add(viewModel.onContactSelected(new ShareContact(recipientId, number))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> {
|
||||
switch (result) {
|
||||
case TRUE:
|
||||
callback.accept(true);
|
||||
break;
|
||||
case FALSE:
|
||||
callback.accept(false);
|
||||
break;
|
||||
case FALSE_AND_SHOW_PERMISSION_TOAST:
|
||||
Toast.makeText(this, R.string.ShareActivity_you_do_not_have_permission_to_send_to_this_group, Toast.LENGTH_SHORT).show();
|
||||
callback.accept(false);
|
||||
break;
|
||||
case FALSE_AND_SHOW_SMS_MULTISELECT_TOAST:
|
||||
Toast.makeText(this, R.string.ShareActivity__sharing_to_multiple_chats_is, Toast.LENGTH_LONG).show();
|
||||
callback.accept(false);
|
||||
break;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {
|
||||
viewModel.onContactDeselected(new ShareContact(recipientId, number));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged() {
|
||||
}
|
||||
|
||||
private void animateInSelection() {
|
||||
contactsRecyclerDivider.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0);
|
||||
contactsRecycler.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0);
|
||||
}
|
||||
|
||||
private void animateOutSelection() {
|
||||
contactsRecyclerDivider.animate()
|
||||
.alpha(0f)
|
||||
.translationY(ViewUtil.dpToPx(48));
|
||||
contactsRecycler.animate()
|
||||
.alpha(0f)
|
||||
.translationY(ViewUtil.dpToPx(48));
|
||||
}
|
||||
|
||||
private void initializeIntent() {
|
||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||
int mode = DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_SELF | DisplayMode.FLAG_HIDE_NEW;
|
||||
|
||||
if (Util.isDefaultSmsProvider(this)) {
|
||||
mode |= DisplayMode.FLAG_SMS;
|
||||
}
|
||||
|
||||
mode |= DisplayMode.FLAG_HIDE_GROUPS_V1;
|
||||
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, mode);
|
||||
}
|
||||
|
||||
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
||||
getIntent().putExtra(ContactSelectionListFragment.RECENTS, true);
|
||||
getIntent().putExtra(ContactSelectionListFragment.SELECTION_LIMITS, FeatureFlags.shareSelectionLimit());
|
||||
getIntent().putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_CHIPS, false);
|
||||
getIntent().putExtra(ContactSelectionListFragment.CAN_SELECT_SELF, true);
|
||||
getIntent().putExtra(ContactSelectionListFragment.RV_CLIP, false);
|
||||
getIntent().putExtra(ContactSelectionListFragment.RV_PADDING_BOTTOM, ViewUtil.dpToPx(48));
|
||||
}
|
||||
|
||||
private void handleDirectShare() {
|
||||
boolean isDirectShare = getIntent().hasExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID);
|
||||
boolean intentHasRecipient = getIntent().hasExtra(EXTRA_RECIPIENT_ID);
|
||||
|
||||
if (intentHasRecipient) {
|
||||
handleDestination();
|
||||
} else if (isDirectShare) {
|
||||
String extraShortcutId = getIntent().getStringExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID);
|
||||
SimpleTask.run(getLifecycle(),
|
||||
() -> getDirectShareExtras(extraShortcutId),
|
||||
extras -> {
|
||||
if (extras != null) {
|
||||
addShortcutExtrasToIntent(extras);
|
||||
handleDestination();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param extraShortcutId EXTRA_SHORTCUT_ID String as included in direct share intent
|
||||
* @return shortcutExtras or null
|
||||
*/
|
||||
@WorkerThread
|
||||
private @Nullable Bundle getDirectShareExtras(@NonNull String extraShortcutId) {
|
||||
Bundle shortcutExtras = getShortcutExtrasFor(extraShortcutId);
|
||||
if (shortcutExtras == null) {
|
||||
shortcutExtras = createExtrasFromExtraShortcutId(extraShortcutId);
|
||||
}
|
||||
return shortcutExtras;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for dynamic shortcut originally declared in {@link ConversationUtil} and return extras
|
||||
*
|
||||
* @param extraShortcutId EXTRA_SHORTCUT_ID String as included in direct share intent
|
||||
* @return shortcutExtras or null
|
||||
*/
|
||||
@WorkerThread
|
||||
private @Nullable Bundle getShortcutExtrasFor(@NonNull String extraShortcutId) {
|
||||
List<ShortcutInfoCompat> shortcuts = ShortcutManagerCompat.getDynamicShortcuts(this);
|
||||
for (ShortcutInfoCompat shortcutInfo : shortcuts) {
|
||||
if (extraShortcutId.equals(shortcutInfo.getId())) {
|
||||
return shortcutInfo.getIntent().getExtras();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param extraShortcutId EXTRA_SHORTCUT_ID string as included in direct share intent
|
||||
*/
|
||||
@WorkerThread
|
||||
private @Nullable Bundle createExtrasFromExtraShortcutId(@NonNull String extraShortcutId) {
|
||||
Bundle extras = new Bundle();
|
||||
RecipientId recipientId = ConversationUtil.getRecipientId(extraShortcutId);
|
||||
Long threadId = null;
|
||||
int distributionType = ThreadDatabase.DistributionTypes.DEFAULT;
|
||||
|
||||
if (recipientId != null) {
|
||||
threadId = SignalDatabase.threads().getThreadIdFor(recipientId);
|
||||
extras.putString(EXTRA_RECIPIENT_ID, recipientId.serialize());
|
||||
extras.putLong(EXTRA_THREAD_ID, threadId != null ? threadId : -1);
|
||||
extras.putInt(EXTRA_DISTRIBUTION_TYPE, distributionType);
|
||||
return extras;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param shortcutExtras as found by {@link ShareActivity#getShortcutExtrasFor)} or
|
||||
* {@link ShareActivity#createExtrasFromExtraShortcutId)}
|
||||
*/
|
||||
private void addShortcutExtrasToIntent(@NonNull Bundle shortcutExtras) {
|
||||
getIntent().putExtra(EXTRA_RECIPIENT_ID, shortcutExtras.getString(EXTRA_RECIPIENT_ID, null));
|
||||
getIntent().putExtra(EXTRA_THREAD_ID, shortcutExtras.getLong(EXTRA_THREAD_ID, -1));
|
||||
getIntent().putExtra(EXTRA_DISTRIBUTION_TYPE, shortcutExtras.getInt(EXTRA_DISTRIBUTION_TYPE, -1));
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
searchToolbar = findViewById(R.id.search_toolbar);
|
||||
searchAction = findViewById(R.id.search_action);
|
||||
shareConfirm = findViewById(R.id.share_confirm);
|
||||
shareContainer = findViewById(R.id.container);
|
||||
contactsFragment = new ContactSelectionListFragment();
|
||||
adapter = new ShareSelectionAdapter();
|
||||
contactsRecycler = findViewById(R.id.selected_list);
|
||||
contactsRecyclerDivider = findViewById(R.id.divider);
|
||||
|
||||
contactsRecycler.setAdapter(adapter);
|
||||
|
||||
RecyclerView.ItemAnimator itemAnimator = Objects.requireNonNull(contactsRecycler.getItemAnimator());
|
||||
ShareFlowConstants.applySelectedContactsRecyclerAnimationSpeeds(itemAnimator);
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.contact_selection_list_fragment, contactsFragment)
|
||||
.commit();
|
||||
|
||||
shareConfirm.setOnClickListener(unused -> {
|
||||
shareConfirm.setEnabled(false);
|
||||
|
||||
Set<ShareContact> shareContacts = viewModel.getShareContacts();
|
||||
|
||||
StoryDialogs.INSTANCE.guardWithAddToYourStoryDialog(this,
|
||||
shareContacts.stream()
|
||||
.filter(contact -> contact.getRecipientId().isPresent())
|
||||
.map(contact -> Recipient.resolved(contact.getRecipientId().get()))
|
||||
.filter(Recipient::isMyStory)
|
||||
.map(myStory -> new ContactSearchKey.Story(myStory.getId()))
|
||||
.collect(java.util.stream.Collectors.toList()),
|
||||
() -> {
|
||||
performSend(shareContacts);
|
||||
return Unit.INSTANCE;
|
||||
},
|
||||
() -> {
|
||||
shareConfirm.setEnabled(true);
|
||||
new HideStoryFromDialogFragment().show(getSupportFragmentManager(), null);
|
||||
return Unit.INSTANCE;
|
||||
},
|
||||
() -> {
|
||||
shareConfirm.setEnabled(true);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
});
|
||||
|
||||
viewModel.getSelectedContactModels().observe(this, models -> {
|
||||
adapter.submitList(models, () -> contactsRecycler.scrollToPosition(models.size() - 1));
|
||||
|
||||
shareConfirm.setEnabled(!models.isEmpty());
|
||||
shareConfirm.setAlpha(models.isEmpty() ? 0.5f : 1f);
|
||||
if (models.isEmpty()) {
|
||||
animateOutSelection();
|
||||
} else {
|
||||
animateInSelection();
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.getSmsShareRestriction().observe(this, smsShareRestriction -> {
|
||||
final int displayMode;
|
||||
|
||||
switch (smsShareRestriction) {
|
||||
case NO_RESTRICTIONS:
|
||||
disallowMultiShare = false;
|
||||
displayMode = getIntent().getIntExtra(ContactSelectionListFragment.DISPLAY_MODE, -1);
|
||||
|
||||
if (displayMode == -1) {
|
||||
Log.w(TAG, "DisplayMode not set yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Util.isDefaultSmsProvider(this) && (displayMode & DisplayMode.FLAG_SMS) == 0) {
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode | DisplayMode.FLAG_SMS);
|
||||
contactsFragment.setQueryFilter(null);
|
||||
}
|
||||
break;
|
||||
case DISALLOW_SMS_CONTACTS:
|
||||
disallowMultiShare = false;
|
||||
displayMode = getIntent().getIntExtra(ContactSelectionListFragment.DISPLAY_MODE, -1);
|
||||
|
||||
if (displayMode == -1) {
|
||||
Log.w(TAG, "DisplayMode not set yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode & ~DisplayMode.FLAG_SMS);
|
||||
contactsFragment.setQueryFilter(null);
|
||||
break;
|
||||
case DISALLOW_MULTI_SHARE:
|
||||
disallowMultiShare = true;
|
||||
break;
|
||||
}
|
||||
|
||||
validateAvailableRecipients();
|
||||
});
|
||||
}
|
||||
|
||||
private void performSend(Set<ShareContact> shareContacts) {
|
||||
if (shareContacts.isEmpty()) throw new AssertionError();
|
||||
else if (shareContacts.size() == 1) onConfirmSingleDestination(shareContacts.iterator().next());
|
||||
else onConfirmMultipleDestinations(shareContacts);
|
||||
}
|
||||
|
||||
private void initializeArgs() {
|
||||
this.args = ShareIntents.Args.from(getIntent());
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
this.viewModel = ViewModelProviders.of(this, new ShareViewModel.Factory()).get(ShareViewModel.class);
|
||||
}
|
||||
|
||||
private void initializeSearch() {
|
||||
//noinspection IntegerDivisionInFloatingPointContext
|
||||
searchAction.setOnClickListener(v -> searchToolbar.display(searchAction.getX() + (searchAction.getWidth() / 2),
|
||||
searchAction.getY() + (searchAction.getHeight() / 2)));
|
||||
|
||||
searchToolbar.setListener(new SearchToolbar.SearchListener() {
|
||||
@Override
|
||||
public void onSearchTextChange(String text) {
|
||||
if (contactsFragment != null) {
|
||||
contactsFragment.setQueryFilter(text);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchClosed() {
|
||||
if (contactsFragment != null) {
|
||||
contactsFragment.resetQueryFilter();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeMedia() {
|
||||
if (Intent.ACTION_SEND_MULTIPLE.equals(getIntent().getAction())) {
|
||||
Log.i(TAG, "Multiple media share.");
|
||||
List<Uri> uris = getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||
|
||||
viewModel.onMultipleMediaShared(uris);
|
||||
} else if (Intent.ACTION_SEND.equals(getIntent().getAction()) || getIntent().hasExtra(Intent.EXTRA_STREAM)) {
|
||||
Log.i(TAG, "Single media share.");
|
||||
Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
String type = getIntent().getType();
|
||||
|
||||
viewModel.onSingleMediaShared(uri, type);
|
||||
} else {
|
||||
Log.i(TAG, "Internal media share.");
|
||||
viewModel.onNonExternalShare();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDestination() {
|
||||
Intent intent = getIntent();
|
||||
long threadId = intent.getLongExtra(EXTRA_THREAD_ID, -1);
|
||||
int distributionType = intent.getIntExtra(EXTRA_DISTRIBUTION_TYPE, -1);
|
||||
RecipientId recipientId = RecipientId.from(intent.getStringExtra(EXTRA_RECIPIENT_ID));
|
||||
|
||||
boolean hasPreexistingDestination = threadId != -1 && distributionType != -1;
|
||||
|
||||
if (hasPreexistingDestination) {
|
||||
if (contactsFragment.getView() != null) {
|
||||
contactsFragment.getView().setVisibility(View.GONE);
|
||||
}
|
||||
onSingleDestinationChosen(threadId, recipientId);
|
||||
}
|
||||
}
|
||||
|
||||
private void onConfirmSingleDestination(@NonNull ShareContact shareContact) {
|
||||
if (shareContact.getRecipientId().isPresent() && Recipient.resolved(shareContact.getRecipientId().get()).isDistributionList()) {
|
||||
onConfirmMultipleDestinations(Collections.singleton(shareContact));
|
||||
return;
|
||||
}
|
||||
|
||||
shareConfirm.setClickable(false);
|
||||
SimpleTask.run(this.getLifecycle(),
|
||||
() -> resolveShareContact(shareContact),
|
||||
result -> onSingleDestinationChosen(result.getThreadId(), result.getRecipientId()));
|
||||
}
|
||||
|
||||
private void onConfirmMultipleDestinations(@NonNull Set<ShareContact> shareContacts) {
|
||||
shareConfirm.setClickable(false);
|
||||
SimpleTask.run(this.getLifecycle(),
|
||||
() -> resolvedShareContacts(shareContacts),
|
||||
this::onMultipleDestinationsChosen);
|
||||
}
|
||||
|
||||
private Set<ShareContactAndThread> resolvedShareContacts(@NonNull Set<ShareContact> sharedContacts) {
|
||||
Set<Recipient> recipients = Stream.of(sharedContacts)
|
||||
.map(contact -> contact.getRecipientId()
|
||||
.map(Recipient::resolved)
|
||||
.orElseGet(() -> Recipient.external(this, contact.getNumber())))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<RecipientId, Long> existingThreads = SignalDatabase.threads()
|
||||
.getThreadIdsIfExistsFor(Stream.of(recipients)
|
||||
.map(Recipient::getId)
|
||||
.toArray(RecipientId[]::new));
|
||||
|
||||
return Stream.of(recipients)
|
||||
.map(recipient -> new ShareContactAndThread(recipient.getId(), Util.getOrDefault(existingThreads, recipient.getId(), -1L), recipient.isForceSmsSelection() || !recipient.isRegistered(), recipient.isDistributionList()))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private ShareContactAndThread resolveShareContact(@NonNull ShareContact shareContact) {
|
||||
Recipient recipient;
|
||||
if (shareContact.getRecipientId().isPresent()) {
|
||||
recipient = Recipient.resolved(shareContact.getRecipientId().get());
|
||||
} else {
|
||||
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
|
||||
recipient = Recipient.external(this, shareContact.getNumber());
|
||||
}
|
||||
|
||||
long existingThread = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
|
||||
return new ShareContactAndThread(recipient.getId(), existingThread, recipient.isForceSmsSelection() || !recipient.isRegistered(), recipient.isDistributionList());
|
||||
}
|
||||
|
||||
private void validateAvailableRecipients() {
|
||||
resolveShareData(data -> {
|
||||
int mode = getIntent().getIntExtra(ContactSelectionListFragment.DISPLAY_MODE, -1);
|
||||
|
||||
if (mode == -1) return;
|
||||
|
||||
boolean isMmsOrSmsSupported = data != null ? data.isMmsOrSmsSupported() : Util.isDefaultSmsProvider(this);
|
||||
boolean isStoriesSupported = Stories.isFeatureEnabled() && data != null && data.isStoriesSupported();
|
||||
|
||||
mode = isMmsOrSmsSupported ? mode | DisplayMode.FLAG_SMS : mode & ~DisplayMode.FLAG_SMS;
|
||||
mode = isStoriesSupported ? mode | DisplayMode.FLAG_STORIES : mode & ~DisplayMode.FLAG_STORIES;
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, mode);
|
||||
|
||||
contactsFragment.reset();
|
||||
});
|
||||
}
|
||||
|
||||
private void resolveShareData(@NonNull Consumer<ShareData> onResolved) {
|
||||
AtomicReference<AlertDialog> progressWheel = new AtomicReference<>();
|
||||
|
||||
if (viewModel.getShareData().getValue() == null) {
|
||||
progressWheel.set(SimpleProgressDialog.show(this));
|
||||
}
|
||||
|
||||
viewModel.getShareData().observe(this, (data) -> {
|
||||
if (data == null) return;
|
||||
|
||||
if (progressWheel.get() != null) {
|
||||
progressWheel.get().dismiss();
|
||||
progressWheel.set(null);
|
||||
}
|
||||
|
||||
if (!data.isPresent() && args.isEmpty()) {
|
||||
Log.w(TAG, "No data to share!");
|
||||
Toast.makeText(this, R.string.ShareActivity_multiple_attachments_are_only_supported, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
onResolved.accept(data.orElse(null));
|
||||
});
|
||||
}
|
||||
|
||||
private void onMultipleDestinationsChosen(@NonNull Set<ShareContactAndThread> shareContactAndThreads) {
|
||||
if (!viewModel.isExternalShare()) {
|
||||
openInterstitial(shareContactAndThreads, null);
|
||||
return;
|
||||
}
|
||||
|
||||
resolveShareData(data -> openInterstitial(shareContactAndThreads, data));
|
||||
}
|
||||
|
||||
private void onSingleDestinationChosen(long threadId, @NonNull RecipientId recipientId) {
|
||||
if (!viewModel.isExternalShare()) {
|
||||
openConversation(threadId, recipientId, null);
|
||||
return;
|
||||
}
|
||||
|
||||
resolveShareData(data -> openConversation(threadId, recipientId, data));
|
||||
}
|
||||
|
||||
private void openConversation(long threadId, @NonNull RecipientId recipientId, @Nullable ShareData shareData) {
|
||||
ConversationIntents.Builder builder = ConversationIntents.createBuilder(this, recipientId, threadId)
|
||||
.withMedia(args.getExtraMedia())
|
||||
.withDraftText(args.getExtraText() != null ? args.getExtraText().toString() : null)
|
||||
.withStickerLocator(args.getExtraSticker())
|
||||
.asBorderless(args.isBorderless());
|
||||
|
||||
if (shareData != null && shareData.isForIntent()) {
|
||||
Log.i(TAG, "Shared data is a single file.");
|
||||
builder.withDataUri(shareData.getUri())
|
||||
.withDataType(shareData.getMimeType());
|
||||
} else if (shareData != null && shareData.isForMedia()) {
|
||||
Log.i(TAG, "Shared data is set of media.");
|
||||
builder.withMedia(shareData.getMedia());
|
||||
} else if (shareData != null && shareData.isForPrimitive()) {
|
||||
Log.i(TAG, "Shared data is a primitive type.");
|
||||
} else if (shareData == null && args.getExtraSticker() != null) {
|
||||
builder.withDataType(getIntent().getType());
|
||||
} else {
|
||||
Log.i(TAG, "Shared data was not external.");
|
||||
}
|
||||
|
||||
viewModel.onSuccessfulShare();
|
||||
|
||||
finish();
|
||||
startActivity(builder.build());
|
||||
}
|
||||
|
||||
private void openInterstitial(@NonNull Set<ShareContactAndThread> shareContactAndThreads, @Nullable ShareData shareData) {
|
||||
MultiShareArgs.Builder builder = new MultiShareArgs.Builder(shareContactAndThreads)
|
||||
.withMedia(args.getExtraMedia())
|
||||
.withDraftText(args.getExtraText() != null ? args.getExtraText().toString() : null)
|
||||
.withStickerLocator(args.getExtraSticker())
|
||||
.asBorderless(args.isBorderless());
|
||||
|
||||
if (shareData != null && shareData.isForIntent()) {
|
||||
Log.i(TAG, "Shared data is a single file.");
|
||||
builder.withDataUri(shareData.getUri())
|
||||
.withDataType(shareData.getMimeType());
|
||||
} else if (shareData != null && shareData.isForMedia()) {
|
||||
Log.i(TAG, "Shared data is set of media.");
|
||||
builder.withMedia(shareData.getMedia());
|
||||
} else if (shareData != null && shareData.isForPrimitive()) {
|
||||
Log.i(TAG, "Shared data is a primitive type.");
|
||||
} else if (shareData == null && args.getExtraSticker() != null) {
|
||||
builder.withDataType(getIntent().getType());
|
||||
} else {
|
||||
Log.i(TAG, "Shared data was not external.");
|
||||
}
|
||||
|
||||
MultiShareArgs multiShareArgs = builder.build();
|
||||
InterstitialContentType interstitialContentType = multiShareArgs.getInterstitialContentType();
|
||||
switch (interstitialContentType) {
|
||||
case TEXT:
|
||||
startActivityForResult(ShareInterstitialActivity.createIntent(this, multiShareArgs), RESULT_TEXT_CONFIRMATION);
|
||||
break;
|
||||
case MEDIA:
|
||||
List<Media> media = new ArrayList<>(multiShareArgs.getMedia());
|
||||
if (media.isEmpty()) {
|
||||
media.add(new Media(multiShareArgs.getDataUri(),
|
||||
multiShareArgs.getDataType(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty()));
|
||||
}
|
||||
|
||||
Intent intent = MediaSelectionActivity.share(this,
|
||||
MultiShareSender.getWorstTransportOption(this, multiShareArgs.getShareContactAndThreads()),
|
||||
media,
|
||||
Stream.of(multiShareArgs.getShareContactAndThreads()).map(ShareContactAndThread::getRecipientId).toList(),
|
||||
multiShareArgs.getDraftText());
|
||||
startActivityForResult(intent, RESULT_MEDIA_CONFIRMATION);
|
||||
break;
|
||||
default:
|
||||
//noinspection CodeBlock2Expr
|
||||
MultiShareSender.send(multiShareArgs, results -> {
|
||||
MultiShareDialogs.displayResultDialog(this, results, () -> {
|
||||
viewModel.onSuccessfulShare();
|
||||
finish();
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuggestedLimitReached(int limit) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHardLimitReached(int limit) {
|
||||
MultiShareDialogs.displayMaxSelectedDialog(this, limit);
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ShareContactAndThread implements Parcelable {
|
||||
private final RecipientId recipientId;
|
||||
private final long threadId;
|
||||
private final boolean forceSms;
|
||||
private final boolean isStory;
|
||||
|
||||
public ShareContactAndThread(@NonNull RecipientId recipientId, long threadId, boolean forceSms, boolean isStory) {
|
||||
this.recipientId = recipientId;
|
||||
this.threadId = threadId;
|
||||
this.forceSms = forceSms;
|
||||
this.isStory = isStory;
|
||||
}
|
||||
|
||||
protected ShareContactAndThread(@NonNull Parcel in) {
|
||||
recipientId = in.readParcelable(RecipientId.class.getClassLoader());
|
||||
threadId = in.readLong();
|
||||
forceSms = in.readByte() == 1;
|
||||
isStory = in.readByte() == 1;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getRecipientId() {
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
public long getThreadId() {
|
||||
return threadId;
|
||||
}
|
||||
|
||||
public boolean isForceSms() {
|
||||
return forceSms;
|
||||
}
|
||||
|
||||
public boolean isStory() {
|
||||
return isStory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ShareContactAndThread that = (ShareContactAndThread) o;
|
||||
return threadId == that.threadId &&
|
||||
forceSms == that.forceSms &&
|
||||
isStory == that.isStory &&
|
||||
recipientId.equals(that.recipientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(recipientId, threadId, forceSms, isStory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeParcelable(recipientId, flags);
|
||||
dest.writeLong(threadId);
|
||||
dest.writeByte((byte) (forceSms ? 1 : 0));
|
||||
dest.writeByte((byte) (isStory ? 1 : 0));
|
||||
}
|
||||
|
||||
public static final Creator<ShareContactAndThread> CREATOR = new Creator<ShareContactAndThread>() {
|
||||
@Override
|
||||
public ShareContactAndThread createFromParcel(@NonNull Parcel in) {
|
||||
return new ShareContactAndThread(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShareContactAndThread[] newArray(int size) {
|
||||
return new ShareContactAndThread[size];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
class ShareData {
|
||||
|
||||
private final Optional<Uri> uri;
|
||||
private final Optional<String> mimeType;
|
||||
private final Optional<ArrayList<Media>> media;
|
||||
private final boolean external;
|
||||
private final boolean isMmsOrSmsSupported;
|
||||
|
||||
static ShareData forIntentData(@NonNull Uri uri, @NonNull String mimeType, boolean external, boolean isMmsOrSmsSupported) {
|
||||
return new ShareData(Optional.of(uri), Optional.of(mimeType), Optional.empty(), external, isMmsOrSmsSupported);
|
||||
}
|
||||
|
||||
static ShareData forPrimitiveTypes() {
|
||||
return new ShareData(Optional.empty(), Optional.empty(), Optional.empty(), true, true);
|
||||
}
|
||||
|
||||
static ShareData forMedia(@NonNull List<Media> media, boolean isMmsOrSmsSupported) {
|
||||
return new ShareData(Optional.empty(), Optional.empty(), Optional.of(new ArrayList<>(media)), true, isMmsOrSmsSupported);
|
||||
}
|
||||
|
||||
private ShareData(Optional<Uri> uri, Optional<String> mimeType, Optional<ArrayList<Media>> media, boolean external, boolean isMmsOrSmsSupported) {
|
||||
this.uri = uri;
|
||||
this.mimeType = mimeType;
|
||||
this.media = media;
|
||||
this.external = external;
|
||||
this.isMmsOrSmsSupported = isMmsOrSmsSupported;
|
||||
}
|
||||
|
||||
boolean isForIntent() {
|
||||
return uri.isPresent();
|
||||
}
|
||||
|
||||
boolean isForPrimitive() {
|
||||
return !uri.isPresent() && !media.isPresent();
|
||||
}
|
||||
|
||||
boolean isForMedia() {
|
||||
return media.isPresent();
|
||||
}
|
||||
|
||||
public @NonNull Uri getUri() {
|
||||
return uri.get();
|
||||
}
|
||||
|
||||
public @NonNull String getMimeType() {
|
||||
return mimeType.get();
|
||||
}
|
||||
|
||||
public @NonNull ArrayList<Media> getMedia() {
|
||||
return media.get();
|
||||
}
|
||||
|
||||
public boolean isExternal() {
|
||||
return external;
|
||||
}
|
||||
|
||||
public boolean isMmsOrSmsSupported() {
|
||||
return isMmsOrSmsSupported;
|
||||
}
|
||||
|
||||
public boolean isStoriesSupported() {
|
||||
if (isForIntent()) {
|
||||
return MediaUtil.isStorySupportedType(getMimeType());
|
||||
} else if (isForMedia()) {
|
||||
return getMedia().stream().allMatch(media -> MediaUtil.isStorySupportedType(media.getMimeType()));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
final class ShareIntents {
|
||||
|
||||
private static final String EXTRA_MEDIA = "extra_media";
|
||||
private static final String EXTRA_BORDERLESS = "extra_borderless";
|
||||
private static final String EXTRA_STICKER = "extra_sticker";
|
||||
|
||||
private ShareIntents() {
|
||||
}
|
||||
|
||||
public static final class Args {
|
||||
|
||||
private final CharSequence extraText;
|
||||
private final ArrayList<Media> extraMedia;
|
||||
private final StickerLocator extraSticker;
|
||||
private final boolean isBorderless;
|
||||
|
||||
public static Args from(@NonNull Intent intent) {
|
||||
return new Args(intent.getStringExtra(Intent.EXTRA_TEXT),
|
||||
intent.getParcelableArrayListExtra(EXTRA_MEDIA),
|
||||
intent.getParcelableExtra(EXTRA_STICKER),
|
||||
intent.getBooleanExtra(EXTRA_BORDERLESS, false));
|
||||
}
|
||||
|
||||
private Args(@Nullable CharSequence extraText,
|
||||
@Nullable ArrayList<Media> extraMedia,
|
||||
@Nullable StickerLocator extraSticker,
|
||||
boolean isBorderless)
|
||||
{
|
||||
this.extraText = extraText;
|
||||
this.extraMedia = extraMedia;
|
||||
this.extraSticker = extraSticker;
|
||||
this.isBorderless = isBorderless;
|
||||
}
|
||||
|
||||
public @Nullable ArrayList<Media> getExtraMedia() {
|
||||
return extraMedia;
|
||||
}
|
||||
|
||||
public @Nullable CharSequence getExtraText() {
|
||||
return extraText;
|
||||
}
|
||||
|
||||
public @Nullable StickerLocator getExtraSticker() {
|
||||
return extraSticker;
|
||||
}
|
||||
|
||||
public boolean isBorderless() {
|
||||
return isBorderless;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return extraSticker == null &&
|
||||
(extraMedia == null || extraMedia.isEmpty()) &&
|
||||
TextUtils.isEmpty(extraText);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.TransportOptions;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendConstants;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.UriUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
class ShareRepository {
|
||||
|
||||
private static final String TAG = Log.tag(ShareRepository.class);
|
||||
|
||||
/**
|
||||
* Handles a single URI that may be local or external.
|
||||
*/
|
||||
void getResolved(@Nullable Uri uri, @Nullable String mimeType, @NonNull Callback<Optional<ShareData>> callback) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
callback.onResult(Optional.of(getResolvedInternal(uri, mimeType)));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to resolve!", e);
|
||||
callback.onResult(Optional.empty());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles multiple URIs that are all assumed to be external images/videos.
|
||||
*/
|
||||
void getResolved(@NonNull List<Uri> uris, @NonNull Callback<Optional<ShareData>> callback) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
callback.onResult(Optional.ofNullable(getResolvedInternal(uris)));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to resolve!", e);
|
||||
callback.onResult(Optional.empty());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull ShareData getResolvedInternal(@Nullable Uri uri, @Nullable String mimeType) throws IOException {
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
|
||||
if (uri == null) {
|
||||
return ShareData.forPrimitiveTypes();
|
||||
}
|
||||
|
||||
if (!UriUtil.isValidExternalUri(context, uri)) {
|
||||
throw new IOException("Invalid external URI!");
|
||||
}
|
||||
|
||||
mimeType = getMimeType(context, uri, mimeType);
|
||||
|
||||
if (PartAuthority.isLocalUri(uri)) {
|
||||
return ShareData.forIntentData(uri, mimeType, false, false);
|
||||
} else {
|
||||
InputStream stream = null;
|
||||
try {
|
||||
stream = context.getContentResolver().openInputStream(uri);
|
||||
} catch (SecurityException e) {
|
||||
Log.w(TAG, "Failed to read stream!", e);
|
||||
}
|
||||
|
||||
if (stream == null) {
|
||||
throw new IOException("Failed to open stream!");
|
||||
}
|
||||
|
||||
long size = getSize(context, uri);
|
||||
String fileName = getFileName(context, uri);
|
||||
|
||||
Uri blobUri;
|
||||
|
||||
if (MediaUtil.isImageType(mimeType) || MediaUtil.isVideoType(mimeType)) {
|
||||
blobUri = BlobProvider.getInstance()
|
||||
.forData(stream, size)
|
||||
.withMimeType(mimeType)
|
||||
.withFileName(fileName)
|
||||
.createForSingleSessionOnDisk(context);
|
||||
} else {
|
||||
blobUri = BlobProvider.getInstance()
|
||||
.forData(stream, size)
|
||||
.withMimeType(mimeType)
|
||||
.withFileName(fileName)
|
||||
.createForSingleSessionOnDisk(context);
|
||||
// TODO Convert to multi-session after file drafts are fixed.
|
||||
}
|
||||
|
||||
return ShareData.forIntentData(blobUri, mimeType, true, isMmsSupported(context, asUriAttachment(blobUri, mimeType, size)));
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull UriAttachment asUriAttachment(@NonNull Uri uri, @NonNull String mimeType, long size) {
|
||||
return new UriAttachment(uri, mimeType, -1, size, null, false, false, false, false, null, null, null, null, null);
|
||||
}
|
||||
|
||||
private boolean isMmsSupported(@NonNull Context context, @NonNull Attachment attachment) {
|
||||
boolean canReadPhoneState = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED;
|
||||
if (!Util.isDefaultSmsProvider(context) || !canReadPhoneState || !Util.isMmsCapable(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
TransportOptions options = new TransportOptions(context, true);
|
||||
options.setDefaultTransport(TransportOption.Type.SMS);
|
||||
MediaConstraints mmsConstraints = MediaConstraints.getMmsMediaConstraints(options.getSelectedTransport().getSimSubscriptionId().orElse(-1));
|
||||
|
||||
return mmsConstraints.isSatisfied(context, attachment) || mmsConstraints.canResize(attachment);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable ShareData getResolvedInternal(@NonNull List<Uri> uris) throws IOException {
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
|
||||
Map<Uri, String> mimeTypes = Stream.of(uris)
|
||||
.map(uri -> new Pair<>(uri, getMimeType(context, uri, null)))
|
||||
.filter(p -> MediaUtil.isImageType(p.second) || MediaUtil.isVideoType(p.second))
|
||||
.collect(Collectors.toMap(p -> p.first, p -> p.second));
|
||||
|
||||
if (mimeTypes.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Media> media = new ArrayList<>(mimeTypes.size());
|
||||
|
||||
for (Map.Entry<Uri, String> entry : mimeTypes.entrySet()) {
|
||||
Uri uri = entry.getKey();
|
||||
String mimeType = entry.getValue();
|
||||
|
||||
InputStream stream;
|
||||
try {
|
||||
stream = context.getContentResolver().openInputStream(uri);
|
||||
if (stream == null) {
|
||||
throw new IOException("Failed to open stream!");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to open: " + uri);
|
||||
continue;
|
||||
}
|
||||
|
||||
long size = getSize(context, uri);
|
||||
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
|
||||
long duration = getDuration(context, uri);
|
||||
Uri blobUri = BlobProvider.getInstance()
|
||||
.forData(stream, size)
|
||||
.withMimeType(mimeType)
|
||||
.createForSingleSessionOnDisk(context);
|
||||
|
||||
media.add(new Media(blobUri,
|
||||
mimeType,
|
||||
System.currentTimeMillis(),
|
||||
dimens.first,
|
||||
dimens.second,
|
||||
size,
|
||||
duration,
|
||||
false,
|
||||
false,
|
||||
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
|
||||
Optional.empty(),
|
||||
Optional.empty()));
|
||||
|
||||
if (media.size() >= MediaSendConstants.MAX_PUSH) {
|
||||
Log.w(TAG, "Exceeded the attachment limit! Skipping the rest.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (media.size() > 0) {
|
||||
boolean isMmsSupported = Stream.of(media)
|
||||
.allMatch(m -> isMmsSupported(context, asUriAttachment(m.getUri(), m.getMimeType(), m.getSize())));
|
||||
return ShareData.forMedia(media, isMmsSupported);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull String getMimeType(@NonNull Context context, @NonNull Uri uri, @Nullable String mimeType) {
|
||||
String updatedMimeType = MediaUtil.getMimeType(context, uri);
|
||||
|
||||
if (updatedMimeType == null) {
|
||||
updatedMimeType = MediaUtil.getCorrectedMimeType(mimeType);
|
||||
}
|
||||
|
||||
return updatedMimeType != null ? updatedMimeType : MediaUtil.UNKNOWN;
|
||||
}
|
||||
|
||||
private static long getSize(@NonNull Context context, @NonNull Uri uri) throws IOException {
|
||||
long size = 0;
|
||||
|
||||
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst() && cursor.getColumnIndex(OpenableColumns.SIZE) >= 0) {
|
||||
size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
|
||||
}
|
||||
}
|
||||
|
||||
if (size <= 0) {
|
||||
size = MediaUtil.getMediaSize(context, uri);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
private static @Nullable String getFileName(@NonNull Context context, @NonNull Uri uri) {
|
||||
if (uri.getScheme().equalsIgnoreCase("file")) {
|
||||
return uri.getLastPathSegment();
|
||||
}
|
||||
|
||||
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst() && cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) >= 0) {
|
||||
return cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long getDuration(@NonNull Context context, @NonNull Uri uri) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
interface Callback<E> {
|
||||
void onResult(@NonNull E result);
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
public class ShareViewModel extends ViewModel {
|
||||
|
||||
private static final String TAG = Log.tag(ShareViewModel.class);
|
||||
|
||||
private final Context context;
|
||||
private final ShareRepository shareRepository;
|
||||
private final MutableLiveData<Optional<ShareData>> shareData;
|
||||
private final MutableLiveData<Set<ShareContact>> selectedContacts;
|
||||
private final LiveData<SmsShareRestriction> smsShareRestriction;
|
||||
|
||||
private boolean mediaUsed;
|
||||
private boolean externalShare;
|
||||
|
||||
private ShareViewModel() {
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
this.shareRepository = new ShareRepository();
|
||||
this.shareData = new MutableLiveData<>();
|
||||
this.selectedContacts = new DefaultValueLiveData<>(Collections.emptySet());
|
||||
this.smsShareRestriction = Transformations.map(selectedContacts, this::updateShareRestriction);
|
||||
}
|
||||
|
||||
void onSingleMediaShared(@NonNull Uri uri, @Nullable String mimeType) {
|
||||
externalShare = true;
|
||||
shareRepository.getResolved(uri, mimeType, shareData::postValue);
|
||||
}
|
||||
|
||||
void onMultipleMediaShared(@NonNull List<Uri> uris) {
|
||||
externalShare = true;
|
||||
shareRepository.getResolved(uris, shareData::postValue);
|
||||
}
|
||||
|
||||
boolean isMultiShare() {
|
||||
return selectedContacts.getValue().size() > 1;
|
||||
}
|
||||
|
||||
@NonNull Single<ContactSelectResult> onContactSelected(@NonNull ShareContact selectedContact) {
|
||||
return Single.fromCallable(() -> {
|
||||
if (selectedContact.getRecipientId().isPresent()) {
|
||||
Recipient recipient = Recipient.resolved(selectedContact.getRecipientId().get());
|
||||
|
||||
if (recipient.isPushV2Group()) {
|
||||
Optional<GroupDatabase.GroupRecord> record = SignalDatabase.groups().getGroup(recipient.requireGroupId());
|
||||
|
||||
if (record.isPresent() && record.get().isAnnouncementGroup() && !record.get().isAdmin(Recipient.self())) {
|
||||
return ContactSelectResult.FALSE_AND_SHOW_PERMISSION_TOAST;
|
||||
}
|
||||
} else if (SmsShareRestriction.DISALLOW_SMS_CONTACTS.equals(smsShareRestriction.getValue()) && isRecipientAnSmsContact(recipient)) {
|
||||
return ContactSelectResult.FALSE_AND_SHOW_SMS_MULTISELECT_TOAST;
|
||||
}
|
||||
}
|
||||
|
||||
Set<ShareContact> contacts = new LinkedHashSet<>(selectedContacts.getValue());
|
||||
if (contacts.add(selectedContact)) {
|
||||
selectedContacts.postValue(contacts);
|
||||
return ContactSelectResult.TRUE;
|
||||
} else {
|
||||
return ContactSelectResult.FALSE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onContactDeselected(@NonNull ShareContact selectedContact) {
|
||||
Set<ShareContact> contacts = new LinkedHashSet<>(selectedContacts.getValue());
|
||||
if (contacts.remove(selectedContact)) {
|
||||
selectedContacts.setValue(contacts);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull Set<ShareContact> getShareContacts() {
|
||||
Set<ShareContact> contacts = selectedContacts.getValue();
|
||||
if (contacts == null) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
return contacts;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull LiveData<MappingModelList> getSelectedContactModels() {
|
||||
return Transformations.map(selectedContacts, set -> Stream.of(set)
|
||||
.mapIndexed((i, c) -> new ShareSelectionMappingModel(c, i == 0))
|
||||
.collect(MappingModelList.toMappingModelList()));
|
||||
}
|
||||
|
||||
@NonNull LiveData<SmsShareRestriction> getSmsShareRestriction() {
|
||||
return Transformations.distinctUntilChanged(smsShareRestriction);
|
||||
}
|
||||
|
||||
void onNonExternalShare() {
|
||||
shareData.setValue(Optional.empty());
|
||||
externalShare = false;
|
||||
}
|
||||
|
||||
public void onSuccessfulShare() {
|
||||
mediaUsed = true;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Optional<ShareData>> getShareData() {
|
||||
return shareData;
|
||||
}
|
||||
|
||||
boolean isExternalShare() {
|
||||
return externalShare;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
ShareData data = shareData.getValue() != null ? shareData.getValue().orElse(null) : null;
|
||||
|
||||
if (data != null && data.isExternal() && data.isForIntent() && !mediaUsed) {
|
||||
Log.i(TAG, "Clearing out unused data.");
|
||||
BlobProvider.getInstance().delete(context, data.getUri());
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull SmsShareRestriction updateShareRestriction(@NonNull Set<ShareContact> shareContacts) {
|
||||
if (shareContacts.isEmpty()) {
|
||||
return SmsShareRestriction.NO_RESTRICTIONS;
|
||||
} else if (shareContacts.size() == 1) {
|
||||
ShareContact shareContact = shareContacts.iterator().next();
|
||||
|
||||
if (shareContact.getRecipientId().isPresent()) {
|
||||
Recipient recipient = Recipient.live(shareContact.getRecipientId().get()).get();
|
||||
|
||||
if (isRecipientAnSmsContact(recipient)) {
|
||||
return SmsShareRestriction.DISALLOW_MULTI_SHARE;
|
||||
} else {
|
||||
return SmsShareRestriction.DISALLOW_SMS_CONTACTS;
|
||||
}
|
||||
} else {
|
||||
return SmsShareRestriction.DISALLOW_MULTI_SHARE;
|
||||
}
|
||||
} else {
|
||||
return SmsShareRestriction.DISALLOW_SMS_CONTACTS;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isRecipientAnSmsContact(@NonNull Recipient recipient) {
|
||||
return !recipient.isDistributionList() && (!recipient.isRegistered() || recipient.isForceSmsSelection());
|
||||
}
|
||||
|
||||
enum ContactSelectResult {
|
||||
TRUE, FALSE, FALSE_AND_SHOW_PERMISSION_TOAST, FALSE_AND_SHOW_SMS_MULTISELECT_TOAST
|
||||
}
|
||||
|
||||
enum SmsShareRestriction {
|
||||
NO_RESTRICTIONS,
|
||||
DISALLOW_SMS_CONTACTS,
|
||||
DISALLOW_MULTI_SHARE
|
||||
}
|
||||
|
||||
public static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
@Override
|
||||
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ShareViewModel());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
@@ -79,21 +79,25 @@ public class ShareInterstitialActivity extends PassphraseRequiredActivity {
|
||||
ShareInterstitialRepository repository = new ShareInterstitialRepository();
|
||||
ShareInterstitialViewModel.Factory factory = new ShareInterstitialViewModel.Factory(args, repository);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(ShareInterstitialViewModel.class);
|
||||
viewModel = new ViewModelProvider(this, factory).get(ShareInterstitialViewModel.class);
|
||||
|
||||
LinkPreviewRepository linkPreviewRepository = new LinkPreviewRepository();
|
||||
LinkPreviewViewModel.Factory linkPreviewViewModelFactory = new LinkPreviewViewModel.Factory(linkPreviewRepository);
|
||||
|
||||
linkPreviewViewModel = ViewModelProviders.of(this, linkPreviewViewModelFactory).get(LinkPreviewViewModel.class);
|
||||
linkPreviewViewModel = new ViewModelProvider(this, linkPreviewViewModelFactory).get(LinkPreviewViewModel.class);
|
||||
|
||||
boolean hasSms = Stream.of(args.getShareContactAndThreads())
|
||||
boolean hasSms = Stream.of(args.getRecipientSearchKeys())
|
||||
.anyMatch(c -> {
|
||||
Recipient recipient = Recipient.resolved(c.getRecipientId());
|
||||
if (recipient.isDistributionList()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !recipient.isRegistered() || recipient.isForceSmsSelection();
|
||||
});
|
||||
|
||||
if (hasSms) {
|
||||
linkPreviewViewModel.onTransportChanged(hasSms);
|
||||
linkPreviewViewModel.onTransportChanged(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,22 +7,22 @@ import androidx.core.util.Consumer;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sharing.ShareContactAndThread;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
class ShareInterstitialRepository {
|
||||
|
||||
void loadRecipients(@NonNull Set<ShareContactAndThread> shareContactAndThreads, Consumer<List<Recipient>> consumer) {
|
||||
SignalExecutors.BOUNDED.execute(() -> consumer.accept(resolveRecipients(shareContactAndThreads)));
|
||||
void loadRecipients(@NonNull Set<ContactSearchKey.RecipientSearchKey> recipientSearchKeys, Consumer<List<Recipient>> consumer) {
|
||||
SignalExecutors.BOUNDED.execute(() -> consumer.accept(resolveRecipients(recipientSearchKeys)));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private List<Recipient> resolveRecipients(@NonNull Set<ShareContactAndThread> shareContactAndThreads) {
|
||||
return Stream.of(shareContactAndThreads)
|
||||
.map(ShareContactAndThread::getRecipientId)
|
||||
private List<Recipient> resolveRecipients(@NonNull Set<ContactSearchKey.RecipientSearchKey> recipientSearchKeys) {
|
||||
return Stream.of(recipientSearchKeys)
|
||||
.map(ContactSearchKey.RecipientSearchKey::getRecipientId)
|
||||
.map(Recipient::resolved)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ private final MultiShareArgs args;
|
||||
this.recipients = new MutableLiveData<>();
|
||||
this.draftText = new DefaultValueLiveData<>(Util.firstNonNull(args.getDraftText(), ""));
|
||||
|
||||
repository.loadRecipients(args.getShareContactAndThreads(),
|
||||
repository.loadRecipients(args.getRecipientSearchKeys(),
|
||||
list -> recipients.postValue(Stream.of(list)
|
||||
.mapIndexed((i, r) -> new ShareInterstitialMappingModel(r, i == 0))
|
||||
.collect(MappingModelList.toMappingModelList())));
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.thoughtcrime.securesms.sharing.v2
|
||||
|
||||
import android.net.Uri
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareArgs
|
||||
import java.lang.UnsupportedOperationException
|
||||
|
||||
sealed class ResolvedShareData {
|
||||
|
||||
abstract val isMmsOrSmsSupported: Boolean
|
||||
|
||||
abstract fun toMultiShareArgs(): MultiShareArgs
|
||||
|
||||
data class Primitive(val text: CharSequence) : ResolvedShareData() {
|
||||
override val isMmsOrSmsSupported: Boolean = true
|
||||
|
||||
override fun toMultiShareArgs(): MultiShareArgs {
|
||||
return MultiShareArgs.Builder(setOf()).withDraftText(text.toString()).build()
|
||||
}
|
||||
}
|
||||
|
||||
data class ExternalUri(
|
||||
val uri: Uri,
|
||||
val mimeType: String,
|
||||
override val isMmsOrSmsSupported: Boolean
|
||||
) : ResolvedShareData() {
|
||||
override fun toMultiShareArgs(): MultiShareArgs {
|
||||
return MultiShareArgs.Builder(setOf()).withDataUri(uri).withDataType(mimeType).build()
|
||||
}
|
||||
}
|
||||
|
||||
data class Media(
|
||||
val media: List<org.thoughtcrime.securesms.mediasend.Media>,
|
||||
override val isMmsOrSmsSupported: Boolean
|
||||
) : ResolvedShareData() {
|
||||
override fun toMultiShareArgs(): MultiShareArgs {
|
||||
return MultiShareArgs.Builder(setOf()).withMedia(media).build()
|
||||
}
|
||||
}
|
||||
|
||||
object Failure : ResolvedShareData() {
|
||||
override val isMmsOrSmsSupported: Boolean get() = throw UnsupportedOperationException()
|
||||
override fun toMultiShareArgs(): MultiShareArgs = throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package org.thoughtcrime.securesms.sharing.v2
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFullScreenDialogFragment
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity.Companion.share
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareDialogs
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareSender
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareSender.MultiShareSendResultCollection
|
||||
import org.thoughtcrime.securesms.sharing.interstitial.ShareInterstitialActivity
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import java.util.Optional
|
||||
|
||||
class ShareActivity : PassphraseRequiredActivity(), MultiselectForwardFragment.Callback {
|
||||
|
||||
private val dynamicTheme = DynamicNoActionBarTheme()
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private lateinit var finishOnOkResultLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
private val viewModel: ShareViewModel by viewModels {
|
||||
ShareViewModel.Factory(getUnresolvedShareData(), directShareTarget, ShareRepository(this))
|
||||
}
|
||||
|
||||
private val directShareTarget: RecipientId?
|
||||
get() = intent.getStringExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID).let { ConversationUtil.getRecipientId(it) }
|
||||
|
||||
override fun onPreCreate() {
|
||||
super.onPreCreate()
|
||||
dynamicTheme.onCreate(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
setContentView(R.layout.share_activity_v2)
|
||||
|
||||
finishOnOkResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleDisposable += viewModel.events.subscribe { shareEvent ->
|
||||
when (shareEvent) {
|
||||
is ShareEvent.OpenConversation -> openConversation(shareEvent)
|
||||
is ShareEvent.OpenMediaInterstitial -> openMediaInterstitial(shareEvent)
|
||||
is ShareEvent.OpenTextInterstitial -> openTextInterstitial(shareEvent)
|
||||
is ShareEvent.SendWithoutInterstitial -> sendWithoutInterstitial(shareEvent)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { shareState ->
|
||||
when (shareState.loadState) {
|
||||
ShareState.ShareDataLoadState.Init -> Unit
|
||||
ShareState.ShareDataLoadState.Failed -> finish()
|
||||
is ShareState.ShareDataLoadState.Loaded -> {
|
||||
ensureFragment(shareState.loadState.resolvedShareData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
dynamicTheme.onResume(this)
|
||||
}
|
||||
|
||||
override fun onFinishForwardAction() = Unit
|
||||
|
||||
override fun exitFlow() = Unit
|
||||
|
||||
override fun onSearchInputFocused() = Unit
|
||||
|
||||
override fun setResult(bundle: Bundle) {
|
||||
if (bundle.containsKey(MultiselectForwardFragment.RESULT_SENT)) {
|
||||
throw AssertionError("Should never happen.")
|
||||
}
|
||||
|
||||
if (!bundle.containsKey(MultiselectForwardFragment.RESULT_SELECTION)) {
|
||||
throw AssertionError("Expected a recipient selection!")
|
||||
}
|
||||
|
||||
val parcelizedKeys: List<ContactSearchKey.ParcelableRecipientSearchKey> = bundle.getParcelableArrayList(MultiselectForwardFragment.RESULT_SELECTION)!!
|
||||
val contactSearchKeys = parcelizedKeys.map { it.asContactSearchKey() }
|
||||
|
||||
viewModel.onContactSelectionConfirmed(contactSearchKeys)
|
||||
}
|
||||
|
||||
override fun getContainer(): ViewGroup = findViewById(R.id.container)
|
||||
|
||||
override fun getDialogBackgroundColor(): Int = ContextCompat.getColor(this, R.color.signal_background_primary)
|
||||
|
||||
private fun getUnresolvedShareData(): UnresolvedShareData {
|
||||
return when {
|
||||
intent.action == Intent.ACTION_SEND_MULTIPLE -> {
|
||||
intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)?.let {
|
||||
UnresolvedShareData.ExternalMultiShare(it)
|
||||
} ?: error("ACTION_SEND_MULTIPLE with EXTRA_STREAM but the EXTRA_STREAM was null")
|
||||
}
|
||||
intent.action == Intent.ACTION_SEND && intent.hasExtra(Intent.EXTRA_STREAM) -> {
|
||||
intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)?.let {
|
||||
UnresolvedShareData.ExternalSingleShare(it, intent.type)
|
||||
} ?: error("ACTION_SEND with EXTRA_STREAM but the EXTRA_STREAM was null")
|
||||
}
|
||||
intent.action == Intent.ACTION_SEND && intent.hasExtra(Intent.EXTRA_TEXT) -> {
|
||||
intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.let {
|
||||
UnresolvedShareData.ExternalPrimitiveShare(it)
|
||||
} ?: error("ACTION_SEND with EXTRA_TEXT but the EXTRA_TEXT was null")
|
||||
}
|
||||
else -> null
|
||||
} ?: error("Intent Action: ${Intent.ACTION_SEND_MULTIPLE} could not be resolved with the given arguments.")
|
||||
}
|
||||
|
||||
private fun ensureFragment(resolvedShareData: ResolvedShareData) {
|
||||
if (supportFragmentManager.fragments.none { it is MultiselectForwardFullScreenDialogFragment }) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(
|
||||
R.id.fragment_container,
|
||||
MultiselectForwardFragment.create(
|
||||
MultiselectForwardFragmentArgs(
|
||||
canSendToNonPush = resolvedShareData.isMmsOrSmsSupported,
|
||||
multiShareArgs = listOf(resolvedShareData.toMultiShareArgs()),
|
||||
title = R.string.MultiselectForwardFragment__share_with,
|
||||
forceDisableAddMessage = true,
|
||||
forceSelectionOnly = true
|
||||
)
|
||||
)
|
||||
).commitNow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openConversation(shareEvent: ShareEvent.OpenConversation) {
|
||||
if (shareEvent.contact.isStory) {
|
||||
error("Can't open a conversation for a story!")
|
||||
}
|
||||
|
||||
val multiShareArgs = shareEvent.getMultiShareArgs()
|
||||
val conversationIntentBuilder = ConversationIntents.createBuilder(this, shareEvent.contact.recipientId, -1L)
|
||||
.withDataUri(multiShareArgs.dataUri)
|
||||
.withDataType(multiShareArgs.dataType)
|
||||
.withMedia(multiShareArgs.media)
|
||||
.withDraftText(multiShareArgs.draftText)
|
||||
.withStickerLocator(multiShareArgs.stickerLocator)
|
||||
.asBorderless(multiShareArgs.isBorderless)
|
||||
|
||||
finish()
|
||||
startActivity(conversationIntentBuilder.build())
|
||||
}
|
||||
|
||||
private fun openMediaInterstitial(shareEvent: ShareEvent.OpenMediaInterstitial) {
|
||||
val multiShareArgs = shareEvent.getMultiShareArgs()
|
||||
val media: MutableList<Media> = ArrayList(multiShareArgs.media)
|
||||
if (media.isEmpty() && multiShareArgs.dataUri != null) {
|
||||
media.add(
|
||||
Media(
|
||||
multiShareArgs.dataUri,
|
||||
multiShareArgs.dataType,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val shareAsTextStory = multiShareArgs.allRecipientsAreStories() && media.isEmpty()
|
||||
|
||||
val intent = share(
|
||||
this,
|
||||
MultiShareSender.getWorstTransportOption(this, multiShareArgs.recipientSearchKeys),
|
||||
media,
|
||||
multiShareArgs.recipientSearchKeys.toList(),
|
||||
multiShareArgs.draftText,
|
||||
shareAsTextStory
|
||||
)
|
||||
|
||||
finishOnOkResultLauncher.launch(intent)
|
||||
}
|
||||
|
||||
private fun openTextInterstitial(shareEvent: ShareEvent.OpenTextInterstitial) {
|
||||
finishOnOkResultLauncher.launch(ShareInterstitialActivity.createIntent(this, shareEvent.getMultiShareArgs()))
|
||||
}
|
||||
|
||||
private fun sendWithoutInterstitial(shareEvent: ShareEvent.SendWithoutInterstitial) {
|
||||
MultiShareSender.send(shareEvent.getMultiShareArgs()) { results: MultiShareSendResultCollection? ->
|
||||
MultiShareDialogs.displayResultDialog(this, results!!) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.thoughtcrime.securesms.sharing.v2
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareArgs
|
||||
|
||||
sealed class ShareEvent {
|
||||
|
||||
protected abstract val shareData: ResolvedShareData
|
||||
protected abstract val contacts: List<ContactSearchKey.RecipientSearchKey>
|
||||
|
||||
fun getMultiShareArgs(): MultiShareArgs {
|
||||
return shareData.toMultiShareArgs().buildUpon(
|
||||
contacts.toSet()
|
||||
).build()
|
||||
}
|
||||
|
||||
data class OpenConversation(override val shareData: ResolvedShareData, val contact: ContactSearchKey.RecipientSearchKey) : ShareEvent() {
|
||||
override val contacts: List<ContactSearchKey.RecipientSearchKey> = listOf(contact)
|
||||
}
|
||||
|
||||
data class OpenMediaInterstitial(override val shareData: ResolvedShareData, override val contacts: List<ContactSearchKey.RecipientSearchKey>) : ShareEvent()
|
||||
data class OpenTextInterstitial(override val shareData: ResolvedShareData, override val contacts: List<ContactSearchKey.RecipientSearchKey>) : ShareEvent()
|
||||
data class SendWithoutInterstitial(override val shareData: ResolvedShareData, override val contacts: List<ContactSearchKey.RecipientSearchKey>) : ShareEvent()
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package org.thoughtcrime.securesms.sharing.v2
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.toKotlinPair
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.TransportOption
|
||||
import org.thoughtcrime.securesms.TransportOptions
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendConstants
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.UriUtil
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.Optional
|
||||
|
||||
class ShareRepository(context: Context) {
|
||||
|
||||
private val appContext = context.applicationContext
|
||||
|
||||
fun resolve(unresolvedShareData: UnresolvedShareData): Single<ResolvedShareData> {
|
||||
return when (unresolvedShareData) {
|
||||
is UnresolvedShareData.ExternalMultiShare -> Single.fromCallable { resolve(unresolvedShareData) }
|
||||
is UnresolvedShareData.ExternalSingleShare -> Single.fromCallable { resolve(unresolvedShareData) }
|
||||
is UnresolvedShareData.ExternalPrimitiveShare -> Single.just(ResolvedShareData.Primitive(unresolvedShareData.text))
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@WorkerThread
|
||||
@Throws(IOException::class)
|
||||
private fun resolve(multiShareExternal: UnresolvedShareData.ExternalSingleShare): ResolvedShareData {
|
||||
if (!UriUtil.isValidExternalUri(appContext, multiShareExternal.uri)) {
|
||||
return ResolvedShareData.Failure
|
||||
}
|
||||
|
||||
val uri = multiShareExternal.uri
|
||||
val mimeType = getMimeType(appContext, uri, multiShareExternal.mimeType)
|
||||
|
||||
val stream: InputStream = try {
|
||||
appContext.contentResolver.openInputStream(uri)
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "Failed to read stream!", e)
|
||||
null
|
||||
} ?: return ResolvedShareData.Failure
|
||||
|
||||
val size = getSize(appContext, uri)
|
||||
val name = getFileName(appContext, uri)
|
||||
|
||||
val blobUri = BlobProvider.getInstance()
|
||||
.forData(stream, size)
|
||||
.withMimeType(mimeType)
|
||||
.withFileName(name)
|
||||
.createForSingleSessionOnDisk(appContext)
|
||||
|
||||
return ResolvedShareData.ExternalUri(
|
||||
uri = blobUri,
|
||||
mimeType = mimeType,
|
||||
isMmsOrSmsSupported = isMmsSupported(appContext, asUriAttachment(blobUri, mimeType, size))
|
||||
)
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@WorkerThread
|
||||
private fun resolve(externalMultiShare: UnresolvedShareData.ExternalMultiShare): ResolvedShareData {
|
||||
val mimeTypes: Map<Uri, String> = externalMultiShare.uris
|
||||
.associateWith { uri -> getMimeType(appContext, uri, null) }
|
||||
.filterValues {
|
||||
MediaUtil.isImageType(it) || MediaUtil.isVideoType(it)
|
||||
}
|
||||
|
||||
if (mimeTypes.isEmpty()) {
|
||||
return ResolvedShareData.Failure
|
||||
}
|
||||
|
||||
val media: List<Media> = mimeTypes.toList()
|
||||
.take(MediaSendConstants.MAX_PUSH)
|
||||
.map { (uri, mimeType) ->
|
||||
val stream: InputStream = try {
|
||||
appContext.contentResolver.openInputStream(uri)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to open: $uri")
|
||||
return@map null
|
||||
} ?: return ResolvedShareData.Failure
|
||||
|
||||
val size = getSize(appContext, uri)
|
||||
val dimens: Pair<Int, Int> = MediaUtil.getDimensions(appContext, mimeType, uri).toKotlinPair()
|
||||
val duration = 0L
|
||||
val blobUri = BlobProvider.getInstance()
|
||||
.forData(stream, size)
|
||||
.withMimeType(mimeType)
|
||||
.createForSingleSessionOnDisk(appContext)
|
||||
|
||||
Media(
|
||||
blobUri,
|
||||
mimeType,
|
||||
System.currentTimeMillis(),
|
||||
dimens.first,
|
||||
dimens.second,
|
||||
size,
|
||||
duration,
|
||||
false,
|
||||
false,
|
||||
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
|
||||
Optional.empty(),
|
||||
Optional.empty()
|
||||
)
|
||||
}.filterNotNull()
|
||||
|
||||
return if (media.isNotEmpty()) {
|
||||
val isMmsSupported = media.all { isMmsSupported(appContext, asUriAttachment(it.uri, it.mimeType, it.size)) }
|
||||
|
||||
ResolvedShareData.Media(media, isMmsSupported)
|
||||
} else {
|
||||
ResolvedShareData.Failure
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ShareRepository::class.java)
|
||||
|
||||
private fun getMimeType(context: Context, uri: Uri, mimeType: String?): String {
|
||||
var updatedMimeType = MediaUtil.getMimeType(context, uri)
|
||||
if (updatedMimeType == null) {
|
||||
updatedMimeType = MediaUtil.getCorrectedMimeType(mimeType)
|
||||
}
|
||||
return updatedMimeType ?: MediaUtil.UNKNOWN
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun getSize(context: Context, uri: Uri): Long {
|
||||
var size: Long = 0
|
||||
|
||||
context.contentResolver.query(uri, null, null, null, null).use { cursor ->
|
||||
if (cursor != null && cursor.moveToFirst() && cursor.getColumnIndex(OpenableColumns.SIZE) >= 0) {
|
||||
size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
|
||||
}
|
||||
}
|
||||
|
||||
if (size <= 0) {
|
||||
size = MediaUtil.getMediaSize(context, uri)
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
private fun getFileName(context: Context, uri: Uri): String? {
|
||||
if (uri.scheme.equals("file", ignoreCase = true)) {
|
||||
return uri.lastPathSegment
|
||||
}
|
||||
|
||||
context.contentResolver.query(uri, null, null, null, null).use { cursor ->
|
||||
if (cursor != null && cursor.moveToFirst() && cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) >= 0) {
|
||||
return cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun asUriAttachment(uri: Uri, mimeType: String, size: Long): UriAttachment {
|
||||
return UriAttachment(uri, mimeType, -1, size, null, false, false, false, false, null, null, null, null, null)
|
||||
}
|
||||
|
||||
private fun isMmsSupported(context: Context, attachment: Attachment): Boolean {
|
||||
val canReadPhoneState = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
if (!Util.isDefaultSmsProvider(context) || !canReadPhoneState || !Util.isMmsCapable(context)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val options = TransportOptions(context, true)
|
||||
options.setDefaultTransport(TransportOption.Type.SMS)
|
||||
val mmsConstraints = MediaConstraints.getMmsMediaConstraints(options.selectedTransport.simSubscriptionId.orElse(-1))
|
||||
return mmsConstraints.isSatisfied(context, attachment) || mmsConstraints.canResize(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.thoughtcrime.securesms.sharing.v2
|
||||
|
||||
data class ShareState(
|
||||
val loadState: ShareDataLoadState = ShareDataLoadState.Init
|
||||
) {
|
||||
sealed class ShareDataLoadState {
|
||||
object Init : ShareDataLoadState()
|
||||
data class Loaded(val resolvedShareData: ResolvedShareData) : ShareDataLoadState()
|
||||
object Failed : ShareDataLoadState()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package org.thoughtcrime.securesms.sharing.v2
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sharing.InterstitialContentType
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
class ShareViewModel(
|
||||
unresolvedShareData: UnresolvedShareData,
|
||||
directShareTarget: RecipientId?,
|
||||
shareRepository: ShareRepository
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ShareViewModel::class.java)
|
||||
}
|
||||
|
||||
private val store = RxStore(ShareState())
|
||||
private val disposables = CompositeDisposable()
|
||||
private val eventSubject = PublishSubject.create<ShareEvent>()
|
||||
|
||||
val state: Flowable<ShareState> = store.stateFlowable
|
||||
val events: Observable<ShareEvent> = eventSubject
|
||||
|
||||
init {
|
||||
disposables += shareRepository.resolve(unresolvedShareData).subscribeBy(
|
||||
onSuccess = { data ->
|
||||
when {
|
||||
data == ResolvedShareData.Failure -> {
|
||||
moveToFailedState()
|
||||
}
|
||||
directShareTarget != null -> {
|
||||
eventSubject.onNext(ShareEvent.OpenConversation(data, ContactSearchKey.RecipientSearchKey.KnownRecipient(directShareTarget)))
|
||||
}
|
||||
else -> {
|
||||
store.update { it.copy(loadState = ShareState.ShareDataLoadState.Loaded(data)) }
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = this::moveToFailedState
|
||||
)
|
||||
}
|
||||
|
||||
fun onContactSelectionConfirmed(contactSearchKeys: List<ContactSearchKey>) {
|
||||
val loadState = store.state.loadState
|
||||
if (loadState !is ShareState.ShareDataLoadState.Loaded) {
|
||||
return
|
||||
}
|
||||
|
||||
val recipientKeys = contactSearchKeys.filterIsInstance(ContactSearchKey.RecipientSearchKey::class.java)
|
||||
val hasStory = recipientKeys.any { it.isStory }
|
||||
val openConversation = !hasStory && recipientKeys.size == 1
|
||||
val resolvedShareData = loadState.resolvedShareData
|
||||
|
||||
if (openConversation) {
|
||||
eventSubject.onNext(ShareEvent.OpenConversation(resolvedShareData, recipientKeys.first()))
|
||||
return
|
||||
}
|
||||
|
||||
val event = when (resolvedShareData.toMultiShareArgs().interstitialContentType) {
|
||||
InterstitialContentType.MEDIA -> ShareEvent.OpenMediaInterstitial(resolvedShareData, recipientKeys)
|
||||
InterstitialContentType.TEXT -> ShareEvent.OpenTextInterstitial(resolvedShareData, recipientKeys)
|
||||
InterstitialContentType.NONE -> ShareEvent.SendWithoutInterstitial(resolvedShareData, recipientKeys)
|
||||
}
|
||||
|
||||
eventSubject.onNext(event)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
private fun moveToFailedState(throwable: Throwable? = null) {
|
||||
Log.w(TAG, "Could not load share data.", throwable)
|
||||
store.update { it.copy(loadState = ShareState.ShareDataLoadState.Failed) }
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val unresolvedShareData: UnresolvedShareData,
|
||||
private val directShareTarget: RecipientId?,
|
||||
private val shareRepository: ShareRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(ShareViewModel(unresolvedShareData, directShareTarget, shareRepository)) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.thoughtcrime.securesms.sharing.v2
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
sealed class UnresolvedShareData {
|
||||
data class ExternalMultiShare(val uris: List<Uri>) : UnresolvedShareData()
|
||||
data class ExternalSingleShare(val uri: Uri, val mimeType: String?) : UnresolvedShareData()
|
||||
data class ExternalPrimitiveShare(val text: CharSequence) : UnresolvedShareData()
|
||||
}
|
||||
Reference in New Issue
Block a user