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
Alex Hart 2022-07-13 12:48:17 -03:00 zatwierdzone przez Cody Henthorne
rodzic c4bef8099f
commit b18542a839
20 zmienionych plików z 384 dodań i 79 usunięć

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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
)
}
}

Wyświetl plik

@ -170,7 +170,7 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor(
isVideoGif,
Optional.empty(),
Optional.ofNullable(caption),
Optional.empty()
Optional.of(transformProperties)
)
}
}

Wyświetl plik

@ -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.
* If true, then guarantees not to affect other attachments.
* @param onlyModifyThisAttachment If false and more than one attachment shares this file and quality, they will all
* be updated. If true, then guarantees not to affect other attachments.
*/
public void updateAttachmentData(@NonNull DatabaseAttachment databaseAttachment,
@NonNull MediaStream mediaStream,
@ -807,7 +807,8 @@ public class AttachmentDatabase extends Database {
File destination = oldDataInfo.file;
if (onlyModifyThisAttachment) {
boolean isSingleUseOfData = onlyModifyThisAttachment || oldDataInfo.hash == null;
if (isSingleUseOfData) {
if (fileReferencedByMoreThanOneAttachment(destination)) {
Log.i(TAG, "Creating a new file as this one is used by more than one attachment");
destination = newFile();
@ -827,7 +828,10 @@ public class AttachmentDatabase extends Database {
contentValues.put(DATA_RANDOM, dataInfo.random);
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.");
}
@ -846,10 +850,32 @@ public class AttachmentDatabase extends Database {
}
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);
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();
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.isNull(cursor.getColumnIndexOrThrow(dataType))) {
return null;
@ -1038,9 +1061,6 @@ public class AttachmentDatabase extends Database {
} else {
return null;
}
} finally {
if (cursor != null)
cursor.close();
}
}
@ -1127,7 +1147,7 @@ public class AttachmentDatabase extends Database {
Pair<String, String[]> selectorArgs = buildSharedFileSelectorArgs(hash, excludedAttachmentId);
try (Cursor cursor = database.query(TABLE_NAME,
new String[]{DATA, DATA_RANDOM, SIZE},
new String[]{DATA, DATA_RANDOM, SIZE, TRANSFORM_PROPERTIES},
selectorArgs.first,
selectorArgs.second,
null,
@ -1298,7 +1318,8 @@ public class AttachmentDatabase extends Database {
template.getTransferState() == TRANSFER_PROGRESS_DONE &&
template.getTransformProperties().shouldSkipTransform() &&
template.getDigest() != null &&
!attachment.getTransformProperties().isVideoEdited();
!attachment.getTransformProperties().isVideoEdited() &&
template.getTransformProperties().sentMediaQuality == attachment.getTransformProperties().getSentMediaQuality();
ContentValues contentValues = new ContentValues();
contentValues.put(MMS_ID, mmsId);
@ -1326,7 +1347,7 @@ public class AttachmentDatabase extends Database {
contentValues.put(TRANSFORM_PROPERTIES, attachment.getTransformProperties().serialize());
} else {
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()) {
@ -1340,7 +1361,7 @@ public class AttachmentDatabase extends Database {
contentValues.put(DATA, dataInfo.file.getAbsolutePath());
contentValues.put(SIZE, dataInfo.length);
contentValues.put(DATA_RANDOM, dataInfo.random);
if (attachment.getTransformProperties().isVideoEdited()) {
if (attachment.getTransformProperties().isVideoEdited() || attachment.getTransformProperties().sentMediaQuality != template.getTransformProperties().getSentMediaQuality()) {
contentValues.putNull(DATA_HASH);
} else {
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);
}
private static class DataInfo {
@VisibleForTesting
static class DataInfo {
private final File file;
private final long length;
private final byte[] random;
@ -1420,6 +1442,22 @@ public class AttachmentDatabase extends Database {
this.random = random;
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 {
@ -1520,6 +1558,10 @@ public class AttachmentDatabase extends Database {
return sentMediaQuality;
}
@NonNull TransformProperties withSkipTransform() {
return new TransformProperties(true, false, 0, 0, sentMediaQuality);
}
@NonNull String serialize() {
return JsonUtil.toJson(this);
}

Wyświetl plik

@ -49,7 +49,6 @@ import org.thoughtcrime.securesms.video.videoconverter.EncodingException;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

Wyświetl plik

@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
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.ResumeLocationInvalidException;
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());
try (NotificationController notification = getNotificationForAttachment(databaseAttachment)) {
SignalServiceAttachment localAttachment = getAttachmentFor(databaseAttachment, notification, resumableUploadSpec);
SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream());
Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.getFastPreflightId()).get();
try (SignalServiceAttachmentStream localAttachment = getAttachmentFor(databaseAttachment, notification, resumableUploadSpec)) {
SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment);
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) {
if (e.getCode() == 400) {
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);
}
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 {
if (attachment.getUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getUri());
SignalServiceAttachment.Builder builder = SignalServiceAttachment.newStreamBuilder()
.withStream(is)
@ -208,7 +213,6 @@ public final class AttachmentUploadJob extends BaseJob {
} else {
return builder.build();
}
} catch (IOException 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.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 {

Wyświetl plik

@ -7,6 +7,8 @@ import android.os.Parcelable;
import androidx.annotation.NonNull;
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 java.io.IOException;
@ -194,4 +196,21 @@ public class Media implements Parcelable {
media.getCaption(),
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());
}
}

Wyświetl plik

@ -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 {
Stories.MediaTransform.getSendRequirements(it) == Stories.MediaTransform.SendRequirements.REQUIRES_CLIP
}.map { media ->
@ -135,12 +135,20 @@ class MediaSelectionRepository(context: Context) {
}.flatten()
} 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.updateCaptions(updatedMedia)
uploadRepository.updateDisplayOrder(updatedMedia)
uploadRepository.getPreUploadResults { uploadResults ->
if (contacts.isNotEmpty()) {
sendMessages(contacts, splitBody, uploadResults, trimmedMentions, isViewOnce, clippedMediaForStories)
sendMessages(contacts, splitBody, uploadResults, trimmedMentions, isViewOnce, clippedVideosForStories + lowResImagesForStories)
uploadRepository.deleteAbandonedAttachments()
emitter.onComplete()
} else if (uploadResults.isNotEmpty()) {

Wyświetl plik

@ -336,7 +336,7 @@ class MediaSelectionViewModel(
}
val filteredPreUploadMedia = if (Stories.isFeatureEnabled()) {
media.filterNot { Stories.MediaTransform.getSendRequirements(media) == Stories.MediaTransform.SendRequirements.REQUIRES_CLIP }
media.filter { Stories.MediaTransform.canPreUploadMedia(it) }
} else {
media
}

Wyświetl plik

@ -448,7 +448,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment) {
private fun computeQualityButtonAnimators(state: MediaSelectionState): List<Animator> {
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))
} else {
listOf(MediaReviewAnimatorController.getFadeOutAnimator(qualityButton))

Wyświetl plik

@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.components.location.SignalMapView;
import org.thoughtcrime.securesms.components.location.SignalPlace;
import org.thoughtcrime.securesms.conversation.MessageSendType;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
import org.thoughtcrime.securesms.maps.PlacePickerActivity;
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");
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 {
if (cursor != null) cursor.close();
@ -326,17 +327,19 @@ public class AttachmentManager {
}
private @NonNull Slide getManuallyCalculatedSlideInfo(Uri uri, int width, int height) throws IOException {
long start = System.currentTimeMillis();
Long mediaSize = null;
String fileName = null;
String mimeType = null;
boolean gif = false;
long start = System.currentTimeMillis();
Long mediaSize = null;
String fileName = null;
String mimeType = null;
boolean gif = false;
AttachmentDatabase.TransformProperties transformProperties = null;
if (PartAuthority.isLocalUri(uri)) {
mediaSize = PartAuthority.getAttachmentSize(context, uri);
fileName = PartAuthority.getAttachmentFileName(context, uri);
mimeType = PartAuthority.getAttachmentContentType(context, uri);
gif = PartAuthority.getAttachmentIsVideoGif(context, uri);
mediaSize = PartAuthority.getAttachmentSize(context, uri);
fileName = PartAuthority.getAttachmentFileName(context, uri);
mimeType = PartAuthority.getAttachmentContentType(context, uri);
gif = PartAuthority.getAttachmentIsVideoGif(context, uri);
transformProperties = PartAuthority.getAttachmentTransformProperties(uri);
}
if (mediaSize == null) {
@ -354,7 +357,7 @@ public class AttachmentManager {
}
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);

Wyświetl plik

@ -39,6 +39,10 @@ public abstract class MediaConstraints {
public abstract int getImageMaxHeight(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
* down the list until the image can be scaled to fit under {@link #getImageMaxSize(Context)}.

Wyświetl plik

@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.emoji.EmojiFiles;
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) {
PartUriParser partUri = new PartUriParser(uri);
return PartProvider.getContentUri(partUri.getPartId());

Wyświetl plik

@ -23,6 +23,11 @@ public class PushMediaConstraints extends MediaConstraints {
currentConfig = getCurrentConfig(ApplicationDependencies.getApplication(), sentMediaQuality);
}
@Override
public boolean isHighQuality() {
return currentConfig == MediaConfig.LEVEL_3;
}
@Override
public int getImageMaxWidth(Context context) {
return currentConfig.imageSizeTargets[0];

Wyświetl plik

@ -13,6 +13,7 @@ import androidx.annotation.WorkerThread;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.IOException;
@ -31,25 +32,25 @@ public final class SlideFactory {
/**
* Generates a slide from the given parameters.
*
* @param context Application context
* @param contentType The contentType of the given Uri
* @param uri The Uri pointing to the resource to create a slide out of
* @param width (Optional) width, can be 0.
* @param height (Optional) height, can be 0.
* @param context Application context
* @param contentType The contentType of the given Uri
* @param uri The Uri pointing to the resource to create a slide out of
* @param width (Optional) width, 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.
*/
@WorkerThread
public static @Nullable Slide getSlide(@NonNull Context context, @Nullable String contentType, @NonNull Uri uri, int width, int height) {
public static @Nullable Slide getSlide(@NonNull Context context, @Nullable String contentType, @NonNull Uri uri, int width, int height, @Nullable AttachmentDatabase.TransformProperties transformProperties) {
MediaType mediaType = MediaType.from(contentType);
try {
if (PartAuthority.isLocalUri(uri)) {
return getManuallyCalculatedSlideInfo(context, mediaType, uri, width, height);
return getManuallyCalculatedSlideInfo(context, mediaType, uri, width, height, transformProperties);
} 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;
}
} 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();
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");
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;
}
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 mediaSize = 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");
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 {
@ -142,17 +158,18 @@ public final class SlideFactory {
@Nullable String fileName,
@Nullable String mimeType,
@Nullable BlurHash blurHash,
long dataSize,
int width,
int height,
boolean gif)
long dataSize,
int width,
int height,
boolean gif,
@Nullable AttachmentDatabase.TransformProperties transformProperties)
{
if (mimeType == null) {
mimeType = "application/octet-stream";
}
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 AUDIO: return new AudioSlide(context, uri, dataSize, false);
case VIDEO: return new VideoSlide(context, uri, dataSize, gif);

Wyświetl plik

@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
import org.thoughtcrime.securesms.conversation.MessageSendType;
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
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.mediasend.Media;
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.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.SentMediaQuality;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.SlideFactory;
@ -266,6 +269,8 @@ public final class MultiShareSender {
.flatMap(slide -> {
if (slide instanceof VideoSlide) {
return expandToClips(context, (VideoSlide) slide).stream();
} else if (slide instanceof ImageSlide) {
return java.util.stream.Stream.of(ensureDefaultQuality(context, (ImageSlide) slide));
} else {
return java.util.stream.Stream.of(slide);
}
@ -273,8 +278,6 @@ public final class MultiShareSender {
.filter(it -> MediaUtil.isStorySupportedType(it.getContentType()))
.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) {
SlideDeck singletonDeck = new SlideDeck();
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,
@NonNull MultiShareArgs multiShareArgs,
@NonNull Recipient recipient,
@ -434,7 +457,7 @@ public final class MultiShareSender {
slideDeck.addSlide(new StickerSlide(context, multiShareArgs.getDataUri(), 0, multiShareArgs.getStickerLocator(), multiShareArgs.getDataType()));
} else if (!multiShareArgs.getMedia().isEmpty()) {
for (Media media : multiShareArgs.getMedia()) {
Slide slide = SlideFactory.getSlide(context, media.getMimeType(), media.getUri(), media.getWidth(), media.getHeight());
Slide slide = SlideFactory.getSlide(context, media.getMimeType(), media.getUri(), media.getWidth(), media.getHeight(), media.getTransformProperties().orElse(null));
if (slide != null) {
slideDeck.addSlide(slide);
} else {
@ -442,7 +465,7 @@ public final class MultiShareSender {
}
}
} 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) {
slideDeck.addSlide(slide);
} else {

Wyświetl plik

@ -148,7 +148,7 @@ public class MessageSender {
MessageDatabase database = SignalDatabase.mms();
List<Long> messageIds = new ArrayList<>(messages.size());
List<Long> threads = new ArrayList<>(messages.size());
UploadDependencyGraph dependencyGraph = UploadDependencyGraph.EMPTY;
UploadDependencyGraph dependencyGraph;
try {
database.beginTransaction();

Wyświetl plik

@ -186,6 +186,23 @@ object Stories {
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
@WorkerThread
fun getSendRequirements(media: Media): SendRequirements {

Wyświetl plik

@ -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.ResumableUploadSpec;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
/**
* 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 long length;
@ -151,4 +153,9 @@ public class SignalServiceAttachmentStream extends SignalServiceAttachment {
public Optional<ResumableUploadSpec> getResumableUploadSpec() {
return resumableUploadSpec;
}
@Override
public void close() throws IOException {
inputStream.close();
}
}

Wyświetl plik

@ -1450,17 +1450,11 @@ public class PushServiceSocket {
connections.add(call);
}
try {
Response response;
try {
response = call.execute();
} catch (IOException e) {
throw new PushNetworkException(e);
}
try (Response response = call.execute()) {
if (response.isSuccessful()) return file.getTransmittedDigest();
else throw new NonSuccessfulResponseCodeException(response.code(), "Response: " + response);
} catch (IOException e) {
throw new PushNetworkException(e);
} finally {
synchronized (connections) {
connections.remove(call);