Add full send attachments.

This commit is contained in:
Michelle Tang
2024-06-28 17:59:26 -04:00
committed by Cody Henthorne
parent 3879a8ffdb
commit a966812bfc
29 changed files with 375 additions and 219 deletions
@@ -134,6 +134,6 @@ object AvatarRenderer {
} }
private fun createMedia(uri: Uri, size: Long): Media { private fun createMedia(uri: Uri, size: Long): Media {
return Media(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), DIMENSIONS, DIMENSIONS, size, 0, false, false, Optional.empty(), Optional.empty(), Optional.empty()) return Media(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), DIMENSIONS, DIMENSIONS, size, 0, false, false, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty())
} }
} }
@@ -199,7 +199,8 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor(
videoGif, videoGif,
Optional.empty(), Optional.empty(),
Optional.ofNullable(caption), Optional.ofNullable(caption),
Optional.ofNullable(transformProperties) Optional.ofNullable(transformProperties),
Optional.ofNullable(fileName)
) )
} }
} }
@@ -249,6 +249,7 @@ import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
import org.thoughtcrime.securesms.mms.AttachmentManager import org.thoughtcrime.securesms.mms.AttachmentManager
import org.thoughtcrime.securesms.mms.AudioSlide import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.mms.DocumentSlide
import org.thoughtcrime.securesms.mms.GifSlide import org.thoughtcrime.securesms.mms.GifSlide
import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.mms.MediaConstraints import org.thoughtcrime.securesms.mms.MediaConstraints
@@ -1206,7 +1207,7 @@ class ConversationFragment :
if (mediaType == SlideFactory.MediaType.VCARD) { if (mediaType == SlideFactory.MediaType.VCARD) {
conversationActivityResultContracts.launchContactShareEditor(uri, viewModel.recipientSnapshot!!.chatColors) conversationActivityResultContracts.launchContactShareEditor(uri, viewModel.recipientSnapshot!!.chatColors)
} else if (mediaType == SlideFactory.MediaType.IMAGE || mediaType == SlideFactory.MediaType.GIF || mediaType == SlideFactory.MediaType.VIDEO) { } else {
val mimeType = MediaUtil.getMimeType(requireContext(), uri) ?: mediaType.toFallbackMimeType() val mimeType = MediaUtil.getMimeType(requireContext(), uri) ?: mediaType.toFallbackMimeType()
val media = Media( val media = Media(
uri, uri,
@@ -1220,11 +1221,10 @@ class ConversationFragment :
videoGif, videoGif,
Optional.empty(), Optional.empty(),
Optional.empty(), Optional.empty(),
Optional.empty(),
Optional.empty() Optional.empty()
) )
conversationActivityResultContracts.launchMediaEditor(listOf(media), recipientId, composeText.textTrimmed) conversationActivityResultContracts.launchMediaEditor(listOf(media), recipientId, composeText.textTrimmed)
} else {
attachmentManager.setMedia(Glide.with(this), uri, mediaType, MediaConstraints.getPushMediaConstraints(), width, height)
} }
} }
@@ -3614,6 +3614,7 @@ class ConversationFragment :
MediaUtil.isVideoType(it.mimeType) -> VideoSlide(requireContext(), it.uri, it.size, it.isVideoGif, it.width, it.height, it.caption.orNull(), it.transformProperties.orNull()) MediaUtil.isVideoType(it.mimeType) -> VideoSlide(requireContext(), it.uri, it.size, it.isVideoGif, it.width, it.height, it.caption.orNull(), it.transformProperties.orNull())
MediaUtil.isGif(it.mimeType) -> GifSlide(requireContext(), it.uri, it.size, it.width, it.height, it.isBorderless, it.caption.orNull()) MediaUtil.isGif(it.mimeType) -> GifSlide(requireContext(), it.uri, it.size, it.width, it.height, it.isBorderless, it.caption.orNull())
MediaUtil.isImageType(it.mimeType) -> ImageSlide(requireContext(), it.uri, it.mimeType, it.size, it.width, it.height, it.isBorderless, it.caption.orNull(), null, it.transformProperties.orNull()) MediaUtil.isImageType(it.mimeType) -> ImageSlide(requireContext(), it.uri, it.mimeType, it.size, it.width, it.height, it.isBorderless, it.caption.orNull(), null, it.transformProperties.orNull())
MediaUtil.isDocumentType(it.mimeType) -> { DocumentSlide(requireContext(), it.uri, it.mimeType, it.size, it.fileName.orNull()) }
else -> { else -> {
Log.w(TAG, "Asked to send an unexpected mimeType: '${it.mimeType}'. Skipping.") Log.w(TAG, "Asked to send an unexpected mimeType: '${it.mimeType}'. Skipping.")
null null
@@ -126,7 +126,7 @@ public class GiphyActivity extends PassphraseRequiredActivity implements Keyboar
mimeType = mediaType.toFallbackMimeType(); mimeType = mediaType.toFallbackMimeType();
} }
Media media = new Media(success.getBlobUri(), mimeType, 0, success.getWidth(), success.getHeight(), 0, 0, false, true, Optional.empty(), Optional.empty(), Optional.empty()); Media media = new Media(success.getBlobUri(), mimeType, 0, success.getWidth(), success.getHeight(), 0, 0, false, true, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
startActivityForResult(MediaSelectionActivity.editor(this, sendType, Collections.singletonList(media), recipientId, text), MEDIA_SENDER); startActivityForResult(MediaSelectionActivity.editor(this, sendType, Collections.singletonList(media), recipientId, text), MEDIA_SENDER);
} }
@@ -131,6 +131,7 @@ fun MediaTable.MediaRecord.toMedia(): Media? {
attachment.videoGif, attachment.videoGif,
Optional.empty(), Optional.empty(),
Optional.ofNullable(attachment.caption), Optional.ofNullable(attachment.caption),
Optional.empty(),
Optional.empty() Optional.empty()
) )
} }
@@ -101,6 +101,7 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera
false, false,
Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.of(Media.ALL_MEDIA_BUCKET_ID),
Optional.empty(), Optional.empty(),
Optional.empty(),
Optional.empty())); Optional.empty()));
} }
@@ -50,7 +50,7 @@ public final class ImageEditorModelRenderMediaTransform implements MediaTransfor
.withMimeType(MediaUtil.IMAGE_JPEG) .withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionOnDisk(context); .createForSingleSessionOnDisk(context);
return new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), 0, false, false, media.getBucketId(), media.getCaption(), Optional.empty()); return new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), 0, false, false, media.getBucketId(), media.getCaption(), Optional.empty(), Optional.empty());
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, "Failed to render image. Using base image."); Log.w(TAG, "Failed to render image. Using base image.");
return media; return media;
@@ -31,9 +31,10 @@ public class Media implements Parcelable {
private final boolean borderless; private final boolean borderless;
private final boolean videoGif; private final boolean videoGif;
private Optional<String> bucketId; private Optional<String> bucketId;
private Optional<String> caption; private Optional<String> caption;
private Optional<AttachmentTable.TransformProperties> transformProperties; private Optional<AttachmentTable.TransformProperties> transformProperties;
private Optional<String> fileName;
public Media(@NonNull Uri uri, public Media(@NonNull Uri uri,
@NonNull String mimeType, @NonNull String mimeType,
@@ -46,7 +47,8 @@ public class Media implements Parcelable {
boolean videoGif, boolean videoGif,
Optional<String> bucketId, Optional<String> bucketId,
Optional<String> caption, Optional<String> caption,
Optional<AttachmentTable.TransformProperties> transformProperties) Optional<AttachmentTable.TransformProperties> transformProperties,
Optional<String> fileName)
{ {
this.uri = uri; this.uri = uri;
this.mimeType = mimeType; this.mimeType = mimeType;
@@ -60,6 +62,7 @@ public class Media implements Parcelable {
this.bucketId = bucketId; this.bucketId = bucketId;
this.caption = caption; this.caption = caption;
this.transformProperties = transformProperties; this.transformProperties = transformProperties;
this.fileName = fileName;
} }
protected Media(Parcel in) { protected Media(Parcel in) {
@@ -80,6 +83,7 @@ public class Media implements Parcelable {
} catch (IOException e) { } catch (IOException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
fileName = Optional.ofNullable(in.readString());
} }
public Uri getUri() { public Uri getUri() {
@@ -130,6 +134,14 @@ public class Media implements Parcelable {
this.caption = Optional.ofNullable(caption); this.caption = Optional.ofNullable(caption);
} }
public Optional<String> getFileName() {
return fileName;
}
public void setFileName(String name) {
this.fileName = Optional.ofNullable(name);
}
public Optional<AttachmentTable.TransformProperties> getTransformProperties() { public Optional<AttachmentTable.TransformProperties> getTransformProperties() {
return transformProperties; return transformProperties;
} }
@@ -153,6 +165,7 @@ public class Media implements Parcelable {
dest.writeString(bucketId.orElse(null)); dest.writeString(bucketId.orElse(null));
dest.writeString(caption.orElse(null)); dest.writeString(caption.orElse(null));
dest.writeString(transformProperties.map(JsonUtil::toJson).orElse(null)); dest.writeString(transformProperties.map(JsonUtil::toJson).orElse(null));
dest.writeString(fileName.orElse(null));
} }
public static final Creator<Media> CREATOR = new Creator<Media>() { public static final Creator<Media> CREATOR = new Creator<Media>() {
@@ -194,7 +207,8 @@ public class Media implements Parcelable {
media.isVideoGif(), media.isVideoGif(),
media.getBucketId(), media.getBucketId(),
media.getCaption(), media.getCaption(),
media.getTransformProperties()); media.getTransformProperties(),
media.getFileName());
} }
public static @NonNull Media stripTransform(@NonNull Media media) { public static @NonNull Media stripTransform(@NonNull Media media) {
@@ -211,6 +225,7 @@ public class Media implements Parcelable {
media.isVideoGif(), media.isVideoGif(),
media.getBucketId(), media.getBucketId(),
media.getCaption(), media.getCaption(),
Optional.empty()); Optional.empty(),
media.getFileName());
} }
} }
@@ -275,7 +275,7 @@ public class MediaRepository {
long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE)); long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE));
long duration = !isImage ? cursor.getInt(cursor.getColumnIndexOrThrow(Video.Media.DURATION)) : 0; long duration = !isImage ? cursor.getInt(cursor.getColumnIndexOrThrow(Video.Media.DURATION)) : 0;
media.add(fixMimeType(context, new Media(uri, mimetype, date, width, height, size, duration, false, false, Optional.of(bucketId), Optional.empty(), Optional.empty()))); media.add(fixMimeType(context, new Media(uri, mimetype, date, width, height, size, duration, false, false, Optional.of(bucketId), Optional.empty(), Optional.empty(), Optional.empty())));
} }
} }
@@ -366,7 +366,7 @@ public class MediaRepository {
height = dimens.second; height = dimens.second;
} }
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.isBorderless(), media.isVideoGif(), media.getBucketId(), media.getCaption(), Optional.empty()); return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.isBorderless(), media.isVideoGif(), media.getBucketId(), media.getCaption(), Optional.empty(), Optional.empty());
} }
private Media getContentResolverPopulatedMedia(@NonNull Context context, @NonNull Media media) throws IOException { private Media getContentResolverPopulatedMedia(@NonNull Context context, @NonNull Media media) throws IOException {
@@ -392,7 +392,7 @@ public class MediaRepository {
height = dimens.second; height = dimens.second;
} }
return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.isBorderless(), media.isVideoGif(), media.getBucketId(), media.getCaption(), Optional.empty()); return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.isBorderless(), media.isVideoGif(), media.getBucketId(), media.getCaption(), Optional.empty(), Optional.empty());
} }
@VisibleForTesting @VisibleForTesting
@@ -0,0 +1,139 @@
package org.thoughtcrime.securesms.mediasend
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import org.signal.core.util.getParcelableCompat
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.Util
import java.io.IOException
import java.util.Optional
/**
* Fragment to show full screen document attachments
*/
class MediaSendDocumentFragment : Fragment(R.layout.mediasend_document_fragment), MediaSendPageFragment {
companion object {
private val TAG = Log.tag(MediaSendDocumentFragment::class.java)
private const val KEY_MEDIA = "media"
fun newInstance(media: Media): MediaSendDocumentFragment {
val args = Bundle()
args.putParcelable(KEY_MEDIA, media)
val fragment = MediaSendDocumentFragment()
fragment.arguments = args
fragment.uri = media.uri
return fragment
}
}
private lateinit var uri: Uri
private lateinit var media: Media
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val name: TextView = view.findViewById(R.id.name)
val size: TextView = view.findViewById(R.id.size)
val extension: TextView = view.findViewById(R.id.extension)
this.media = requireNotNull(requireArguments().getParcelableCompat(KEY_MEDIA, Media::class.java))
val fileInfo: Pair<String?, Long>? = getFileInfo()
if (fileInfo != null) {
media.setFileName(fileInfo.first)
name.text = fileInfo.first ?: getString(R.string.DocumentView_unnamed_file)
size.text = Util.getPrettyFileSize(fileInfo.second)
val extensionText: String = MediaUtil.getFileType(requireContext(), Optional.ofNullable(fileInfo.first), media.uri).orElse("")
if (extensionText.length <= 3) {
extension.text = extensionText
extension.setTextAppearance(requireContext(), R.style.Signal_Text_BodySmall)
} else if (extensionText.length == 4) {
extension.text = extensionText
extension.setTextAppearance(requireContext(), R.style.Signal_Text_Caption)
}
} else {
Toast.makeText(requireContext(), R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment, Toast.LENGTH_SHORT).show()
requireActivity().finishAfterTransition()
}
}
override fun getUri(): Uri {
return uri
}
override fun setUri(uri: Uri) {
this.uri = uri
}
override fun saveState(): Any = Unit
override fun restoreState(state: Any) = Unit
override fun notifyHidden() = Unit
private fun getFileInfo(): Pair<String?, Long>? {
val fileInfo: Pair<String?, Long>
try {
if (PartAuthority.isLocalUri(uri)) {
fileInfo = getManuallyCalculatedFileInfo(uri)
} else {
val result = getContentResolverFileInfo(uri)
fileInfo = if ((result == null)) getManuallyCalculatedFileInfo(uri) else result
}
} catch (e: IOException) {
Log.w(TAG, e)
return null
}
return fileInfo
}
@Throws(IOException::class)
private fun getManuallyCalculatedFileInfo(uri: Uri): Pair<String?, Long> {
var fileName: String? = null
var fileSize: Long? = null
if (PartAuthority.isLocalUri(uri)) {
fileSize = PartAuthority.getAttachmentSize(requireContext(), uri)
fileName = PartAuthority.getAttachmentFileName(requireContext(), uri)
}
if (fileSize == null) {
fileSize = MediaUtil.getMediaSize(context, uri)
}
return Pair(fileName, fileSize)
}
private fun getContentResolverFileInfo(uri: Uri): Pair<String, Long>? {
var cursor: Cursor? = null
try {
cursor = requireContext().contentResolver.query(uri, null, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
val fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
val fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
media.setFileName(fileName)
return Pair(fileName, fileSize)
}
} finally {
cursor?.close()
}
return null
}
}
@@ -37,6 +37,7 @@ public final class SentMediaQualityTransform implements MediaTransform {
media.isVideoGif(), media.isVideoGif(),
media.getBucketId(), media.getBucketId(),
media.getCaption(), media.getCaption(),
Optional.of(AttachmentTable.TransformProperties.forSentMediaQuality(media.getTransformProperties(), sentMediaQuality))); Optional.of(AttachmentTable.TransformProperties.forSentMediaQuality(media.getTransformProperties(), sentMediaQuality)),
media.getFileName());
} }
} }
@@ -22,7 +22,8 @@ class VideoTrimTransform(private val data: VideoTrimData) : MediaTransform {
media.isVideoGif, media.isVideoGif,
media.bucketId, media.bucketId,
media.caption, media.caption,
Optional.of(TransformProperties(false, data.isDurationEdited, data.startTimeUs, data.endTimeUs, SentMediaQuality.STANDARD.code, false)) Optional.of(TransformProperties(false, data.isDurationEdited, data.startTimeUs, data.endTimeUs, SentMediaQuality.STANDARD.code, false)),
media.fileName
) )
} }
} }
@@ -18,6 +18,7 @@ object MediaBuilder {
videoGif: Boolean = false, videoGif: Boolean = false,
bucketId: Optional<String> = Optional.empty(), bucketId: Optional<String> = Optional.empty(),
caption: Optional<String> = Optional.empty(), caption: Optional<String> = Optional.empty(),
transformProperties: Optional<AttachmentTable.TransformProperties> = Optional.empty() transformProperties: Optional<AttachmentTable.TransformProperties> = Optional.empty(),
) = Media(uri, mimeType, date, width, height, size, duration, borderless, videoGif, bucketId, caption, transformProperties) fileName: Optional<String> = Optional.empty()
) = Media(uri, mimeType, date, width, height, size, duration, borderless, videoGif, bucketId, caption, transformProperties, fileName)
} }
@@ -151,6 +151,21 @@ class MediaSelectionRepository(context: Context) {
scheduleMessages(sendType, contacts.map { it.recipientId }, trimmedBody, updatedMedia, trimmedMentions, trimmedBodyRanges, isViewOnce, scheduledTime) scheduleMessages(sendType, contacts.map { it.recipientId }, trimmedBody, updatedMedia, trimmedMentions, trimmedBodyRanges, isViewOnce, scheduledTime)
emitter.onComplete() emitter.onComplete()
} }
} else if (MediaUtil.isDocumentType(selectedMedia.first().mimeType)) {
Log.i(TAG, "Document. Skipping pre-upload.")
emitter.onSuccess(
MediaSendActivityResult(
recipientId = singleRecipient!!.id,
nonUploadedMedia = updatedMedia,
body = trimmedBody,
messageSendType = sendType,
isViewOnce = isViewOnce,
mentions = trimmedMentions,
bodyRanges = trimmedBodyRanges,
storyType = StoryType.NONE,
scheduledTime = scheduledTime
)
)
} else { } else {
val splitMessage = MessageUtil.getSplitMessage(context, trimmedBody, sendType.calculateCharacters(trimmedBody).maxPrimaryMessageSize) val splitMessage = MessageUtil.getSplitMessage(context, trimmedBody, sendType.calculateCharacters(trimmedBody).maxPrimaryMessageSize)
val splitBody = splitMessage.body val splitBody = splitMessage.body
@@ -421,7 +421,7 @@ class MediaSelectionViewModel(
} }
val filteredPreUploadMedia = if (destination is MediaSelectionDestination.SingleRecipient || !Stories.isFeatureEnabled()) { val filteredPreUploadMedia = if (destination is MediaSelectionDestination.SingleRecipient || !Stories.isFeatureEnabled()) {
media media.filter { !MediaUtil.isDocumentType(it.mimeType) }
} else { } else {
media.filter { Stories.MediaTransform.canPreUploadMedia(it) } media.filter { Stories.MediaTransform.canPreUploadMedia(it) }
} }
@@ -17,7 +17,7 @@ object MediaValidator {
var error: FilterError? = null var error: FilterError? = null
if (!isAllMediaValid) { if (!isAllMediaValid) {
error = if (media.all { MediaUtil.isImageOrVideoType(it.mimeType) }) { error = if (media.all { MediaUtil.isImageOrVideoType(it.mimeType) || MediaUtil.isDocumentType(it.mimeType) }) {
FilterError.ItemTooLarge FilterError.ItemTooLarge
} else { } else {
FilterError.ItemInvalidType FilterError.ItemInvalidType
@@ -53,7 +53,7 @@ object MediaValidator {
return media return media
.filter { m -> isSupportedMediaType(m.mimeType) } .filter { m -> isSupportedMediaType(m.mimeType) }
.filter { m -> .filter { m ->
MediaUtil.isImageAndNotGif(m.mimeType) || isValidGif(context, m, mediaConstraints) || isValidVideo(context, m, mediaConstraints) MediaUtil.isImageAndNotGif(m.mimeType) || isValidGif(context, m, mediaConstraints) || isValidVideo(context, m, mediaConstraints) || isValidDocument(context, m, mediaConstraints)
} }
.filter { m -> .filter { m ->
!isStory || Stories.MediaTransform.getSendRequirements(m) != Stories.MediaTransform.SendRequirements.CAN_NOT_SEND !isStory || Stories.MediaTransform.getSendRequirements(m) != Stories.MediaTransform.SendRequirements.CAN_NOT_SEND
@@ -68,8 +68,12 @@ object MediaValidator {
return MediaUtil.isVideoType(media.mimeType) && media.size < mediaConstraints.getUncompressedVideoMaxSize(context) return MediaUtil.isVideoType(media.mimeType) && media.size < mediaConstraints.getUncompressedVideoMaxSize(context)
} }
private fun isValidDocument(context: Context, media: Media, mediaConstraints: MediaConstraints): Boolean {
return MediaUtil.isDocumentType(media.mimeType) && media.size < mediaConstraints.getDocumentMaxSize(context)
}
private fun isSupportedMediaType(mimeType: String): Boolean { private fun isSupportedMediaType(mimeType: String): Boolean {
return MediaUtil.isGif(mimeType) || MediaUtil.isImageType(mimeType) || MediaUtil.isVideoType(mimeType) return MediaUtil.isGif(mimeType) || MediaUtil.isImageType(mimeType) || MediaUtil.isVideoType(mimeType) || MediaUtil.isDocumentType(mimeType)
} }
data class FilterResult(val filteredMedia: List<Media>, val filterError: FilterError?, val bucketId: String?) data class FilterResult(val filteredMedia: List<Media>, val filterError: FilterError?, val bucketId: String?)
@@ -106,6 +106,7 @@ class MediaCaptureRepository(context: Context) {
false, false,
Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.of(Media.ALL_MEDIA_BUCKET_ID),
Optional.empty(), Optional.empty(),
Optional.empty(),
Optional.empty() Optional.empty()
) )
} catch (e: IOException) { } catch (e: IOException) {
@@ -160,6 +161,7 @@ class MediaCaptureRepository(context: Context) {
false, false,
Optional.of(bucketId), Optional.of(bucketId),
Optional.empty(), Optional.empty(),
Optional.empty(),
Optional.empty() Optional.empty()
) )
) )
@@ -0,0 +1,63 @@
package org.thoughtcrime.securesms.mediasend.v2.documents
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import org.signal.core.util.getParcelableCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendDocumentFragment
import org.thoughtcrime.securesms.mediasend.v2.HudCommand
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
private const val DOCUMENT_TAG = "media.send.document.fragment"
/**
* Fragment which ensures we fire off ResumeEntryTransition when viewing a document.
*/
class MediaReviewDocumentPageFragment : Fragment(R.layout.fragment_container) {
private lateinit var mediaSendDocumentFragment: MediaSendDocumentFragment
private val sharedViewModel: MediaSelectionViewModel by viewModels(ownerProducer = { requireActivity() })
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mediaSendDocumentFragment = ensureFragment()
sharedViewModel.sendCommand(HudCommand.ResumeEntryTransition)
}
private fun ensureFragment(): MediaSendDocumentFragment {
val fragmentInManager: MediaSendDocumentFragment? = childFragmentManager.findFragmentByTag(DOCUMENT_TAG) as? MediaSendDocumentFragment
return if (fragmentInManager != null) {
fragmentInManager
} else {
val mediaSendDocumentFragment = MediaSendDocumentFragment.newInstance(requireMedia())
childFragmentManager.beginTransaction()
.replace(
R.id.fragment_container,
mediaSendDocumentFragment,
DOCUMENT_TAG
)
.commitAllowingStateLoss()
mediaSendDocumentFragment
}
}
private fun requireMedia(): Media = requireNotNull(requireArguments().getParcelableCompat(ARG_MEDIA, Media::class.java))
companion object {
private const val ARG_MEDIA = "arg.media"
fun newInstance(media: Media): Fragment {
return MediaReviewDocumentPageFragment().apply {
arguments = Bundle().apply {
putParcelable(ARG_MEDIA, media)
}
}
}
}
}
@@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.views.Stub import org.thoughtcrime.securesms.util.views.Stub
import org.thoughtcrime.securesms.util.visible import org.thoughtcrime.securesms.util.visible
@@ -133,7 +134,7 @@ class AddMessageDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_a
binding.content.addAMessageInput.text = null binding.content.addAMessageInput.text = null
dismiss() dismiss()
} }
binding.content.viewOnceToggle.visible = state.selectedMedia.size == 1 && !state.isStory binding.content.viewOnceToggle.visible = state.selectedMedia.size == 1 && !state.isStory && !MediaUtil.isDocumentType(state.focusedMedia?.mimeType)
} }
initializeMentions() initializeMentions()
@@ -655,7 +655,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
} }
private fun computeViewOnceButtonAnimators(state: MediaSelectionState): List<Animator> { private fun computeViewOnceButtonAnimators(state: MediaSelectionState): List<Animator> {
return if (state.isTouchEnabled && state.selectedMedia.size == 1 && !state.isStory) { return if (state.isTouchEnabled && state.selectedMedia.size == 1 && !state.isStory && !MediaUtil.isDocumentType(state.focusedMedia?.mimeType)) {
listOf(MediaReviewAnimatorController.getFadeInAnimator(viewOnceButton)) listOf(MediaReviewAnimatorController.getFadeInAnimator(viewOnceButton))
} else { } else {
listOf(MediaReviewAnimatorController.getFadeOutAnimator(viewOnceButton)) listOf(MediaReviewAnimatorController.getFadeOutAnimator(viewOnceButton))
@@ -672,7 +672,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
private fun computeAddMediaButtonsAnimators(state: MediaSelectionState): List<Animator> { private fun computeAddMediaButtonsAnimators(state: MediaSelectionState): List<Animator> {
return when { return when {
!state.isTouchEnabled || state.viewOnceToggleState == MediaSelectionState.ViewOnceToggleState.ONCE -> { !state.isTouchEnabled || state.viewOnceToggleState == MediaSelectionState.ViewOnceToggleState.ONCE || MediaUtil.isDocumentType(state.focusedMedia?.mimeType) -> {
listOf( listOf(
MediaReviewAnimatorController.getFadeOutAnimator(addMediaButton), MediaReviewAnimatorController.getFadeOutAnimator(addMediaButton),
MediaReviewAnimatorController.getFadeOutAnimator(selectionRecycler) MediaReviewAnimatorController.getFadeOutAnimator(selectionRecycler)
@@ -706,7 +706,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
} }
private fun computeSaveButtonAnimators(state: MediaSelectionState): List<Animator> { private fun computeSaveButtonAnimators(state: MediaSelectionState): List<Animator> {
return if (state.isTouchEnabled && !MediaUtil.isVideo(state.focusedMedia?.mimeType)) { return if (state.isTouchEnabled && !MediaUtil.isVideo(state.focusedMedia?.mimeType) && !MediaUtil.isDocumentType(state.focusedMedia?.mimeType)) {
listOf( listOf(
MediaReviewAnimatorController.getFadeInAnimator(saveButton) MediaReviewAnimatorController.getFadeInAnimator(saveButton)
) )
@@ -718,7 +718,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
} }
private fun computeQualityButtonAnimators(state: MediaSelectionState): List<Animator> { private fun computeQualityButtonAnimators(state: MediaSelectionState): List<Animator> {
return if (state.isTouchEnabled && !state.isStory) { return if (state.isTouchEnabled && !state.isStory && !MediaUtil.isDocumentType(state.focusedMedia?.mimeType)) {
listOf(MediaReviewAnimatorController.getFadeInAnimator(qualityButton)) listOf(MediaReviewAnimatorController.getFadeInAnimator(qualityButton))
} else { } else {
listOf(MediaReviewAnimatorController.getFadeOutAnimator(qualityButton)) listOf(MediaReviewAnimatorController.getFadeOutAnimator(qualityButton))
@@ -5,6 +5,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.v2.documents.MediaReviewDocumentPageFragment
import org.thoughtcrime.securesms.mediasend.v2.gif.MediaReviewGifPageFragment import org.thoughtcrime.securesms.mediasend.v2.gif.MediaReviewGifPageFragment
import org.thoughtcrime.securesms.mediasend.v2.images.MediaReviewImagePageFragment import org.thoughtcrime.securesms.mediasend.v2.images.MediaReviewImagePageFragment
import org.thoughtcrime.securesms.mediasend.v2.videos.MediaReviewVideoPageFragment import org.thoughtcrime.securesms.mediasend.v2.videos.MediaReviewVideoPageFragment
@@ -46,6 +47,7 @@ class MediaReviewFragmentPagerAdapter(fragment: Fragment) : FragmentStateAdapter
MediaUtil.isGif(mediaItem.mimeType) -> MediaReviewGifPageFragment.newInstance(mediaItem.uri) MediaUtil.isGif(mediaItem.mimeType) -> MediaReviewGifPageFragment.newInstance(mediaItem.uri)
MediaUtil.isImageType(mediaItem.mimeType) -> MediaReviewImagePageFragment.newInstance(mediaItem.uri) MediaUtil.isImageType(mediaItem.mimeType) -> MediaReviewImagePageFragment.newInstance(mediaItem.uri)
MediaUtil.isVideoType(mediaItem.mimeType) -> MediaReviewVideoPageFragment.newInstance(mediaItem.uri, mediaItem.isVideoGif) MediaUtil.isVideoType(mediaItem.mimeType) -> MediaReviewVideoPageFragment.newInstance(mediaItem.uri, mediaItem.isVideoGif)
MediaUtil.isDocumentType(mediaItem.mimeType) -> MediaReviewDocumentPageFragment.newInstance(mediaItem)
else -> { else -> {
throw UnsupportedOperationException("Can only render images and videos. Found mimetype: '" + mediaItem.mimeType + "'") throw UnsupportedOperationException("Can only render images and videos. Found mimetype: '" + mediaItem.mimeType + "'")
} }
@@ -17,25 +17,19 @@
package org.thoughtcrime.securesms.mms; package org.thoughtcrime.securesms.mms;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.provider.OpenableColumns;
import android.util.Pair;
import android.view.View; import android.view.View;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@@ -50,7 +44,6 @@ import org.signal.core.util.concurrent.SettableFuture;
import org.signal.core.util.concurrent.SimpleTask; import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.AudioView; import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView; import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.RemovableEditableMediaView; import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
@@ -58,7 +51,6 @@ 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.AttachmentTable;
import org.thoughtcrime.securesms.database.MediaTable; import org.thoughtcrime.securesms.database.MediaTable;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
@@ -256,149 +248,6 @@ public class AttachmentManager {
attachmentListener.onAttachmentChanged(); attachmentListener.onAttachmentChanged();
} }
@SuppressLint("StaticFieldLeak")
public ListenableFuture<Boolean> setMedia(@NonNull final RequestManager requestManager,
@NonNull final Uri uri,
@NonNull final SlideFactory.MediaType mediaType,
@NonNull final MediaConstraints constraints,
final int width,
final int height)
{
inflateStub();
final SettableFuture<Boolean> result = new SettableFuture<>();
new AsyncTask<Void, Void, Slide>() {
private boolean areConstraintsSatisfied = false;
@Override
protected void onPreExecute() {
thumbnail.clear(requestManager);
thumbnail.showProgressSpinner();
attachmentViewStub.get().setVisibility(View.VISIBLE);
}
@Override
protected @Nullable Slide doInBackground(Void... params) {
Slide slide;
try {
if (PartAuthority.isLocalUri(uri)) {
slide = getManuallyCalculatedSlideInfo(uri, width, height);
} else {
Slide result = getContentResolverSlideInfo(uri, width, height);
slide = (result == null) ? getManuallyCalculatedSlideInfo(uri, width, height) : result;
}
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
this.areConstraintsSatisfied = areConstraintsSatisfied(context, slide, constraints);
return slide;
}
@Override
protected void onPostExecute(@Nullable final Slide slide) {
if (slide == null) {
attachmentViewStub.get().setVisibility(View.GONE);
Toast.makeText(context,
R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
Toast.LENGTH_SHORT).show();
result.set(false);
} else if (!areConstraintsSatisfied) {
attachmentViewStub.get().setVisibility(View.GONE);
Toast.makeText(context,
R.string.ConversationActivity_attachment_exceeds_size_limits,
Toast.LENGTH_SHORT).show();
result.set(false);
} else {
setSlide(slide);
attachmentViewStub.get().setVisibility(View.VISIBLE);
if (slide.hasAudio()) {
audioView.setAudio((AudioSlide) slide, null, false, false);
removableMediaView.display(audioView, false);
result.set(true);
} else if (slide.hasDocument()) {
documentView.setDocument((DocumentSlide) slide, false);
removableMediaView.display(documentView, false);
result.set(true);
} else {
Attachment attachment = slide.asAttachment();
result.deferTo(thumbnail.setImageResource(requestManager, slide, false, true, attachment.width, attachment.height));
removableMediaView.display(thumbnail, mediaType == SlideFactory.MediaType.IMAGE);
}
attachmentListener.onAttachmentChanged();
}
}
private @Nullable Slide getContentResolverSlideInfo(Uri uri, int width, int height) {
Cursor cursor = null;
long start = System.currentTimeMillis();
try {
cursor = context.getContentResolver().query(uri, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
String fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME));
long fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE));
String mimeType = context.getContentResolver().getType(uri);
if (width == 0 || height == 0) {
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
width = dimens.first;
height = dimens.second;
}
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, null);
}
} finally {
if (cursor != null) cursor.close();
}
return null;
}
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;
AttachmentTable.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);
transformProperties = PartAuthority.getAttachmentTransformProperties(uri);
}
if (mediaSize == null) {
mediaSize = MediaUtil.getMediaSize(context, uri);
}
if (mimeType == null) {
mimeType = MediaUtil.getMimeType(context, uri);
}
if (width == 0 || height == 0) {
Pair<Integer, Integer> dimens = MediaUtil.getDimensions(context, mimeType, uri);
width = dimens.first;
height = dimens.second;
}
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, transformProperties);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return result;
}
public boolean isAttachmentPresent() { public boolean isAttachmentPresent() {
return attachmentViewStub.resolved() && attachmentViewStub.get().getVisibility() == View.VISIBLE; return attachmentViewStub.resolved() && attachmentViewStub.get().getVisibility() == View.VISIBLE;
} }
@@ -531,16 +380,6 @@ public class AttachmentManager {
} }
} }
@WorkerThread
private boolean areConstraintsSatisfied(final @NonNull Context context,
final @Nullable Slide slide,
final @NonNull MediaConstraints constraints)
{
return slide == null ||
constraints.isSatisfied(context, slide.asAttachment()) ||
constraints.canResize(slide.asAttachment());
}
private void previewImageDraft(final @NonNull Slide slide) { private void previewImageDraft(final @NonNull Slide slide) {
if (MediaPreviewV2Fragment.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) { if (MediaPreviewV2Fragment.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs( MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs(
@@ -230,34 +230,7 @@ public abstract class Slide {
} }
public @NonNull Optional<String> getFileType(@NonNull Context context) { public @NonNull Optional<String> getFileType(@NonNull Context context) {
Optional<String> fileName = getFileName(); return MediaUtil.getFileType(context, getFileName(), getUri());
if (fileName.isPresent()) {
String fileType = getFileType(fileName);
if (!fileType.isEmpty()) {
return Optional.of(fileType);
}
}
return Optional.ofNullable(MediaUtil.getExtension(context, getUri()));
}
private static @NonNull String getFileType(Optional<String> fileName) {
if (!fileName.isPresent()) return "";
String[] parts = fileName.get().split("\\.");
if (parts.length < 2) {
return "";
}
String suffix = parts[parts.length - 1];
if (suffix.length() <= 3) {
return suffix;
}
return "";
} }
@Override @Override
@@ -286,6 +286,7 @@ class ShareActivity : PassphraseRequiredActivity(), MultiselectForwardFragment.C
false, false,
Optional.empty(), Optional.empty(),
Optional.empty(), Optional.empty(),
Optional.empty(),
Optional.empty() Optional.empty()
) )
) )
@@ -116,6 +116,7 @@ class ShareRepository(context: Context) {
false, false,
Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.of(Media.ALL_MEDIA_BUCKET_ID),
Optional.empty(), Optional.empty(),
Optional.empty(),
Optional.empty() Optional.empty()
) )
}.filterNotNull() }.filterNotNull()
@@ -359,7 +359,8 @@ object Stories {
media.isVideoGif, media.isVideoGif,
media.bucketId, media.bucketId,
media.caption, media.caption,
Optional.of(transformProperties) Optional.of(transformProperties),
media.fileName
) )
} }
@@ -398,6 +399,7 @@ object Stories {
videoSlide.isVideoGif, videoSlide.isVideoGif,
Optional.empty(), Optional.empty(),
videoSlide.caption, videoSlide.caption,
Optional.empty(),
Optional.empty() Optional.empty()
) )
} }
@@ -51,6 +51,7 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URLConnection; import java.net.URLConnection;
import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
public class MediaUtil { public class MediaUtil {
@@ -137,6 +138,35 @@ public class MediaUtil {
return getCorrectedMimeType(type); return getCorrectedMimeType(type);
} }
public static @NonNull Optional<String> getFileType(@NonNull Context context, Optional<String> fileName, Uri uri) {
if (fileName.isPresent()) {
String fileType = getFileType(fileName);
if (!fileType.isEmpty()) {
return Optional.of(fileType);
}
}
return Optional.ofNullable(MediaUtil.getExtension(context, uri));
}
private static @NonNull String getFileType(Optional<String> fileName) {
if (!fileName.isPresent()) return "";
String[] parts = fileName.get().split("\\.");
if (parts.length < 2) {
return "";
}
String suffix = parts[parts.length - 1];
if (suffix.length() <= 3) {
return suffix;
}
return "";
}
public static @Nullable String getExtension(@NonNull Context context, @Nullable Uri uri) { public static @Nullable String getExtension(@NonNull Context context, @Nullable Uri uri) {
return MimeTypeMap.getSingleton() return MimeTypeMap.getSingleton()
.getExtensionFromMimeType(getMimeType(context, uri)); .getExtensionFromMimeType(getMimeType(context, uri));
@@ -384,6 +414,10 @@ public class MediaUtil {
return OCTET.equals(contentType); return OCTET.equals(contentType);
} }
public static boolean isDocumentType(String contentType) {
return !isImageOrVideoType(contentType) && !isGif(contentType) && !isLongTextType(contentType) && !isViewOnceType(contentType);
}
public static boolean hasVideoThumbnail(@NonNull Context context, @Nullable Uri uri) { public static boolean hasVideoThumbnail(@NonNull Context context, @Nullable Uri uri) {
if (uri == null) { if (uri == null) {
return false; return false;
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="70dp"
android:minHeight="94dp"
android:src="@drawable/ic_document_large" />
<TextView
android:id="@+id/extension"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="@color/signal_light_colorOnSurface"
style="@style/Signal.Text.Caption"
tools:text="pdf" />
</FrameLayout>
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="4dp"
android:layout_marginHorizontal="16dp"
android:singleLine="true"
android:ellipsize="middle"
android:textColor="@color/signal_colorOnSurface"
android:gravity="center"
style="@style/Signal.Text.BodyLarge"
android:letterSpacing="0"
tools:text="thoughts.pdf" />
<TextView
android:id="@+id/size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/signal_colorOnSurfaceVariant"
style="@style/Signal.Text.BodyLarge"
tools:text="12 KB" />
</LinearLayout>
@@ -116,7 +116,8 @@ class MediaRepositoryTest {
videoGif: Boolean = false, videoGif: Boolean = false,
bucketId: Optional<String> = Optional.empty(), bucketId: Optional<String> = Optional.empty(),
caption: Optional<String> = Optional.empty(), caption: Optional<String> = Optional.empty(),
transformProperties: Optional<TransformProperties> = Optional.empty() transformProperties: Optional<TransformProperties> = Optional.empty(),
fileName: Optional<String> = Optional.empty()
): Media { ): Media {
return Media( return Media(
uri, uri,
@@ -130,7 +131,8 @@ class MediaRepositoryTest {
videoGif, videoGif,
bucketId, bucketId,
caption, caption,
transformProperties transformProperties,
fileName
) )
} }
} }