Added support for view-once video.
This commit is contained in:
parent
50a81c0e60
commit
d698d3bd6f
23 changed files with 405 additions and 46 deletions
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -245,4 +245,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
public boolean isUnidentified() {
|
||||
return unidentified;
|
||||
}
|
||||
|
||||
public boolean isViewOnce() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
85
src/org/thoughtcrime/securesms/video/exo/BlobDataSource.java
Normal file
85
src/org/thoughtcrime/securesms/video/exo/BlobDataSource.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
Loading…
Add table
Reference in a new issue