Added support for view-once video.

master
Alex Hart 2019-09-27 10:10:30 -03:00 committed by Greyson Parrelli
parent 50a81c0e60
commit d698d3bd6f
23 changed files with 405 additions and 46 deletions

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:background="@color/core_black">
<ImageView
@ -12,6 +12,12 @@
android:layout_height="match_parent"
android:scaleType="fitCenter"/>
<org.thoughtcrime.securesms.video.VideoPlayer
android:id="@+id/view_once_video"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/view_once_close_button"
android:layout_width="wrap_content"
@ -21,4 +27,16 @@
android:tint="@color/core_white"
app:srcCompat="@drawable/ic_x"/>
<TextView
android:id="@+id/view_once_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginTop="15dp"
android:layout_marginEnd="16dp"
android:textAppearance="@style/ViewOnceVideo.Duration"
android:visibility="gone"
tools:visibility="visible"
tools:text="00:23" />
</FrameLayout>

View File

@ -671,9 +671,10 @@
<string name="RegistrationActivity_call">Call</string>
<!-- RevealableMessageView -->
<string name="RevealableMessageView_view_photo">View Photo</string>
<string name="RevealableMessageView_viewed">Viewed</string>
<string name="RevealableMessageView_photo">Photo</string>
<string name="RevealableMessageView_video">Video</string>
<string name="RevealableMessageView_viewed">Viewed</string>
<string name="RevealableMessageView_outgoing_media">Media</string>
<!-- ScribbleActivity -->
<string name="ScribbleActivity_save_failure">Failed to save image changes</string>
@ -746,6 +747,7 @@
<string name="ThreadRecord_media_message">Media message</string>
<string name="ThreadRecord_sticker">Sticker</string>
<string name="ThreadRecord_disappearing_photo">Disappearing photo</string>
<string name="ThreadRecord_disappearing_video">Disappearing video</string>
<string name="ThreadRecord_s_is_on_signal">%s is on Signal!</string>
<string name="ThreadRecord_disappearing_messages_disabled">Disappearing messages disabled</string>
<string name="ThreadRecord_disappearing_message_time_updated_to_s">Disappearing message time set to %s</string>
@ -786,6 +788,9 @@
<string name="VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied">Signal needs the Camera permission in order to scan a QR code, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Camera\".</string>
<string name="VerifyIdentityActivity_unable_to_scan_qr_code_without_camera_permission">Unable to scan QR code without Camera permission</string>
<!-- ViewOnceMessageActivity -->
<string name="ViewOnceMessageActivity_video_duration" translatable="false">%1$02d:%2$02d</string>
<!-- MessageDisplayHelper -->
<string name="MessageDisplayHelper_bad_encrypted_message">Bad encrypted message</string>
<string name="MessageDisplayHelper_message_encrypted_for_non_existing_session">Message encrypted for non-existing session</string>
@ -834,6 +839,7 @@
<string name="MessageNotifier_media_message">Media message</string>
<string name="MessageNotifier_sticker">Sticker</string>
<string name="MessageNotifier_disappearing_photo">Disappearing photo</string>
<string name="MessageNotifier_disappearing_video">Disappearing video</string>
<string name="MessageNotifier_reply">Reply</string>
<string name="MessageNotifier_signal_message">Signal Message</string>
<string name="MessageNotifier_unsecured_sms">Unsecured SMS</string>
@ -1000,6 +1006,7 @@
<string name="QuoteView_audio">Audio</string>
<string name="QuoteView_video">Video</string>
<string name="QuoteView_photo">Photo</string>
<string name="QuoteView_media">Media message</string>
<string name="QuoteView_sticker">Sticker</string>
<string name="QuoteView_document">Document</string>
<string name="QuoteView_you">You</string>

View File

@ -142,6 +142,16 @@
<item name="android:lineSpacingMultiplier">1.25</item>
</style>
<style name="ViewOnceVideo.Duration" parent="@android:style/TextAppearance">
<item name="android:textSize">16sp</item>
<item name="android:textColor">@color/white</item>
<!-- TODO: change to transparent_black_60 after color swap -->
<item name="android:shadowColor">#99000000</item>
<item name="android:shadowDx">0</item>
<item name="android:shadowDy">0</item>
<item name="android:shadowRadius">2</item>
</style>
<!-- For Holo Light Dialog Activity Styling Emulation -->
<style name="Widget.ProgressBar.Horizontal" parent="@android:style/Widget.ProgressBar.Horizontal">

View File

@ -150,8 +150,14 @@ public class InputPanel extends LinearLayout
composeText.setMediaListener(listener);
}
public void setQuote(@NonNull GlideRequests glideRequests, long id, @NonNull Recipient author, @NonNull String body, @NonNull SlideDeck attachments) {
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments);
public void setQuote(@NonNull GlideRequests glideRequests,
long id,
@NonNull Recipient author,
@NonNull String body,
@NonNull SlideDeck attachments,
boolean isViewOnce)
{
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments, isViewOnce);
this.quoteView.setVisibility(View.VISIBLE);
if (this.linkPreview.getVisibility() == View.VISIBLE) {

View File

@ -149,7 +149,8 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
@NonNull Recipient author,
@Nullable String body,
boolean originalMissing,
@NonNull SlideDeck attachments)
@NonNull SlideDeck attachments,
boolean isViewOnce)
{
if (this.author != null) this.author.removeForeverObserver(this);
@ -160,7 +161,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
this.author.observeForever(this);
setQuoteAuthor(author);
setQuoteText(body, attachments);
setQuoteText(body, attachments, isViewOnce);
setQuoteAttachment(glideRequests, attachments);
setQuoteMissingFooter(originalMissing);
}
@ -197,7 +198,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
mainView.setBackgroundColor(author.getColor().toQuoteBackgroundColor(getContext(), outgoing));
}
private void setQuoteText(@Nullable String body, @NonNull SlideDeck attachments) {
private void setQuoteText(@Nullable String body, @NonNull SlideDeck attachments, boolean isViewOnce) {
if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
bodyView.setVisibility(VISIBLE);
bodyView.setText(body == null ? "" : body);
@ -215,7 +216,9 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
List<Slide> stickerSlides = Stream.of(attachments.getSlides()).filter(Slide::hasSticker).limit(1).toList();
// Given that most types have images, we specifically check images last
if (!audioSlides.isEmpty()) {
if (isViewOnce) {
mediaDescriptionText.setText(R.string.QuoteView_media);
} else if (!audioSlides.isEmpty()) {
mediaDescriptionText.setText(R.string.QuoteView_audio);
} else if (!documentSlides.isEmpty()) {
mediaDescriptionText.setVisibility(GONE);

View File

@ -2687,7 +2687,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
messageRecord.getDateSent(),
author,
body,
slideDeck);
slideDeck,
messageRecord.isViewOnce());
} else if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) {
LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
@ -2701,7 +2702,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
messageRecord.getDateSent(),
author,
messageRecord.getBody(),
slideDeck);
slideDeck,
messageRecord.isViewOnce());
} else {
SlideDeck slideDeck = messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck();
@ -2715,7 +2717,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
messageRecord.getDateSent(),
author,
messageRecord.getBody(),
slideDeck);
slideDeck,
messageRecord.isViewOnce());
}
inputPanel.clickOnComposeInput();

View File

@ -1014,8 +1014,11 @@ public class ConversationFragment extends Fragment
Log.i(TAG, "Copying the view-once photo to temp storage and deleting underlying media.");
try {
InputStream inputStream = PartAuthority.getAttachmentStream(requireContext(), messageRecord.getSlideDeck().getThumbnailSlide().getUri());
Uri tempUri = BlobProvider.getInstance().forData(inputStream, 0).createForSingleSessionOnDisk(requireContext());
Slide thumbnailSlide = messageRecord.getSlideDeck().getThumbnailSlide();
InputStream inputStream = PartAuthority.getAttachmentStream(requireContext(), thumbnailSlide.getUri());
Uri tempUri = BlobProvider.getInstance().forData(inputStream, thumbnailSlide.getFileSize())
.withMimeType(thumbnailSlide.getContentType())
.createForSingleSessionOnDisk(requireContext());
DatabaseFactory.getAttachmentDatabase(requireContext()).deleteAttachmentFilesForMessage(messageRecord.getId());

View File

@ -848,7 +848,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (current.isMms() && !current.isMmsNotification() && ((MediaMmsMessageRecord)current).getQuote() != null) {
Quote quote = ((MediaMmsMessageRecord)current).getQuote();
//noinspection ConstantConditions
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getText(), quote.isOriginalMissing(), quote.getAttachment());
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getText(), quote.isOriginalMissing(), quote.getAttachment(), messageRecord.isViewOnce());
quoteView.setVisibility(View.VISIBLE);
quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;

View File

@ -245,4 +245,8 @@ public abstract class MessageRecord extends DisplayRecord {
public boolean isUnidentified() {
return unidentified;
}
public boolean isViewOnce() {
return false;
}
}

View File

@ -65,6 +65,11 @@ public abstract class MmsMessageRecord extends MessageRecord {
return false;
}
@Override
public boolean isViewOnce() {
return viewOnce;
}
public boolean containsMediaSlide() {
return slideDeck.containsMediaSlide();
}
@ -80,8 +85,4 @@ public abstract class MmsMessageRecord extends MessageRecord {
public @NonNull List<LinkPreview> getLinkPreviews() {
return linkPreviews;
}
public boolean isViewOnce() {
return viewOnce;
}
}

View File

@ -122,8 +122,8 @@ public class ThreadRecord extends DisplayRecord {
if (TextUtils.isEmpty(getBody())) {
if (extra != null && extra.isSticker()) {
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_sticker)));
} else if (extra != null && extra.isRevealable() && MediaUtil.isImageType(contentType)) {
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_photo)));
} else if (extra != null && extra.isRevealable()) {
return new SpannableString(emphasisAdded(getViewOnceDescription(context, contentType)));
} else {
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
}
@ -144,6 +144,14 @@ public class ThreadRecord extends DisplayRecord {
return spannable;
}
private String getViewOnceDescription(@NonNull Context context, @Nullable String contentType) {
if (MediaUtil.isVideoType(contentType)) {
return context.getString(R.string.ThreadRecord_disappearing_video);
} else {
return context.getString(R.string.ThreadRecord_disappearing_photo);
}
}
public long getCount() {
return count;
}

View File

@ -41,7 +41,6 @@ import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
@ -99,6 +98,7 @@ import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
@ -1177,15 +1177,19 @@ public class PushDecryptJob extends BaseJob {
private boolean isInvalidMessage(@NonNull SignalServiceDataMessage message) {
if (message.isViewOnce()) {
return !message.getAttachments().isPresent() ||
message.getAttachments().get().size() != 1 ||
!MediaUtil.isImageType(message.getAttachments().get().get(0).getContentType().toLowerCase());
List<SignalServiceAttachment> attachments = message.getAttachments().or(Collections.emptyList());
return attachments.size() != 1 ||
!isViewOnceSupportedContentType(attachments.get(0).getContentType().toLowerCase());
}
return false;
}
private boolean isViewOnceSupportedContentType(@NonNull String contentType) {
return MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType);
}
private Optional<QuoteModel> getValidatedQuote(Optional<SignalServiceDataMessage.Quote> quote) {
if (!quote.isPresent()) return Optional.absent();

View File

@ -494,7 +494,8 @@ class MediaSendViewModel extends ViewModel {
}
private boolean mediaSupportsRevealableMessage(@NonNull List<Media> media) {
return media.size() == 1 && MediaUtil.isImageType(media.get(0).getMimeType());
if (media.size() != 1) return false;
return MediaUtil.isImageOrVideoType(media.get(0).getMimeType());
}
@Override

View File

@ -34,6 +34,7 @@ import android.os.AsyncTask;
import android.os.Build;
import android.service.notification.StatusBarNotification;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import android.text.TextUtils;
@ -51,10 +52,12 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -466,7 +469,7 @@ public class MessageNotifier {
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_sticker));
slideDeck = ((MmsMessageRecord) record).getSlideDeck();
} else if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) {
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_disappearing_photo));
body = SpanUtil.italic(context.getString(getViewOnceDescription((MmsMessageRecord) record)));
} else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message));
slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
@ -486,6 +489,24 @@ public class MessageNotifier {
return notificationState;
}
private static @StringRes int getViewOnceDescription(@NonNull MmsMessageRecord messageRecord) {
final String contentType = getMessageContentType(messageRecord);
if (MediaUtil.isImageType(contentType)) {
return R.string.MessageNotifier_disappearing_photo;
}
return R.string.MessageNotifier_disappearing_video;
}
private static String getMessageContentType(@NonNull MmsMessageRecord messageRecord) {
Slide thumbnailSlide = messageRecord.getSlideDeck().getThumbnailSlide();
if (thumbnailSlide == null) {
Log.w(TAG, "Could not distinguish view-once content type from message record, defaulting to JPEG");
return MediaUtil.IMAGE_JPEG;
}
return thumbnailSlide.getContentType();
}
private static void updateBadge(Context context, int count) {
try {
if (count == 0) ShortcutBadger.removeCount(context);

View File

@ -81,6 +81,14 @@ public class BlobProvider {
* @throws IOException If the stream fails to open or the spec of the URI doesn't match.
*/
public synchronized @NonNull InputStream getStream(@NonNull Context context, @NonNull Uri uri) throws IOException {
return getStream(context, uri, 0L);
}
/**
* Retrieve a stream for the content with the specified URI starting from the specified position.
* @throws IOException If the stream fails to open or the spec of the URI doesn't match.
*/
public synchronized @NonNull InputStream getStream(@NonNull Context context, @NonNull Uri uri, long position) throws IOException {
if (isAuthority(uri)) {
StorageType storageType = StorageType.decode(uri.getPathSegments().get(STORAGE_TYPE_PATH_SEGMENT));
@ -100,7 +108,7 @@ public class BlobProvider {
String directory = getDirectory(storageType);
File file = new File(getOrCreateCacheDirectory(context, directory), buildFileName(id));
return ModernDecryptingPartInputStream.createFor(AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(), file, 0);
return ModernDecryptingPartInputStream.createFor(AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(), file, position);
}
} else {
throw new IOException("Provided URI does not match this spec. Uri: " + uri);

View File

@ -4,8 +4,13 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProviders;
@ -15,20 +20,48 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.video.VideoPlayer;
public class ViewOnceMessageActivity extends PassphraseRequiredActionBarActivity {
import java.util.concurrent.TimeUnit;
public class ViewOnceMessageActivity extends PassphraseRequiredActionBarActivity implements VideoPlayer.PlayerStateCallback {
private static final String TAG = Log.tag(ViewOnceMessageActivity.class);
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_URI = "uri";
private static final int OVERLAY_TIMEOUT_S = 2;
private static final int FADE_OUT_DURATION_MS = 200;
private ImageView image;
private VideoPlayer video;
private View closeButton;
private TextView duration;
private ViewOnceMessageViewModel viewModel;
private Uri uri;
private int updateCounter;
private final Handler handler = new Handler(Looper.getMainLooper());
private final Runnable durationUpdateRunnable = () -> {
long timeLeft = TimeUnit.MILLISECONDS.toSeconds(video.getDuration()) - updateCounter;
long minutes = timeLeft / 60;
long seconds = timeLeft % 60;
duration.setText(getString(R.string.ViewOnceMessageActivity_video_duration, minutes, seconds));
updateCounter++;
if (updateCounter > OVERLAY_TIMEOUT_S) {
animateOutOverlay();
} else {
scheduleDurationUpdate();
}
};
public static Intent getIntent(@NonNull Context context, long messageId, @NonNull Uri uri) {
Intent intent = new Intent(context, ViewOnceMessageActivity.class);
intent.putExtra(KEY_MESSAGE_ID, messageId);
@ -42,12 +75,24 @@ public class ViewOnceMessageActivity extends PassphraseRequiredActionBarActivity
setContentView(R.layout.view_once_message_activity);
this.image = findViewById(R.id.view_once_image);
this.video = findViewById(R.id.view_once_video);
this.duration = findViewById(R.id.view_once_duration);
this.closeButton = findViewById(R.id.view_once_close_button);
this.uri = getIntent().getParcelableExtra(KEY_URI);
image.setOnClickListener(v -> finish());
closeButton.setOnClickListener(v -> finish());
ViewOnceGestureListener imageListener = new ViewOnceGestureListener(image);
GestureDetector imageDetector = new GestureDetector(this, imageListener);
ViewOnceGestureListener videoListener = new ViewOnceGestureListener(video);
GestureDetector videoDetector = new GestureDetector(this, videoListener);
image.setOnTouchListener((view, event) -> imageDetector.onTouchEvent(event));
image.setOnClickListener(v -> finish());
video.setOnTouchListener((view, event) -> videoDetector.onTouchEvent(event));
video.setOnClickListener(v -> finish());
closeButton.setOnClickListener(v -> finish());
initViewModel(getIntent().getLongExtra(KEY_MESSAGE_ID, -1), uri);
}
@ -55,10 +100,18 @@ public class ViewOnceMessageActivity extends PassphraseRequiredActionBarActivity
@Override
protected void onStop() {
super.onStop();
cancelDurationUpdate();
video.cleanup();
BlobProvider.getInstance().delete(this, uri);
finish();
}
@Override
public void onPlayerReady() {
updateCounter = 0;
handler.post(durationUpdateRunnable);
}
private void initViewModel(long messageId, @NonNull Uri uri) {
ViewOnceMessageRepository repository = new ViewOnceMessageRepository(this);
@ -69,13 +122,83 @@ public class ViewOnceMessageActivity extends PassphraseRequiredActionBarActivity
if (message == null) return;
if (message.isPresent()) {
GlideApp.with(this)
.load(new DecryptableUri(uri))
.into(image);
displayMedia(uri);
} else {
image.setImageDrawable(null);
finish();
}
});
}
private void displayMedia(@NonNull Uri uri) {
if (MediaUtil.isVideoType(PartAuthority.getAttachmentContentType(this, uri))) {
displayVideo(uri);
} else {
displayImage(uri);
}
}
private void displayVideo(@NonNull Uri uri) {
video.setVisibility(View.VISIBLE);
image.setVisibility(View.GONE);
duration.setVisibility(View.VISIBLE);
VideoSlide videoSlide = new VideoSlide(this, uri, 0);
video.setWindow(getWindow());
video.setPlayerStateCallbacks(this);
video.setVideoSource(videoSlide, true);
video.hideControls();
video.loopForever();
}
private void displayImage(@NonNull Uri uri) {
video.setVisibility(View.GONE);
image.setVisibility(View.VISIBLE);
duration.setVisibility(View.GONE);
GlideApp.with(this)
.load(new DecryptableUri(uri))
.into(image);
}
private void animateOutOverlay() {
duration.animate().alpha(0f).setDuration(200).start();
closeButton.animate().alpha(0f).setDuration(200).start();
}
private void scheduleDurationUpdate() {
handler.postDelayed(durationUpdateRunnable, 1000L);
}
private void cancelDurationUpdate() {
handler.removeCallbacks(durationUpdateRunnable);
}
private class ViewOnceGestureListener extends GestureDetector.SimpleOnGestureListener {
private final View view;
private ViewOnceGestureListener(View view) {
this.view = view;
}
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
view.performClick();
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
finish();
return true;
}
}
}

View File

@ -10,6 +10,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import com.pnikosis.materialishprogress.ProgressWheel;
@ -22,6 +23,8 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
public class ViewOnceMessageView extends LinearLayout {
@ -101,12 +104,12 @@ public class ViewOnceMessageView extends LinearLayout {
private void presentText(@NonNull MmsMessageRecord messageRecord) {
if (messageRecord.isOutgoing()) {
foregroundColor = openedForegroundColor;
text.setText(R.string.RevealableMessageView_photo);
text.setText(R.string.RevealableMessageView_outgoing_media);
icon.setImageResource(R.drawable.ic_play_outline_24);
progress.setVisibility(GONE);
} else if (ViewOnceUtil.isViewable(messageRecord)) {
foregroundColor = unopenedForegroundColor;
text.setText(R.string.RevealableMessageView_view_photo);
text.setText(getDescriptionId(messageRecord));
icon.setImageResource(R.drawable.ic_play_solid_24);
progress.setVisibility(GONE);
} else if (networkInProgress(messageRecord)) {
@ -146,6 +149,16 @@ public class ViewOnceMessageView extends LinearLayout {
return Util.getPrettyFileSize(size);
}
private static @StringRes int getDescriptionId(@NonNull MmsMessageRecord messageRecord) {
Slide thumbnailSlide = messageRecord.getSlideDeck().getThumbnailSlide();
if (thumbnailSlide != null && MediaUtil.isVideoType(thumbnailSlide.getContentType())) {
return R.string.RevealableMessageView_video;
}
return R.string.RevealableMessageView_photo;
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventAsync(final PartProgressEvent event) {
if (event.attachment.equals(attachment)) {

View File

@ -5,6 +5,6 @@ package org.thoughtcrime.securesms.util;
* After a feature has been launched, the flag should be removed.
*/
public class FeatureFlags {
/** Send support for view-once photos. */
/** Send support for view-once media. */
public static final boolean VIEW_ONCE_SENDING = false;
}

View File

@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.StickerSlide;
import org.thoughtcrime.securesms.mms.TextSlide;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.providers.BlobProvider;
import java.io.FileNotFoundException;
import java.io.IOException;
@ -239,6 +240,10 @@ public class MediaUtil {
return (null != contentType) && contentType.startsWith("video/");
}
public static boolean isImageOrVideoType(String contentType) {
return isImageType(contentType) || isVideoType(contentType);
}
public static boolean isLongTextType(String contentType) {
return (null != contentType) && contentType.equals(LONG_TEXT);
}

View File

@ -58,6 +58,7 @@ public class VideoPlayer extends FrameLayout {
private SimpleExoPlayer exoPlayer;
private PlayerControlView exoControls;
private Window window;
private PlayerStateCallback playerStateCallback;
public VideoPlayer(Context context) {
this(context, null);
@ -84,7 +85,7 @@ public class VideoPlayer extends FrameLayout {
LoadControl loadControl = new DefaultLoadControl();
exoPlayer = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, loadControl);
exoPlayer.addListener(new ExoPlayerListener(window));
exoPlayer.addListener(new ExoPlayerListener(window, playerStateCallback));
exoView.setPlayer(exoPlayer);
exoControls.setPlayer(exoPlayer);
@ -121,15 +122,34 @@ public class VideoPlayer extends FrameLayout {
}
}
public void loopForever() {
if (this.exoPlayer != null) {
exoPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
}
}
public long getDuration() {
if (this.exoPlayer != null) {
return this.exoPlayer.getDuration();
}
return 0L;
}
public void setWindow(@Nullable Window window) {
this.window = window;
}
private static class ExoPlayerListener extends Player.DefaultEventListener {
private final Window window;
public void setPlayerStateCallbacks(@Nullable PlayerStateCallback playerStateCallback) {
this.playerStateCallback = playerStateCallback;
}
ExoPlayerListener(Window window) {
private static class ExoPlayerListener extends Player.DefaultEventListener {
private final Window window;
private final PlayerStateCallback playerStateCallback;
ExoPlayerListener(Window window, PlayerStateCallback playerStateCallback) {
this.window = window;
this.playerStateCallback = playerStateCallback;
}
@Override
@ -146,10 +166,19 @@ public class VideoPlayer extends FrameLayout {
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
notifyPlayerReady();
break;
default:
break;
}
}
private void notifyPlayerReady() {
if (playerStateCallback != null) playerStateCallback.onPlayerReady();
}
}
public interface PlayerStateCallback {
void onPlayerReady();
}
}

View File

@ -9,6 +9,7 @@ import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.providers.BlobProvider;
import java.io.IOException;
import java.util.Collections;
@ -19,12 +20,16 @@ public class AttachmentDataSource implements DataSource {
private final DefaultDataSource defaultDataSource;
private final PartDataSource partDataSource;
private final BlobDataSource blobDataSource;
private DataSource dataSource;
public AttachmentDataSource(DefaultDataSource defaultDataSource, PartDataSource partDataSource) {
public AttachmentDataSource(DefaultDataSource defaultDataSource,
PartDataSource partDataSource,
BlobDataSource blobDataSource) {
this.defaultDataSource = defaultDataSource;
this.partDataSource = partDataSource;
this.blobDataSource = blobDataSource;
}
@Override
@ -33,8 +38,9 @@ public class AttachmentDataSource implements DataSource {
@Override
public long open(DataSpec dataSpec) throws IOException {
if (PartAuthority.isLocalUri(dataSpec.uri)) dataSource = partDataSource;
else dataSource = defaultDataSource;
if (BlobProvider.isAuthority(dataSpec.uri)) dataSource = blobDataSource;
else if (PartAuthority.isLocalUri(dataSpec.uri)) dataSource = partDataSource;
else dataSource = defaultDataSource;
return dataSource.open(dataSpec);
}

View File

@ -28,6 +28,7 @@ public class AttachmentDataSourceFactory implements DataSource.Factory {
@Override
public AttachmentDataSource createDataSource() {
return new AttachmentDataSource(defaultDataSourceFactory.createDataSource(),
new PartDataSource(context, listener));
new PartDataSource(context, listener),
new BlobDataSource(context, listener));
}
}

View File

@ -0,0 +1,85 @@
package org.thoughtcrime.securesms.video.exo;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.TransferListener;
import org.thoughtcrime.securesms.providers.BlobProvider;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class BlobDataSource implements DataSource {
private final @NonNull Context context;
private final @Nullable TransferListener listener;
private Uri uri;
private InputStream inputStream;
BlobDataSource(@NonNull Context context, @Nullable TransferListener listener) {
this.context = context.getApplicationContext();
this.listener = listener;
}
@Override
public void addTransferListener(TransferListener transferListener) {
}
@Override
public long open(DataSpec dataSpec) throws IOException {
this.uri = dataSpec.uri;
this.inputStream = BlobProvider.getInstance().getStream(context, uri, dataSpec.position);
if (listener != null) {
listener.onTransferStart(this, dataSpec, false);
}
long size = unwrapLong(BlobProvider.getFileSize(uri));
if (size - dataSpec.position <= 0) throw new EOFException("No more data");
return size - dataSpec.position;
}
private long unwrapLong(@Nullable Long boxed) {
return boxed == null ? 0L : boxed;
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws IOException {
int read = inputStream.read(buffer, offset, readLength);
if (read > 0 && listener != null) {
listener.onBytesTransferred(this, null, false, read);
}
return read;
}
@Override
public Uri getUri() {
return uri;
}
@Override
public Map<String, List<String>> getResponseHeaders() {
return Collections.emptyMap();
}
@Override
public void close() throws IOException {
inputStream.close();
}
}