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

@@ -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 + "'")
}