Prevent sending videos over 30s in length to a story.

fork-5.53.8
Alex Hart 2022-04-15 15:14:59 -03:00 zatwierdzone przez Greyson Parrelli
rodzic fa13b464f8
commit 043f06e188
16 zmienionych plików z 127 dodań i 15 usunięć

Wyświetl plik

@ -1359,6 +1359,11 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
public void onDismissForwardSheet() {
}
@Override
public boolean canSendMediaToStories() {
return true;
}
public interface ConversationFragmentListener extends VoiceNoteMediaControllerOwner {
boolean isKeyboardOpen();
void setThreadId(long threadId);

Wyświetl plik

@ -11,6 +11,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
import org.thoughtcrime.securesms.util.fragments.findListener
import org.thoughtcrime.securesms.util.fragments.requireListener
class MultiselectForwardBottomSheet : FixedRoundedCornerBottomSheetDialogFragment(), MultiselectForwardFragment.Callback {
@ -43,6 +44,10 @@ class MultiselectForwardBottomSheet : FixedRoundedCornerBottomSheetDialogFragmen
return backgroundColor
}
override fun canSendMediaToStories(): Boolean {
return requireListener<Callback>().canSendMediaToStories()
}
override fun setResult(bundle: Bundle) {
setFragmentResult(MultiselectForwardFragment.RESULT_KEY, bundle)
}
@ -67,5 +72,6 @@ class MultiselectForwardBottomSheet : FixedRoundedCornerBottomSheetDialogFragmen
interface Callback {
fun onFinishForwardAction()
fun onDismissForwardSheet()
fun canSendMediaToStories(): Boolean = true
}
}

Wyświetl plik

@ -371,7 +371,7 @@ class MultiselectForwardFragment :
}
private fun isSelectedMediaValidForStories(): Boolean {
return getMultiShareArgs().all { it.isValidForStories }
return requireListener<Callback>().canSendMediaToStories() && getMultiShareArgs().all { it.isValidForStories }
}
private fun isSelectedMediaValidForNonStories(): Boolean {
@ -393,6 +393,7 @@ class MultiselectForwardFragment :
fun setResult(bundle: Bundle)
fun getContainer(): ViewGroup
fun getDialogBackgroundColor(): Int
fun canSendMediaToStories(): Boolean = true
}
companion object {

Wyświetl plik

@ -47,7 +47,12 @@ class MultiselectForwardFullScreenDialogFragment : FullScreenDialogFragment(), M
override fun onSearchInputFocused() = Unit
override fun canSendMediaToStories(): Boolean {
return findListener<Callback>()?.canSendMediaToStories() ?: true
}
interface Callback {
fun onFinishForwardAction()
fun onFinishForwardAction() = Unit
fun canSendMediaToStories(): Boolean = true
}
}

Wyświetl plik

@ -145,6 +145,11 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera
return MediaConstraints.getPushMediaConstraints();
}
@Override
public int getMaxVideoDuration() {
return -1;
}
@Override
public void onTouchEventsNeeded(boolean needed) {
}

Wyświetl plik

@ -57,5 +57,6 @@ public interface CameraFragment {
void onCameraCountButtonClicked();
@NonNull LiveData<Optional<Media>> getMostRecentMediaItem();
@NonNull MediaConstraints getMediaConstraints();
int getMaxVideoDuration();
}
}

Wyświetl plik

@ -305,6 +305,10 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
camera.setCaptureMode(SignalCameraView.CaptureMode.MIXED);
int maxDuration = VideoUtil.getMaxVideoRecordDurationInSeconds(requireContext(), controller.getMediaConstraints());
if (controller.getMaxVideoDuration() > 0) {
maxDuration = controller.getMaxVideoDuration();
}
Log.d(TAG, "Max duration: " + maxDuration + " sec");
captureButton.setVideoCaptureListener(new CameraXVideoCaptureHelper(

Wyświetl plik

@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.video.VideoBitRateCalculator;
import org.thoughtcrime.securesms.video.VideoPlayer;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class VideoEditorFragment extends Fragment implements VideoEditorHud.EventListener,
MediaSendPageFragment {
@ -34,6 +35,7 @@ public class VideoEditorFragment extends Fragment implements VideoEditorHud.Even
private static final String KEY_MAX_OUTPUT = "max_output_size";
private static final String KEY_MAX_SEND = "max_send_size";
private static final String KEY_IS_VIDEO_GIF = "is_video_gif";
private static final String KEY_MAX_DURATION = "max_duration";
private final Throttler videoScanThrottle = new Throttler(150);
private final Handler handler = new Handler(Looper.getMainLooper());
@ -46,14 +48,16 @@ public class VideoEditorFragment extends Fragment implements VideoEditorHud.Even
@Nullable private VideoEditorHud hud;
private Runnable updatePosition;
private boolean isInEdit;
private boolean wasPlayingBeforeEdit;
private boolean wasPlayingBeforeEdit;
private long maxVideoDurationUs;
public static VideoEditorFragment newInstance(@NonNull Uri uri, long maxCompressedVideoSize, long maxAttachmentSize, boolean isVideoGif) {
public static VideoEditorFragment newInstance(@NonNull Uri uri, long maxCompressedVideoSize, long maxAttachmentSize, boolean isVideoGif, long maxVideoDuration) {
Bundle args = new Bundle();
args.putParcelable(KEY_URI, uri);
args.putLong(KEY_MAX_OUTPUT, maxCompressedVideoSize);
args.putLong(KEY_MAX_SEND, maxAttachmentSize);
args.putBoolean(KEY_IS_VIDEO_GIF, isVideoGif);
args.putLong(KEY_MAX_DURATION, maxVideoDuration);
VideoEditorFragment fragment = new VideoEditorFragment();
fragment.setArguments(args);
@ -84,8 +88,9 @@ public class VideoEditorFragment extends Fragment implements VideoEditorHud.Even
player = view.findViewById(R.id.video_player);
uri = requireArguments().getParcelable(KEY_URI);
isVideoGif = requireArguments().getBoolean(KEY_IS_VIDEO_GIF);
uri = requireArguments().getParcelable(KEY_URI);
isVideoGif = requireArguments().getBoolean(KEY_IS_VIDEO_GIF);
maxVideoDurationUs = TimeUnit.MILLISECONDS.toMicros(requireArguments().getLong(KEY_MAX_DURATION));
long maxOutput = requireArguments().getLong(KEY_MAX_OUTPUT);
long maxSend = requireArguments().getLong(KEY_MAX_SEND);
@ -117,6 +122,7 @@ public class VideoEditorFragment extends Fragment implements VideoEditorHud.Even
} else if (MediaConstraints.isVideoTranscodeAvailable()) {
hud = view.findViewById(R.id.video_editor_hud);
hud.setEventListener(this);
clampToMaxVideoDuration(data, true);
updateHud(data);
if (data.durationEdited) {
player.clip(data.startTimeUs, data.endTimeUs, autoplay);
@ -279,12 +285,15 @@ public class VideoEditorFragment extends Fragment implements VideoEditorHud.Even
boolean wasEdited = data.durationEdited;
boolean durationEdited = clampedStartTime > 0 || endTimeUs < totalDurationUs;
boolean endMoved = data.endTimeUs != endTimeUs;
data.durationEdited = durationEdited;
data.totalDurationUs = totalDurationUs;
data.startTimeUs = clampedStartTime;
data.endTimeUs = endTimeUs;
clampToMaxVideoDuration(data, !endMoved);
if (editingComplete) {
isInEdit = false;
videoScanThrottle.clear();
@ -338,6 +347,26 @@ public class VideoEditorFragment extends Fragment implements VideoEditorHud.Even
});
}
private void clampToMaxVideoDuration(@NonNull Data data, boolean clampEnd) {
if (!MediaConstraints.isVideoTranscodeAvailable()) {
return;
}
if ((data.endTimeUs - data.startTimeUs) <= maxVideoDurationUs) {
return;
}
data.durationEdited = true;
if (clampEnd) {
data.endTimeUs = data.startTimeUs + maxVideoDurationUs;
} else {
data.startTimeUs = data.endTimeUs - maxVideoDurationUs;
}
updateHud(data);
}
public static class Data {
boolean durationEdited;
long totalDurationUs;

Wyświetl plik

@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiEventListener
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFullScreenDialogFragment
import org.thoughtcrime.securesms.conversation.mutiselect.forward.SearchConfigurationProvider
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
@ -51,7 +52,8 @@ class MediaSelectionActivity :
EmojiKeyboardPageFragment.Callback,
EmojiEventListener,
EmojiSearchFragment.Callback,
SearchConfigurationProvider {
SearchConfigurationProvider,
MultiselectForwardFullScreenDialogFragment.Callback {
private var animateInShadowLayerValueAnimator: ValueAnimator? = null
private var animateInTextColorValueAnimator: ValueAnimator? = null
@ -464,4 +466,8 @@ class MediaSelectionActivity :
}
}
}
override fun canSendMediaToStories(): Boolean {
return viewModel.canShareSelectedMediaToStory()
}
}

Wyświetl plik

@ -55,11 +55,11 @@ class MediaSelectionRepository(context: Context) {
val uploadRepository = MediaUploadRepository(this.context)
val isMetered: Observable<Boolean> = MeteredConnectivity.isMetered(this.context)
fun populateAndFilterMedia(media: List<Media>, mediaConstraints: MediaConstraints, maxSelection: Int): Single<MediaValidator.FilterResult> {
fun populateAndFilterMedia(media: List<Media>, mediaConstraints: MediaConstraints, maxSelection: Int, isStory: Boolean): Single<MediaValidator.FilterResult> {
return Single.fromCallable {
val populatedMedia = mediaRepository.getPopulatedMedia(context, media)
MediaValidator.filterMedia(context, populatedMedia, mediaConstraints, maxSelection)
MediaValidator.filterMedia(context, populatedMedia, mediaConstraints, maxSelection, isStory)
}.subscribeOn(Schedulers.io())
}

Wyświetl plik

@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
import org.thoughtcrime.securesms.sharing.MultiShareArgs
import org.thoughtcrime.securesms.util.SingleLiveEvent
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.livedata.Store
@ -105,6 +106,10 @@ class MediaSelectionViewModel(
addMedia(listOf(media))
}
fun isStory(): Boolean {
return store.state.isStory
}
private fun addMedia(media: List<Media>) {
val newSelectionList: List<Media> = linkedSetOf<Media>().apply {
addAll(store.state.selectedMedia)
@ -113,7 +118,7 @@ class MediaSelectionViewModel(
disposables.add(
repository
.populateAndFilterMedia(newSelectionList, getMediaConstraints(), store.state.maxSelection)
.populateAndFilterMedia(newSelectionList, getMediaConstraints(), store.state.maxSelection, store.state.isStory)
.subscribe { filterResult ->
if (filterResult.filteredMedia.isNotEmpty()) {
store.update {
@ -340,6 +345,10 @@ class MediaSelectionViewModel(
return store.state.selectedMedia.isNotEmpty()
}
fun canShareSelectedMediaToStory(): Boolean {
return store.state.selectedMedia.all { MultiShareArgs.isValidStoryDuration(it) }
}
fun onRestoreState(savedInstanceState: Bundle) {
val selection: List<Media> = savedInstanceState.getParcelableArrayList(STATE_SELECTION) ?: emptyList()
val focused: Media? = savedInstanceState.getParcelable(STATE_FOCUSED)

Wyświetl plik

@ -3,13 +3,14 @@ package org.thoughtcrime.securesms.mediasend.v2
import android.content.Context
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.sharing.MultiShareArgs
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.Util
object MediaValidator {
fun filterMedia(context: Context, media: List<Media>, mediaConstraints: MediaConstraints, maxSelection: Int): FilterResult {
val filteredMedia = filterForValidMedia(context, media, mediaConstraints)
fun filterMedia(context: Context, media: List<Media>, mediaConstraints: MediaConstraints, maxSelection: Int, isStory: Boolean): FilterResult {
val filteredMedia = filterForValidMedia(context, media, mediaConstraints, isStory)
val isAllMediaValid = filteredMedia.size == media.size
var error: FilterError? = null
@ -45,12 +46,15 @@ object MediaValidator {
return FilterResult(truncatedMedia, error, bucketId)
}
private fun filterForValidMedia(context: Context, media: List<Media>, mediaConstraints: MediaConstraints): List<Media> {
private fun filterForValidMedia(context: Context, media: List<Media>, mediaConstraints: MediaConstraints, isStory: Boolean): List<Media> {
return media
.filter { m -> isSupportedMediaType(m.mimeType) }
.filter { m ->
MediaUtil.isImageAndNotGif(m.mimeType) || isValidGif(context, m, mediaConstraints) || isValidVideo(context, m, mediaConstraints)
}
.filter { m ->
MediaConstraints.isVideoTranscodeAvailable() || !isStory || MultiShareArgs.isValidStoryDuration(m)
}
}
private fun isValidGif(context: Context, media: Media, mediaConstraints: MediaConstraints): Boolean {

Wyświetl plik

@ -20,10 +20,12 @@ import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionNavigator.Companion
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import java.io.FileDescriptor
import java.util.Optional
import java.util.concurrent.TimeUnit
private val TAG = Log.tag(MediaCaptureFragment::class.java)
@ -165,6 +167,10 @@ class MediaCaptureFragment : Fragment(R.layout.fragment_container), CameraFragme
return sharedViewModel.getMediaConstraints()
}
override fun getMaxVideoDuration(): Int {
return if (sharedViewModel.isStory()) TimeUnit.MILLISECONDS.toSeconds(Stories.MAX_VIDEO_DURATION_MILLIS).toInt() else -1
}
private fun isFirst(): Boolean {
return arguments?.getBoolean("first") == true
}

Wyświetl plik

@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mediasend.VideoEditorFragment
import org.thoughtcrime.securesms.mediasend.v2.HudCommand
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
import org.thoughtcrime.securesms.stories.Stories
private const val VIDEO_EDITOR_TAG = "video.editor.fragment"
@ -81,7 +82,8 @@ class MediaReviewVideoPageFragment : Fragment(R.layout.fragment_container), Vide
requireUri(),
requireMaxCompressedVideoSize(),
requireMaxAttachmentSize(),
requireIsVideoGif()
requireIsVideoGif(),
requireMaxVideoDuration()
)
childFragmentManager.beginTransaction()
@ -100,6 +102,7 @@ class MediaReviewVideoPageFragment : Fragment(R.layout.fragment_container), Vide
private fun requireMaxCompressedVideoSize(): Long = sharedViewModel.getMediaConstraints().getCompressedVideoMaxSize(requireContext()).toLong()
private fun requireMaxAttachmentSize(): Long = sharedViewModel.getMediaConstraints().getVideoMaxSize(requireContext()).toLong()
private fun requireIsVideoGif(): Boolean = requireNotNull(requireArguments().getBoolean(ARG_IS_VIDEO_GIF))
private fun requireMaxVideoDuration(): Long = if (sharedViewModel.isStory()) Stories.MAX_VIDEO_DURATION_MILLIS else Long.MAX_VALUE
companion object {
private const val ARG_URI = "arg.uri"

Wyświetl plik

@ -12,6 +12,7 @@ import com.annimon.stream.Stream;
import org.signal.core.util.BreakIteratorCompat;
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mediasend.Media;
@ -26,6 +27,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public final class MultiShareArgs implements Parcelable {
@ -150,7 +152,10 @@ public final class MultiShareArgs implements Parcelable {
public boolean isValidForStories() {
return isTextStory ||
!media.isEmpty() && media.stream().allMatch(m -> MediaUtil.isImageOrVideoType(m.getMimeType()) && !MediaUtil.isGif(m.getMimeType())) ||
!media.isEmpty() && media.stream().allMatch(
m -> MediaUtil.isImageOrVideoType(m.getMimeType()) &&
isValidStoryDuration(m)
) ||
MediaUtil.isImageType(dataType) ||
MediaUtil.isVideoType(dataType) ||
isValidForTextStoryGeneration();
@ -160,6 +165,25 @@ public final class MultiShareArgs implements Parcelable {
return !isTextStory;
}
public static boolean isValidStoryDuration(@NonNull Media media) {
if (MediaUtil.isVideoType(media.getMimeType())) {
if (media.getDuration() > 0 && media.getDuration() <= Stories.MAX_VIDEO_DURATION_MILLIS) {
return true;
} else if (media.getTransformProperties().isPresent()) {
AttachmentDatabase.TransformProperties transformProperties = media.getTransformProperties().get();
if (transformProperties.isVideoTrim()) {
return transformProperties.getVideoTrimEndTimeUs() - transformProperties.getVideoTrimStartTimeUs() <= TimeUnit.MILLISECONDS.toMicros(Stories.MAX_VIDEO_DURATION_MILLIS);
} else {
return false;
}
} else {
return false;
}
} else {
return true;
}
}
public boolean isValidForTextStoryGeneration() {
if (isTextStory || !media.isEmpty()) {
return false;

Wyświetl plik

@ -18,11 +18,15 @@ import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import java.util.concurrent.TimeUnit
object Stories {
const val MAX_BODY_SIZE = 700
@JvmField
val MAX_VIDEO_DURATION_MILLIS = TimeUnit.SECONDS.toMillis(30)
@JvmStatic
fun isFeatureAvailable(): Boolean {
return FeatureFlags.stories() && Recipient.self().storiesCapability == Recipient.Capability.SUPPORTED