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

View File

@@ -134,6 +134,6 @@ object AvatarRenderer {
}
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())
}
}

View File

@@ -199,7 +199,8 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor(
videoGif,
Optional.empty(),
Optional.ofNullable(caption),
Optional.ofNullable(transformProperties)
Optional.ofNullable(transformProperties),
Optional.ofNullable(fileName)
)
}
}

View File

@@ -249,6 +249,7 @@ import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
import org.thoughtcrime.securesms.mms.AttachmentManager
import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.mms.DocumentSlide
import org.thoughtcrime.securesms.mms.GifSlide
import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.mms.MediaConstraints
@@ -1206,7 +1207,7 @@ class ConversationFragment :
if (mediaType == SlideFactory.MediaType.VCARD) {
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 media = Media(
uri,
@@ -1220,11 +1221,10 @@ class ConversationFragment :
videoGif,
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty()
)
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.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.isDocumentType(it.mimeType) -> { DocumentSlide(requireContext(), it.uri, it.mimeType, it.size, it.fileName.orNull()) }
else -> {
Log.w(TAG, "Asked to send an unexpected mimeType: '${it.mimeType}'. Skipping.")
null

View File

@@ -126,7 +126,7 @@ public class GiphyActivity extends PassphraseRequiredActivity implements Keyboar
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);
}

View File

@@ -131,6 +131,7 @@ fun MediaTable.MediaRecord.toMedia(): Media? {
attachment.videoGif,
Optional.empty(),
Optional.ofNullable(attachment.caption),
Optional.empty(),
Optional.empty()
)
}

View File

@@ -101,6 +101,7 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera
false,
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
Optional.empty(),
Optional.empty(),
Optional.empty()));
}

View File

@@ -50,7 +50,7 @@ public final class ImageEditorModelRenderMediaTransform implements MediaTransfor
.withMimeType(MediaUtil.IMAGE_JPEG)
.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) {
Log.w(TAG, "Failed to render image. Using base image.");
return media;

View File

@@ -31,9 +31,10 @@ public class Media implements Parcelable {
private final boolean borderless;
private final boolean videoGif;
private Optional<String> bucketId;
private Optional<String> bucketId;
private Optional<String> caption;
private Optional<AttachmentTable.TransformProperties> transformProperties;
private Optional<String> fileName;
public Media(@NonNull Uri uri,
@NonNull String mimeType,
@@ -46,7 +47,8 @@ public class Media implements Parcelable {
boolean videoGif,
Optional<String> bucketId,
Optional<String> caption,
Optional<AttachmentTable.TransformProperties> transformProperties)
Optional<AttachmentTable.TransformProperties> transformProperties,
Optional<String> fileName)
{
this.uri = uri;
this.mimeType = mimeType;
@@ -60,6 +62,7 @@ public class Media implements Parcelable {
this.bucketId = bucketId;
this.caption = caption;
this.transformProperties = transformProperties;
this.fileName = fileName;
}
protected Media(Parcel in) {
@@ -80,6 +83,7 @@ public class Media implements Parcelable {
} catch (IOException e) {
throw new AssertionError(e);
}
fileName = Optional.ofNullable(in.readString());
}
public Uri getUri() {
@@ -130,6 +134,14 @@ public class Media implements Parcelable {
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() {
return transformProperties;
}
@@ -153,6 +165,7 @@ public class Media implements Parcelable {
dest.writeString(bucketId.orElse(null));
dest.writeString(caption.orElse(null));
dest.writeString(transformProperties.map(JsonUtil::toJson).orElse(null));
dest.writeString(fileName.orElse(null));
}
public static final Creator<Media> CREATOR = new Creator<Media>() {
@@ -194,7 +207,8 @@ public class Media implements Parcelable {
media.isVideoGif(),
media.getBucketId(),
media.getCaption(),
media.getTransformProperties());
media.getTransformProperties(),
media.getFileName());
}
public static @NonNull Media stripTransform(@NonNull Media media) {
@@ -211,6 +225,7 @@ public class Media implements Parcelable {
media.isVideoGif(),
media.getBucketId(),
media.getCaption(),
Optional.empty());
Optional.empty(),
media.getFileName());
}
}

View File

@@ -275,7 +275,7 @@ public class MediaRepository {
long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE));
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;
}
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 {
@@ -392,7 +392,7 @@ public class MediaRepository {
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

View File

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

View File

@@ -37,6 +37,7 @@ public final class SentMediaQualityTransform implements MediaTransform {
media.isVideoGif(),
media.getBucketId(),
media.getCaption(),
Optional.of(AttachmentTable.TransformProperties.forSentMediaQuality(media.getTransformProperties(), sentMediaQuality)));
Optional.of(AttachmentTable.TransformProperties.forSentMediaQuality(media.getTransformProperties(), sentMediaQuality)),
media.getFileName());
}
}

View File

@@ -22,7 +22,8 @@ class VideoTrimTransform(private val data: VideoTrimData) : MediaTransform {
media.isVideoGif,
media.bucketId,
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
)
}
}

View File

@@ -18,6 +18,7 @@ object MediaBuilder {
videoGif: Boolean = false,
bucketId: Optional<String> = Optional.empty(),
caption: Optional<String> = Optional.empty(),
transformProperties: Optional<AttachmentTable.TransformProperties> = Optional.empty()
) = Media(uri, mimeType, date, width, height, size, duration, borderless, videoGif, bucketId, caption, transformProperties)
transformProperties: Optional<AttachmentTable.TransformProperties> = Optional.empty(),
fileName: Optional<String> = Optional.empty()
) = Media(uri, mimeType, date, width, height, size, duration, borderless, videoGif, bucketId, caption, transformProperties, fileName)
}

View File

@@ -151,6 +151,21 @@ class MediaSelectionRepository(context: Context) {
scheduleMessages(sendType, contacts.map { it.recipientId }, trimmedBody, updatedMedia, trimmedMentions, trimmedBodyRanges, isViewOnce, scheduledTime)
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 {
val splitMessage = MessageUtil.getSplitMessage(context, trimmedBody, sendType.calculateCharacters(trimmedBody).maxPrimaryMessageSize)
val splitBody = splitMessage.body

View File

@@ -421,7 +421,7 @@ class MediaSelectionViewModel(
}
val filteredPreUploadMedia = if (destination is MediaSelectionDestination.SingleRecipient || !Stories.isFeatureEnabled()) {
media
media.filter { !MediaUtil.isDocumentType(it.mimeType) }
} else {
media.filter { Stories.MediaTransform.canPreUploadMedia(it) }
}

View File

@@ -17,7 +17,7 @@ object MediaValidator {
var error: FilterError? = null
if (!isAllMediaValid) {
error = if (media.all { MediaUtil.isImageOrVideoType(it.mimeType) }) {
error = if (media.all { MediaUtil.isImageOrVideoType(it.mimeType) || MediaUtil.isDocumentType(it.mimeType) }) {
FilterError.ItemTooLarge
} else {
FilterError.ItemInvalidType
@@ -53,7 +53,7 @@ object MediaValidator {
return media
.filter { m -> isSupportedMediaType(m.mimeType) }
.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 ->
!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)
}
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 {
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?)

View File

@@ -106,6 +106,7 @@ class MediaCaptureRepository(context: Context) {
false,
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
Optional.empty(),
Optional.empty(),
Optional.empty()
)
} catch (e: IOException) {
@@ -160,6 +161,7 @@ class MediaCaptureRepository(context: Context) {
false,
Optional.of(bucketId),
Optional.empty(),
Optional.empty(),
Optional.empty()
)
)

View File

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

View File

@@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.views.Stub
import org.thoughtcrime.securesms.util.visible
@@ -133,7 +134,7 @@ class AddMessageDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_a
binding.content.addAMessageInput.text = null
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()

View File

@@ -655,7 +655,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
}
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))
} else {
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> {
return when {
!state.isTouchEnabled || state.viewOnceToggleState == MediaSelectionState.ViewOnceToggleState.ONCE -> {
!state.isTouchEnabled || state.viewOnceToggleState == MediaSelectionState.ViewOnceToggleState.ONCE || MediaUtil.isDocumentType(state.focusedMedia?.mimeType) -> {
listOf(
MediaReviewAnimatorController.getFadeOutAnimator(addMediaButton),
MediaReviewAnimatorController.getFadeOutAnimator(selectionRecycler)
@@ -706,7 +706,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
}
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(
MediaReviewAnimatorController.getFadeInAnimator(saveButton)
)
@@ -718,7 +718,7 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment), Schedul
}
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))
} else {
listOf(MediaReviewAnimatorController.getFadeOutAnimator(qualityButton))

View File

@@ -5,6 +5,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter
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.images.MediaReviewImagePageFragment
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.isImageType(mediaItem.mimeType) -> MediaReviewImagePageFragment.newInstance(mediaItem.uri)
MediaUtil.isVideoType(mediaItem.mimeType) -> MediaReviewVideoPageFragment.newInstance(mediaItem.uri, mediaItem.isVideoGif)
MediaUtil.isDocumentType(mediaItem.mimeType) -> MediaReviewDocumentPageFragment.newInstance(mediaItem)
else -> {
throw UnsupportedOperationException("Can only render images and videos. Found mimetype: '" + mediaItem.mimeType + "'")
}

View File

@@ -17,25 +17,19 @@
package org.thoughtcrime.securesms.mms;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.AsyncTask;
import android.provider.ContactsContract;
import android.provider.OpenableColumns;
import android.util.Pair;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.content.ContextCompat;
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.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView;
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.SignalPlace;
import org.thoughtcrime.securesms.conversation.MessageSendType;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.MediaTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
@@ -256,149 +248,6 @@ public class AttachmentManager {
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() {
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) {
if (MediaPreviewV2Fragment.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs(

View File

@@ -230,34 +230,7 @@ public abstract class Slide {
}
public @NonNull Optional<String> getFileType(@NonNull Context context) {
Optional<String> fileName = getFileName();
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 "";
return MediaUtil.getFileType(context, getFileName(), getUri());
}
@Override

View File

@@ -286,6 +286,7 @@ class ShareActivity : PassphraseRequiredActivity(), MultiselectForwardFragment.C
false,
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty()
)
)

View File

@@ -116,6 +116,7 @@ class ShareRepository(context: Context) {
false,
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
Optional.empty(),
Optional.empty(),
Optional.empty()
)
}.filterNotNull()

View File

@@ -359,7 +359,8 @@ object Stories {
media.isVideoGif,
media.bucketId,
media.caption,
Optional.of(transformProperties)
Optional.of(transformProperties),
media.fileName
)
}
@@ -398,6 +399,7 @@ object Stories {
videoSlide.isVideoGif,
Optional.empty(),
videoSlide.caption,
Optional.empty(),
Optional.empty()
)
}

View File

@@ -51,6 +51,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLConnection;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
public class MediaUtil {
@@ -137,6 +138,35 @@ public class MediaUtil {
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) {
return MimeTypeMap.getSingleton()
.getExtensionFromMimeType(getMimeType(context, uri));
@@ -384,6 +414,10 @@ public class MediaUtil {
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) {
if (uri == null) {
return false;

View File

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

View File

@@ -116,7 +116,8 @@ class MediaRepositoryTest {
videoGif: Boolean = false,
bucketId: 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 {
return Media(
uri,
@@ -130,7 +131,8 @@ class MediaRepositoryTest {
videoGif,
bucketId,
caption,
transformProperties
transformProperties,
fileName
)
}
}