mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 00:29:11 +01:00
Add the ability to forward content to multiple chats at once.
This commit is contained in:
committed by
Greyson Parrelli
parent
eacf03768f
commit
8d187c8ba1
@@ -0,0 +1,7 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
public enum InterstitialContentType {
|
||||
MEDIA,
|
||||
TEXT,
|
||||
NONE
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class MultiShareArgs implements Parcelable {
|
||||
|
||||
private static final String ARGS = "ShareInterstitialArgs";
|
||||
|
||||
private final Set<ShareContactAndThread> shareContactAndThreads;
|
||||
private final ArrayList<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 MultiShareArgs(@NonNull Builder builder) {
|
||||
shareContactAndThreads = builder.shareContactAndThreads;
|
||||
media = builder.media == null ? 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;
|
||||
}
|
||||
|
||||
protected MultiShareArgs(Parcel in) {
|
||||
shareContactAndThreads = new HashSet<>(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;
|
||||
|
||||
LinkPreview preview;
|
||||
try {
|
||||
preview = LinkPreview.deserialize(in.readString());
|
||||
} catch (IOException e) {
|
||||
preview = null;
|
||||
}
|
||||
|
||||
linkPreview = preview;
|
||||
}
|
||||
|
||||
public Set<ShareContactAndThread> getShareContactAndThreads() {
|
||||
return shareContactAndThreads;
|
||||
}
|
||||
|
||||
public ArrayList<Media> getMedia() {
|
||||
return media;
|
||||
}
|
||||
|
||||
public StickerLocator getStickerLocator() {
|
||||
return stickerLocator;
|
||||
}
|
||||
|
||||
public String getDataType() {
|
||||
return dataType;
|
||||
}
|
||||
|
||||
public String getDraftText() {
|
||||
return draftText;
|
||||
}
|
||||
|
||||
public Uri getDataUri() {
|
||||
return dataUri;
|
||||
}
|
||||
|
||||
public boolean isBorderless() {
|
||||
return borderless;
|
||||
}
|
||||
|
||||
public boolean isViewOnce() {
|
||||
return viewOnce;
|
||||
}
|
||||
|
||||
public @Nullable LinkPreview getLinkPreview() {
|
||||
return linkPreview;
|
||||
}
|
||||
|
||||
public @NonNull InterstitialContentType getInterstitialContentType() {
|
||||
if (!requiresInterstitial()) {
|
||||
return InterstitialContentType.NONE;
|
||||
} else if (!this.getMedia().isEmpty() ||
|
||||
(this.getDataUri() != null && this.getDataUri() != Uri.EMPTY && this.getDataType() != null))
|
||||
{
|
||||
return InterstitialContentType.MEDIA;
|
||||
} else if (!TextUtils.isEmpty(this.getDraftText())) {
|
||||
return InterstitialContentType.TEXT;
|
||||
} else {
|
||||
return InterstitialContentType.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static final Creator<MultiShareArgs> CREATOR = new Creator<MultiShareArgs>() {
|
||||
@Override
|
||||
public MultiShareArgs createFromParcel(Parcel in) {
|
||||
return new MultiShareArgs(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiShareArgs[] newArray(int size) {
|
||||
return new MultiShareArgs[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeTypedList(Stream.of(shareContactAndThreads).toList());
|
||||
dest.writeTypedList(media);
|
||||
dest.writeString(draftText);
|
||||
dest.writeParcelable(stickerLocator, flags);
|
||||
dest.writeByte((byte) (borderless ? 1 : 0));
|
||||
dest.writeParcelable(dataUri, flags);
|
||||
dest.writeString(dataType);
|
||||
dest.writeByte((byte) (viewOnce ? 1 : 0));
|
||||
|
||||
if (linkPreview != null) {
|
||||
try {
|
||||
dest.writeString(linkPreview.serialize());
|
||||
} catch (IOException e) {
|
||||
dest.writeString("");
|
||||
}
|
||||
} else {
|
||||
dest.writeString("");
|
||||
}
|
||||
}
|
||||
|
||||
public Builder buildUpon() {
|
||||
return new Builder(shareContactAndThreads).asBorderless(borderless)
|
||||
.asViewOnce(viewOnce)
|
||||
.withDataType(dataType)
|
||||
.withDataUri(dataUri)
|
||||
.withDraftText(draftText)
|
||||
.withLinkPreview(linkPreview)
|
||||
.withMedia(media)
|
||||
.withStickerLocator(stickerLocator);
|
||||
}
|
||||
|
||||
private boolean requiresInterstitial() {
|
||||
return !media.isEmpty() || !TextUtils.isEmpty(draftText) || MediaUtil.isImageOrVideoType(dataType);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private final Set<ShareContactAndThread> shareContactAndThreads;
|
||||
|
||||
private ArrayList<Media> media;
|
||||
private String draftText;
|
||||
private StickerLocator stickerLocator;
|
||||
private boolean borderless;
|
||||
private Uri dataUri;
|
||||
private String dataType;
|
||||
private LinkPreview linkPreview;
|
||||
private boolean viewOnce;
|
||||
|
||||
public Builder(@NonNull Set<ShareContactAndThread> shareContactAndThreads) {
|
||||
this.shareContactAndThreads = shareContactAndThreads;
|
||||
}
|
||||
|
||||
public @NonNull Builder withMedia(@Nullable ArrayList<Media> media) {
|
||||
this.media = media;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withDraftText(@Nullable String draftText) {
|
||||
this.draftText = draftText;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withStickerLocator(@Nullable StickerLocator stickerLocator) {
|
||||
this.stickerLocator = stickerLocator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder asBorderless(boolean borderless) {
|
||||
this.borderless = borderless;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withDataUri(@Nullable Uri dataUri) {
|
||||
this.dataUri = dataUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withDataType(@Nullable String dataType) {
|
||||
this.dataType = dataType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withLinkPreview(@Nullable LinkPreview linkPreview) {
|
||||
this.linkPreview = linkPreview;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder asViewOnce(boolean viewOnce) {
|
||||
this.viewOnce = viewOnce;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull MultiShareArgs build() {
|
||||
return new MultiShareArgs(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public final class MultiShareDialogs {
|
||||
private MultiShareDialogs() {
|
||||
}
|
||||
|
||||
public static void displayResultDialog(@NonNull Context context,
|
||||
@NonNull MultiShareSender.MultiShareSendResultCollection resultCollection,
|
||||
@NonNull Runnable onDismiss)
|
||||
{
|
||||
if (resultCollection.containsFailures()) {
|
||||
displayFailuresDialog(context, onDismiss);
|
||||
} else {
|
||||
onDismiss.run();
|
||||
}
|
||||
}
|
||||
|
||||
public static void displayMaxSelectedDialog(@NonNull Context context, int hardLimit) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(context.getString(R.string.MultiShareDialogs__you_can_only_share_with_up_to, hardLimit))
|
||||
.setPositiveButton(android.R.string.ok, ((dialog, which) -> dialog.dismiss()))
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
}
|
||||
|
||||
private static void displayFailuresDialog(@NonNull Context context,
|
||||
@NonNull Runnable onDismiss)
|
||||
{
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(R.string.MultiShareDialogs__failed_to_send_to_some_users)
|
||||
.setPositiveButton(android.R.string.ok, ((dialog, which) -> dialog.dismiss()))
|
||||
.setOnDismissListener(dialog -> onDismiss.run())
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.TransportOptions;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.mms.SlideFactory;
|
||||
import org.thoughtcrime.securesms.mms.StickerSlide;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.util.MessageUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* MultiShareSender encapsulates send logic (stolen from {@link org.thoughtcrime.securesms.conversation.ConversationActivity}
|
||||
* and provides a means to:
|
||||
*
|
||||
* 1. Send messages based off a {@link MultiShareArgs} object and
|
||||
* 1. Parse through the result of the send via a {@link MultiShareSendResultCollection}
|
||||
*/
|
||||
public final class MultiShareSender {
|
||||
|
||||
private static final String TAG = Log.tag(MultiShareSender.class);
|
||||
|
||||
private MultiShareSender() {
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public static void send(@NonNull MultiShareArgs multiShareArgs, @NonNull Consumer<MultiShareSendResultCollection> results) {
|
||||
SimpleTask.run(() -> sendInternal(multiShareArgs), results::accept);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static MultiShareSendResultCollection sendInternal(@NonNull MultiShareArgs multiShareArgs) {
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
boolean isMmsEnabled = Util.isMmsCapable(context);
|
||||
String message = multiShareArgs.getDraftText();
|
||||
SlideDeck slideDeck = buildSlideDeck(context, multiShareArgs);
|
||||
|
||||
List<MultiShareSendResult> results = new ArrayList<>(multiShareArgs.getShareContactAndThreads().size());
|
||||
|
||||
for (ShareContactAndThread shareContactAndThread : multiShareArgs.getShareContactAndThreads()) {
|
||||
Recipient recipient = Recipient.resolved(shareContactAndThread.getRecipientId());
|
||||
|
||||
TransportOption transport = resolveTransportOption(context, recipient);
|
||||
boolean forceSms = recipient.isForceSmsSelection() && transport.isSms();
|
||||
int subscriptionId = transport.getSimSubscriptionId().or(-1);
|
||||
long expiresIn = recipient.getExpireMessages() * 1000L;
|
||||
boolean needsSplit = !transport.isSms() && message.length() > transport.calculateCharacters(message).maxPrimaryMessageSize;
|
||||
boolean isMediaMessage = !multiShareArgs.getMedia().isEmpty() ||
|
||||
(multiShareArgs.getDataUri() != null && multiShareArgs.getDataUri() != Uri.EMPTY) ||
|
||||
multiShareArgs.getStickerLocator() != null ||
|
||||
multiShareArgs.getLinkPreview() != null ||
|
||||
recipient.isGroup() ||
|
||||
recipient.getEmail().isPresent() ||
|
||||
needsSplit;
|
||||
|
||||
if ((recipient.isMmsGroup() || recipient.getEmail().isPresent()) && !isMmsEnabled) {
|
||||
results.add(new MultiShareSendResult(shareContactAndThread, MultiShareSendResult.Type.MMS_NOT_ENABLED));
|
||||
} else if (isMediaMessage) {
|
||||
sendMediaMessage(context, multiShareArgs, recipient, slideDeck, transport, shareContactAndThread.getThreadId(), forceSms, expiresIn, multiShareArgs.isViewOnce(), subscriptionId);
|
||||
results.add(new MultiShareSendResult(shareContactAndThread, MultiShareSendResult.Type.SUCCESS));
|
||||
} else {
|
||||
sendTextMessage(context, multiShareArgs, recipient, shareContactAndThread.getThreadId() ,forceSms, expiresIn, subscriptionId);
|
||||
results.add(new MultiShareSendResult(shareContactAndThread, MultiShareSendResult.Type.SUCCESS));
|
||||
}
|
||||
}
|
||||
|
||||
return new MultiShareSendResultCollection(results);
|
||||
}
|
||||
|
||||
public static @NonNull TransportOption getWorseTransportOption(@NonNull Context context, @NonNull Set<ShareContactAndThread> shareContactAndThreads) {
|
||||
for (ShareContactAndThread shareContactAndThread : shareContactAndThreads) {
|
||||
TransportOption option = resolveTransportOption(context, shareContactAndThread.isForceSms());
|
||||
if (option.isSms()) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
|
||||
return TransportOptions.getPushTransportOption(context);
|
||||
}
|
||||
|
||||
private static @NonNull TransportOption resolveTransportOption(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
return resolveTransportOption(context, recipient.isForceSmsSelection() || !recipient.isRegistered());
|
||||
}
|
||||
|
||||
public static @NonNull TransportOption resolveTransportOption(@NonNull Context context, boolean forceSms) {
|
||||
if (forceSms) {
|
||||
TransportOptions options = new TransportOptions(context, false);
|
||||
options.setDefaultTransport(TransportOption.Type.SMS);
|
||||
return options.getSelectedTransport();
|
||||
} else {
|
||||
return TransportOptions.getPushTransportOption(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendMediaMessage(@NonNull Context context,
|
||||
@NonNull MultiShareArgs multiShareArgs,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull SlideDeck slideDeck,
|
||||
@NonNull TransportOption transportOption,
|
||||
long threadId,
|
||||
boolean forceSms,
|
||||
long expiresIn,
|
||||
boolean isViewOnce,
|
||||
int subscriptionId)
|
||||
{
|
||||
String body = multiShareArgs.getDraftText();
|
||||
if (transportOption.isType(TransportOption.Type.TEXTSECURE) && !forceSms) {
|
||||
MessageUtil.SplitResult splitMessage = MessageUtil.getSplitMessage(context, body, transportOption.calculateCharacters(body).maxPrimaryMessageSize);
|
||||
body = splitMessage.getBody();
|
||||
|
||||
if (splitMessage.getTextSlide().isPresent()) {
|
||||
slideDeck.addSlide(splitMessage.getTextSlide().get());
|
||||
}
|
||||
}
|
||||
|
||||
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient,
|
||||
slideDeck,
|
||||
body,
|
||||
System.currentTimeMillis(),
|
||||
subscriptionId,
|
||||
expiresIn,
|
||||
isViewOnce,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
multiShareArgs.getLinkPreview() != null ? Collections.singletonList(multiShareArgs.getLinkPreview())
|
||||
: Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
MessageSender.send(context, outgoingMediaMessage, threadId, forceSms, null);
|
||||
}
|
||||
|
||||
private static void sendTextMessage(@NonNull Context context,
|
||||
@NonNull MultiShareArgs multiShareArgs,
|
||||
@NonNull Recipient recipient,
|
||||
long threadId,
|
||||
boolean forceSms,
|
||||
long expiresIn,
|
||||
int subscriptionId)
|
||||
{
|
||||
OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, multiShareArgs.getDraftText(), expiresIn, subscriptionId);
|
||||
|
||||
MessageSender.send(context, outgoingTextMessage, threadId, forceSms, null);
|
||||
}
|
||||
|
||||
private static @NonNull SlideDeck buildSlideDeck(@NonNull Context context, @NonNull MultiShareArgs multiShareArgs) {
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
if (multiShareArgs.getStickerLocator() != null) {
|
||||
slideDeck.addSlide(buildStickerSlide(context, multiShareArgs.getStickerLocator()));
|
||||
} else if (!multiShareArgs.getMedia().isEmpty()) {
|
||||
for (Media media : multiShareArgs.getMedia()) {
|
||||
slideDeck.addSlide(SlideFactory.getSlide(context, media.getMimeType(), media.getUri(), media.getWidth(), media.getHeight()));
|
||||
}
|
||||
} else if (multiShareArgs.getDataUri() != null) {
|
||||
slideDeck.addSlide(SlideFactory.getSlide(context, multiShareArgs.getDataType(), multiShareArgs.getDataUri(), 0, 0));
|
||||
}
|
||||
|
||||
return slideDeck;
|
||||
}
|
||||
|
||||
private static @NonNull StickerSlide buildStickerSlide(@NonNull Context context, @NonNull StickerLocator stickerLocator) {
|
||||
StickerDatabase stickerDatabase = DatabaseFactory.getStickerDatabase(context);
|
||||
StickerRecord stickerRecord = stickerDatabase.getSticker(stickerLocator.getPackId(), stickerLocator.getStickerId(), false);
|
||||
|
||||
return new StickerSlide(context, stickerRecord.getUri(), stickerRecord.getSize(), stickerLocator, stickerRecord.getContentType());
|
||||
}
|
||||
|
||||
public static final class MultiShareSendResultCollection {
|
||||
private final List<MultiShareSendResult> results;
|
||||
|
||||
private MultiShareSendResultCollection(List<MultiShareSendResult> results) {
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
public boolean containsFailures() {
|
||||
return Stream.of(results).anyMatch(result -> result.type != MultiShareSendResult.Type.SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MultiShareSendResult {
|
||||
private final ShareContactAndThread contactAndThread;
|
||||
private final Type type;
|
||||
|
||||
private MultiShareSendResult(ShareContactAndThread contactAndThread, Type type) {
|
||||
this.contactAndThread = contactAndThread;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public ShareContactAndThread getContactAndThread() {
|
||||
return contactAndThread;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
private enum Type {
|
||||
MMS_NOT_ENABLED,
|
||||
SUCCESS
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,19 @@ 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.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.transition.TransitionManager;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||
@@ -41,19 +49,25 @@ import org.thoughtcrime.securesms.components.SearchToolbar;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sharing.interstitial.ShareInterstitialActivity;
|
||||
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.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
@@ -63,10 +77,14 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
* is known (such as choosing someone in a direct share).
|
||||
*/
|
||||
public class ShareActivity extends PassphraseRequiredActivity
|
||||
implements ContactSelectionListFragment.OnContactSelectedListener, SwipeRefreshLayout.OnRefreshListener
|
||||
implements ContactSelectionListFragment.OnContactSelectedListener,
|
||||
ContactSelectionListFragment.OnSelectionLimitReachedListener
|
||||
{
|
||||
private static final String TAG = ShareActivity.class.getSimpleName();
|
||||
|
||||
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";
|
||||
@@ -74,9 +92,12 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
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 ShareSelectionAdapter adapter;
|
||||
|
||||
private ShareViewModel viewModel;
|
||||
|
||||
@@ -88,31 +109,14 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||
int mode = DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_SELF;
|
||||
|
||||
if (TextSecurePreferences.isSmsEnabled(this)) {
|
||||
mode |= DisplayMode.FLAG_SMS;
|
||||
}
|
||||
|
||||
if (FeatureFlags.groupsV1ForcedMigration()) {
|
||||
mode |= DisplayMode.FLAG_HIDE_GROUPS_V1;
|
||||
}
|
||||
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, mode);
|
||||
}
|
||||
|
||||
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
|
||||
getIntent().putExtra(ContactSelectionListFragment.RECENTS, true);
|
||||
|
||||
setContentView(R.layout.share_activity);
|
||||
|
||||
initializeViewModel();
|
||||
initializeMedia();
|
||||
initializeIntent();
|
||||
initializeToolbar();
|
||||
initializeResources();
|
||||
initializeSearch();
|
||||
initializeViewModel();
|
||||
initializeMedia();
|
||||
|
||||
handleDestination();
|
||||
}
|
||||
|
||||
@@ -128,7 +132,7 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
|
||||
if (!isFinishing()) {
|
||||
if (!isFinishing() && !viewModel.isMultiShare()) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -149,31 +153,75 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
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.onSuccessulShare();
|
||||
finish();
|
||||
break;
|
||||
default:
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
} else {
|
||||
shareConfirm.setClickable(true);
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
|
||||
SimpleTask.run(this.getLifecycle(), () -> {
|
||||
Recipient recipient;
|
||||
if (recipientId.isPresent()) {
|
||||
recipient = Recipient.resolved(recipientId.get());
|
||||
} else {
|
||||
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
|
||||
recipient = Recipient.external(this, number);
|
||||
}
|
||||
|
||||
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
|
||||
return new Pair<>(existingThread, recipient);
|
||||
}, result -> onDestinationChosen(result.first(), result.second().getId()));
|
||||
|
||||
return true;
|
||||
return viewModel.onContactSelected(new ShareContact(recipientId, number));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {
|
||||
viewModel.onContactDeselected(new ShareContact(recipientId, number));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
private void animateInSelection() {
|
||||
TransitionManager.endTransitions(shareContainer);
|
||||
TransitionManager.beginDelayedTransition(shareContainer);
|
||||
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(shareContainer);
|
||||
constraintSet.setVisibility(R.id.selection_group, ConstraintSet.VISIBLE);
|
||||
constraintSet.applyTo(shareContainer);
|
||||
}
|
||||
|
||||
private void animateOutSelection() {
|
||||
TransitionManager.endTransitions(shareContainer);
|
||||
TransitionManager.beginDelayedTransition(shareContainer);
|
||||
|
||||
ConstraintSet constraintSet = new ConstraintSet();
|
||||
constraintSet.clone(shareContainer);
|
||||
constraintSet.setVisibility(R.id.selection_group, ConstraintSet.GONE);
|
||||
constraintSet.applyTo(shareContainer);
|
||||
}
|
||||
|
||||
private void initializeIntent() {
|
||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||
int mode = DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_SELF;
|
||||
|
||||
if (TextSecurePreferences.isSmsEnabled(this) && viewModel.isExternalShare()) {
|
||||
mode |= DisplayMode.FLAG_SMS;
|
||||
}
|
||||
|
||||
if (FeatureFlags.groupsV1ForcedMigration()) {
|
||||
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);
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
@@ -190,14 +238,37 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
private void initializeResources() {
|
||||
searchToolbar = findViewById(R.id.search_toolbar);
|
||||
searchAction = findViewById(R.id.search_action);
|
||||
contactsFragment = (ContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
|
||||
shareConfirm = findViewById(R.id.share_confirm);
|
||||
shareContainer = findViewById(R.id.container);
|
||||
contactsFragment = new ContactSelectionListFragment();
|
||||
adapter = new ShareSelectionAdapter();
|
||||
|
||||
if (contactsFragment == null) {
|
||||
throw new IllegalStateException("Could not find contacts fragment!");
|
||||
}
|
||||
RecyclerView contactsRecycler = findViewById(R.id.selected_list);
|
||||
contactsRecycler.setAdapter(adapter);
|
||||
|
||||
contactsFragment.setOnContactSelectedListener(this);
|
||||
contactsFragment.setOnRefreshListener(this);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.contact_selection_list_fragment, contactsFragment)
|
||||
.commit();
|
||||
|
||||
shareConfirm.setOnClickListener(unused -> {
|
||||
Set<ShareContact> shareContacts = viewModel.getShareContacts();
|
||||
|
||||
if (shareContacts.isEmpty()) throw new AssertionError();
|
||||
else if (shareContacts.size() == 1) onConfirmSingleDestination(shareContacts.iterator().next());
|
||||
else onConfirmMultipleDestinations(shareContacts);
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
@@ -260,16 +331,71 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
if (contactsFragment.getView() != null) {
|
||||
contactsFragment.getView().setVisibility(View.GONE);
|
||||
}
|
||||
onDestinationChosen(threadId, recipientId);
|
||||
onSingleDestinationChosen(threadId, recipientId);
|
||||
} else if (viewModel.isExternalShare()) {
|
||||
validateAvailableRecipients();
|
||||
}
|
||||
}
|
||||
|
||||
private void onDestinationChosen(long threadId, @NonNull RecipientId recipientId) {
|
||||
if (!viewModel.isExternalShare()) {
|
||||
openConversation(threadId, recipientId, null);
|
||||
return;
|
||||
private void onConfirmSingleDestination(@NonNull ShareContact shareContact) {
|
||||
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()
|
||||
.transform(Recipient::resolved)
|
||||
.or(() -> Recipient.external(this, contact.getNumber())))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<RecipientId, Long> existingThreads = DatabaseFactory.getThreadDatabase(this)
|
||||
.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()))
|
||||
.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 = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
|
||||
return new ShareContactAndThread(recipient.getId(), existingThread, recipient.isForceSmsSelection() || !recipient.isRegistered());
|
||||
}
|
||||
|
||||
private void validateAvailableRecipients() {
|
||||
resolveShareData(data -> {
|
||||
int mode = getIntent().getIntExtra(ContactSelectionListFragment.DISPLAY_MODE, -1);
|
||||
|
||||
if (mode == -1) return;
|
||||
|
||||
mode = data.isMmsOrSmsSupported() ? mode | DisplayMode.FLAG_SMS : mode & ~DisplayMode.FLAG_SMS;
|
||||
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) {
|
||||
@@ -291,10 +417,28 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
return;
|
||||
}
|
||||
|
||||
openConversation(threadId, recipientId, data.get());
|
||||
onResolved.accept(data.get());
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
ShareIntents.Args args = ShareIntents.Args.from(getIntent());
|
||||
ConversationIntents.Builder builder = ConversationIntents.createBuilder(this, recipientId, threadId)
|
||||
@@ -322,4 +466,77 @@ public class ShareActivity extends PassphraseRequiredActivity
|
||||
|
||||
startActivity(builder.build());
|
||||
}
|
||||
|
||||
private void openInterstitial(@NonNull Set<ShareContactAndThread> shareContactAndThreads, @Nullable ShareData shareData) {
|
||||
ShareIntents.Args args = ShareIntents.Args.from(getIntent());
|
||||
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,
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent()));
|
||||
}
|
||||
|
||||
startActivityForResult(MediaSendActivity.buildShareIntent(this,
|
||||
media,
|
||||
Stream.of(multiShareArgs.getShareContactAndThreads()).map(ShareContactAndThread::getRecipientId).toList(),
|
||||
multiShareArgs.getDraftText(),
|
||||
MultiShareSender.getWorseTransportOption(this, multiShareArgs.getShareContactAndThreads())),
|
||||
RESULT_MEDIA_CONFIRMATION);
|
||||
break;
|
||||
default:
|
||||
//noinspection CodeBlock2Expr
|
||||
MultiShareSender.send(multiShareArgs, results -> {
|
||||
MultiShareDialogs.displayResultDialog(this, results, () -> {
|
||||
viewModel.onSuccessulShare();
|
||||
finish();
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuggestedLimitReached(int limit) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHardLimitReached(int limit) {
|
||||
MultiShareDialogs.displayMaxSelectedDialog(this, limit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
final class ShareContact {
|
||||
private final Optional<RecipientId> recipientId;
|
||||
private final String number;
|
||||
|
||||
ShareContact(@NonNull Optional<RecipientId> recipientId, @Nullable String number) {
|
||||
this.recipientId = recipientId;
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
public Optional<RecipientId> getRecipientId() {
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
return number;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ShareContact that = (ShareContact) o;
|
||||
return recipientId.equals(that.recipientId) &&
|
||||
Objects.equals(number, that.number);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(recipientId, number);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
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;
|
||||
|
||||
ShareContactAndThread(@NonNull RecipientId recipientId, long threadId, boolean forceSms) {
|
||||
this.recipientId = recipientId;
|
||||
this.threadId = threadId;
|
||||
this.forceSms = forceSms;
|
||||
}
|
||||
|
||||
protected ShareContactAndThread(@NonNull Parcel in) {
|
||||
recipientId = in.readParcelable(RecipientId.class.getClassLoader());
|
||||
threadId = in.readLong();
|
||||
forceSms = in.readByte() == 1;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getRecipientId() {
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
public long getThreadId() {
|
||||
return threadId;
|
||||
}
|
||||
|
||||
public boolean isForceSms() {
|
||||
return forceSms;
|
||||
}
|
||||
|
||||
@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 &&
|
||||
recipientId.equals(that.recipientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(recipientId, threadId, forceSms);
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -16,24 +16,26 @@ class ShareData {
|
||||
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) {
|
||||
return new ShareData(Optional.of(uri), Optional.of(mimeType), Optional.absent(), external);
|
||||
static ShareData forIntentData(@NonNull Uri uri, @NonNull String mimeType, boolean external, boolean isMmsOrSmsSupported) {
|
||||
return new ShareData(Optional.of(uri), Optional.of(mimeType), Optional.absent(), external, isMmsOrSmsSupported);
|
||||
}
|
||||
|
||||
static ShareData forPrimitiveTypes() {
|
||||
return new ShareData(Optional.absent(), Optional.absent(), Optional.absent(), true);
|
||||
return new ShareData(Optional.absent(), Optional.absent(), Optional.absent(), true, true);
|
||||
}
|
||||
|
||||
static ShareData forMedia(@NonNull List<Media> media) {
|
||||
return new ShareData(Optional.absent(), Optional.absent(), Optional.of(new ArrayList<>(media)), true);
|
||||
static ShareData forMedia(@NonNull List<Media> media, boolean isMmsOrSmsSupported) {
|
||||
return new ShareData(Optional.absent(), Optional.absent(), Optional.of(new ArrayList<>(media)), true, isMmsOrSmsSupported);
|
||||
}
|
||||
|
||||
private ShareData(Optional<Uri> uri, Optional<String> mimeType, Optional<ArrayList<Media>> media, boolean external) {
|
||||
this.uri = uri;
|
||||
this.mimeType = mimeType;
|
||||
this.media = media;
|
||||
this.external = external;
|
||||
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() {
|
||||
@@ -63,4 +65,8 @@ class ShareData {
|
||||
public boolean isExternal() {
|
||||
return external;
|
||||
}
|
||||
|
||||
public boolean isMmsOrSmsSupported() {
|
||||
return isMmsOrSmsSupported;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,17 @@ 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.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.mms.PushMediaConstraints;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -72,7 +77,7 @@ class ShareRepository {
|
||||
mimeType = getMimeType(context, uri, mimeType);
|
||||
|
||||
if (PartAuthority.isLocalUri(uri)) {
|
||||
return ShareData.forIntentData(uri, mimeType, false);
|
||||
return ShareData.forIntentData(uri, mimeType, false, false);
|
||||
} else {
|
||||
InputStream stream = context.getContentResolver().openInputStream(uri);
|
||||
|
||||
@@ -99,10 +104,35 @@ class ShareRepository {
|
||||
.createForMultipleSessionsOnDisk(context);
|
||||
}
|
||||
|
||||
return ShareData.forIntentData(blobUri, mimeType, true);
|
||||
return ShareData.forIntentData(blobUri, mimeType, true, isMmsSupported(context, mimeType, size));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMmsSupported(@NonNull Context context, @NonNull String mimeType, long size) {
|
||||
if (!Util.isMmsCapable(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
TransportOptions options = new TransportOptions(context, true);
|
||||
options.setDefaultTransport(TransportOption.Type.SMS);
|
||||
MediaConstraints mmsConstraints = MediaConstraints.getMmsMediaConstraints(options.getSelectedTransport().getSimSubscriptionId().or(-1));
|
||||
|
||||
final boolean canMmsSupportFileSize;
|
||||
if (MediaUtil.isGif(mimeType)) {
|
||||
canMmsSupportFileSize = size <= mmsConstraints.getGifMaxSize(context);
|
||||
} else if (MediaUtil.isVideo(mimeType)) {
|
||||
canMmsSupportFileSize = size <= mmsConstraints.getVideoMaxSize(context);
|
||||
} else if (MediaUtil.isImageType(mimeType)) {
|
||||
canMmsSupportFileSize = size <= mmsConstraints.getImageMaxSize(context);
|
||||
} else if (MediaUtil.isAudioType(mimeType)) {
|
||||
canMmsSupportFileSize = size <= mmsConstraints.getAudioMaxSize(context);
|
||||
} else {
|
||||
canMmsSupportFileSize = size <= mmsConstraints.getDocumentMaxSize(context);
|
||||
}
|
||||
|
||||
return canMmsSupportFileSize;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @Nullable ShareData getResolvedInternal(@NonNull List<Uri> uris) throws IOException {
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
@@ -160,7 +190,9 @@ class ShareRepository {
|
||||
}
|
||||
|
||||
if (media.size() > 0) {
|
||||
return ShareData.forMedia(media);
|
||||
boolean isMmsSupported = Stream.of(media)
|
||||
.allMatch(m -> isMmsSupported(context, m.getMimeType(), m.getSize()));
|
||||
return ShareData.forMedia(media, isMmsSupported);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
|
||||
class ShareSelectionAdapter extends MappingAdapter {
|
||||
ShareSelectionAdapter() {
|
||||
registerFactory(ShareSelectionMappingModel.class,
|
||||
ShareSelectionViewHolder.createFactory(R.layout.share_contact_selection_item));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
|
||||
class ShareSelectionMappingModel implements MappingModel<ShareSelectionMappingModel> {
|
||||
|
||||
private final ShareContact shareContact;
|
||||
private final boolean isLast;
|
||||
|
||||
ShareSelectionMappingModel(@NonNull ShareContact shareContact, boolean isLast) {
|
||||
this.shareContact = shareContact;
|
||||
this.isLast = isLast;
|
||||
}
|
||||
|
||||
@NonNull String getName(@NonNull Context context) {
|
||||
String name = shareContact.getRecipientId()
|
||||
.transform(Recipient::resolved)
|
||||
.transform(recipient -> recipient.isSelf() ? context.getString(R.string.note_to_self)
|
||||
: recipient.getShortDisplayNameIncludingUsername(context))
|
||||
.or(shareContact.getNumber());
|
||||
|
||||
return isLast ? name : context.getString(R.string.ShareActivity__s_comma, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull ShareSelectionMappingModel newItem) {
|
||||
return newItem.shareContact.equals(shareContact);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull ShareSelectionMappingModel newItem) {
|
||||
return areItemsTheSame(newItem) && newItem.isLast == isLast;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.thoughtcrime.securesms.sharing;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
|
||||
|
||||
public class ShareSelectionViewHolder extends MappingViewHolder<ShareSelectionMappingModel> {
|
||||
|
||||
protected final @NonNull TextView name;
|
||||
|
||||
public ShareSelectionViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
name = findViewById(R.id.recipient_view_name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull ShareSelectionMappingModel model) {
|
||||
name.setText(model.getName(context));
|
||||
}
|
||||
|
||||
public static @NonNull MappingAdapter.Factory<ShareSelectionMappingModel> createFactory(@LayoutRes int layout) {
|
||||
return new MappingAdapter.LayoutFactory<>(ShareSelectionViewHolder::new, layout);
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,23 @@ 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.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class ShareViewModel extends ViewModel {
|
||||
|
||||
@@ -24,14 +32,16 @@ public class ShareViewModel extends ViewModel {
|
||||
private final Context context;
|
||||
private final ShareRepository shareRepository;
|
||||
private final MutableLiveData<Optional<ShareData>> shareData;
|
||||
private final MutableLiveData<Set<ShareContact>> selectedContacts;
|
||||
|
||||
private boolean mediaUsed;
|
||||
private boolean externalShare;
|
||||
|
||||
private ShareViewModel() {
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
this.shareRepository = new ShareRepository();
|
||||
this.shareData = new MutableLiveData<>();
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
this.shareRepository = new ShareRepository();
|
||||
this.shareData = new MutableLiveData<>();
|
||||
this.selectedContacts = new DefaultValueLiveData<>(Collections.emptySet());
|
||||
}
|
||||
|
||||
void onSingleMediaShared(@NonNull Uri uri, @Nullable String mimeType) {
|
||||
@@ -44,11 +54,47 @@ public class ShareViewModel extends ViewModel {
|
||||
shareRepository.getResolved(uris, shareData::postValue);
|
||||
}
|
||||
|
||||
boolean isMultiShare() {
|
||||
return selectedContacts.getValue().size() > 1;
|
||||
}
|
||||
|
||||
boolean onContactSelected(@NonNull ShareContact selectedContact) {
|
||||
Set<ShareContact> contacts = new LinkedHashSet<>(selectedContacts.getValue());
|
||||
if (contacts.add(selectedContact)) {
|
||||
selectedContacts.setValue(contacts);
|
||||
return true;
|
||||
} else {
|
||||
return 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<List<MappingModel<?>>> getSelectedContactModels() {
|
||||
return Transformations.map(selectedContacts, set -> Stream.of(set)
|
||||
.<MappingModel<?>>mapIndexed((i, c) -> new ShareSelectionMappingModel(c, i == set.size() - 1))
|
||||
.toList());
|
||||
}
|
||||
|
||||
void onNonExternalShare() {
|
||||
externalShare = false;
|
||||
}
|
||||
|
||||
void onSuccessulShare() {
|
||||
public void onSuccessulShare() {
|
||||
mediaUsed = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package org.thoughtcrime.securesms.sharing.interstitial;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.LinkPreviewView;
|
||||
import org.thoughtcrime.securesms.components.SelectionAwareEmojiEditText;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareArgs;
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareDialogs;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
|
||||
/**
|
||||
* Handles display and editing of a text message (with possible link preview) before it is forwarded
|
||||
* to multiple users.
|
||||
*/
|
||||
public class ShareInterstitialActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private static final String ARGS = "args";
|
||||
|
||||
private ShareInterstitialViewModel viewModel;
|
||||
private LinkPreviewViewModel linkPreviewViewModel;
|
||||
private CircularProgressButton confirm;
|
||||
private RecyclerView contactsRecycler;
|
||||
private Toolbar toolbar;
|
||||
private LinkPreviewView preview;
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final ShareInterstitialSelectionAdapter adapter = new ShareInterstitialSelectionAdapter();
|
||||
|
||||
public static Intent createIntent(@NonNull Context context, @NonNull MultiShareArgs multiShareArgs) {
|
||||
Intent intent = new Intent(context, ShareInterstitialActivity.class);
|
||||
|
||||
intent.putExtra(ARGS, multiShareArgs);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
dynamicTheme.onCreate(this);
|
||||
setContentView(R.layout.share_interstitial_activity);
|
||||
|
||||
MultiShareArgs args = getIntent().getParcelableExtra(ARGS);
|
||||
|
||||
initializeViewModels(args);
|
||||
initializeViews(args);
|
||||
initializeObservers();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
private void initializeViewModels(@NonNull MultiShareArgs args) {
|
||||
ShareInterstitialRepository repository = new ShareInterstitialRepository();
|
||||
ShareInterstitialViewModel.Factory factory = new ShareInterstitialViewModel.Factory(args, repository);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(ShareInterstitialViewModel.class);
|
||||
|
||||
LinkPreviewRepository linkPreviewRepository = new LinkPreviewRepository();
|
||||
LinkPreviewViewModel.Factory linkPreviewViewModelFactory = new LinkPreviewViewModel.Factory(linkPreviewRepository);
|
||||
|
||||
linkPreviewViewModel = ViewModelProviders.of(this, linkPreviewViewModelFactory).get(LinkPreviewViewModel.class);
|
||||
}
|
||||
|
||||
private void initializeViews(@NonNull MultiShareArgs args) {
|
||||
confirm = findViewById(R.id.share_confirm);
|
||||
toolbar = findViewById(R.id.toolbar);
|
||||
preview = findViewById(R.id.link_preview);
|
||||
|
||||
confirm.setOnClickListener(unused -> onConfirm());
|
||||
|
||||
SelectionAwareEmojiEditText text = findViewById(R.id.text);
|
||||
|
||||
toolbar.setNavigationOnClickListener(unused -> finish());
|
||||
|
||||
text.addTextChangedListener(new AfterTextChanged(editable -> {
|
||||
linkPreviewViewModel.onTextChanged(this, editable.toString(), text.getSelectionStart(), text.getSelectionEnd());
|
||||
viewModel.onDraftTextChanged(editable.toString());
|
||||
}));
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
text.setOnSelectionChangedListener(((selStart, selEnd) -> {
|
||||
linkPreviewViewModel.onTextChanged(this, text.getText().toString(), text.getSelectionStart(), text.getSelectionEnd());
|
||||
}));
|
||||
|
||||
preview.setCloseClickedListener(linkPreviewViewModel::onUserCancel);
|
||||
|
||||
int defaultRadius = getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius);
|
||||
preview.setCorners(defaultRadius, defaultRadius);
|
||||
|
||||
text.setText(args.getDraftText());
|
||||
ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(text);
|
||||
|
||||
contactsRecycler = findViewById(R.id.selected_list);
|
||||
contactsRecycler.setAdapter(adapter);
|
||||
|
||||
confirm.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
||||
int pad = Math.abs(v.getWidth() + ViewUtil.dpToPx(16));
|
||||
ViewUtil.setPaddingEnd(contactsRecycler, pad);
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeObservers() {
|
||||
viewModel.getRecipients().observe(this, models -> adapter.submitList(models,
|
||||
() -> contactsRecycler.scrollToPosition(models.size() - 1)));
|
||||
viewModel.hasDraftText().observe(this, this::handleHasDraftText);
|
||||
|
||||
linkPreviewViewModel.getLinkPreviewState().observe(this, linkPreviewState -> {
|
||||
preview.setVisibility(View.VISIBLE);
|
||||
if (linkPreviewState.getError() != null) {
|
||||
preview.setNoPreview(linkPreviewState.getError());
|
||||
viewModel.onLinkPreviewChanged(null);
|
||||
} else if (linkPreviewState.isLoading()) {
|
||||
preview.setLoading();
|
||||
viewModel.onLinkPreviewChanged(null);
|
||||
} else if (linkPreviewState.getLinkPreview().isPresent()) {
|
||||
preview.setLinkPreview(GlideApp.with(this), linkPreviewState.getLinkPreview().get(), true);
|
||||
viewModel.onLinkPreviewChanged(linkPreviewState.getLinkPreview().get());
|
||||
} else if (!linkPreviewState.hasLinks()) {
|
||||
preview.setVisibility(View.GONE);
|
||||
viewModel.onLinkPreviewChanged(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleHasDraftText(boolean hasDraftText) {
|
||||
confirm.setEnabled(hasDraftText);
|
||||
confirm.setAlpha(hasDraftText ? 1f : 0.5f);
|
||||
}
|
||||
|
||||
private void onConfirm() {
|
||||
confirm.setClickable(false);
|
||||
confirm.setIndeterminateProgressMode(true);
|
||||
confirm.setProgress(50);
|
||||
|
||||
viewModel.send(results -> {
|
||||
MultiShareDialogs.displayResultDialog(this, results, () -> {
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.thoughtcrime.securesms.sharing.interstitial;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientMappingModel;
|
||||
|
||||
class ShareInterstitialMappingModel extends RecipientMappingModel<ShareInterstitialMappingModel> {
|
||||
|
||||
private final Recipient recipient;
|
||||
private final boolean isLast;
|
||||
|
||||
ShareInterstitialMappingModel(@NonNull Recipient recipient, boolean isLast) {
|
||||
this.recipient = recipient;
|
||||
this.isLast = isLast;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getName(@NonNull Context context) {
|
||||
String name = recipient.isSelf() ? context.getString(R.string.note_to_self)
|
||||
: recipient.getShortDisplayNameIncludingUsername(context);
|
||||
|
||||
return isLast ? name : context.getString(R.string.ShareActivity__s_comma, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull ShareInterstitialMappingModel newItem) {
|
||||
return super.areContentsTheSame(newItem) && isLast == newItem.isLast;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.thoughtcrime.securesms.sharing.interstitial;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
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)));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private List<Recipient> resolveRecipients(@NonNull Set<ShareContactAndThread> shareContactAndThreads) {
|
||||
return Stream.of(shareContactAndThreads)
|
||||
.map(ShareContactAndThread::getRecipientId)
|
||||
.map(Recipient::resolved)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.thoughtcrime.securesms.sharing.interstitial;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||
import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
|
||||
|
||||
class ShareInterstitialSelectionAdapter extends MappingAdapter {
|
||||
ShareInterstitialSelectionAdapter() {
|
||||
registerFactory(ShareInterstitialMappingModel.class, RecipientViewHolder.createFactory(R.layout.share_contact_selection_item, null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.thoughtcrime.securesms.sharing.interstitial;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Consumer;
|
||||
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.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareArgs;
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareSender;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class ShareInterstitialViewModel extends ViewModel {
|
||||
|
||||
private final MultiShareArgs args;
|
||||
private final MutableLiveData<List<MappingModel<?>>> recipients;
|
||||
private final MutableLiveData<String> draftText;
|
||||
|
||||
private LinkPreview linkPreview;
|
||||
|
||||
ShareInterstitialViewModel(@NonNull MultiShareArgs args, @NonNull ShareInterstitialRepository repository) {
|
||||
this.args = args;
|
||||
this.recipients = new MutableLiveData<>();
|
||||
this.draftText = new DefaultValueLiveData<>(Util.firstNonNull(args.getDraftText(), ""));
|
||||
|
||||
repository.loadRecipients(args.getShareContactAndThreads(),
|
||||
list -> recipients.postValue(Stream.of(list)
|
||||
.<MappingModel<?>>mapIndexed((i, r) -> new ShareInterstitialMappingModel(r, i == list.size() - 1))
|
||||
.toList()));
|
||||
|
||||
}
|
||||
|
||||
LiveData<List<MappingModel<?>>> getRecipients() {
|
||||
return recipients;
|
||||
}
|
||||
|
||||
LiveData<Boolean> hasDraftText() {
|
||||
return Transformations.map(draftText, text -> !TextUtils.isEmpty(text));
|
||||
}
|
||||
|
||||
void onDraftTextChanged(@NonNull String change) {
|
||||
draftText.setValue(change);
|
||||
}
|
||||
|
||||
void onLinkPreviewChanged(@Nullable LinkPreview linkPreview) {
|
||||
this.linkPreview = linkPreview;
|
||||
}
|
||||
|
||||
void send(@NonNull Consumer<MultiShareSender.MultiShareSendResultCollection> resultsConsumer) {
|
||||
LinkPreview linkPreview = this.linkPreview;
|
||||
String draftText = this.draftText.getValue();
|
||||
|
||||
MultiShareArgs.Builder builder = args.buildUpon()
|
||||
.withDraftText(draftText)
|
||||
.withLinkPreview(linkPreview);
|
||||
|
||||
MultiShareSender.send(builder.build(), resultsConsumer);
|
||||
}
|
||||
|
||||
static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final MultiShareArgs args;
|
||||
private final ShareInterstitialRepository repository;
|
||||
|
||||
Factory(@NonNull MultiShareArgs args, @NonNull ShareInterstitialRepository repository) {
|
||||
this.args = args;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return modelClass.cast(new ShareInterstitialViewModel(args, repository));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user