mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-30 21:38:29 +00:00
Optimize uploads during media composition.
By uploading in advance (when on unmetered connections), media messages can send almost instantly.
This commit is contained in:
parent
92e97e61c1
commit
fadcc606f8
@ -37,6 +37,7 @@ import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.signal.ringrtc.CallConnectionFactory;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||
@ -72,6 +73,7 @@ import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.stickers.BlessedPacks;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||
import org.webrtc.voiceengine.WebRtcAudioUtils;
|
||||
@ -129,6 +131,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
initializeRingRtc();
|
||||
initializePendingMessages();
|
||||
initializeBlobProvider();
|
||||
initializeCleanup();
|
||||
initializeCameraX();
|
||||
NotificationChannels.create(this);
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
|
||||
@ -367,11 +370,18 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||
}
|
||||
|
||||
private void initializeBlobProvider() {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
BlobProvider.getInstance().onSessionStart(this);
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeCleanup() {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
|
||||
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private void initializeCameraX() {
|
||||
if (CameraXUtil.isSupported()) {
|
||||
|
@ -1,12 +1,15 @@
|
||||
package org.thoughtcrime.securesms.attachments;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class AttachmentId {
|
||||
public class AttachmentId implements Parcelable {
|
||||
|
||||
@JsonProperty
|
||||
private final long rowId;
|
||||
@ -19,6 +22,11 @@ public class AttachmentId {
|
||||
this.uniqueId = uniqueId;
|
||||
}
|
||||
|
||||
private AttachmentId(Parcel in) {
|
||||
this.rowId = in.readLong();
|
||||
this.uniqueId = in.readLong();
|
||||
}
|
||||
|
||||
public long getRowId() {
|
||||
return rowId;
|
||||
}
|
||||
@ -54,4 +62,28 @@ public class AttachmentId {
|
||||
public int hashCode() {
|
||||
return Util.hashCode(rowId, uniqueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeLong(rowId);
|
||||
dest.writeLong(uniqueId);
|
||||
}
|
||||
|
||||
public static final Creator<AttachmentId> CREATOR = new Creator<AttachmentId>() {
|
||||
@Override
|
||||
public AttachmentId createFromParcel(Parcel in) {
|
||||
return new AttachmentId(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttachmentId[] newArray(int size) {
|
||||
return new AttachmentId[size];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -11,12 +11,15 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformPropertie
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class DatabaseAttachment extends Attachment {
|
||||
|
||||
private final AttachmentId attachmentId;
|
||||
private final long mmsId;
|
||||
private final boolean hasData;
|
||||
private final boolean hasThumbnail;
|
||||
private final int displayOrder;
|
||||
|
||||
public DatabaseAttachment(AttachmentId attachmentId, long mmsId,
|
||||
boolean hasData, boolean hasThumbnail,
|
||||
@ -25,13 +28,14 @@ public class DatabaseAttachment extends Attachment {
|
||||
byte[] digest, String fastPreflightId, boolean voiceNote,
|
||||
int width, int height, boolean quote, @Nullable String caption,
|
||||
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash,
|
||||
@Nullable TransformProperties transformProperties)
|
||||
@Nullable TransformProperties transformProperties, int displayOrder)
|
||||
{
|
||||
super(contentType, transferProgress, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, caption, stickerLocator, blurHash, transformProperties);
|
||||
this.attachmentId = attachmentId;
|
||||
this.hasData = hasData;
|
||||
this.hasThumbnail = hasThumbnail;
|
||||
this.mmsId = mmsId;
|
||||
this.displayOrder = displayOrder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -58,6 +62,10 @@ public class DatabaseAttachment extends Attachment {
|
||||
return attachmentId;
|
||||
}
|
||||
|
||||
public int getDisplayOrder() {
|
||||
return displayOrder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other != null &&
|
||||
@ -81,4 +89,11 @@ public class DatabaseAttachment extends Attachment {
|
||||
public boolean hasThumbnail() {
|
||||
return hasThumbnail;
|
||||
}
|
||||
|
||||
public static class DisplayOrderComparator implements Comparator<DatabaseAttachment> {
|
||||
@Override
|
||||
public int compare(DatabaseAttachment lhs, DatabaseAttachment rhs) {
|
||||
return Integer.compare(lhs.getDisplayOrder(), rhs.getDisplayOrder());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import androidx.annotation.NonNull;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
@ -15,6 +14,7 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
@ -44,7 +44,6 @@ import android.provider.Telephony;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Pair;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@ -72,7 +71,6 @@ import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
import androidx.core.view.MenuItemCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
@ -164,6 +162,7 @@ import org.thoughtcrime.securesms.maps.PlacePickerActivity;
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult;
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestFragment;
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestFragmentViewModel;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager;
|
||||
@ -184,7 +183,6 @@ import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.mms.StickerSlide;
|
||||
import org.thoughtcrime.securesms.mms.TextSlide;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
@ -216,11 +214,11 @@ import org.thoughtcrime.securesms.util.Dialogs;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences.MediaKeyboardMode;
|
||||
@ -233,15 +231,13 @@ import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@ -591,24 +587,21 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
initializeSecurity(isSecureText, isDefaultSms);
|
||||
break;
|
||||
case MEDIA_SENDER:
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
|
||||
boolean initiating = threadId == -1;
|
||||
TransportOption transport = data.getParcelableExtra(MediaSendActivity.EXTRA_TRANSPORT);
|
||||
String message = data.getStringExtra(MediaSendActivity.EXTRA_MESSAGE);
|
||||
boolean viewOnce = data.getBooleanExtra(MediaSendActivity.EXTRA_VIEW_ONCE, false);
|
||||
QuoteModel quote = viewOnce ? null : inputPanel.getQuote().orNull();
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
MediaSendActivityResult result = data.getParcelableExtra(MediaSendActivity.EXTRA_RESULT);
|
||||
sendButton.setTransport(result.getTransport());
|
||||
|
||||
if (transport == null) {
|
||||
throw new IllegalStateException("Received a null transport from the MediaSendActivity.");
|
||||
if (result.isPushPreUpload()) {
|
||||
sendMediaMessage(result);
|
||||
return;
|
||||
}
|
||||
|
||||
sendButton.setTransport(transport);
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
|
||||
boolean initiating = threadId == -1;
|
||||
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orNull();
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
|
||||
List<Media> mediaList = data.getParcelableArrayListExtra(MediaSendActivity.EXTRA_MEDIA);
|
||||
|
||||
for (Media mediaItem : mediaList) {
|
||||
for (Media mediaItem : result.getNonUploadedMedia()) {
|
||||
if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
|
||||
slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), 0, mediaItem.getCaption().orNull()));
|
||||
} else if (MediaUtil.isGif(mediaItem.getMimeType())) {
|
||||
@ -622,14 +615,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
final Context context = ConversationActivity.this.getApplicationContext();
|
||||
|
||||
sendMediaMessage(transport.isSms(),
|
||||
message,
|
||||
sendMediaMessage(result.getTransport().isSms(),
|
||||
result.getBody(),
|
||||
slideDeck,
|
||||
quote,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
expiresIn,
|
||||
viewOnce,
|
||||
result.isViewOnce(),
|
||||
subscriptionId,
|
||||
initiating,
|
||||
true).addListener(new AssertedSuccessListener<Void>() {
|
||||
@ -1540,12 +1533,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(@NonNull Pair<IdentityRecordList, String> result) {
|
||||
Log.i(TAG, "Got identity records: " + result.first.isUnverified());
|
||||
identityRecords.replaceWith(result.first);
|
||||
Log.i(TAG, "Got identity records: " + result.first().isUnverified());
|
||||
identityRecords.replaceWith(result.first());
|
||||
|
||||
if (result.second != null) {
|
||||
if (result.second() != null) {
|
||||
Log.d(TAG, "Replacing banner...");
|
||||
unverifiedBannerView.get().display(result.second, result.first.getUnverifiedRecords(),
|
||||
unverifiedBannerView.get().display(result.second(), result.first().getUnverifiedRecords(),
|
||||
new UnverifiedClickedListener(),
|
||||
new UnverifiedDismissedListener());
|
||||
} else if (unverifiedBannerView.resolved()) {
|
||||
@ -2114,28 +2107,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
return rawText;
|
||||
}
|
||||
|
||||
private Pair<String, Optional<Slide>> getSplitMessage(String rawText, int maxPrimaryMessageSize) {
|
||||
String bodyText = rawText;
|
||||
Optional<Slide> textSlide = Optional.absent();
|
||||
|
||||
if (bodyText.length() > maxPrimaryMessageSize) {
|
||||
bodyText = rawText.substring(0, maxPrimaryMessageSize);
|
||||
|
||||
byte[] textData = rawText.getBytes();
|
||||
String timestamp = new SimpleDateFormat("yyyy-MM-dd-HHmmss", Locale.US).format(new Date());
|
||||
String filename = String.format("signal-%s.txt", timestamp);
|
||||
Uri textUri = BlobProvider.getInstance()
|
||||
.forData(textData)
|
||||
.withMimeType(MediaUtil.LONG_TEXT)
|
||||
.withFileName(filename)
|
||||
.createForSingleSessionInMemory();
|
||||
|
||||
textSlide = Optional.of(new TextSlide(this, textUri, filename, textData.length));
|
||||
}
|
||||
|
||||
return new Pair<>(bodyText, textSlide);
|
||||
}
|
||||
|
||||
private MediaConstraints getCurrentMediaConstraints() {
|
||||
return sendButton.getSelectedTransport().getType() == Type.TEXTSECURE
|
||||
? MediaConstraints.getPushMediaConstraints()
|
||||
@ -2241,6 +2212,35 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMediaMessage(@NonNull MediaSendActivityResult result) {
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orNull();
|
||||
boolean initiating = threadId == -1;
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, quote, Collections.emptyList(), Collections.emptyList());
|
||||
OutgoingMediaMessage secureMessage = new OutgoingSecureMediaMessage(message );
|
||||
|
||||
ApplicationContext.getInstance(this).getTypingStatusSender().onTypingStopped(threadId);
|
||||
|
||||
inputPanel.clearQuote();
|
||||
attachmentManager.clear(glideRequests, false);
|
||||
silentlySetComposeText("");
|
||||
|
||||
long id = fragment.stageOutgoingMessage(message);
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
if (initiating) {
|
||||
DatabaseFactory.getRecipientDatabase(this).setProfileSharing(recipient.getId(), true);
|
||||
}
|
||||
|
||||
long resultId = MessageSender.sendPushWithPreUploadedMedia(this, secureMessage, result.getPreUploadResults(), threadId, () -> fragment.releaseOutgoingMessage(id));
|
||||
|
||||
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
|
||||
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
|
||||
|
||||
return resultId;
|
||||
}, this::sendComplete);
|
||||
}
|
||||
|
||||
private void sendMediaMessage(final boolean forceSms, final long expiresIn, final boolean viewOnce, final int subscriptionId, boolean initiating)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
@ -2266,11 +2266,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
if (isSecureText && !forceSms) {
|
||||
Pair<String, Optional<Slide>> splitMessage = getSplitMessage(body, sendButton.getSelectedTransport().calculateCharacters(body).maxPrimaryMessageSize);
|
||||
body = splitMessage.first;
|
||||
MessageUtil.SplitResult splitMessage = MessageUtil.getSplitMessage(this, body, sendButton.getSelectedTransport().calculateCharacters(body).maxPrimaryMessageSize);
|
||||
body = splitMessage.getBody();
|
||||
|
||||
if (splitMessage.second.isPresent()) {
|
||||
slideDeck.addSlide(splitMessage.second.get());
|
||||
if (splitMessage.getTextSlide().isPresent()) {
|
||||
slideDeck.addSlide(splitMessage.getTextSlide().get());
|
||||
}
|
||||
}
|
||||
|
||||
@ -2301,22 +2301,16 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
final long id = fragment.stageOutgoingMessage(outgoingMessage);
|
||||
|
||||
new AsyncTask<Void, Void, Long>() {
|
||||
@Override
|
||||
protected Long doInBackground(Void... param) {
|
||||
if (initiating) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient.getId(), true);
|
||||
}
|
||||
|
||||
return MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
|
||||
SimpleTask.run(() -> {
|
||||
if (initiating) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient.getId(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Long result) {
|
||||
sendComplete(result);
|
||||
future.set(null);
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
return MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
|
||||
}, result -> {
|
||||
sendComplete(result);
|
||||
future.set(null);
|
||||
});
|
||||
})
|
||||
.onAnyDenied(() -> future.set(null))
|
||||
.execute();
|
||||
@ -2471,7 +2465,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
boolean initiating = threadId == -1;
|
||||
AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, result.first, result.second, MediaUtil.AUDIO_AAC, true);
|
||||
AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, result.first(), result.second(), MediaUtil.AUDIO_AAC, true);
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
slideDeck.addSlide(audioSlide);
|
||||
|
||||
@ -2481,7 +2475,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
BlobProvider.getInstance().delete(ConversationActivity.this, result.first);
|
||||
BlobProvider.getInstance().delete(ConversationActivity.this, result.first());
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
@ -2511,7 +2505,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
BlobProvider.getInstance().delete(ConversationActivity.this, result.first);
|
||||
BlobProvider.getInstance().delete(ConversationActivity.this, result.first());
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
|
@ -76,6 +76,7 @@ import java.io.OutputStream;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
@ -120,6 +121,7 @@ public class AttachmentDatabase extends Database {
|
||||
private static final String DATA_HASH = "data_hash";
|
||||
static final String BLUR_HASH = "blur_hash";
|
||||
static final String TRANSFORM_PROPERTIES = "transform_properties";
|
||||
static final String DISPLAY_ORDER = "display_order";
|
||||
|
||||
public static final String DIRECTORY = "parts";
|
||||
|
||||
@ -128,6 +130,8 @@ public class AttachmentDatabase extends Database {
|
||||
public static final int TRANSFER_PROGRESS_PENDING = 2;
|
||||
public static final int TRANSFER_PROGRESS_FAILED = 3;
|
||||
|
||||
public static final long PREUPLOAD_MESSAGE_ID = -8675309;
|
||||
|
||||
private static final String PART_ID_WHERE = ROW_ID + " = ? AND " + UNIQUE_ID + " = ?";
|
||||
private static final String PART_ID_WHERE_NOT = ROW_ID + " != ? AND " + UNIQUE_ID + " != ?";
|
||||
|
||||
@ -138,7 +142,8 @@ public class AttachmentDatabase extends Database {
|
||||
UNIQUE_ID, DIGEST, FAST_PREFLIGHT_ID, VOICE_NOTE,
|
||||
QUOTE, DATA_RANDOM, THUMBNAIL_RANDOM, WIDTH, HEIGHT,
|
||||
CAPTION, STICKER_PACK_ID, STICKER_PACK_KEY, STICKER_ID,
|
||||
DATA_HASH, BLUR_HASH, TRANSFORM_PROPERTIES, TRANSFER_FILE };
|
||||
DATA_HASH, BLUR_HASH, TRANSFORM_PROPERTIES, TRANSFER_FILE,
|
||||
DISPLAY_ORDER };
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " +
|
||||
MMS_ID + " INTEGER, " + "seq" + " INTEGER DEFAULT 0, " +
|
||||
@ -154,7 +159,8 @@ public class AttachmentDatabase extends Database {
|
||||
CAPTION + " TEXT DEFAULT NULL, " + STICKER_PACK_ID + " TEXT DEFAULT NULL, " +
|
||||
STICKER_PACK_KEY + " DEFAULT NULL, " + STICKER_ID + " INTEGER DEFAULT -1, " +
|
||||
DATA_HASH + " TEXT DEFAULT NULL, " + BLUR_HASH + " TEXT DEFAULT NULL, " +
|
||||
TRANSFORM_PROPERTIES + " TEXT DEFAULT NULL, " + TRANSFER_FILE + " TEXT DEFAULT NULL);";
|
||||
TRANSFORM_PROPERTIES + " TEXT DEFAULT NULL, " + TRANSFER_FILE + " TEXT DEFAULT NULL, " +
|
||||
DISPLAY_ORDER + " INTEGER DEFAULT 0);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");",
|
||||
@ -320,6 +326,33 @@ public class AttachmentDatabase extends Database {
|
||||
notifyAttachmentListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all attachments with an ID of {@link #PREUPLOAD_MESSAGE_ID}. These represent
|
||||
* attachments that were pre-uploaded and haven't been assigned to a message. This should only be
|
||||
* done when you *know* that all attachments *should* be assigned a real mmsId. For instance, when
|
||||
* the app starts. Otherwise you could delete attachments that are legitimately being
|
||||
* pre-uploaded.
|
||||
*/
|
||||
public int deleteAbandonedPreuploadedAttachments() {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String query = MMS_ID + " = ?";
|
||||
String[] args = new String[] { String.valueOf(PREUPLOAD_MESSAGE_ID) };
|
||||
int count = 0;
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, null, query, args, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID));
|
||||
long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID));
|
||||
AttachmentId id = new AttachmentId(rowId, uniqueId);
|
||||
|
||||
deleteAttachment(id);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public void deleteAttachmentFilesForViewOnceMessage(long mmsId) {
|
||||
Log.d(TAG, "[deleteAttachmentFilesForViewOnceMessage] mmsId: " + mmsId);
|
||||
|
||||
@ -538,6 +571,32 @@ public class AttachmentDatabase extends Database {
|
||||
database.update(TABLE_NAME, contentValues, PART_ID_WHERE, destinationId.toStrings());
|
||||
}
|
||||
|
||||
public void updateAttachmentCaption(@NonNull AttachmentId id, @Nullable String caption) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(CAPTION, caption);
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings());
|
||||
}
|
||||
|
||||
public void updateDisplayOrder(@NonNull Map<AttachmentId, Integer> orderMap) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
for (Map.Entry<AttachmentId, Integer> entry : orderMap.entrySet()) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(DISPLAY_ORDER, entry.getValue());
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, PART_ID_WHERE, entry.getKey().toStrings());
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void updateAttachmentAfterUpload(@NonNull AttachmentId id, @NonNull Attachment attachment) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
@ -554,6 +613,42 @@ public class AttachmentDatabase extends Database {
|
||||
database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings());
|
||||
}
|
||||
|
||||
public @NonNull DatabaseAttachment insertAttachmentForPreUpload(@NonNull Attachment attachment) throws MmsException {
|
||||
Map<Attachment, AttachmentId> result = insertAttachmentsForMessage(PREUPLOAD_MESSAGE_ID,
|
||||
Collections.singletonList(attachment),
|
||||
Collections.emptyList());
|
||||
|
||||
if (result.values().isEmpty()) {
|
||||
throw new MmsException("Bad attachment result!");
|
||||
}
|
||||
|
||||
DatabaseAttachment databaseAttachment = getAttachment(result.values().iterator().next());
|
||||
|
||||
if (databaseAttachment == null) {
|
||||
throw new MmsException("Failed to retrieve attachment we just inserted!");
|
||||
}
|
||||
|
||||
return databaseAttachment;
|
||||
}
|
||||
|
||||
public void updateMessageId(@NonNull Collection<AttachmentId> attachmentIds, long mmsId) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(MMS_ID, mmsId);
|
||||
|
||||
for (AttachmentId attachmentId : attachmentIds) {
|
||||
db.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings());
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull Map<Attachment, AttachmentId> insertAttachmentsForMessage(long mmsId, @NonNull List<Attachment> attachments, @NonNull List<Attachment> quoteAttachment)
|
||||
throws MmsException
|
||||
{
|
||||
@ -957,7 +1052,8 @@ public class AttachmentDatabase extends Database {
|
||||
object.getInt(STICKER_ID))
|
||||
: null,
|
||||
BlurHash.parseOrNull(object.getString(BLUR_HASH)),
|
||||
TransformProperties.parse(object.getString(TRANSFORM_PROPERTIES))));
|
||||
TransformProperties.parse(object.getString(TRANSFORM_PROPERTIES)),
|
||||
object.getInt(DISPLAY_ORDER)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -988,7 +1084,8 @@ public class AttachmentDatabase extends Database {
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(STICKER_ID)))
|
||||
: null,
|
||||
BlurHash.parseOrNull(cursor.getString(cursor.getColumnIndexOrThrow(BLUR_HASH))),
|
||||
TransformProperties.parse(cursor.getString(cursor.getColumnIndexOrThrow(TRANSFORM_PROPERTIES)))));
|
||||
TransformProperties.parse(cursor.getString(cursor.getColumnIndexOrThrow(TRANSFORM_PROPERTIES))),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(DISPLAY_ORDER))));
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new AssertionError(e);
|
||||
|
@ -44,6 +44,7 @@ public class MediaDatabase extends Database {
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BLUR_HASH + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", "
|
||||
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", "
|
||||
@ -247,9 +248,9 @@ public class MediaDatabase extends Database {
|
||||
}
|
||||
|
||||
public enum Sorting {
|
||||
Newest (AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " DESC"),
|
||||
Oldest (AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " ASC" ),
|
||||
Largest(AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + " DESC");
|
||||
Newest (AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " DESC, " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + " DESC"),
|
||||
Oldest (AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " ASC, " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + " DESC"),
|
||||
Largest(AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + " DESC, " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER + " DESC");
|
||||
|
||||
private final String postFix;
|
||||
|
||||
|
@ -212,7 +212,8 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
"'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " +
|
||||
"'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " +
|
||||
"'" + AttachmentDatabase.BLUR_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BLUR_HASH + ", " +
|
||||
"'" + AttachmentDatabase.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES +
|
||||
"'" + AttachmentDatabase.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " +
|
||||
"'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER +
|
||||
")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS,
|
||||
};
|
||||
|
||||
@ -702,6 +703,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
List<Attachment> attachments = Stream.of(associatedAttachments).filterNot(Attachment::isQuote)
|
||||
.filterNot(contactAttachments::contains)
|
||||
.filterNot(previewAttachments::contains)
|
||||
.sorted(new DatabaseAttachment.DisplayOrderComparator())
|
||||
.map(a -> (Attachment)a).toList();
|
||||
|
||||
Recipient recipient = Recipient.resolved(RecipientId.from(recipientId));
|
||||
@ -865,7 +867,8 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
databaseAttachment.getCaption(),
|
||||
databaseAttachment.getSticker(),
|
||||
databaseAttachment.getBlurHash(),
|
||||
databaseAttachment.getTransformProperties()));
|
||||
databaseAttachment.getTransformProperties(),
|
||||
databaseAttachment.getDisplayOrder()));
|
||||
}
|
||||
|
||||
return insertMediaMessage(request.getBody(),
|
||||
@ -1563,7 +1566,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
List<NetworkFailure> networkFailures = getFailures(networkDocument);
|
||||
List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachment(cursor);
|
||||
List<Contact> contacts = getSharedContacts(cursor, attachments);
|
||||
Set<Attachment> contactAttachments = Stream.of(contacts).map(Contact::getAvatarAttachment).filter(a -> a != null).collect(Collectors.toSet());
|
||||
Set<Attachment> contactAttachments = Stream.of(contacts).map(Contact::getAvatarAttachment).withoutNulls().collect(Collectors.toSet());
|
||||
List<LinkPreview> previews = getLinkPreviews(cursor, attachments);
|
||||
Set<Attachment> previewAttachments = Stream.of(previews).filter(lp -> lp.getThumbnail().isPresent()).map(lp -> lp.getThumbnail().get()).collect(Collectors.toSet());
|
||||
SlideDeck slideDeck = getSlideDeck(Stream.of(attachments).filterNot(contactAttachments::contains).filterNot(previewAttachments::contains).toList());
|
||||
@ -1601,9 +1604,10 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
}
|
||||
|
||||
private SlideDeck getSlideDeck(@NonNull List<DatabaseAttachment> attachments) {
|
||||
List<? extends Attachment> messageAttachments = Stream.of(attachments)
|
||||
.filterNot(Attachment::isQuote)
|
||||
.toList();
|
||||
List<DatabaseAttachment> messageAttachments = Stream.of(attachments)
|
||||
.filterNot(Attachment::isQuote)
|
||||
.sorted(new DatabaseAttachment.DisplayOrderComparator())
|
||||
.toList();
|
||||
return new SlideDeck(context, messageAttachments);
|
||||
}
|
||||
|
||||
|
@ -296,7 +296,8 @@ public class MmsSmsDatabase extends Database {
|
||||
"'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " +
|
||||
"'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ", " +
|
||||
"'" + AttachmentDatabase.BLUR_HASH + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.BLUR_HASH + ", " +
|
||||
"'" + AttachmentDatabase.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES +
|
||||
"'" + AttachmentDatabase.TRANSFORM_PROPERTIES + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFORM_PROPERTIES + ", " +
|
||||
"'" + AttachmentDatabase.DISPLAY_ORDER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DISPLAY_ORDER +
|
||||
")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS,
|
||||
SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID,
|
||||
SmsDatabase.TYPE, SmsDatabase.RECIPIENT_ID, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE,
|
||||
|
@ -101,8 +101,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int REACTIONS_UNREAD_INDEX = 39;
|
||||
private static final int RESUMABLE_DOWNLOADS = 40;
|
||||
private static final int KEY_VALUE_STORE = 41;
|
||||
private static final int ATTACHMENT_DISPLAY_ORDER = 42;
|
||||
|
||||
private static final int DATABASE_VERSION = 41;
|
||||
private static final int DATABASE_VERSION = 42;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@ -694,6 +695,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
"type INTEGER)");
|
||||
}
|
||||
|
||||
if (oldVersion < ATTACHMENT_DISPLAY_ORDER) {
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN display_order INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
@ -133,6 +133,7 @@ class JobController {
|
||||
Log.w(TAG, JobLogger.format(job, "Canceling while inactive."));
|
||||
Log.w(TAG, JobLogger.format(job, "Job failed."));
|
||||
|
||||
job.cancel();
|
||||
job.onFailure();
|
||||
onFailure(job);
|
||||
} else {
|
||||
|
@ -4,6 +4,7 @@ import android.content.Context;
|
||||
import android.media.MediaDataSource;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
@ -143,7 +144,7 @@ public final class AttachmentCompressionJob extends BaseJob {
|
||||
{
|
||||
try {
|
||||
if (MediaUtil.isVideo(attachment) && MediaConstraints.isVideoTranscodeAvailable()) {
|
||||
transcodeVideoIfNeededToDatabase(context, attachmentDatabase, attachment, constraints, EventBus.getDefault());
|
||||
transcodeVideoIfNeededToDatabase(context, attachmentDatabase, attachment, constraints, EventBus.getDefault(), this::isCanceled);
|
||||
} else if (constraints.isSatisfied(context, attachment)) {
|
||||
if (MediaUtil.isJpeg(attachment)) {
|
||||
MediaStream stripped = getResizedMedia(context, attachment, constraints);
|
||||
@ -167,7 +168,8 @@ public final class AttachmentCompressionJob extends BaseJob {
|
||||
@NonNull AttachmentDatabase attachmentDatabase,
|
||||
@NonNull DatabaseAttachment attachment,
|
||||
@NonNull MediaConstraints constraints,
|
||||
@NonNull EventBus eventBus)
|
||||
@NonNull EventBus eventBus,
|
||||
@NonNull InMemoryTranscoder.CancelationSignal cancelationSignal)
|
||||
throws UndeliverableMessageException
|
||||
{
|
||||
try (NotificationController notification = GenericForegroundService.startForegroundTask(context, context.getString(R.string.AttachmentUploadJob_compressing_video_start))) {
|
||||
@ -190,7 +192,7 @@ public final class AttachmentCompressionJob extends BaseJob {
|
||||
PartProgressEvent.Type.COMPRESSION,
|
||||
100,
|
||||
percent));
|
||||
});
|
||||
}, cancelationSignal);
|
||||
|
||||
attachmentDatabase.updateAttachmentData(attachment, mediaStream);
|
||||
attachmentDatabase.markAttachmentAsTransformed(attachment.getAttachmentId());
|
||||
|
@ -115,7 +115,11 @@ public final class AttachmentUploadJob extends BaseJob {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() { }
|
||||
public void onFailure() {
|
||||
if (isCanceled()) {
|
||||
DatabaseFactory.getAttachmentDatabase(context).deleteAttachment(attachmentId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception exception) {
|
||||
@ -135,6 +139,7 @@ public final class AttachmentUploadJob extends BaseJob {
|
||||
.withWidth(attachment.getWidth())
|
||||
.withHeight(attachment.getHeight())
|
||||
.withCaption(attachment.getCaption())
|
||||
.withCancelationSignal(this::isCanceled)
|
||||
.withListener((total, progress) -> {
|
||||
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress));
|
||||
if (notification != null) {
|
||||
|
@ -111,7 +111,7 @@ public class Camera1Fragment extends Fragment implements CameraFragment,
|
||||
GestureDetector gestureDetector = new GestureDetector(flipGestureListener);
|
||||
cameraPreview.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
|
||||
|
||||
viewModel.getMostRecentMediaItem(requireContext()).observe(this, this::presentRecentItemThumbnail);
|
||||
viewModel.getMostRecentMediaItem().observe(this, this::presentRecentItemThumbnail);
|
||||
viewModel.getHudState().observe(this, this::presentHud);
|
||||
}
|
||||
|
||||
|
@ -107,7 +107,7 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
||||
|
||||
onOrientationChanged(getResources().getConfiguration().orientation);
|
||||
|
||||
viewModel.getMostRecentMediaItem(requireContext()).observe(this, this::presentRecentItemThumbnail);
|
||||
viewModel.getMostRecentMediaItem().observe(this, this::presentRecentItemThumbnail);
|
||||
viewModel.getHudState().observe(this, this::presentHud);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import android.Manifest;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore.Images;
|
||||
@ -17,18 +18,23 @@ import android.util.Pair;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -38,6 +44,8 @@ import java.util.Map;
|
||||
*/
|
||||
class MediaRepository {
|
||||
|
||||
private static final String TAG = Log.tag(MediaRepository.class);
|
||||
|
||||
/**
|
||||
* Retrieves a list of folders that contain media.
|
||||
*/
|
||||
@ -69,6 +77,14 @@ class MediaRepository {
|
||||
SignalExecutors.BOUNDED.execute(() -> callback.onComplete(getMostRecentItem(context)));
|
||||
}
|
||||
|
||||
void renderMedia(@NonNull Context context,
|
||||
@NonNull List<Media> currentMedia,
|
||||
@NonNull Map<Media, EditorModel> modelsToRender,
|
||||
@NonNull Callback<LinkedHashMap<Media, Media>> callback)
|
||||
{
|
||||
SignalExecutors.BOUNDED.execute(() -> callback.onComplete(renderMedia(context, currentMedia, modelsToRender)));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<MediaFolder> getFolders(@NonNull Context context) {
|
||||
if (!Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
||||
@ -231,6 +247,43 @@ class MediaRepository {
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private LinkedHashMap<Media, Media> renderMedia(@NonNull Context context,
|
||||
@NonNull List<Media> currentMedia,
|
||||
@NonNull Map<Media, EditorModel> modelsToRender)
|
||||
{
|
||||
LinkedHashMap<Media, Media> updatedMedia = new LinkedHashMap<>(currentMedia.size());
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
for (Media media : currentMedia) {
|
||||
EditorModel modelToRender = modelsToRender.get(media);
|
||||
if (modelToRender != null) {
|
||||
Bitmap bitmap = modelToRender.render(context);
|
||||
try {
|
||||
outputStream.reset();
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
|
||||
|
||||
Uri uri = BlobProvider.getInstance()
|
||||
.forData(outputStream.toByteArray())
|
||||
.withMimeType(MediaUtil.IMAGE_JPEG)
|
||||
.createForSingleSessionOnDisk(context);
|
||||
|
||||
Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), media.getBucketId(), media.getCaption());
|
||||
|
||||
updatedMedia.put(media, updated);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to render image. Using base image.");
|
||||
updatedMedia.put(media, media);
|
||||
} finally {
|
||||
bitmap.recycle();
|
||||
}
|
||||
} else {
|
||||
updatedMedia.put(media, media);
|
||||
}
|
||||
}
|
||||
return updatedMedia;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private Optional<Media> getMostRecentItem(@NonNull Context context) {
|
||||
if (!Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
||||
|
@ -1,14 +1,11 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
import android.text.Editable;
|
||||
@ -46,41 +43,30 @@ import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendViewModel.ViewOnceState;
|
||||
import org.thoughtcrime.securesms.mms.GifSlide;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.thoughtcrime.securesms.util.Function3;
|
||||
import org.thoughtcrime.securesms.util.IOFunction;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.thoughtcrime.securesms.video.VideoUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
@ -110,11 +96,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||
{
|
||||
private static final String TAG = MediaSendActivity.class.getSimpleName();
|
||||
|
||||
public static final String EXTRA_MEDIA = "media";
|
||||
public static final String EXTRA_MESSAGE = "message";
|
||||
public static final String EXTRA_TRANSPORT = "transport";
|
||||
public static final String EXTRA_VIEW_ONCE = "view_once";
|
||||
|
||||
public static final String EXTRA_RESULT = "result";
|
||||
|
||||
private static final String KEY_RECIPIENT = "recipient_id";
|
||||
private static final String KEY_BODY = "body";
|
||||
@ -150,6 +132,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||
private TextView charactersLeft;
|
||||
private RecyclerView mediaRail;
|
||||
private MediaRailAdapter mediaRailAdapter;
|
||||
private AlertDialog progressDialog;
|
||||
|
||||
private int visibleHeight;
|
||||
|
||||
@ -232,6 +215,10 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||
viewModel = ViewModelProviders.of(this, new MediaSendViewModel.Factory(getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||
transport = getIntent().getParcelableExtra(KEY_TRANSPORT);
|
||||
|
||||
MeteredConnectivityObserver meteredConnectivityObserver = new MeteredConnectivityObserver(this, this);
|
||||
meteredConnectivityObserver.isMetered().observe(this, viewModel::onMeteredConnectivityStatusChanged);
|
||||
viewModel.onMeteredConnectivityStatusChanged(Optional.fromNullable(meteredConnectivityObserver.isMetered().getValue()).or(false));
|
||||
|
||||
viewModel.setTransport(transport);
|
||||
viewModel.setRecipient(recipient != null ? recipient.get() : null);
|
||||
viewModel.onBodyChanged(getIntent().getStringExtra(KEY_BODY));
|
||||
@ -259,23 +246,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||
.commit();
|
||||
}
|
||||
|
||||
sendButton.setOnClickListener(v -> {
|
||||
if (hud.isKeyboardOpen()) {
|
||||
hud.hideSoftkey(composeText, null);
|
||||
}
|
||||
|
||||
sendButton.setEnabled(false);
|
||||
|
||||
MediaSendFragment fragment = getMediaSendFragment();
|
||||
|
||||
if (fragment != null) {
|
||||
processMedia(fragment.getAllMedia(), fragment.getSavedState(), processedMedia -> {
|
||||
setActivityResultAndFinish(processedMedia, composeText.getTextTrimmed(), transport);
|
||||
});
|
||||
} else {
|
||||
throw new AssertionError("No editor fragment available!");
|
||||
}
|
||||
});
|
||||
sendButton.setOnClickListener(v -> onSendClicked());
|
||||
|
||||
sendButton.setOnLongClickListener(v -> true);
|
||||
|
||||
@ -418,7 +389,6 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||
0);
|
||||
}
|
||||
|
||||
|
||||
private <T> void onMediaCaptured(Supplier<T> dataSupplier,
|
||||
IOFunction<T, Long> getLength,
|
||||
Function3<BlobProvider, T, Long, BlobProvider.BlobBuilder> createBlobBuilder,
|
||||
@ -428,7 +398,6 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||
{
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
try {
|
||||
|
||||
T data = dataSupplier.get();
|
||||
long length = getLength.apply(data);
|
||||
|
||||
@ -542,16 +511,51 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||
MediaSendFragment fragment = getMediaSendFragment();
|
||||
|
||||
if (fragment != null) {
|
||||
processMedia(fragment.getAllMedia(), fragment.getSavedState(), processedMedia -> {
|
||||
String body = viewModel.isViewOnce() ? "" : composeText.getTextTrimmed();
|
||||
sendMessages(recipients, processedMedia, body, transport);
|
||||
viewModel.onSendClicked(buildModelsToRender(fragment), recipients).observe(this, result -> {
|
||||
finish();
|
||||
});
|
||||
} else {
|
||||
throw new AssertionError("No editor fragment available!");
|
||||
}
|
||||
}
|
||||
|
||||
public void onAddMediaClicked(@NonNull String bucketId) {
|
||||
private void onSendClicked() {
|
||||
MediaSendFragment fragment = getMediaSendFragment();
|
||||
|
||||
if (fragment == null) {
|
||||
throw new AssertionError("No editor fragment available!");
|
||||
}
|
||||
|
||||
if (hud.isKeyboardOpen()) {
|
||||
hud.hideSoftkey(composeText, null);
|
||||
}
|
||||
|
||||
sendButton.setEnabled(false);
|
||||
|
||||
viewModel.onSendClicked(buildModelsToRender(fragment), Collections.emptyList()).observe(this, this::setActivityResultAndFinish);
|
||||
}
|
||||
|
||||
private Map<Media, EditorModel> buildModelsToRender(@NonNull MediaSendFragment fragment) {
|
||||
List<Media> mediaList = fragment.getAllMedia();
|
||||
Map<Uri, Object> savedState = fragment.getSavedState();
|
||||
Map<Media, EditorModel> modelsToRender = new HashMap<>();
|
||||
|
||||
for (Media media : mediaList) {
|
||||
Object state = savedState.get(media.getUri());
|
||||
|
||||
if (state instanceof ImageEditorFragment.Data) {
|
||||
EditorModel model = ((ImageEditorFragment.Data) state).readModel();
|
||||
if (model != null && model.isChanged()) {
|
||||
modelsToRender.put(media, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modelsToRender;
|
||||
}
|
||||
|
||||
|
||||
private void onAddMediaClicked(@NonNull String bucketId) {
|
||||
hud.hideCurrentInput(composeText);
|
||||
|
||||
// TODO: Get actual folder title somehow
|
||||
@ -569,7 +573,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||
.commit();
|
||||
}
|
||||
|
||||
public void onNoMediaAvailable() {
|
||||
private void onNoMediaAvailable() {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
@ -700,13 +704,24 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||
});
|
||||
|
||||
viewModel.getEvents().observe(this, event -> {
|
||||
if (event == MediaSendViewModel.Event.VIEW_ONCE_TOOLTIP) {
|
||||
TooltipPopup.forTarget(revealButton)
|
||||
.setText(R.string.MediaSendActivity_tap_here_to_make_this_message_disappear_after_it_is_viewed)
|
||||
.setBackgroundTint(getResources().getColor(R.color.core_blue))
|
||||
.setTextColor(getResources().getColor(R.color.core_white))
|
||||
.setOnDismissListener(() -> TextSecurePreferences.setHasSeenViewOnceTooltip(this, true))
|
||||
.show(TooltipPopup.POSITION_ABOVE);
|
||||
switch (event) {
|
||||
case VIEW_ONCE_TOOLTIP:
|
||||
TooltipPopup.forTarget(revealButton)
|
||||
.setText(R.string.MediaSendActivity_tap_here_to_make_this_message_disappear_after_it_is_viewed)
|
||||
.setBackgroundTint(getResources().getColor(R.color.core_blue))
|
||||
.setTextColor(getResources().getColor(R.color.core_white))
|
||||
.setOnDismissListener(() -> TextSecurePreferences.setHasSeenViewOnceTooltip(this, true))
|
||||
.show(TooltipPopup.POSITION_ABOVE);
|
||||
break;
|
||||
case SHOW_RENDER_PROGRESS:
|
||||
progressDialog = SimpleProgressDialog.show(new ContextThemeWrapper(MediaSendActivity.this, R.style.TextSecure_MediaSendProgressDialog));
|
||||
break;
|
||||
case HIDE_RENDER_PROGRESS:
|
||||
if (progressDialog != null) {
|
||||
progressDialog.dismiss();
|
||||
progressDialog = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -836,163 +851,19 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void processMedia(@NonNull List<Media> mediaList, @NonNull Map<Uri, Object> savedState, @NonNull OnProcessComplete callback) {
|
||||
Map<Media, EditorModel> modelsToRender = new HashMap<>();
|
||||
|
||||
for (Media media : mediaList) {
|
||||
Object state = savedState.get(media.getUri());
|
||||
|
||||
if (state instanceof ImageEditorFragment.Data) {
|
||||
EditorModel model = ((ImageEditorFragment.Data) state).readModel();
|
||||
if (model != null && model.isChanged()) {
|
||||
modelsToRender.put(media, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new AsyncTask<Void, Void, List<Media>>() {
|
||||
|
||||
private Stopwatch renderTimer;
|
||||
private Runnable progressTimer;
|
||||
private AlertDialog dialog;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
renderTimer = new Stopwatch("ProcessMedia");
|
||||
progressTimer = () -> {
|
||||
dialog = SimpleProgressDialog.show(new ContextThemeWrapper(MediaSendActivity.this, R.style.TextSecure_MediaSendProgressDialog));
|
||||
};
|
||||
Util.runOnMainDelayed(progressTimer, 250);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Media> doInBackground(Void... voids) {
|
||||
Context context = MediaSendActivity.this;
|
||||
List<Media> updatedMedia = new ArrayList<>(mediaList.size());
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
for (Media media : mediaList) {
|
||||
EditorModel modelToRender = modelsToRender.get(media);
|
||||
if (modelToRender != null) {
|
||||
Bitmap bitmap = modelToRender.render(context);
|
||||
try {
|
||||
outputStream.reset();
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
|
||||
|
||||
Uri uri = BlobProvider.getInstance()
|
||||
.forData(outputStream.toByteArray())
|
||||
.withMimeType(MediaUtil.IMAGE_JPEG)
|
||||
.createForSingleSessionOnDisk(context);
|
||||
|
||||
Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), media.getBucketId(), media.getCaption());
|
||||
|
||||
updatedMedia.add(updated);
|
||||
renderTimer.split("item");
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to render image. Using base image.");
|
||||
updatedMedia.add(media);
|
||||
} finally {
|
||||
bitmap.recycle();
|
||||
}
|
||||
} else {
|
||||
updatedMedia.add(media);
|
||||
}
|
||||
}
|
||||
return updatedMedia;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<Media> media) {
|
||||
callback.onComplete(media);
|
||||
Util.cancelRunnableOnMain(progressTimer);
|
||||
if (dialog != null) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
renderTimer.stop(TAG);
|
||||
}
|
||||
}.executeOnExecutor(SignalExecutors.BOUNDED);
|
||||
}
|
||||
|
||||
private @Nullable MediaSendFragment getMediaSendFragment() {
|
||||
return (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
||||
}
|
||||
|
||||
private void setActivityResultAndFinish(@NonNull List<Media> media, @NonNull String message, @NonNull TransportOption transport) {
|
||||
viewModel.onSendClicked();
|
||||
|
||||
ArrayList<Media> mediaList = new ArrayList<>(media);
|
||||
|
||||
if (mediaList.size() > 0) {
|
||||
Intent intent = new Intent();
|
||||
|
||||
intent.putParcelableArrayListExtra(EXTRA_MEDIA, mediaList);
|
||||
intent.putExtra(EXTRA_MESSAGE, viewModel.isViewOnce() ? "" : message);
|
||||
intent.putExtra(EXTRA_TRANSPORT, transport);
|
||||
intent.putExtra(EXTRA_VIEW_ONCE, viewModel.isViewOnce());
|
||||
|
||||
setResult(RESULT_OK, intent);
|
||||
} else {
|
||||
setResult(RESULT_CANCELED);
|
||||
}
|
||||
private void setActivityResultAndFinish(@NonNull MediaSendActivityResult result) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(EXTRA_RESULT, result);
|
||||
setResult(RESULT_OK, intent);
|
||||
|
||||
finish();
|
||||
|
||||
overridePendingTransition(R.anim.stationary, R.anim.camera_slide_to_bottom);
|
||||
}
|
||||
|
||||
private void sendMessages(@NonNull List<Recipient> recipients, @NonNull List<Media> media, @NonNull String body, @NonNull TransportOption transport) {
|
||||
SimpleTask.run(() -> {
|
||||
List<OutgoingSecureMediaMessage> messages = new ArrayList<>(recipients.size());
|
||||
|
||||
for (Recipient recipient : recipients) {
|
||||
SlideDeck slideDeck = buildSlideDeck(media);
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient,
|
||||
body,
|
||||
slideDeck.asAttachments(),
|
||||
System.currentTimeMillis(),
|
||||
-1,
|
||||
recipient.getExpireMessages() * 1000,
|
||||
viewModel.isViewOnce(),
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
messages.add(new OutgoingSecureMediaMessage(message));
|
||||
|
||||
// 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.
|
||||
Util.sleep(5);
|
||||
}
|
||||
|
||||
MessageSender.sendMediaBroadcast(this, messages);
|
||||
return null;
|
||||
}, (nothing) -> {
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull SlideDeck buildSlideDeck(@NonNull List<Media> mediaList) {
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
|
||||
for (Media mediaItem : mediaList) {
|
||||
if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
|
||||
slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), 0, mediaItem.getCaption().orNull()));
|
||||
} else if (MediaUtil.isGif(mediaItem.getMimeType())) {
|
||||
slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull()));
|
||||
} else if (MediaUtil.isImageType(mediaItem.getMimeType())) {
|
||||
slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull(), null));
|
||||
} else {
|
||||
Log.w(TAG, "Asked to send an unexpected mimeType: '" + mediaItem.getMimeType() + "'. Skipping.");
|
||||
}
|
||||
}
|
||||
|
||||
return slideDeck;
|
||||
}
|
||||
|
||||
private class ComposeKeyPressedListener implements View.OnKeyListener, View.OnClickListener, TextWatcher, View.OnFocusChangeListener {
|
||||
|
||||
int beforeLength;
|
||||
@ -1033,8 +904,4 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {}
|
||||
}
|
||||
|
||||
private interface OnProcessComplete {
|
||||
void onComplete(@NonNull List<Media> media);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,116 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender.PreUploadResult;
|
||||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A class that lets us nicely format data that we'll send back to {@link ConversationActivity}.
|
||||
*/
|
||||
public class MediaSendActivityResult implements Parcelable {
|
||||
private final Collection<PreUploadResult> uploadResults;
|
||||
private final Collection<Media> nonUploadedMedia;
|
||||
private final String body;
|
||||
private final TransportOption transport;
|
||||
private final boolean viewOnce;
|
||||
|
||||
static @NonNull MediaSendActivityResult forPreUpload(@NonNull Collection<PreUploadResult> uploadResults,
|
||||
@NonNull String body,
|
||||
@NonNull TransportOption transport,
|
||||
boolean viewOnce)
|
||||
{
|
||||
Preconditions.checkArgument(uploadResults.size() > 0, "Must supply uploadResults!");
|
||||
return new MediaSendActivityResult(uploadResults, Collections.emptyList(), body, transport, viewOnce);
|
||||
}
|
||||
|
||||
static @NonNull MediaSendActivityResult forTraditionalSend(@NonNull List<Media> nonUploadedMedia,
|
||||
@NonNull String body,
|
||||
@NonNull TransportOption transport,
|
||||
boolean viewOnce)
|
||||
{
|
||||
Preconditions.checkArgument(nonUploadedMedia.size() > 0, "Must supply media!");
|
||||
return new MediaSendActivityResult(Collections.emptyList(), nonUploadedMedia, body, transport, viewOnce);
|
||||
}
|
||||
|
||||
private MediaSendActivityResult(@NonNull Collection<PreUploadResult> uploadResults,
|
||||
@NonNull List<Media> nonUploadedMedia,
|
||||
@NonNull String body,
|
||||
@NonNull TransportOption transport,
|
||||
boolean viewOnce)
|
||||
{
|
||||
this.uploadResults = uploadResults;
|
||||
this.nonUploadedMedia = nonUploadedMedia;
|
||||
this.body = body;
|
||||
this.transport = transport;
|
||||
this.viewOnce = viewOnce;
|
||||
}
|
||||
|
||||
private MediaSendActivityResult(Parcel in) {
|
||||
this.uploadResults = ParcelUtil.readParcelableCollection(in, PreUploadResult.class);
|
||||
this.nonUploadedMedia = ParcelUtil.readParcelableCollection(in, Media.class);
|
||||
this.body = in.readString();
|
||||
this.transport = in.readParcelable(TransportOption.class.getClassLoader());
|
||||
this.viewOnce = ParcelUtil.readBoolean(in);
|
||||
}
|
||||
|
||||
public boolean isPushPreUpload() {
|
||||
return uploadResults.size() > 0;
|
||||
}
|
||||
|
||||
public @NonNull Collection<PreUploadResult> getPreUploadResults() {
|
||||
return uploadResults;
|
||||
}
|
||||
|
||||
public @NonNull Collection<Media> getNonUploadedMedia() {
|
||||
return nonUploadedMedia;
|
||||
}
|
||||
|
||||
public @NonNull String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public @NonNull TransportOption getTransport() {
|
||||
return transport;
|
||||
}
|
||||
|
||||
public boolean isViewOnce() {
|
||||
return viewOnce;
|
||||
}
|
||||
|
||||
public static final Creator<MediaSendActivityResult> CREATOR = new Creator<MediaSendActivityResult>() {
|
||||
@Override
|
||||
public MediaSendActivityResult createFromParcel(Parcel in) {
|
||||
return new MediaSendActivityResult(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaSendActivityResult[] newArray(int size) {
|
||||
return new MediaSendActivityResult[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
ParcelUtil.writeParcelableCollection(dest, uploadResults);
|
||||
ParcelUtil.writeParcelableCollection(dest, nonUploadedMedia);
|
||||
dest.writeString(body);
|
||||
dest.writeParcelable(transport, 0);
|
||||
ParcelUtil.writeBoolean(dest, viewOnce);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.mediasend;
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
@ -10,26 +11,41 @@ import androidx.lifecycle.ViewModelProvider;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender.PreUploadResult;
|
||||
import org.thoughtcrime.securesms.util.DiffHelper;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageUtil;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Manages the observable datasets available in {@link MediaSendActivity}.
|
||||
@ -43,6 +59,7 @@ class MediaSendViewModel extends ViewModel {
|
||||
|
||||
private final Application application;
|
||||
private final MediaRepository repository;
|
||||
private final MediaUploadRepository uploadRepository;
|
||||
private final MutableLiveData<List<Media>> selectedMedia;
|
||||
private final MutableLiveData<List<Media>> bucketMedia;
|
||||
private final MutableLiveData<Optional<Media>> mostRecentMedia;
|
||||
@ -54,13 +71,16 @@ class MediaSendViewModel extends ViewModel {
|
||||
private final SingleLiveEvent<Event> event;
|
||||
private final Map<Uri, Object> savedDrawState;
|
||||
|
||||
private TransportOption transport;
|
||||
private MediaConstraints mediaConstraints;
|
||||
private CharSequence body;
|
||||
private boolean sentMedia;
|
||||
private int maxSelection;
|
||||
private Page page;
|
||||
private boolean isSms;
|
||||
private boolean meteredConnection;
|
||||
private Optional<Media> lastCameraCapture;
|
||||
private boolean preUploadEnabled;
|
||||
|
||||
private boolean hudVisible;
|
||||
private boolean composeVisible;
|
||||
@ -69,11 +89,16 @@ class MediaSendViewModel extends ViewModel {
|
||||
private RailState railState;
|
||||
private ViewOnceState viewOnceState;
|
||||
|
||||
|
||||
private @Nullable Recipient recipient;
|
||||
|
||||
private MediaSendViewModel(@NonNull Application application, @NonNull MediaRepository repository) {
|
||||
private MediaSendViewModel(@NonNull Application application,
|
||||
@NonNull MediaRepository repository,
|
||||
@NonNull MediaUploadRepository uploadRepository)
|
||||
{
|
||||
this.application = application;
|
||||
this.repository = repository;
|
||||
this.uploadRepository = uploadRepository;
|
||||
this.selectedMedia = new MutableLiveData<>();
|
||||
this.bucketMedia = new MutableLiveData<>();
|
||||
this.mostRecentMedia = new MutableLiveData<>();
|
||||
@ -90,11 +115,14 @@ class MediaSendViewModel extends ViewModel {
|
||||
this.railState = RailState.GONE;
|
||||
this.viewOnceState = ViewOnceState.GONE;
|
||||
this.page = Page.UNKNOWN;
|
||||
this.preUploadEnabled = true;
|
||||
|
||||
position.setValue(-1);
|
||||
}
|
||||
|
||||
void setTransport(@NonNull TransportOption transport) {
|
||||
this.transport = transport;
|
||||
|
||||
if (transport.isSms()) {
|
||||
isSms = true;
|
||||
maxSelection = MAX_SMS;
|
||||
@ -104,20 +132,24 @@ class MediaSendViewModel extends ViewModel {
|
||||
maxSelection = MAX_PUSH;
|
||||
mediaConstraints = MediaConstraints.getPushMediaConstraints();
|
||||
}
|
||||
|
||||
preUploadEnabled = shouldPreUpload(application, meteredConnection, isSms, recipient);
|
||||
}
|
||||
|
||||
void setRecipient(@Nullable Recipient recipient) {
|
||||
this.recipient = recipient;
|
||||
this.recipient = recipient;
|
||||
this.preUploadEnabled = shouldPreUpload(application, meteredConnection, isSms, recipient);
|
||||
}
|
||||
|
||||
void onSelectedMediaChanged(@NonNull Context context, @NonNull List<Media> newMedia) {
|
||||
List<Media> originalMedia = getSelectedMediaOrDefault();
|
||||
|
||||
if (!newMedia.isEmpty()) {
|
||||
selectedMedia.setValue(newMedia);
|
||||
}
|
||||
|
||||
repository.getPopulatedMedia(context, newMedia, populatedMedia -> {
|
||||
Util.runOnMain(() -> {
|
||||
|
||||
List<Media> filteredMedia = getFilteredMedia(context, populatedMedia, mediaConstraints);
|
||||
|
||||
if (filteredMedia.size() != newMedia.size()) {
|
||||
@ -153,6 +185,8 @@ class MediaSendViewModel extends ViewModel {
|
||||
selectedMedia.setValue(filteredMedia);
|
||||
hudState.setValue(buildHudState());
|
||||
}
|
||||
|
||||
updateAttachmentUploads(originalMedia, getSelectedMediaOrDefault());
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -221,6 +255,7 @@ class MediaSendViewModel extends ViewModel {
|
||||
selected.remove(lastCameraCapture.get());
|
||||
selectedMedia.setValue(selected);
|
||||
BlobProvider.getInstance().delete(application, lastCameraCapture.get().getUri());
|
||||
cancelUpload(lastCameraCapture.get());
|
||||
}
|
||||
|
||||
hudState.setValue(buildHudState());
|
||||
@ -350,6 +385,8 @@ class MediaSendViewModel extends ViewModel {
|
||||
BlobProvider.getInstance().delete(context, removed.getUri());
|
||||
}
|
||||
|
||||
cancelUpload(removed);
|
||||
|
||||
if (page == Page.EDITOR && getSelectedMediaOrDefault().isEmpty()) {
|
||||
error.setValue(Error.NO_ITEMS);
|
||||
} else {
|
||||
@ -385,6 +422,8 @@ class MediaSendViewModel extends ViewModel {
|
||||
selectedMedia.setValue(selected);
|
||||
position.setValue(selected.size() - 1);
|
||||
bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID);
|
||||
|
||||
startUpload(media);
|
||||
}
|
||||
|
||||
void onCaptionChanged(@NonNull String newCaption) {
|
||||
@ -397,13 +436,66 @@ class MediaSendViewModel extends ViewModel {
|
||||
repository.getMostRecentItem(application, mostRecentMedia::postValue);
|
||||
}
|
||||
|
||||
void onMeteredConnectivityStatusChanged(boolean metered) {
|
||||
Log.i(TAG, "Metered connectivity status set to: " + metered);
|
||||
|
||||
meteredConnection = metered;
|
||||
preUploadEnabled = shouldPreUpload(application, metered, isSms, recipient);
|
||||
}
|
||||
|
||||
void saveDrawState(@NonNull Map<Uri, Object> state) {
|
||||
savedDrawState.clear();
|
||||
savedDrawState.putAll(state);
|
||||
}
|
||||
|
||||
void onSendClicked() {
|
||||
@NonNull LiveData<MediaSendActivityResult> onSendClicked(Map<Media, EditorModel> modelsToRender, @NonNull List<Recipient> recipients) {
|
||||
if (isSms && recipients.size() > 0) {
|
||||
throw new IllegalStateException("Provided recipients to send to, but this is SMS!");
|
||||
}
|
||||
|
||||
MutableLiveData<MediaSendActivityResult> result = new MutableLiveData<>();
|
||||
Runnable dialogRunnable = () -> event.postValue(Event.SHOW_RENDER_PROGRESS);
|
||||
String trimmedBody = isViewOnce() ? "" : body.toString().trim();
|
||||
List<Media> initialMedia = getSelectedMediaOrDefault();
|
||||
|
||||
Preconditions.checkState(initialMedia.size() > 0, "No media to send!");
|
||||
|
||||
Util.runOnMainDelayed(dialogRunnable, 250);
|
||||
|
||||
repository.renderMedia(application, initialMedia, modelsToRender, (oldToNew) -> {
|
||||
List<Media> updatedMedia = new ArrayList<>(oldToNew.values());
|
||||
|
||||
if (isSms || MessageSender.isLocalSelfSend(application, recipient, isSms)) {
|
||||
Log.i(TAG, "SMS or local self-send. Skipping pre-upload.");
|
||||
result.postValue(MediaSendActivityResult.forTraditionalSend(updatedMedia, trimmedBody, transport, isViewOnce()));
|
||||
return;
|
||||
}
|
||||
|
||||
MessageUtil.SplitResult splitMessage = MessageUtil.getSplitMessage(application, trimmedBody, transport.calculateCharacters(trimmedBody).maxPrimaryMessageSize);
|
||||
String splitBody = splitMessage.getBody();
|
||||
|
||||
if (splitMessage.getTextSlide().isPresent()) {
|
||||
Slide slide = splitMessage.getTextSlide().get();
|
||||
uploadRepository.startUpload(new Media(Objects.requireNonNull(slide.getUri()), slide.getContentType(), System.currentTimeMillis(), 0, 0, slide.getFileSize(), Optional.absent(), Optional.absent()), recipient);
|
||||
}
|
||||
|
||||
uploadRepository.applyMediaUpdates(oldToNew, recipient);
|
||||
uploadRepository.updateCaptions(updatedMedia);
|
||||
uploadRepository.updateDisplayOrder(updatedMedia);
|
||||
uploadRepository.getPreUploadResults(uploadResults -> {
|
||||
if (recipients.size() > 0) {
|
||||
sendMessages(recipients, splitBody, uploadResults);
|
||||
uploadRepository.deleteAbandonedAttachments();
|
||||
}
|
||||
|
||||
Util.cancelRunnableOnMain(dialogRunnable);
|
||||
result.postValue(MediaSendActivityResult.forPreUpload(uploadResults, splitBody, transport, isViewOnce()));
|
||||
});
|
||||
});
|
||||
|
||||
sentMedia = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@NonNull Map<Uri, Object> getDrawState() {
|
||||
@ -424,7 +516,7 @@ class MediaSendViewModel extends ViewModel {
|
||||
return folders;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Optional<Media>> getMostRecentMediaItem(@NonNull Context context) {
|
||||
@NonNull LiveData<Optional<Media>> getMostRecentMediaItem() {
|
||||
return mostRecentMedia;
|
||||
}
|
||||
|
||||
@ -512,10 +604,63 @@ class MediaSendViewModel extends ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAttachmentUploads(@NonNull List<Media> oldMedia, @NonNull List<Media> newMedia) {
|
||||
if (!preUploadEnabled) return;
|
||||
|
||||
DiffHelper.Result<Media> result = DiffHelper.calculate(oldMedia, newMedia);
|
||||
|
||||
uploadRepository.cancelUpload(result.getRemoved());
|
||||
uploadRepository.startUpload(result.getInserted(), recipient);
|
||||
}
|
||||
|
||||
private void cancelUpload(@NonNull Media media) {
|
||||
uploadRepository.cancelUpload(media);
|
||||
}
|
||||
|
||||
private void startUpload(@NonNull Media media) {
|
||||
if (!preUploadEnabled) return;
|
||||
uploadRepository.startUpload(media, recipient);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void sendMessages(@NonNull List<Recipient> recipients, @NonNull String body, @NonNull Collection<PreUploadResult> preUploadResults) {
|
||||
List<OutgoingSecureMediaMessage> messages = new ArrayList<>(recipients.size());
|
||||
|
||||
for (Recipient recipient : recipients) {
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient,
|
||||
body,
|
||||
Collections.emptyList(),
|
||||
System.currentTimeMillis(),
|
||||
-1,
|
||||
recipient.getExpireMessages() * 1000,
|
||||
isViewOnce(),
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
messages.add(new OutgoingSecureMediaMessage(message));
|
||||
|
||||
// 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.
|
||||
Util.sleep(5);
|
||||
}
|
||||
|
||||
MessageSender.sendMediaBroadcast(application, messages, preUploadResults);
|
||||
}
|
||||
|
||||
private static boolean shouldPreUpload(@NonNull Context context, boolean metered, boolean isSms, @Nullable Recipient recipient) {
|
||||
return !metered && !isSms && !MessageSender.isLocalSelfSend(context, recipient, isSms);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
if (!sentMedia) {
|
||||
clearPersistedMedia();
|
||||
uploadRepository.cancelAllUploads();
|
||||
uploadRepository.deleteAbandonedAttachments();
|
||||
}
|
||||
}
|
||||
|
||||
@ -524,7 +669,7 @@ class MediaSendViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
enum Event {
|
||||
VIEW_ONCE_TOOLTIP
|
||||
VIEW_ONCE_TOOLTIP, SHOW_RENDER_PROGRESS, HIDE_RENDER_PROGRESS
|
||||
}
|
||||
|
||||
enum Page {
|
||||
@ -611,7 +756,7 @@ class MediaSendViewModel extends ViewModel {
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return modelClass.cast(new MediaSendViewModel(application, repository));
|
||||
return modelClass.cast(new MediaSendViewModel(application, repository, new MediaUploadRepository(application)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,207 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GifSlide;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.TextSlide;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender.PreUploadResult;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Manages the proactive upload of media during the selection process. Upload/cancel operations
|
||||
* need to be serialized, because they're asynchronous operations that depend on ordered completion.
|
||||
*
|
||||
* For example, if we begin upload of a {@link Media) but then immediately cancel it (before it was
|
||||
* enqueued on the {@link JobManager}), we need to wait until we have the jobId to cancel. This
|
||||
* class manages everything by using a single thread executor.
|
||||
*
|
||||
* This also means that unlike most repositories, the class itself is stateful. Keep that in mind
|
||||
* when using it.
|
||||
*/
|
||||
class MediaUploadRepository {
|
||||
|
||||
private static final String TAG = Log.tag(MediaUploadRepository.class);
|
||||
|
||||
private final Context context;
|
||||
private final LinkedHashMap<Media, PreUploadResult> uploadResults;
|
||||
private final Executor executor;
|
||||
|
||||
MediaUploadRepository(@NonNull Context context) {
|
||||
this.context = context;
|
||||
this.uploadResults = new LinkedHashMap<>();
|
||||
this.executor = SignalExecutors.newCachedSingleThreadExecutor("signal-MediaUpload");
|
||||
}
|
||||
|
||||
void startUpload(@NonNull Media media, @Nullable Recipient recipient) {
|
||||
executor.execute(() -> uploadMediaInternal(media, recipient));
|
||||
}
|
||||
|
||||
void startUpload(@NonNull Collection<Media> mediaItems, @Nullable Recipient recipient) {
|
||||
executor.execute(() -> {
|
||||
for (Media media : mediaItems) {
|
||||
cancelUploadInternal(media);
|
||||
uploadMediaInternal(media, recipient);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a map of old->new, cancel medias that were changed and upload their replacements. Will
|
||||
* also upload any media in the map that wasn't yet uploaded.
|
||||
*/
|
||||
void applyMediaUpdates(@NonNull Map<Media, Media> oldToNew, @Nullable Recipient recipient) {
|
||||
executor.execute(() -> {
|
||||
for (Map.Entry<Media, Media> entry : oldToNew.entrySet()) {
|
||||
if (!entry.getKey().equals(entry.getValue()) || !uploadResults.containsKey(entry.getValue())) {
|
||||
cancelUploadInternal(entry.getKey());
|
||||
uploadMediaInternal(entry.getValue(), recipient);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void cancelUpload(@NonNull Media media) {
|
||||
executor.execute(() -> cancelUploadInternal(media));
|
||||
}
|
||||
|
||||
void cancelUpload(@NonNull Collection<Media> mediaItems) {
|
||||
executor.execute(() -> {
|
||||
for (Media media : mediaItems) {
|
||||
cancelUploadInternal(media);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void cancelAllUploads() {
|
||||
executor.execute(() -> {
|
||||
for (Media media : new HashSet<>(uploadResults.keySet())) {
|
||||
cancelUploadInternal(media);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void getPreUploadResults(@NonNull Callback<Collection<PreUploadResult>> callback) {
|
||||
executor.execute(() -> callback.onResult(uploadResults.values()));
|
||||
}
|
||||
|
||||
void updateCaptions(@NonNull List<Media> updatedMedia) {
|
||||
executor.execute(() -> updateCaptionsInternal(updatedMedia));
|
||||
}
|
||||
|
||||
void updateDisplayOrder(@NonNull List<Media> mediaInOrder) {
|
||||
executor.execute(() -> updateDisplayOrderInternal(mediaInOrder));
|
||||
}
|
||||
|
||||
void deleteAbandonedAttachments() {
|
||||
executor.execute(() -> {
|
||||
int deleted = DatabaseFactory.getAttachmentDatabase(context).deleteAbandonedPreuploadedAttachments();
|
||||
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void uploadMediaInternal(@NonNull Media media, @Nullable Recipient recipient) {
|
||||
Attachment attachment = asAttachment(context, media);
|
||||
PreUploadResult result = MessageSender.preUploadPushAttachment(context, attachment, recipient);
|
||||
|
||||
if (result != null) {
|
||||
uploadResults.put(media, result);
|
||||
} else {
|
||||
Log.w(TAG, "Failed to upload media with URI: " + media.getUri());
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelUploadInternal(@NonNull Media media) {
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
PreUploadResult result = uploadResults.get(media);
|
||||
|
||||
if (result != null) {
|
||||
Stream.of(result.getJobIds()).forEach(jobManager::cancel);
|
||||
uploadResults.remove(media);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void updateCaptionsInternal(@NonNull List<Media> updatedMedia) {
|
||||
AttachmentDatabase db = DatabaseFactory.getAttachmentDatabase(context);
|
||||
|
||||
for (Media updated : updatedMedia) {
|
||||
PreUploadResult result = uploadResults.get(updated);
|
||||
|
||||
if (result != null) {
|
||||
db.updateAttachmentCaption(result.getAttachmentId(), updated.getCaption().orNull());
|
||||
} else {
|
||||
Log.w(TAG,"When updating captions, no pre-upload result could be found for media with URI: " + updated.getUri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void updateDisplayOrderInternal(@NonNull List<Media> mediaInOrder) {
|
||||
Map<AttachmentId, Integer> orderMap = new HashMap<>();
|
||||
Map<Media, PreUploadResult> orderedUploadResults = new LinkedHashMap<>();
|
||||
|
||||
for (int i = 0; i < mediaInOrder.size(); i++) {
|
||||
Media media = mediaInOrder.get(i);
|
||||
PreUploadResult result = uploadResults.get(media);
|
||||
|
||||
if (result != null) {
|
||||
orderMap.put(result.getAttachmentId(), i);
|
||||
orderedUploadResults.put(media, result);
|
||||
} else {
|
||||
Log.w(TAG, "When updating display order, no pre-upload result could be found for media with URI: " + media.getUri());
|
||||
}
|
||||
}
|
||||
|
||||
DatabaseFactory.getAttachmentDatabase(context).updateDisplayOrder(orderMap);
|
||||
|
||||
if (orderedUploadResults.size() == uploadResults.size()) {
|
||||
uploadResults.clear();
|
||||
uploadResults.putAll(orderedUploadResults);
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull Attachment asAttachment(@NonNull Context context, @NonNull Media media) {
|
||||
if (MediaUtil.isVideoType(media.getMimeType())) {
|
||||
return new VideoSlide(context, media.getUri(), 0, media.getCaption().orNull()).asAttachment();
|
||||
} else if (MediaUtil.isGif(media.getMimeType())) {
|
||||
return new GifSlide(context, media.getUri(), 0, media.getWidth(), media.getHeight(), media.getCaption().orNull()).asAttachment();
|
||||
} else if (MediaUtil.isImageType(media.getMimeType())) {
|
||||
return new ImageSlide(context, media.getUri(), 0, media.getWidth(), media.getHeight(), media.getCaption().orNull(), null).asAttachment();
|
||||
} else if (MediaUtil.isTextType(media.getMimeType())) {
|
||||
return new TextSlide(context, media.getUri(), null, media.getSize()).asAttachment();
|
||||
} else {
|
||||
throw new AssertionError("Unexpected mimeType: " + media.getMimeType());
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback<E> {
|
||||
void onResult(@NonNull E result);
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.net.ConnectivityManagerCompat;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
|
||||
/**
|
||||
* Lifecycle-bound observer for whether or not the active network connection is metered.
|
||||
*/
|
||||
class MeteredConnectivityObserver extends BroadcastReceiver implements DefaultLifecycleObserver {
|
||||
|
||||
private final Context context;
|
||||
private final ConnectivityManager connectivityManager;
|
||||
private final MutableLiveData<Boolean> metered;
|
||||
|
||||
@MainThread
|
||||
MeteredConnectivityObserver(@NonNull Context context, @NonNull LifecycleOwner lifecycleOwner) {
|
||||
this.context = context;
|
||||
this.connectivityManager = ServiceUtil.getConnectivityManager(context);
|
||||
this.metered = new MutableLiveData<>();
|
||||
|
||||
this.metered.setValue(ConnectivityManagerCompat.isActiveNetworkMetered(connectivityManager));
|
||||
lifecycleOwner.getLifecycle().addObserver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@NonNull LifecycleOwner owner) {
|
||||
context.registerReceiver(this, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(@NonNull LifecycleOwner owner) {
|
||||
context.unregisterReceiver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
metered.postValue(ConnectivityManagerCompat.isActiveNetworkMetered(connectivityManager));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An observable value that is false when the network is unmetered, and true if the
|
||||
* network is either metered or unavailable.
|
||||
*/
|
||||
@NonNull LiveData<Boolean> isMetered() {
|
||||
return metered;
|
||||
}
|
||||
}
|
@ -17,8 +17,11 @@
|
||||
package org.thoughtcrime.securesms.sms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
@ -59,14 +62,16 @@ import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class MessageSender {
|
||||
|
||||
@ -118,7 +123,7 @@ public class MessageSender {
|
||||
Recipient recipient = message.getRecipient();
|
||||
long messageId = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
|
||||
|
||||
sendMediaMessage(context, recipient, forceSms, messageId, message.getExpiresIn());
|
||||
sendMediaMessage(context, recipient, forceSms, messageId, Collections.emptyList());
|
||||
|
||||
return allocatedThreadId;
|
||||
} catch (MmsException e) {
|
||||
@ -127,72 +132,97 @@ public class MessageSender {
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendMediaBroadcast(@NonNull Context context, @NonNull List<OutgoingSecureMediaMessage> messages) {
|
||||
if (messages.isEmpty()) {
|
||||
Log.w(TAG, "sendMediaBroadcast() - No messages!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidBroadcastList(messages)) {
|
||||
Log.w(TAG, "sendMediaBroadcast() - Invalid message list!");
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
List<List<DatabaseAttachment>> databaseAttachments = new ArrayList<>(messages.get(0).getAttachments().size());
|
||||
List<Long> messageIds = new ArrayList<>(messages.size());
|
||||
|
||||
for (int i = 0; i < messages.get(0).getAttachments().size(); i++) {
|
||||
databaseAttachments.add(new ArrayList<>(messages.size()));
|
||||
}
|
||||
public static long sendPushWithPreUploadedMedia(final Context context,
|
||||
final OutgoingMediaMessage message,
|
||||
final Collection<PreUploadResult> preUploadResults,
|
||||
final long threadId,
|
||||
final SmsDatabase.InsertListener insertListener)
|
||||
{
|
||||
Preconditions.checkArgument(message.getAttachments().isEmpty(), "If the media is pre-uploaded, there should be no attachments on the message.");
|
||||
|
||||
try {
|
||||
try {
|
||||
mmsDatabase.beginTransaction();
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
|
||||
for (OutgoingSecureMediaMessage message : messages) {
|
||||
long allocatedThreadId = threadDatabase.getThreadIdFor(message.getRecipient(), message.getDistributionType());
|
||||
long messageId = mmsDatabase.insertMessageOutbox(message, allocatedThreadId, false, null);
|
||||
List<DatabaseAttachment> attachments = attachmentDatabase.getAttachmentsForMessage(messageId);
|
||||
long allocatedThreadId;
|
||||
|
||||
if (attachments.size() != databaseAttachments.size()) {
|
||||
Log.w(TAG, "Got back an attachment list that was a different size than expected. Expected: " + databaseAttachments.size() + " Actual: "+ attachments.size());
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < attachments.size(); i++) {
|
||||
databaseAttachments.get(i).add(attachments.get(i));
|
||||
if (threadId == -1) {
|
||||
allocatedThreadId = threadDatabase.getThreadIdFor(message.getRecipient(), message.getDistributionType());
|
||||
} else {
|
||||
allocatedThreadId = threadId;
|
||||
}
|
||||
|
||||
Recipient recipient = message.getRecipient();
|
||||
long messageId = mmsDatabase.insertMessageOutbox(message, allocatedThreadId, false, insertListener);
|
||||
|
||||
List<AttachmentId> attachmentIds = Stream.of(preUploadResults).map(PreUploadResult::getAttachmentId).toList();
|
||||
List<String> jobIds = Stream.of(preUploadResults).map(PreUploadResult::getJobIds).flatMap(Stream::of).toList();
|
||||
|
||||
attachmentDatabase.updateMessageId(attachmentIds, messageId);
|
||||
|
||||
sendMediaMessage(context, recipient, false, messageId, jobIds);
|
||||
|
||||
return allocatedThreadId;
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, e);
|
||||
return threadId;
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendMediaBroadcast(@NonNull Context context, @NonNull List<OutgoingSecureMediaMessage> messages, @NonNull Collection<PreUploadResult> preUploadResults) {
|
||||
Preconditions.checkArgument(messages.size() > 0, "No messages!");
|
||||
Preconditions.checkArgument(Stream.of(messages).allMatch(m -> m.getAttachments().isEmpty()), "Messages can't have attachments! They should be pre-uploaded.");
|
||||
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
List<AttachmentId> preUploadAttachmentIds = Stream.of(preUploadResults).map(PreUploadResult::getAttachmentId).toList();
|
||||
List<String> preUploadJobIds = Stream.of(preUploadResults).map(PreUploadResult::getJobIds).flatMap(Stream::of).toList();
|
||||
List<Long> messageIds = new ArrayList<>(messages.size());
|
||||
List<String> messageDependsOnIds = new ArrayList<>(preUploadJobIds);
|
||||
|
||||
mmsDatabase.beginTransaction();
|
||||
try {
|
||||
OutgoingSecureMediaMessage primaryMessage = messages.get(0);
|
||||
long primaryThreadId = threadDatabase.getThreadIdFor(primaryMessage.getRecipient(), primaryMessage.getDistributionType());
|
||||
long primaryMessageId = mmsDatabase.insertMessageOutbox(primaryMessage, primaryThreadId, false, null);
|
||||
|
||||
attachmentDatabase.updateMessageId(preUploadAttachmentIds, primaryMessageId);
|
||||
messageIds.add(primaryMessageId);
|
||||
|
||||
if (messages.size() > 0) {
|
||||
List<OutgoingSecureMediaMessage> secondaryMessages = messages.subList(1, messages.size());
|
||||
List<List<AttachmentId>> attachmentCopies = new ArrayList<>();
|
||||
List<DatabaseAttachment> preUploadAttachments = Stream.of(preUploadAttachmentIds)
|
||||
.map(attachmentDatabase::getAttachment)
|
||||
.toList();
|
||||
|
||||
for (int i = 0; i < preUploadAttachmentIds.size(); i++) {
|
||||
attachmentCopies.add(new ArrayList<>(messages.size()));
|
||||
}
|
||||
|
||||
for (OutgoingSecureMediaMessage secondaryMessage : secondaryMessages) {
|
||||
long allocatedThreadId = threadDatabase.getThreadIdFor(secondaryMessage.getRecipient(), secondaryMessage.getDistributionType());
|
||||
long messageId = mmsDatabase.insertMessageOutbox(secondaryMessage, allocatedThreadId, false, null);
|
||||
List<AttachmentId> attachmentIds = new ArrayList<>(preUploadAttachmentIds.size());
|
||||
|
||||
for (int i = 0; i < preUploadAttachments.size(); i++) {
|
||||
AttachmentId attachmentId = attachmentDatabase.insertAttachmentForPreUpload(preUploadAttachments.get(i)).getAttachmentId();
|
||||
attachmentCopies.get(i).add(attachmentId);
|
||||
attachmentIds.add(attachmentId);
|
||||
}
|
||||
|
||||
attachmentDatabase.updateMessageId(attachmentIds, messageId);
|
||||
messageIds.add(messageId);
|
||||
}
|
||||
|
||||
mmsDatabase.setTransactionSuccessful();
|
||||
} finally {
|
||||
mmsDatabase.endTransaction();
|
||||
}
|
||||
|
||||
List<Job> compressionJobs = new ArrayList<>(databaseAttachments.size());
|
||||
List<Job> uploadJobs = new ArrayList<>(databaseAttachments.size());
|
||||
List<Job> copyJobs = new ArrayList<>(databaseAttachments.size());
|
||||
List<Job> messageJobs = new ArrayList<>(databaseAttachments.get(0).size());
|
||||
|
||||
for (List<DatabaseAttachment> attachmentList : databaseAttachments) {
|
||||
DatabaseAttachment source = attachmentList.get(0);
|
||||
|
||||
compressionJobs.add(AttachmentCompressionJob.fromAttachment(source, false, -1));
|
||||
|
||||
uploadJobs.add(new AttachmentUploadJob(source.getAttachmentId()));
|
||||
|
||||
if (attachmentList.size() > 1) {
|
||||
AttachmentId sourceId = source.getAttachmentId();
|
||||
List<AttachmentId> destinationIds = Stream.of(attachmentList.subList(1, attachmentList.size()))
|
||||
.map(DatabaseAttachment::getAttachmentId)
|
||||
.toList();
|
||||
|
||||
copyJobs.add(new AttachmentCopyJob(sourceId, destinationIds));
|
||||
for (int i = 0; i < attachmentCopies.size(); i++) {
|
||||
Job copyJob = new AttachmentCopyJob(preUploadAttachmentIds.get(i), attachmentCopies.get(i));
|
||||
jobManager.add(copyJob, preUploadJobIds);
|
||||
messageDependsOnIds.add(copyJob.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,32 +234,47 @@ public class MessageSender {
|
||||
if (isLocalSelfSend(context, recipient, false)) {
|
||||
sendLocalMediaSelf(context, messageId);
|
||||
} else if (isGroupPushSend(recipient)) {
|
||||
messageJobs.add(new PushGroupSendJob(messageId, recipient.getId(), null));
|
||||
jobManager.add(new PushGroupSendJob(messageId, recipient.getId(), null), messageDependsOnIds);
|
||||
} else {
|
||||
messageJobs.add(new PushMediaSendJob(messageId, recipient));
|
||||
jobManager.add(new PushMediaSendJob(messageId, recipient), messageDependsOnIds);
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, String.format(Locale.ENGLISH, "sendMediaBroadcast() - Uploading %d attachment(s), copying %d of them, then sending %d messages.",
|
||||
uploadJobs.size(),
|
||||
copyJobs.size(),
|
||||
messageJobs.size()));
|
||||
|
||||
JobManager.Chain chain = ApplicationDependencies.getJobManager()
|
||||
.startChain(compressionJobs)
|
||||
.then(uploadJobs);
|
||||
|
||||
if (copyJobs.size() > 0) {
|
||||
chain = chain.then(copyJobs);
|
||||
}
|
||||
|
||||
chain = chain.then(messageJobs);
|
||||
chain.enqueue();
|
||||
mmsDatabase.setTransactionSuccessful();
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, "sendMediaBroadcast() - Failed to send messages!", e);
|
||||
Log.w(TAG, "Failed to send messages.", e);
|
||||
} finally {
|
||||
mmsDatabase.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A result if the attachment was enqueued, or null if it failed to enqueue or shouldn't
|
||||
* be enqueued (like in the case of a local self-send).
|
||||
*/
|
||||
public static @Nullable PreUploadResult preUploadPushAttachment(@NonNull Context context, @NonNull Attachment attachment, @Nullable Recipient recipient) {
|
||||
if (recipient != null && isLocalSelfSend(context, recipient, false)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
DatabaseAttachment databaseAttachment = attachmentDatabase.insertAttachmentForPreUpload(attachment);
|
||||
|
||||
Job compressionJob = AttachmentCompressionJob.fromAttachment(databaseAttachment, false, -1);
|
||||
Job uploadJob = new AttachmentUploadJob(databaseAttachment.getAttachmentId());
|
||||
|
||||
ApplicationDependencies.getJobManager()
|
||||
.startChain(compressionJob)
|
||||
.then(uploadJob)
|
||||
.enqueue();
|
||||
|
||||
return new PreUploadResult(databaseAttachment.getAttachmentId(), Arrays.asList(compressionJob.getId(), uploadJob.getId()));
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, "preUploadPushAttachment() - Failed to upload!", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendNewReaction(@NonNull Context context, long messageId, boolean isMms, @NonNull String emoji) {
|
||||
MessagingDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
||||
@ -258,31 +303,30 @@ public class MessageSender {
|
||||
|
||||
public static void resendGroupMessage(Context context, MessageRecord messageRecord, RecipientId filterRecipientId) {
|
||||
if (!messageRecord.isMms()) throw new AssertionError("Not Group");
|
||||
sendGroupPush(context, messageRecord.getRecipient(), messageRecord.getId(), filterRecipientId);
|
||||
sendGroupPush(context, messageRecord.getRecipient(), messageRecord.getId(), filterRecipientId, Collections.emptyList());
|
||||
}
|
||||
|
||||
public static void resend(Context context, MessageRecord messageRecord) {
|
||||
long messageId = messageRecord.getId();
|
||||
boolean forceSms = messageRecord.isForcedSms();
|
||||
boolean keyExchange = messageRecord.isKeyExchange();
|
||||
long expiresIn = messageRecord.getExpiresIn();
|
||||
Recipient recipient = messageRecord.getRecipient();
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
sendMediaMessage(context, recipient, forceSms, messageId, expiresIn);
|
||||
sendMediaMessage(context, recipient, forceSms, messageId, Collections.emptyList());
|
||||
} else {
|
||||
sendTextMessage(context, recipient, forceSms, keyExchange, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendMediaMessage(Context context, Recipient recipient, boolean forceSms, long messageId, long expiresIn)
|
||||
private static void sendMediaMessage(Context context, Recipient recipient, boolean forceSms, long messageId, @NonNull Collection<String> uploadJobIds)
|
||||
{
|
||||
if (isLocalSelfSend(context, recipient, forceSms)) {
|
||||
sendLocalMediaSelf(context, messageId);
|
||||
} else if (isGroupPushSend(recipient)) {
|
||||
sendGroupPush(context, recipient, messageId, null);
|
||||
sendGroupPush(context, recipient, messageId, null, uploadJobIds);
|
||||
} else if (!forceSms && isPushMediaSend(context, recipient)) {
|
||||
sendMediaPush(context, recipient, messageId);
|
||||
sendMediaPush(context, recipient, messageId, uploadJobIds);
|
||||
} else {
|
||||
sendMms(context, messageId);
|
||||
}
|
||||
@ -295,25 +339,37 @@ public class MessageSender {
|
||||
if (isLocalSelfSend(context, recipient, forceSms)) {
|
||||
sendLocalTextSelf(context, messageId);
|
||||
} else if (!forceSms && isPushTextSend(context, recipient, keyExchange)) {
|
||||
sendTextPush(context, recipient, messageId);
|
||||
sendTextPush(recipient, messageId);
|
||||
} else {
|
||||
sendSms(context, recipient, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendTextPush(Context context, Recipient recipient, long messageId) {
|
||||
private static void sendTextPush(Recipient recipient, long messageId) {
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
jobManager.add(new PushTextSendJob(messageId, recipient));
|
||||
}
|
||||
|
||||
private static void sendMediaPush(Context context, Recipient recipient, long messageId) {
|
||||
private static void sendMediaPush(Context context, Recipient recipient, long messageId, @NonNull Collection<String> uploadJobIds) {
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient);
|
||||
|
||||
if (uploadJobIds.size() > 0) {
|
||||
Job mediaSend = new PushMediaSendJob(messageId, recipient);
|
||||
jobManager.add(mediaSend, uploadJobIds);
|
||||
} else {
|
||||
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendGroupPush(Context context, Recipient recipient, long messageId, RecipientId filterRecipientId) {
|
||||
private static void sendGroupPush(Context context, Recipient recipient, long messageId, RecipientId filterRecipientId, @NonNull Collection<String> uploadJobIds) {
|
||||
JobManager jobManager = ApplicationDependencies.getJobManager();
|
||||
PushGroupSendJob.enqueue(context, jobManager, messageId, recipient.getId(), filterRecipientId);
|
||||
|
||||
if (uploadJobIds.size() > 0) {
|
||||
Job groupSend = new PushGroupSendJob(messageId, recipient.getId(), filterRecipientId);
|
||||
jobManager.add(groupSend, uploadJobIds);
|
||||
} else {
|
||||
PushGroupSendJob.enqueue(context, jobManager, messageId, recipient.getId(), filterRecipientId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendSms(Context context, Recipient recipient, long messageId) {
|
||||
@ -370,8 +426,9 @@ public class MessageSender {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLocalSelfSend(@NonNull Context context, @NonNull Recipient recipient, boolean forceSms) {
|
||||
return recipient.isLocalNumber() &&
|
||||
public static boolean isLocalSelfSend(@NonNull Context context, @Nullable Recipient recipient, boolean forceSms) {
|
||||
return recipient != null &&
|
||||
recipient.isLocalNumber() &&
|
||||
!forceSms &&
|
||||
TextSecurePreferences.isPushRegistered(context) &&
|
||||
!TextSecurePreferences.isMultiDevice(context);
|
||||
@ -428,19 +485,49 @@ public class MessageSender {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isValidBroadcastList(@NonNull List<OutgoingSecureMediaMessage> messages) {
|
||||
if (messages.isEmpty()) {
|
||||
return false;
|
||||
public static class PreUploadResult implements Parcelable {
|
||||
private final AttachmentId attachmentId;
|
||||
private final Collection<String> jobIds;
|
||||
|
||||
PreUploadResult(@NonNull AttachmentId attachmentId, @NonNull Collection<String> jobIds) {
|
||||
this.attachmentId = attachmentId;
|
||||
this.jobIds = jobIds;
|
||||
}
|
||||
|
||||
int attachmentSize = messages.get(0).getAttachments().size();
|
||||
private PreUploadResult(Parcel in) {
|
||||
this.attachmentId = in.readParcelable(AttachmentId.class.getClassLoader());
|
||||
this.jobIds = ParcelUtil.readStringCollection(in);
|
||||
}
|
||||
|
||||
for (OutgoingSecureMediaMessage message : messages) {
|
||||
if (message.getAttachments().size() != attachmentSize) {
|
||||
return false;
|
||||
public @NonNull AttachmentId getAttachmentId() {
|
||||
return attachmentId;
|
||||
}
|
||||
|
||||
public @NonNull Collection<String> getJobIds() {
|
||||
return jobIds;
|
||||
}
|
||||
|
||||
public static final Creator<PreUploadResult> CREATOR = new Creator<PreUploadResult>() {
|
||||
@Override
|
||||
public PreUploadResult createFromParcel(Parcel in) {
|
||||
return new PreUploadResult(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreUploadResult[] newArray(int size) {
|
||||
return new PreUploadResult[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeParcelable(attachmentId, flags);
|
||||
ParcelUtil.writeStringCollection(dest, jobIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,44 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Helps determine the difference between two collections based on their {@link #equals(Object)}
|
||||
* implementations.
|
||||
*/
|
||||
public class DiffHelper {
|
||||
|
||||
/**
|
||||
* @return Result indicating the differences between the two collections. Important: The iteration
|
||||
* order of the result will not necessarily match the iteration order of the original
|
||||
* collection.
|
||||
*/
|
||||
public static <E> Result<E> calculate(@NonNull Collection<E> oldList, @NonNull Collection<E> newList) {
|
||||
Set<E> inserted = SetUtil.difference(newList, oldList);
|
||||
Set<E> removed = SetUtil.difference(oldList, newList);
|
||||
|
||||
return new Result<>(inserted, removed);
|
||||
}
|
||||
|
||||
public static class Result<E> {
|
||||
private final Collection<E> inserted;
|
||||
private final Collection<E> removed;
|
||||
|
||||
public Result(@NonNull Collection<E> inserted, @NonNull Collection<E> removed) {
|
||||
this.removed = removed;
|
||||
this.inserted = inserted;
|
||||
}
|
||||
|
||||
public @NonNull Collection<E> getInserted() {
|
||||
return inserted;
|
||||
}
|
||||
|
||||
public @NonNull Collection<E> getRemoved() {
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.mms.TextSlide;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class MessageUtil {
|
||||
|
||||
private MessageUtil() {}
|
||||
|
||||
/**
|
||||
* @return If the message is longer than the allowed text size, this will return trimmed text with
|
||||
* an accompanying TextSlide. Otherwise it'll just return the original text.
|
||||
*/
|
||||
public static SplitResult getSplitMessage(@NonNull Context context, @NonNull String rawText, int maxPrimaryMessageSize) {
|
||||
String bodyText = rawText;
|
||||
Optional<TextSlide> textSlide = Optional.absent();
|
||||
|
||||
if (bodyText.length() > maxPrimaryMessageSize) {
|
||||
bodyText = rawText.substring(0, maxPrimaryMessageSize);
|
||||
|
||||
byte[] textData = rawText.getBytes();
|
||||
String timestamp = new SimpleDateFormat("yyyy-MM-dd-HHmmss", Locale.US).format(new Date());
|
||||
String filename = String.format("signal-%s.txt", timestamp);
|
||||
Uri textUri = BlobProvider.getInstance()
|
||||
.forData(textData)
|
||||
.withMimeType(MediaUtil.LONG_TEXT)
|
||||
.withFileName(filename)
|
||||
.createForSingleSessionInMemory();
|
||||
|
||||
textSlide = Optional.of(new TextSlide(context, textUri, filename, textData.length));
|
||||
}
|
||||
|
||||
return new SplitResult(bodyText, textSlide);
|
||||
}
|
||||
|
||||
public static class SplitResult {
|
||||
private final String body;
|
||||
private final Optional<TextSlide> textSlide;
|
||||
|
||||
private SplitResult(@NonNull String body, @NonNull Optional<TextSlide> textSlide) {
|
||||
this.body = body;
|
||||
this.textSlide = textSlide;
|
||||
}
|
||||
|
||||
public @NonNull String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public @NonNull Optional<TextSlide> getTextSlide() {
|
||||
return textSlide;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,15 @@ package org.thoughtcrime.securesms.util;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class ParcelUtil {
|
||||
|
||||
public static byte[] serialize(Parcelable parceable) {
|
||||
@ -25,4 +34,31 @@ public class ParcelUtil {
|
||||
return creator.createFromParcel(parcel);
|
||||
}
|
||||
|
||||
public static void writeStringCollection(@NonNull Parcel dest, @NonNull Collection<String> collection) {
|
||||
dest.writeStringList(new ArrayList<>(collection));
|
||||
}
|
||||
|
||||
public static @NonNull Collection<String> readStringCollection(@NonNull Parcel in) {
|
||||
List<String> list = new ArrayList<>();
|
||||
in.readStringList(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
public static void writeParcelableCollection(@NonNull Parcel dest, @NonNull Collection<? extends Parcelable> collection) {
|
||||
Parcelable[] values = collection.toArray(new Parcelable[0]);
|
||||
dest.writeParcelableArray(values, 0);
|
||||
}
|
||||
|
||||
public static @NonNull <E> Collection<E> readParcelableCollection(@NonNull Parcel in, Class<E> clazz) {
|
||||
//noinspection unchecked
|
||||
return Arrays.asList((E[]) in.readParcelableArray(clazz.getClassLoader()));
|
||||
}
|
||||
|
||||
public static void writeBoolean(@NonNull Parcel dest, boolean value) {
|
||||
dest.writeByte(value ? (byte) 1 : 0);
|
||||
}
|
||||
|
||||
public static boolean readBoolean(@NonNull Parcel in) {
|
||||
return in.readByte() != 0;
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,19 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class SetUtil {
|
||||
private SetUtil() {}
|
||||
|
||||
public static <E> Set<E> intersection(Set<E> a, Set<E> b) {
|
||||
public static <E> Set<E> intersection(Collection<E> a, Collection<E> b) {
|
||||
Set<E> intersection = new LinkedHashSet<>(a);
|
||||
intersection.retainAll(b);
|
||||
return intersection;
|
||||
}
|
||||
|
||||
public static <E> Set<E> difference(Set<E> a, Set<E> b) {
|
||||
public static <E> Set<E> difference(Collection<E> a, Collection<E> b) {
|
||||
Set<E> difference = new LinkedHashSet<>(a);
|
||||
difference.removeAll(b);
|
||||
return difference;
|
||||
|
@ -84,7 +84,9 @@ public final class InMemoryTranscoder implements Closeable {
|
||||
: OUTPUT_FORMAT;
|
||||
}
|
||||
|
||||
public @NonNull MediaStream transcode(@NonNull Progress progress) throws IOException, EncodingException, VideoSizeException {
|
||||
public @NonNull MediaStream transcode(@NonNull Progress progress, @Nullable CancelationSignal cancelationSignal)
|
||||
throws IOException, EncodingException, VideoSizeException
|
||||
{
|
||||
if (memoryFile != null) throw new AssertionError("Not expecting to reuse transcoder");
|
||||
|
||||
float durationSec = duration / 1000f;
|
||||
@ -131,7 +133,7 @@ public final class InMemoryTranscoder implements Closeable {
|
||||
|
||||
converter.setListener(percent -> {
|
||||
progress.onProgress(percent);
|
||||
return false;
|
||||
return cancelationSignal != null && cancelationSignal.isCanceled();
|
||||
});
|
||||
|
||||
converter.convert();
|
||||
@ -211,7 +213,10 @@ public final class InMemoryTranscoder implements Closeable {
|
||||
}
|
||||
|
||||
public interface Progress {
|
||||
|
||||
void onProgress(int percent);
|
||||
}
|
||||
|
||||
public interface CancelationSignal {
|
||||
boolean isCanceled();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,57 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class DiffHelperTest {
|
||||
|
||||
private static final Object A = new Object();
|
||||
private static final Object B = new Object();
|
||||
private static final Object C = new Object();
|
||||
private static final Object D = new Object();
|
||||
|
||||
@Test
|
||||
public void calculate_allRemoved() {
|
||||
DiffHelper.Result result = DiffHelper.calculate(Arrays.asList(A, B), Collections.emptyList());
|
||||
|
||||
assertContentsEqual(Collections.emptyList(), result.getInserted());
|
||||
assertContentsEqual(Arrays.asList(A, B), result.getRemoved());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculate_allInserted() {
|
||||
DiffHelper.Result result = DiffHelper.calculate(Collections.emptyList(), Arrays.asList(A, B));
|
||||
|
||||
assertContentsEqual(Arrays.asList(A, B), result.getInserted());
|
||||
assertContentsEqual(Collections.emptyList(), result.getRemoved());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculate_completeSwap() {
|
||||
DiffHelper.Result result = DiffHelper.calculate(Collections.singleton(A), Collections.singleton(B));
|
||||
|
||||
assertContentsEqual(Collections.singleton(B), result.getInserted());
|
||||
assertContentsEqual(Collections.singleton(A), result.getRemoved());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculate_bothEmpty() {
|
||||
DiffHelper.Result result = DiffHelper.calculate(Collections.emptyList(), Collections.emptyList());
|
||||
|
||||
assertContentsEqual(Collections.emptyList(), result.getInserted());
|
||||
assertContentsEqual(Collections.emptyList(), result.getRemoved());
|
||||
}
|
||||
|
||||
private void assertContentsEqual(@NonNull Collection expected, @NonNull Collection actual) {
|
||||
assertEquals(expected.size(), actual.size());
|
||||
assertTrue(expected.containsAll(actual));
|
||||
}
|
||||
}
|
@ -341,7 +341,8 @@ public class SignalServiceMessageSender {
|
||||
dataStream,
|
||||
ciphertextLength,
|
||||
new AttachmentCipherOutputStreamFactory(attachmentKey),
|
||||
attachment.getListener());
|
||||
attachment.getListener(),
|
||||
attachment.getCancelationSignal());
|
||||
|
||||
AttachmentUploadAttributes uploadAttributes = null;
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
package org.whispersystems.signalservice.api.messages;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
@ -39,16 +40,17 @@ public abstract class SignalServiceAttachment {
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private InputStream inputStream;
|
||||
private String contentType;
|
||||
private String fileName;
|
||||
private long length;
|
||||
private ProgressListener listener;
|
||||
private boolean voiceNote;
|
||||
private int width;
|
||||
private int height;
|
||||
private String caption;
|
||||
private String blurHash;
|
||||
private InputStream inputStream;
|
||||
private String contentType;
|
||||
private String fileName;
|
||||
private long length;
|
||||
private ProgressListener listener;
|
||||
private CancelationSignal cancelationSignal;
|
||||
private boolean voiceNote;
|
||||
private int width;
|
||||
private int height;
|
||||
private String caption;
|
||||
private String blurHash;
|
||||
|
||||
private Builder() {}
|
||||
|
||||
@ -77,6 +79,11 @@ public abstract class SignalServiceAttachment {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withCancelationSignal(CancelationSignal cancelationSignal) {
|
||||
this.cancelationSignal = cancelationSignal;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withVoiceNote(boolean voiceNote) {
|
||||
this.voiceNote = voiceNote;
|
||||
return this;
|
||||
@ -117,7 +124,8 @@ public abstract class SignalServiceAttachment {
|
||||
height,
|
||||
Optional.fromNullable(caption),
|
||||
Optional.fromNullable(blurHash),
|
||||
listener);
|
||||
listener,
|
||||
cancelationSignal);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
package org.whispersystems.signalservice.api.messages;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
@ -15,33 +16,47 @@ import java.io.InputStream;
|
||||
*/
|
||||
public class SignalServiceAttachmentStream extends SignalServiceAttachment {
|
||||
|
||||
private final InputStream inputStream;
|
||||
private final long length;
|
||||
private final Optional<String> fileName;
|
||||
private final ProgressListener listener;
|
||||
private final Optional<byte[]> preview;
|
||||
private final boolean voiceNote;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final Optional<String> caption;
|
||||
private final Optional<String> blurHash;
|
||||
private final InputStream inputStream;
|
||||
private final long length;
|
||||
private final Optional<String> fileName;
|
||||
private final ProgressListener listener;
|
||||
private final CancelationSignal cancelationSignal;
|
||||
private final Optional<byte[]> preview;
|
||||
private final boolean voiceNote;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final Optional<String> caption;
|
||||
private final Optional<String> blurHash;
|
||||
|
||||
public SignalServiceAttachmentStream(InputStream inputStream, String contentType, long length, Optional<String> fileName, boolean voiceNote, ProgressListener listener) {
|
||||
this(inputStream, contentType, length, fileName, voiceNote, Optional.<byte[]>absent(), 0, 0, Optional.<String>absent(), Optional.<String>absent(), listener);
|
||||
public SignalServiceAttachmentStream(InputStream inputStream, String contentType, long length, Optional<String> fileName, boolean voiceNote, ProgressListener listener, CancelationSignal cancelationSignal) {
|
||||
this(inputStream, contentType, length, fileName, voiceNote, Optional.<byte[]>absent(), 0, 0, Optional.<String>absent(), Optional.<String>absent(), listener, cancelationSignal);
|
||||
}
|
||||
|
||||
public SignalServiceAttachmentStream(InputStream inputStream, String contentType, long length, Optional<String> fileName, boolean voiceNote, Optional<byte[]> preview, int width, int height, Optional<String> caption, Optional<String> blurHash, ProgressListener listener) {
|
||||
public SignalServiceAttachmentStream(InputStream inputStream,
|
||||
String contentType,
|
||||
long length,
|
||||
Optional<String> fileName,
|
||||
boolean voiceNote,
|
||||
Optional<byte[]> preview,
|
||||
int width,
|
||||
int height,
|
||||
Optional<String> caption,
|
||||
Optional<String> blurHash,
|
||||
ProgressListener listener,
|
||||
CancelationSignal cancelationSignal)
|
||||
{
|
||||
super(contentType);
|
||||
this.inputStream = inputStream;
|
||||
this.length = length;
|
||||
this.fileName = fileName;
|
||||
this.listener = listener;
|
||||
this.voiceNote = voiceNote;
|
||||
this.preview = preview;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.caption = caption;
|
||||
this.blurHash = blurHash;
|
||||
this.inputStream = inputStream;
|
||||
this.length = length;
|
||||
this.fileName = fileName;
|
||||
this.listener = listener;
|
||||
this.voiceNote = voiceNote;
|
||||
this.preview = preview;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.caption = caption;
|
||||
this.blurHash = blurHash;
|
||||
this.cancelationSignal = cancelationSignal;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -70,6 +85,10 @@ public class SignalServiceAttachmentStream extends SignalServiceAttachment {
|
||||
return listener;
|
||||
}
|
||||
|
||||
public CancelationSignal getCancelationSignal() {
|
||||
return cancelationSignal;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getPreview() {
|
||||
return preview;
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ public class DeviceContactsInputStream extends ChunkedInputStream {
|
||||
InputStream avatarStream = new LimitedInputStream(in, avatarLength);
|
||||
String avatarContentType = details.getAvatar().getContentType();
|
||||
|
||||
avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.<String>absent(), false, null));
|
||||
avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.<String>absent(), false, null, null));
|
||||
}
|
||||
|
||||
if (details.hasVerified()) {
|
||||
|
@ -52,7 +52,7 @@ public class DeviceGroupsInputStream extends ChunkedInputStream{
|
||||
InputStream avatarStream = new ChunkedInputStream.LimitedInputStream(in, avatarLength);
|
||||
String avatarContentType = details.getAvatar().getContentType();
|
||||
|
||||
avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.<String>absent(), false, null));
|
||||
avatar = Optional.of(new SignalServiceAttachmentStream(avatarStream, avatarContentType, avatarLength, Optional.<String>absent(), false, null, null));
|
||||
}
|
||||
|
||||
if (details.hasExpireTimer() && details.getExpireTimer() > 0) {
|
||||
|
@ -7,6 +7,7 @@
|
||||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
|
||||
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
|
||||
import org.whispersystems.signalservice.internal.push.http.OutputStreamFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
@ -18,15 +19,18 @@ public class PushAttachmentData {
|
||||
private final long dataSize;
|
||||
private final OutputStreamFactory outputStreamFactory;
|
||||
private final ProgressListener listener;
|
||||
private final CancelationSignal cancelationSignal;
|
||||
|
||||
public PushAttachmentData(String contentType, InputStream data, long dataSize,
|
||||
OutputStreamFactory outputStreamFactory, ProgressListener listener)
|
||||
OutputStreamFactory outputStreamFactory, ProgressListener listener,
|
||||
CancelationSignal cancelationSignal)
|
||||
{
|
||||
this.contentType = contentType;
|
||||
this.data = data;
|
||||
this.dataSize = dataSize;
|
||||
this.outputStreamFactory = outputStreamFactory;
|
||||
this.listener = listener;
|
||||
this.cancelationSignal = cancelationSignal;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
@ -48,4 +52,8 @@ public class PushAttachmentData {
|
||||
public ProgressListener getListener() {
|
||||
return listener;
|
||||
}
|
||||
|
||||
public CancelationSignal getCancelationSignal() {
|
||||
return cancelationSignal;
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResp
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
|
||||
import org.whispersystems.signalservice.internal.push.http.DigestingRequestBody;
|
||||
import org.whispersystems.signalservice.internal.push.http.OutputStreamFactory;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ReadOperation;
|
||||
@ -577,7 +578,7 @@ public class PushServiceSocket {
|
||||
formAttributes.getCredential(), formAttributes.getDate(),
|
||||
formAttributes.getSignature(), profileAvatar.getData(),
|
||||
profileAvatar.getContentType(), profileAvatar.getDataLength(),
|
||||
profileAvatar.getOutputStreamFactory(), null);
|
||||
profileAvatar.getOutputStreamFactory(), null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -763,7 +764,8 @@ public class PushServiceSocket {
|
||||
uploadAttributes.getCredential(), uploadAttributes.getDate(),
|
||||
uploadAttributes.getSignature(), attachment.getData(),
|
||||
"application/octet-stream", attachment.getDataSize(),
|
||||
attachment.getOutputStreamFactory(), attachment.getListener());
|
||||
attachment.getOutputStreamFactory(), attachment.getListener(),
|
||||
attachment.getCancelationSignal());
|
||||
|
||||
return new Pair<>(id, digest);
|
||||
}
|
||||
@ -851,7 +853,8 @@ public class PushServiceSocket {
|
||||
private byte[] uploadToCdn(String path, String acl, String key, String policy, String algorithm,
|
||||
String credential, String date, String signature,
|
||||
InputStream data, String contentType, long length,
|
||||
OutputStreamFactory outputStreamFactory, ProgressListener progressListener)
|
||||
OutputStreamFactory outputStreamFactory, ProgressListener progressListener,
|
||||
CancelationSignal cancelationSignal)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
ConnectionHolder connectionHolder = getRandom(cdnClients, random);
|
||||
@ -861,7 +864,7 @@ public class PushServiceSocket {
|
||||
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
||||
DigestingRequestBody file = new DigestingRequestBody(data, outputStreamFactory, contentType, length, progressListener);
|
||||
DigestingRequestBody file = new DigestingRequestBody(data, outputStreamFactory, contentType, length, progressListener, cancelationSignal);
|
||||
|
||||
RequestBody requestBody = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
|
@ -0,0 +1,8 @@
|
||||
package org.whispersystems.signalservice.internal.push.http;
|
||||
|
||||
/**
|
||||
* Used to communicate to observers whether or not something is canceled.
|
||||
*/
|
||||
public interface CancelationSignal {
|
||||
boolean isCanceled();
|
||||
}
|
@ -18,19 +18,22 @@ public class DigestingRequestBody extends RequestBody {
|
||||
private final String contentType;
|
||||
private final long contentLength;
|
||||
private final ProgressListener progressListener;
|
||||
private final CancelationSignal cancelationSignal;
|
||||
|
||||
private byte[] digest;
|
||||
|
||||
public DigestingRequestBody(InputStream inputStream,
|
||||
OutputStreamFactory outputStreamFactory,
|
||||
String contentType, long contentLength,
|
||||
ProgressListener progressListener)
|
||||
ProgressListener progressListener,
|
||||
CancelationSignal cancelationSignal)
|
||||
{
|
||||
this.inputStream = inputStream;
|
||||
this.outputStreamFactory = outputStreamFactory;
|
||||
this.contentType = contentType;
|
||||
this.contentLength = contentLength;
|
||||
this.progressListener = progressListener;
|
||||
this.cancelationSignal = cancelationSignal;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -47,6 +50,10 @@ public class DigestingRequestBody extends RequestBody {
|
||||
long total = 0;
|
||||
|
||||
while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
|
||||
if (cancelationSignal != null && cancelationSignal.isCanceled()) {
|
||||
throw new IOException("Canceled!");
|
||||
}
|
||||
|
||||
outputStream.write(buffer, 0, read);
|
||||
total += read;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user