kopia lustrzana https://github.com/ryukoposting/Signal-Android
Ensure images sent to stories respect media quality settings.
Stories should always use "Standard" quality, not L3 (high quality). This change ensures that we: 1. Always send stories at the appropriate quality 2. Do not corrupt or overwrite pre-existing image attachments 3. Close several streams when done (thanks StrictMode!)fork-5.53.8
rodzic
c4bef8099f
commit
b18542a839
|
@ -0,0 +1,106 @@
|
||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||||
|
import org.thoughtcrime.securesms.mms.MediaStream
|
||||||
|
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||||
|
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||||
|
import org.thoughtcrime.securesms.util.MediaUtil
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class AttachmentDatabaseTest {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
SignalDatabase.attachments.deleteAllAttachments()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenABlob_whenIInsert2AttachmentsForPreUpload_thenIExpectDistinctIdsButSameFileName() {
|
||||||
|
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
||||||
|
val highQualityProperties = createHighQualityTransformProperties()
|
||||||
|
val highQualityImage = createAttachment(1, blob, highQualityProperties)
|
||||||
|
val attachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
|
||||||
|
val attachment2 = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
|
||||||
|
|
||||||
|
assertNotEquals(attachment2.attachmentId, attachment.attachmentId)
|
||||||
|
assertEquals(attachment2.fileName, attachment.fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenABlobAndDifferentTransformQuality_whenIInsert2AttachmentsForPreUpload_thenIExpectDifferentFileInfos() {
|
||||||
|
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
||||||
|
val highQualityProperties = createHighQualityTransformProperties()
|
||||||
|
val highQualityImage = createAttachment(1, blob, highQualityProperties)
|
||||||
|
val lowQualityImage = createAttachment(1, blob, AttachmentDatabase.TransformProperties.empty())
|
||||||
|
val attachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
|
||||||
|
val attachment2 = SignalDatabase.attachments.insertAttachmentForPreUpload(lowQualityImage)
|
||||||
|
|
||||||
|
SignalDatabase.attachments.updateAttachmentData(
|
||||||
|
attachment,
|
||||||
|
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
SignalDatabase.attachments.updateAttachmentData(
|
||||||
|
attachment2,
|
||||||
|
createMediaStream(byteArrayOf(1, 2, 3)),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentDatabase.DATA)
|
||||||
|
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentDatabase.DATA)
|
||||||
|
|
||||||
|
assertNotEquals(attachment1Info, attachment2Info)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenIdenticalAttachmentsInsertedForPreUpload_whenIUpdateAttachmentDataAndSpecifyOnlyModifyThisAttachment_thenIExpectDifferentFileInfos() {
|
||||||
|
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
||||||
|
val highQualityProperties = createHighQualityTransformProperties()
|
||||||
|
val highQualityImage = createAttachment(1, blob, highQualityProperties)
|
||||||
|
val attachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
|
||||||
|
val attachment2 = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
|
||||||
|
|
||||||
|
SignalDatabase.attachments.updateAttachmentData(
|
||||||
|
attachment,
|
||||||
|
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
SignalDatabase.attachments.updateAttachmentData(
|
||||||
|
attachment2,
|
||||||
|
createMediaStream(byteArrayOf(1, 2, 3, 4)),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentDatabase.DATA)
|
||||||
|
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentDatabase.DATA)
|
||||||
|
|
||||||
|
assertNotEquals(attachment1Info, attachment2Info)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAttachment(id: Long, uri: Uri, transformProperties: AttachmentDatabase.TransformProperties): UriAttachment {
|
||||||
|
return UriAttachmentBuilder.build(
|
||||||
|
id,
|
||||||
|
uri = uri,
|
||||||
|
contentType = MediaUtil.IMAGE_JPEG,
|
||||||
|
transformProperties = transformProperties
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createHighQualityTransformProperties(): AttachmentDatabase.TransformProperties {
|
||||||
|
return AttachmentDatabase.TransformProperties.forSentMediaQuality(Optional.empty(), SentMediaQuality.HIGH)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMediaStream(byteArray: ByteArray): MediaStream {
|
||||||
|
return MediaStream(byteArray.inputStream(), MediaUtil.IMAGE_JPEG, 2, 2)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||||
|
import org.thoughtcrime.securesms.audio.AudioHash
|
||||||
|
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||||
|
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||||
|
|
||||||
|
object UriAttachmentBuilder {
|
||||||
|
fun build(
|
||||||
|
id: Long,
|
||||||
|
uri: Uri = Uri.parse("content://$id"),
|
||||||
|
contentType: String,
|
||||||
|
transferState: Int = AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
|
||||||
|
size: Long = 0L,
|
||||||
|
fileName: String = "file$id",
|
||||||
|
voiceNote: Boolean = false,
|
||||||
|
borderless: Boolean = false,
|
||||||
|
videoGif: Boolean = false,
|
||||||
|
quote: Boolean = false,
|
||||||
|
caption: String? = null,
|
||||||
|
stickerLocator: StickerLocator? = null,
|
||||||
|
blurHash: BlurHash? = null,
|
||||||
|
audioHash: AudioHash? = null,
|
||||||
|
transformProperties: AttachmentDatabase.TransformProperties? = null
|
||||||
|
): UriAttachment {
|
||||||
|
return UriAttachment(
|
||||||
|
uri,
|
||||||
|
contentType,
|
||||||
|
transferState,
|
||||||
|
size,
|
||||||
|
fileName,
|
||||||
|
voiceNote,
|
||||||
|
borderless,
|
||||||
|
videoGif,
|
||||||
|
quote,
|
||||||
|
caption,
|
||||||
|
stickerLocator,
|
||||||
|
blurHash,
|
||||||
|
audioHash,
|
||||||
|
transformProperties
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -170,7 +170,7 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor(
|
||||||
isVideoGif,
|
isVideoGif,
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.ofNullable(caption),
|
Optional.ofNullable(caption),
|
||||||
Optional.empty()
|
Optional.of(transformProperties)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -790,8 +790,8 @@ public class AttachmentDatabase extends Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param onlyModifyThisAttachment If false and more than one attachment shares this file, they will all be updated.
|
* @param onlyModifyThisAttachment If false and more than one attachment shares this file and quality, they will all
|
||||||
* If true, then guarantees not to affect other attachments.
|
* be updated. If true, then guarantees not to affect other attachments.
|
||||||
*/
|
*/
|
||||||
public void updateAttachmentData(@NonNull DatabaseAttachment databaseAttachment,
|
public void updateAttachmentData(@NonNull DatabaseAttachment databaseAttachment,
|
||||||
@NonNull MediaStream mediaStream,
|
@NonNull MediaStream mediaStream,
|
||||||
|
@ -807,7 +807,8 @@ public class AttachmentDatabase extends Database {
|
||||||
|
|
||||||
File destination = oldDataInfo.file;
|
File destination = oldDataInfo.file;
|
||||||
|
|
||||||
if (onlyModifyThisAttachment) {
|
boolean isSingleUseOfData = onlyModifyThisAttachment || oldDataInfo.hash == null;
|
||||||
|
if (isSingleUseOfData) {
|
||||||
if (fileReferencedByMoreThanOneAttachment(destination)) {
|
if (fileReferencedByMoreThanOneAttachment(destination)) {
|
||||||
Log.i(TAG, "Creating a new file as this one is used by more than one attachment");
|
Log.i(TAG, "Creating a new file as this one is used by more than one attachment");
|
||||||
destination = newFile();
|
destination = newFile();
|
||||||
|
@ -827,7 +828,10 @@ public class AttachmentDatabase extends Database {
|
||||||
contentValues.put(DATA_RANDOM, dataInfo.random);
|
contentValues.put(DATA_RANDOM, dataInfo.random);
|
||||||
contentValues.put(DATA_HASH, dataInfo.hash);
|
contentValues.put(DATA_HASH, dataInfo.hash);
|
||||||
|
|
||||||
int updateCount = updateAttachmentAndMatchingHashes(database, databaseAttachment.getAttachmentId(), oldDataInfo.hash, contentValues);
|
int updateCount = updateAttachmentAndMatchingHashes(database,
|
||||||
|
databaseAttachment.getAttachmentId(),
|
||||||
|
isSingleUseOfData ? dataInfo.hash : oldDataInfo.hash,
|
||||||
|
contentValues);
|
||||||
Log.i(TAG, "[updateAttachmentData] Updated " + updateCount + " rows.");
|
Log.i(TAG, "[updateAttachmentData] Updated " + updateCount + " rows.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -846,10 +850,32 @@ public class AttachmentDatabase extends Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void markAttachmentAsTransformed(@NonNull AttachmentId attachmentId) {
|
public void markAttachmentAsTransformed(@NonNull AttachmentId attachmentId) {
|
||||||
updateAttachmentTransformProperties(attachmentId, TransformProperties.forSkipTransform());
|
getWritableDatabase().beginTransaction();
|
||||||
|
try {
|
||||||
|
updateAttachmentTransformProperties(attachmentId, getTransformProperties(attachmentId).withSkipTransform());
|
||||||
|
getWritableDatabase().setTransactionSuccessful();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Could not mark attachment as transformed.", e);
|
||||||
|
} finally {
|
||||||
|
getWritableDatabase().endTransaction();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateAttachmentTransformProperties(@NonNull AttachmentId attachmentId, @NonNull TransformProperties transformProperties) {
|
public @NonNull TransformProperties getTransformProperties(@NonNull AttachmentId attachmentId) {
|
||||||
|
String[] projection = SqlUtil.buildArgs(TRANSFORM_PROPERTIES);
|
||||||
|
String[] args = attachmentId.toStrings();
|
||||||
|
|
||||||
|
try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, projection, PART_ID_WHERE, args, null, null, null, null)) {
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
String serializedProperties = CursorUtil.requireString(cursor, TRANSFORM_PROPERTIES);
|
||||||
|
return TransformProperties.parse(serializedProperties);
|
||||||
|
} else {
|
||||||
|
throw new AssertionError("No such attachment.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAttachmentTransformProperties(@NonNull AttachmentId attachmentId, @NonNull TransformProperties transformProperties) {
|
||||||
DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, DATA);
|
DataInfo dataInfo = getAttachmentDataFileInfo(attachmentId, DATA);
|
||||||
|
|
||||||
if (dataInfo == null) {
|
if (dataInfo == null) {
|
||||||
|
@ -1017,15 +1043,12 @@ public class AttachmentDatabase extends Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable DataInfo getAttachmentDataFileInfo(@NonNull AttachmentId attachmentId, @NonNull String dataType)
|
@VisibleForTesting
|
||||||
|
@Nullable DataInfo getAttachmentDataFileInfo(@NonNull AttachmentId attachmentId, @NonNull String dataType)
|
||||||
{
|
{
|
||||||
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
|
SQLiteDatabase database = databaseHelper.getSignalReadableDatabase();
|
||||||
Cursor cursor = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
cursor = database.query(TABLE_NAME, new String[]{dataType, SIZE, DATA_RANDOM, DATA_HASH}, PART_ID_WHERE, attachmentId.toStrings(),
|
|
||||||
null, null, null);
|
|
||||||
|
|
||||||
|
try (Cursor cursor = database.query(TABLE_NAME, new String[] { dataType, SIZE, DATA_RANDOM, DATA_HASH }, PART_ID_WHERE, attachmentId.toStrings(), null, null, null)) {
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
if (cursor.isNull(cursor.getColumnIndexOrThrow(dataType))) {
|
if (cursor.isNull(cursor.getColumnIndexOrThrow(dataType))) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -1038,9 +1061,6 @@ public class AttachmentDatabase extends Database {
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
if (cursor != null)
|
|
||||||
cursor.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1127,7 +1147,7 @@ public class AttachmentDatabase extends Database {
|
||||||
|
|
||||||
Pair<String, String[]> selectorArgs = buildSharedFileSelectorArgs(hash, excludedAttachmentId);
|
Pair<String, String[]> selectorArgs = buildSharedFileSelectorArgs(hash, excludedAttachmentId);
|
||||||
try (Cursor cursor = database.query(TABLE_NAME,
|
try (Cursor cursor = database.query(TABLE_NAME,
|
||||||
new String[]{DATA, DATA_RANDOM, SIZE},
|
new String[]{DATA, DATA_RANDOM, SIZE, TRANSFORM_PROPERTIES},
|
||||||
selectorArgs.first,
|
selectorArgs.first,
|
||||||
selectorArgs.second,
|
selectorArgs.second,
|
||||||
null,
|
null,
|
||||||
|
@ -1298,7 +1318,8 @@ public class AttachmentDatabase extends Database {
|
||||||
template.getTransferState() == TRANSFER_PROGRESS_DONE &&
|
template.getTransferState() == TRANSFER_PROGRESS_DONE &&
|
||||||
template.getTransformProperties().shouldSkipTransform() &&
|
template.getTransformProperties().shouldSkipTransform() &&
|
||||||
template.getDigest() != null &&
|
template.getDigest() != null &&
|
||||||
!attachment.getTransformProperties().isVideoEdited();
|
!attachment.getTransformProperties().isVideoEdited() &&
|
||||||
|
template.getTransformProperties().sentMediaQuality == attachment.getTransformProperties().getSentMediaQuality();
|
||||||
|
|
||||||
ContentValues contentValues = new ContentValues();
|
ContentValues contentValues = new ContentValues();
|
||||||
contentValues.put(MMS_ID, mmsId);
|
contentValues.put(MMS_ID, mmsId);
|
||||||
|
@ -1326,7 +1347,7 @@ public class AttachmentDatabase extends Database {
|
||||||
contentValues.put(TRANSFORM_PROPERTIES, attachment.getTransformProperties().serialize());
|
contentValues.put(TRANSFORM_PROPERTIES, attachment.getTransformProperties().serialize());
|
||||||
} else {
|
} else {
|
||||||
contentValues.put(VISUAL_HASH, getVisualHashStringOrNull(template));
|
contentValues.put(VISUAL_HASH, getVisualHashStringOrNull(template));
|
||||||
contentValues.put(TRANSFORM_PROPERTIES, template.getTransformProperties().serialize());
|
contentValues.put(TRANSFORM_PROPERTIES, (useTemplateUpload ? template : attachment).getTransformProperties().serialize());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachment.isSticker()) {
|
if (attachment.isSticker()) {
|
||||||
|
@ -1340,7 +1361,7 @@ public class AttachmentDatabase extends Database {
|
||||||
contentValues.put(DATA, dataInfo.file.getAbsolutePath());
|
contentValues.put(DATA, dataInfo.file.getAbsolutePath());
|
||||||
contentValues.put(SIZE, dataInfo.length);
|
contentValues.put(SIZE, dataInfo.length);
|
||||||
contentValues.put(DATA_RANDOM, dataInfo.random);
|
contentValues.put(DATA_RANDOM, dataInfo.random);
|
||||||
if (attachment.getTransformProperties().isVideoEdited()) {
|
if (attachment.getTransformProperties().isVideoEdited() || attachment.getTransformProperties().sentMediaQuality != template.getTransformProperties().getSentMediaQuality()) {
|
||||||
contentValues.putNull(DATA_HASH);
|
contentValues.putNull(DATA_HASH);
|
||||||
} else {
|
} else {
|
||||||
contentValues.put(DATA_HASH, dataInfo.hash);
|
contentValues.put(DATA_HASH, dataInfo.hash);
|
||||||
|
@ -1408,7 +1429,8 @@ public class AttachmentDatabase extends Database {
|
||||||
return EncryptedMediaDataSource.createFor(attachmentSecret, dataInfo.file, dataInfo.random, dataInfo.length);
|
return EncryptedMediaDataSource.createFor(attachmentSecret, dataInfo.file, dataInfo.random, dataInfo.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class DataInfo {
|
@VisibleForTesting
|
||||||
|
static class DataInfo {
|
||||||
private final File file;
|
private final File file;
|
||||||
private final long length;
|
private final long length;
|
||||||
private final byte[] random;
|
private final byte[] random;
|
||||||
|
@ -1420,6 +1442,22 @@ public class AttachmentDatabase extends Database {
|
||||||
this.random = random;
|
this.random = random;
|
||||||
this.hash = hash;
|
this.hash = hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
final DataInfo dataInfo = (DataInfo) o;
|
||||||
|
return length == dataInfo.length &&
|
||||||
|
Objects.equals(file, dataInfo.file) &&
|
||||||
|
Arrays.equals(random, dataInfo.random) &&
|
||||||
|
Objects.equals(hash, dataInfo.hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public int hashCode() {
|
||||||
|
int result = Objects.hash(file, length, hash);
|
||||||
|
result = 31 * result + Arrays.hashCode(random);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class DataUsageResult {
|
private static final class DataUsageResult {
|
||||||
|
@ -1520,6 +1558,10 @@ public class AttachmentDatabase extends Database {
|
||||||
return sentMediaQuality;
|
return sentMediaQuality;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull TransformProperties withSkipTransform() {
|
||||||
|
return new TransformProperties(true, false, 0, 0, sentMediaQuality);
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull String serialize() {
|
@NonNull String serialize() {
|
||||||
return JsonUtil.toJson(this);
|
return JsonUtil.toJson(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,6 @@ import org.thoughtcrime.securesms.video.videoconverter.EncodingException;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
|
@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResumableUploadResponseCodeException;
|
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResumableUploadResponseCodeException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.ResumeLocationInvalidException;
|
import org.whispersystems.signalservice.api.push.exceptions.ResumeLocationInvalidException;
|
||||||
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
||||||
|
@ -142,11 +143,12 @@ public final class AttachmentUploadJob extends BaseJob {
|
||||||
Log.i(TAG, "Uploading attachment for message " + databaseAttachment.getMmsId() + " with ID " + databaseAttachment.getAttachmentId());
|
Log.i(TAG, "Uploading attachment for message " + databaseAttachment.getMmsId() + " with ID " + databaseAttachment.getAttachmentId());
|
||||||
|
|
||||||
try (NotificationController notification = getNotificationForAttachment(databaseAttachment)) {
|
try (NotificationController notification = getNotificationForAttachment(databaseAttachment)) {
|
||||||
SignalServiceAttachment localAttachment = getAttachmentFor(databaseAttachment, notification, resumableUploadSpec);
|
try (SignalServiceAttachmentStream localAttachment = getAttachmentFor(databaseAttachment, notification, resumableUploadSpec)) {
|
||||||
SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream());
|
SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment);
|
||||||
Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.getFastPreflightId()).get();
|
Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.getFastPreflightId()).get();
|
||||||
|
|
||||||
database.updateAttachmentAfterUpload(databaseAttachment.getAttachmentId(), attachment, remoteAttachment.getUploadTimestamp());
|
database.updateAttachmentAfterUpload(databaseAttachment.getAttachmentId(), attachment, remoteAttachment.getUploadTimestamp());
|
||||||
|
}
|
||||||
} catch (NonSuccessfulResumableUploadResponseCodeException e) {
|
} catch (NonSuccessfulResumableUploadResponseCodeException e) {
|
||||||
if (e.getCode() == 400) {
|
if (e.getCode() == 400) {
|
||||||
Log.w(TAG, "Failed to upload due to a 400 when getting resumable upload information. Downgrading to attachments v2", e);
|
Log.w(TAG, "Failed to upload due to a 400 when getting resumable upload information. Downgrading to attachments v2", e);
|
||||||
|
@ -177,9 +179,12 @@ public final class AttachmentUploadJob extends BaseJob {
|
||||||
return exception instanceof IOException && !(exception instanceof NotPushRegisteredException);
|
return exception instanceof IOException && !(exception instanceof NotPushRegisteredException);
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NonNull SignalServiceAttachment getAttachmentFor(Attachment attachment, @Nullable NotificationController notification, @Nullable ResumableUploadSpec resumableUploadSpec) throws InvalidAttachmentException {
|
private @NonNull SignalServiceAttachmentStream getAttachmentFor(Attachment attachment, @Nullable NotificationController notification, @Nullable ResumableUploadSpec resumableUploadSpec) throws InvalidAttachmentException {
|
||||||
|
if (attachment.getUri() == null || attachment.getSize() == 0) {
|
||||||
|
throw new InvalidAttachmentException(new IOException("Assertion failed, outgoing attachment has no data!"));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (attachment.getUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
|
|
||||||
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getUri());
|
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getUri());
|
||||||
SignalServiceAttachment.Builder builder = SignalServiceAttachment.newStreamBuilder()
|
SignalServiceAttachment.Builder builder = SignalServiceAttachment.newStreamBuilder()
|
||||||
.withStream(is)
|
.withStream(is)
|
||||||
|
@ -208,7 +213,6 @@ public final class AttachmentUploadJob extends BaseJob {
|
||||||
} else {
|
} else {
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
throw new InvalidAttachmentException(ioe);
|
throw new InvalidAttachmentException(ioe);
|
||||||
}
|
}
|
||||||
|
@ -218,7 +222,9 @@ public final class AttachmentUploadJob extends BaseJob {
|
||||||
if (attachment.getBlurHash() != null) return attachment.getBlurHash().getHash();
|
if (attachment.getBlurHash() != null) return attachment.getBlurHash().getHash();
|
||||||
if (attachment.getUri() == null) return null;
|
if (attachment.getUri() == null) return null;
|
||||||
|
|
||||||
return BlurHashEncoder.encode(PartAuthority.getAttachmentStream(context, attachment.getUri()));
|
try (InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.getUri())) {
|
||||||
|
return BlurHashEncoder.encode(inputStream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable String getVideoBlurHash(@NonNull Attachment attachment) throws IOException {
|
private @Nullable String getVideoBlurHash(@NonNull Attachment attachment) throws IOException {
|
||||||
|
|
|
@ -7,6 +7,8 @@ import android.os.Parcelable;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
|
import org.whispersystems.signalservice.api.util.Preconditions;
|
||||||
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -194,4 +196,21 @@ public class Media implements Parcelable {
|
||||||
media.getCaption(),
|
media.getCaption(),
|
||||||
media.getTransformProperties());
|
media.getTransformProperties());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static @NonNull Media stripTransform(@NonNull Media media) {
|
||||||
|
Preconditions.checkArgument(MediaUtil.isImageType(media.mimeType));
|
||||||
|
|
||||||
|
return new Media(media.getUri(),
|
||||||
|
media.getMimeType(),
|
||||||
|
media.getDate(),
|
||||||
|
media.getWidth(),
|
||||||
|
media.getHeight(),
|
||||||
|
media.getSize(),
|
||||||
|
media.getDuration(),
|
||||||
|
media.isBorderless(),
|
||||||
|
media.isVideoGif(),
|
||||||
|
media.getBucketId(),
|
||||||
|
media.getCaption(),
|
||||||
|
Optional.empty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,7 +127,7 @@ class MediaSelectionRepository(context: Context) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val clippedMediaForStories = if (singleContact?.isStory == true || contacts.any { it.isStory }) {
|
val clippedVideosForStories: List<Media> = if (singleContact?.isStory == true || contacts.any { it.isStory }) {
|
||||||
updatedMedia.filter {
|
updatedMedia.filter {
|
||||||
Stories.MediaTransform.getSendRequirements(it) == Stories.MediaTransform.SendRequirements.REQUIRES_CLIP
|
Stories.MediaTransform.getSendRequirements(it) == Stories.MediaTransform.SendRequirements.REQUIRES_CLIP
|
||||||
}.map { media ->
|
}.map { media ->
|
||||||
|
@ -135,12 +135,20 @@ class MediaSelectionRepository(context: Context) {
|
||||||
}.flatten()
|
}.flatten()
|
||||||
} else emptyList()
|
} else emptyList()
|
||||||
|
|
||||||
|
val lowResImagesForStories: List<Media> = if (singleContact?.isStory == true || contacts.any { it.isStory }) {
|
||||||
|
updatedMedia.filter {
|
||||||
|
Stories.MediaTransform.hasHighQualityTransform(it)
|
||||||
|
}.map {
|
||||||
|
Media.stripTransform(it)
|
||||||
|
}
|
||||||
|
} else emptyList()
|
||||||
|
|
||||||
uploadRepository.applyMediaUpdates(oldToNewMediaMap, singleRecipient)
|
uploadRepository.applyMediaUpdates(oldToNewMediaMap, singleRecipient)
|
||||||
uploadRepository.updateCaptions(updatedMedia)
|
uploadRepository.updateCaptions(updatedMedia)
|
||||||
uploadRepository.updateDisplayOrder(updatedMedia)
|
uploadRepository.updateDisplayOrder(updatedMedia)
|
||||||
uploadRepository.getPreUploadResults { uploadResults ->
|
uploadRepository.getPreUploadResults { uploadResults ->
|
||||||
if (contacts.isNotEmpty()) {
|
if (contacts.isNotEmpty()) {
|
||||||
sendMessages(contacts, splitBody, uploadResults, trimmedMentions, isViewOnce, clippedMediaForStories)
|
sendMessages(contacts, splitBody, uploadResults, trimmedMentions, isViewOnce, clippedVideosForStories + lowResImagesForStories)
|
||||||
uploadRepository.deleteAbandonedAttachments()
|
uploadRepository.deleteAbandonedAttachments()
|
||||||
emitter.onComplete()
|
emitter.onComplete()
|
||||||
} else if (uploadResults.isNotEmpty()) {
|
} else if (uploadResults.isNotEmpty()) {
|
||||||
|
|
|
@ -336,7 +336,7 @@ class MediaSelectionViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
val filteredPreUploadMedia = if (Stories.isFeatureEnabled()) {
|
val filteredPreUploadMedia = if (Stories.isFeatureEnabled()) {
|
||||||
media.filterNot { Stories.MediaTransform.getSendRequirements(media) == Stories.MediaTransform.SendRequirements.REQUIRES_CLIP }
|
media.filter { Stories.MediaTransform.canPreUploadMedia(it) }
|
||||||
} else {
|
} else {
|
||||||
media
|
media
|
||||||
}
|
}
|
||||||
|
|
|
@ -448,7 +448,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment) {
|
||||||
private fun computeQualityButtonAnimators(state: MediaSelectionState): List<Animator> {
|
private fun computeQualityButtonAnimators(state: MediaSelectionState): List<Animator> {
|
||||||
val slide = listOf(MediaReviewAnimatorController.getSlideInAnimator(qualityButton))
|
val slide = listOf(MediaReviewAnimatorController.getSlideInAnimator(qualityButton))
|
||||||
|
|
||||||
return slide + if (state.isTouchEnabled && state.selectedMedia.any { MediaUtil.isImageType(it.mimeType) }) {
|
return slide + if (state.isTouchEnabled && !state.isStory && state.selectedMedia.any { MediaUtil.isImageType(it.mimeType) }) {
|
||||||
listOf(MediaReviewAnimatorController.getFadeInAnimator(qualityButton))
|
listOf(MediaReviewAnimatorController.getFadeInAnimator(qualityButton))
|
||||||
} else {
|
} else {
|
||||||
listOf(MediaReviewAnimatorController.getFadeOutAnimator(qualityButton))
|
listOf(MediaReviewAnimatorController.getFadeOutAnimator(qualityButton))
|
||||||
|
|
|
@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||||
import org.thoughtcrime.securesms.components.location.SignalMapView;
|
import org.thoughtcrime.securesms.components.location.SignalMapView;
|
||||||
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
||||||
import org.thoughtcrime.securesms.conversation.MessageSendType;
|
import org.thoughtcrime.securesms.conversation.MessageSendType;
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||||
import org.thoughtcrime.securesms.maps.PlacePickerActivity;
|
import org.thoughtcrime.securesms.maps.PlacePickerActivity;
|
||||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity;
|
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity;
|
||||||
|
@ -316,7 +317,7 @@ public class AttachmentManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms");
|
Log.d(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms");
|
||||||
return mediaType.createSlide(context, uri, fileName, mimeType, null, fileSize, width, height, false);
|
return mediaType.createSlide(context, uri, fileName, mimeType, null, fileSize, width, height, false, null);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (cursor != null) cursor.close();
|
if (cursor != null) cursor.close();
|
||||||
|
@ -326,17 +327,19 @@ public class AttachmentManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NonNull Slide getManuallyCalculatedSlideInfo(Uri uri, int width, int height) throws IOException {
|
private @NonNull Slide getManuallyCalculatedSlideInfo(Uri uri, int width, int height) throws IOException {
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
Long mediaSize = null;
|
Long mediaSize = null;
|
||||||
String fileName = null;
|
String fileName = null;
|
||||||
String mimeType = null;
|
String mimeType = null;
|
||||||
boolean gif = false;
|
boolean gif = false;
|
||||||
|
AttachmentDatabase.TransformProperties transformProperties = null;
|
||||||
|
|
||||||
if (PartAuthority.isLocalUri(uri)) {
|
if (PartAuthority.isLocalUri(uri)) {
|
||||||
mediaSize = PartAuthority.getAttachmentSize(context, uri);
|
mediaSize = PartAuthority.getAttachmentSize(context, uri);
|
||||||
fileName = PartAuthority.getAttachmentFileName(context, uri);
|
fileName = PartAuthority.getAttachmentFileName(context, uri);
|
||||||
mimeType = PartAuthority.getAttachmentContentType(context, uri);
|
mimeType = PartAuthority.getAttachmentContentType(context, uri);
|
||||||
gif = PartAuthority.getAttachmentIsVideoGif(context, uri);
|
gif = PartAuthority.getAttachmentIsVideoGif(context, uri);
|
||||||
|
transformProperties = PartAuthority.getAttachmentTransformProperties(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaSize == null) {
|
if (mediaSize == null) {
|
||||||
|
@ -354,7 +357,7 @@ public class AttachmentManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms");
|
Log.d(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms");
|
||||||
return mediaType.createSlide(context, uri, fileName, mimeType, null, mediaSize, width, height, gif);
|
return mediaType.createSlide(context, uri, fileName, mimeType, null, mediaSize, width, height, gif, transformProperties);
|
||||||
}
|
}
|
||||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,10 @@ public abstract class MediaConstraints {
|
||||||
public abstract int getImageMaxHeight(Context context);
|
public abstract int getImageMaxHeight(Context context);
|
||||||
public abstract int getImageMaxSize(Context context);
|
public abstract int getImageMaxSize(Context context);
|
||||||
|
|
||||||
|
public boolean isHighQuality() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide a list of dimensions that should be attempted during compression. We will keep moving
|
* Provide a list of dimensions that should be attempted during compression. We will keep moving
|
||||||
* down the list until the image can be scaled to fit under {@link #getImageMaxSize(Context)}.
|
* down the list until the image can be scaled to fit under {@link #getImageMaxSize(Context)}.
|
||||||
|
|
|
@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.BuildConfig;
|
||||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.emoji.EmojiFiles;
|
import org.thoughtcrime.securesms.emoji.EmojiFiles;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
|
@ -152,6 +153,16 @@ public class PartAuthority {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static @Nullable AttachmentDatabase.TransformProperties getAttachmentTransformProperties(@NonNull Uri uri) {
|
||||||
|
int match = uriMatcher.match(uri);
|
||||||
|
switch (match) {
|
||||||
|
case PART_ROW:
|
||||||
|
return SignalDatabase.attachments().getTransformProperties(new PartUriParser(uri).getPartId());
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static Uri getAttachmentPublicUri(Uri uri) {
|
public static Uri getAttachmentPublicUri(Uri uri) {
|
||||||
PartUriParser partUri = new PartUriParser(uri);
|
PartUriParser partUri = new PartUriParser(uri);
|
||||||
return PartProvider.getContentUri(partUri.getPartId());
|
return PartProvider.getContentUri(partUri.getPartId());
|
||||||
|
|
|
@ -23,6 +23,11 @@ public class PushMediaConstraints extends MediaConstraints {
|
||||||
currentConfig = getCurrentConfig(ApplicationDependencies.getApplication(), sentMediaQuality);
|
currentConfig = getCurrentConfig(ApplicationDependencies.getApplication(), sentMediaQuality);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isHighQuality() {
|
||||||
|
return currentConfig == MediaConfig.LEVEL_3;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getImageMaxWidth(Context context) {
|
public int getImageMaxWidth(Context context) {
|
||||||
return currentConfig.imageSizeTargets[0];
|
return currentConfig.imageSizeTargets[0];
|
||||||
|
|
|
@ -13,6 +13,7 @@ import androidx.annotation.WorkerThread;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -31,25 +32,25 @@ public final class SlideFactory {
|
||||||
/**
|
/**
|
||||||
* Generates a slide from the given parameters.
|
* Generates a slide from the given parameters.
|
||||||
*
|
*
|
||||||
* @param context Application context
|
* @param context Application context
|
||||||
* @param contentType The contentType of the given Uri
|
* @param contentType The contentType of the given Uri
|
||||||
* @param uri The Uri pointing to the resource to create a slide out of
|
* @param uri The Uri pointing to the resource to create a slide out of
|
||||||
* @param width (Optional) width, can be 0.
|
* @param width (Optional) width, can be 0.
|
||||||
* @param height (Optional) height, can be 0.
|
* @param height (Optional) height, can be 0.
|
||||||
|
* @param transformProperties (Optional) transformProperties, can be 0.
|
||||||
*
|
*
|
||||||
* @return A Slide with all the information we can gather about it.
|
* @return A Slide with all the information we can gather about it.
|
||||||
*/
|
*/
|
||||||
@WorkerThread
|
public static @Nullable Slide getSlide(@NonNull Context context, @Nullable String contentType, @NonNull Uri uri, int width, int height, @Nullable AttachmentDatabase.TransformProperties transformProperties) {
|
||||||
public static @Nullable Slide getSlide(@NonNull Context context, @Nullable String contentType, @NonNull Uri uri, int width, int height) {
|
|
||||||
MediaType mediaType = MediaType.from(contentType);
|
MediaType mediaType = MediaType.from(contentType);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (PartAuthority.isLocalUri(uri)) {
|
if (PartAuthority.isLocalUri(uri)) {
|
||||||
return getManuallyCalculatedSlideInfo(context, mediaType, uri, width, height);
|
return getManuallyCalculatedSlideInfo(context, mediaType, uri, width, height, transformProperties);
|
||||||
} else {
|
} else {
|
||||||
Slide result = getContentResolverSlideInfo(context, mediaType, uri, width, height);
|
Slide result = getContentResolverSlideInfo(context, mediaType, uri, width, height, transformProperties);
|
||||||
|
|
||||||
if (result == null) return getManuallyCalculatedSlideInfo(context, mediaType, uri, width, height);
|
if (result == null) return getManuallyCalculatedSlideInfo(context, mediaType, uri, width, height, transformProperties);
|
||||||
else return result;
|
else return result;
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -58,7 +59,14 @@ public final class SlideFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @Nullable Slide getContentResolverSlideInfo(@NonNull Context context, @Nullable MediaType mediaType, @NonNull Uri uri, int width, int height) {
|
private static @Nullable Slide getContentResolverSlideInfo(
|
||||||
|
@NonNull Context context,
|
||||||
|
@Nullable MediaType mediaType,
|
||||||
|
@NonNull Uri uri,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
@Nullable AttachmentDatabase.TransformProperties transformProperties
|
||||||
|
) {
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
|
try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
|
||||||
|
@ -78,14 +86,22 @@ public final class SlideFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms");
|
Log.d(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms");
|
||||||
return mediaType.createSlide(context, uri, fileName, mimeType, null, fileSize, width, height, false);
|
return mediaType.createSlide(context, uri, fileName, mimeType, null, fileSize, width, height, false, transformProperties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NonNull Slide getManuallyCalculatedSlideInfo(@NonNull Context context, @Nullable MediaType mediaType, @NonNull Uri uri, int width, int height) throws IOException {
|
private static @NonNull Slide getManuallyCalculatedSlideInfo(
|
||||||
|
@NonNull Context context,
|
||||||
|
@Nullable MediaType mediaType,
|
||||||
|
@NonNull Uri uri,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
@Nullable AttachmentDatabase.TransformProperties transformProperties
|
||||||
|
) throws IOException
|
||||||
|
{
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
Long mediaSize = null;
|
Long mediaSize = null;
|
||||||
String fileName = null;
|
String fileName = null;
|
||||||
|
@ -118,7 +134,7 @@ public final class SlideFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms");
|
Log.d(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms");
|
||||||
return mediaType.createSlide(context, uri, fileName, mimeType, null, mediaSize, width, height, gif);
|
return mediaType.createSlide(context, uri, fileName, mimeType, null, mediaSize, width, height, gif, transformProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum MediaType {
|
public enum MediaType {
|
||||||
|
@ -142,17 +158,18 @@ public final class SlideFactory {
|
||||||
@Nullable String fileName,
|
@Nullable String fileName,
|
||||||
@Nullable String mimeType,
|
@Nullable String mimeType,
|
||||||
@Nullable BlurHash blurHash,
|
@Nullable BlurHash blurHash,
|
||||||
long dataSize,
|
long dataSize,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
boolean gif)
|
boolean gif,
|
||||||
|
@Nullable AttachmentDatabase.TransformProperties transformProperties)
|
||||||
{
|
{
|
||||||
if (mimeType == null) {
|
if (mimeType == null) {
|
||||||
mimeType = "application/octet-stream";
|
mimeType = "application/octet-stream";
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case IMAGE: return new ImageSlide(context, uri, dataSize, width, height, blurHash);
|
case IMAGE: return new ImageSlide(context, uri, mimeType, dataSize, width, height, false, null, blurHash, transformProperties);
|
||||||
case GIF: return new GifSlide(context, uri, dataSize, width, height);
|
case GIF: return new GifSlide(context, uri, dataSize, width, height);
|
||||||
case AUDIO: return new AudioSlide(context, uri, dataSize, false);
|
case AUDIO: return new AudioSlide(context, uri, dataSize, false);
|
||||||
case VIDEO: return new VideoSlide(context, uri, dataSize, gif);
|
case VIDEO: return new VideoSlide(context, uri, dataSize, gif);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
|
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
|
||||||
import org.thoughtcrime.securesms.conversation.MessageSendType;
|
import org.thoughtcrime.securesms.conversation.MessageSendType;
|
||||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
import org.thoughtcrime.securesms.database.model.Mention;
|
import org.thoughtcrime.securesms.database.model.Mention;
|
||||||
|
@ -32,9 +33,11 @@ import org.thoughtcrime.securesms.keyvalue.StorySend;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||||
import org.thoughtcrime.securesms.mediasend.Media;
|
import org.thoughtcrime.securesms.mediasend.Media;
|
||||||
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryBackgroundColors;
|
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryBackgroundColors;
|
||||||
|
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
|
import org.thoughtcrime.securesms.mms.SentMediaQuality;
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
import org.thoughtcrime.securesms.mms.SlideFactory;
|
import org.thoughtcrime.securesms.mms.SlideFactory;
|
||||||
|
@ -266,6 +269,8 @@ public final class MultiShareSender {
|
||||||
.flatMap(slide -> {
|
.flatMap(slide -> {
|
||||||
if (slide instanceof VideoSlide) {
|
if (slide instanceof VideoSlide) {
|
||||||
return expandToClips(context, (VideoSlide) slide).stream();
|
return expandToClips(context, (VideoSlide) slide).stream();
|
||||||
|
} else if (slide instanceof ImageSlide) {
|
||||||
|
return java.util.stream.Stream.of(ensureDefaultQuality(context, (ImageSlide) slide));
|
||||||
} else {
|
} else {
|
||||||
return java.util.stream.Stream.of(slide);
|
return java.util.stream.Stream.of(slide);
|
||||||
}
|
}
|
||||||
|
@ -273,8 +278,6 @@ public final class MultiShareSender {
|
||||||
.filter(it -> MediaUtil.isStorySupportedType(it.getContentType()))
|
.filter(it -> MediaUtil.isStorySupportedType(it.getContentType()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
// For each video slide, we want to convert it into a media, then clip it, and then transform it BACK into a slide.
|
|
||||||
|
|
||||||
for (final Slide slide : storySupportedSlides) {
|
for (final Slide slide : storySupportedSlides) {
|
||||||
SlideDeck singletonDeck = new SlideDeck();
|
SlideDeck singletonDeck = new SlideDeck();
|
||||||
singletonDeck.addSlide(slide);
|
singletonDeck.addSlide(slide);
|
||||||
|
@ -348,6 +351,26 @@ public final class MultiShareSender {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Slide ensureDefaultQuality(@NonNull Context context, @NonNull ImageSlide imageSlide) {
|
||||||
|
Attachment attachment = imageSlide.asAttachment();
|
||||||
|
if (attachment.getTransformProperties().getSentMediaQuality() == SentMediaQuality.HIGH.getCode()) {
|
||||||
|
return new ImageSlide(
|
||||||
|
context,
|
||||||
|
attachment.getUri(),
|
||||||
|
attachment.getContentType(),
|
||||||
|
attachment.getSize(),
|
||||||
|
attachment.getWidth(),
|
||||||
|
attachment.getHeight(),
|
||||||
|
attachment.isBorderless(),
|
||||||
|
attachment.getCaption(),
|
||||||
|
attachment.getBlurHash(),
|
||||||
|
AttachmentDatabase.TransformProperties.empty()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return imageSlide;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void sendTextMessage(@NonNull Context context,
|
private static void sendTextMessage(@NonNull Context context,
|
||||||
@NonNull MultiShareArgs multiShareArgs,
|
@NonNull MultiShareArgs multiShareArgs,
|
||||||
@NonNull Recipient recipient,
|
@NonNull Recipient recipient,
|
||||||
|
@ -434,7 +457,7 @@ public final class MultiShareSender {
|
||||||
slideDeck.addSlide(new StickerSlide(context, multiShareArgs.getDataUri(), 0, multiShareArgs.getStickerLocator(), multiShareArgs.getDataType()));
|
slideDeck.addSlide(new StickerSlide(context, multiShareArgs.getDataUri(), 0, multiShareArgs.getStickerLocator(), multiShareArgs.getDataType()));
|
||||||
} else if (!multiShareArgs.getMedia().isEmpty()) {
|
} else if (!multiShareArgs.getMedia().isEmpty()) {
|
||||||
for (Media media : multiShareArgs.getMedia()) {
|
for (Media media : multiShareArgs.getMedia()) {
|
||||||
Slide slide = SlideFactory.getSlide(context, media.getMimeType(), media.getUri(), media.getWidth(), media.getHeight());
|
Slide slide = SlideFactory.getSlide(context, media.getMimeType(), media.getUri(), media.getWidth(), media.getHeight(), media.getTransformProperties().orElse(null));
|
||||||
if (slide != null) {
|
if (slide != null) {
|
||||||
slideDeck.addSlide(slide);
|
slideDeck.addSlide(slide);
|
||||||
} else {
|
} else {
|
||||||
|
@ -442,7 +465,7 @@ public final class MultiShareSender {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (multiShareArgs.getDataUri() != null) {
|
} else if (multiShareArgs.getDataUri() != null) {
|
||||||
Slide slide = SlideFactory.getSlide(context, multiShareArgs.getDataType(), multiShareArgs.getDataUri(), 0, 0);
|
Slide slide = SlideFactory.getSlide(context, multiShareArgs.getDataType(), multiShareArgs.getDataUri(), 0, 0, null);
|
||||||
if (slide != null) {
|
if (slide != null) {
|
||||||
slideDeck.addSlide(slide);
|
slideDeck.addSlide(slide);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -148,7 +148,7 @@ public class MessageSender {
|
||||||
MessageDatabase database = SignalDatabase.mms();
|
MessageDatabase database = SignalDatabase.mms();
|
||||||
List<Long> messageIds = new ArrayList<>(messages.size());
|
List<Long> messageIds = new ArrayList<>(messages.size());
|
||||||
List<Long> threads = new ArrayList<>(messages.size());
|
List<Long> threads = new ArrayList<>(messages.size());
|
||||||
UploadDependencyGraph dependencyGraph = UploadDependencyGraph.EMPTY;
|
UploadDependencyGraph dependencyGraph;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
database.beginTransaction();
|
database.beginTransaction();
|
||||||
|
|
|
@ -186,6 +186,23 @@ object Stories {
|
||||||
object None : DurationResult()
|
object None : DurationResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@WorkerThread
|
||||||
|
fun canPreUploadMedia(media: Media): Boolean {
|
||||||
|
return if (MediaUtil.isVideo(media.mimeType)) {
|
||||||
|
getSendRequirements(media) != SendRequirements.REQUIRES_CLIP
|
||||||
|
} else {
|
||||||
|
!hasHighQualityTransform(media)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checkst to see if the given media has the "High Quality" toggled in its transform properties.
|
||||||
|
*/
|
||||||
|
fun hasHighQualityTransform(media: Media): Boolean {
|
||||||
|
return MediaUtil.isImageType(media.mimeType) && media.transformProperties.map { it.sentMediaQuality == SentMediaQuality.HIGH.code }.orElse(false)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun getSendRequirements(media: Media): SendRequirements {
|
fun getSendRequirements(media: Media): SendRequirements {
|
||||||
|
|
|
@ -10,13 +10,15 @@ package org.whispersystems.signalservice.api.messages;
|
||||||
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
|
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
|
||||||
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a local SignalServiceAttachment to be sent.
|
* Represents a local SignalServiceAttachment to be sent.
|
||||||
*/
|
*/
|
||||||
public class SignalServiceAttachmentStream extends SignalServiceAttachment {
|
public class SignalServiceAttachmentStream extends SignalServiceAttachment implements Closeable {
|
||||||
|
|
||||||
private final InputStream inputStream;
|
private final InputStream inputStream;
|
||||||
private final long length;
|
private final long length;
|
||||||
|
@ -151,4 +153,9 @@ public class SignalServiceAttachmentStream extends SignalServiceAttachment {
|
||||||
public Optional<ResumableUploadSpec> getResumableUploadSpec() {
|
public Optional<ResumableUploadSpec> getResumableUploadSpec() {
|
||||||
return resumableUploadSpec;
|
return resumableUploadSpec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
inputStream.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1450,17 +1450,11 @@ public class PushServiceSocket {
|
||||||
connections.add(call);
|
connections.add(call);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try (Response response = call.execute()) {
|
||||||
Response response;
|
|
||||||
|
|
||||||
try {
|
|
||||||
response = call.execute();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new PushNetworkException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.isSuccessful()) return file.getTransmittedDigest();
|
if (response.isSuccessful()) return file.getTransmittedDigest();
|
||||||
else throw new NonSuccessfulResponseCodeException(response.code(), "Response: " + response);
|
else throw new NonSuccessfulResponseCodeException(response.code(), "Response: " + response);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new PushNetworkException(e);
|
||||||
} finally {
|
} finally {
|
||||||
synchronized (connections) {
|
synchronized (connections) {
|
||||||
connections.remove(call);
|
connections.remove(call);
|
||||||
|
|
Ładowanie…
Reference in New Issue