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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:background="@color/core_black">
|
android:background="@color/core_black">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
@ -12,6 +12,12 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:scaleType="fitCenter"/>
|
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
|
<ImageView
|
||||||
android:id="@+id/view_once_close_button"
|
android:id="@+id/view_once_close_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -21,4 +27,16 @@
|
||||||
android:tint="@color/core_white"
|
android:tint="@color/core_white"
|
||||||
app:srcCompat="@drawable/ic_x"/>
|
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>
|
</FrameLayout>
|
|
@ -671,9 +671,10 @@
|
||||||
<string name="RegistrationActivity_call">Call</string>
|
<string name="RegistrationActivity_call">Call</string>
|
||||||
|
|
||||||
<!-- RevealableMessageView -->
|
<!-- RevealableMessageView -->
|
||||||
<string name="RevealableMessageView_view_photo">View Photo</string>
|
|
||||||
<string name="RevealableMessageView_viewed">Viewed</string>
|
|
||||||
<string name="RevealableMessageView_photo">Photo</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 -->
|
<!-- ScribbleActivity -->
|
||||||
<string name="ScribbleActivity_save_failure">Failed to save image changes</string>
|
<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_media_message">Media message</string>
|
||||||
<string name="ThreadRecord_sticker">Sticker</string>
|
<string name="ThreadRecord_sticker">Sticker</string>
|
||||||
<string name="ThreadRecord_disappearing_photo">Disappearing photo</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_s_is_on_signal">%s is on Signal!</string>
|
||||||
<string name="ThreadRecord_disappearing_messages_disabled">Disappearing messages disabled</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>
|
<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_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>
|
<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 -->
|
<!-- MessageDisplayHelper -->
|
||||||
<string name="MessageDisplayHelper_bad_encrypted_message">Bad encrypted message</string>
|
<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>
|
<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_media_message">Media message</string>
|
||||||
<string name="MessageNotifier_sticker">Sticker</string>
|
<string name="MessageNotifier_sticker">Sticker</string>
|
||||||
<string name="MessageNotifier_disappearing_photo">Disappearing photo</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_reply">Reply</string>
|
||||||
<string name="MessageNotifier_signal_message">Signal Message</string>
|
<string name="MessageNotifier_signal_message">Signal Message</string>
|
||||||
<string name="MessageNotifier_unsecured_sms">Unsecured SMS</string>
|
<string name="MessageNotifier_unsecured_sms">Unsecured SMS</string>
|
||||||
|
@ -1000,6 +1006,7 @@
|
||||||
<string name="QuoteView_audio">Audio</string>
|
<string name="QuoteView_audio">Audio</string>
|
||||||
<string name="QuoteView_video">Video</string>
|
<string name="QuoteView_video">Video</string>
|
||||||
<string name="QuoteView_photo">Photo</string>
|
<string name="QuoteView_photo">Photo</string>
|
||||||
|
<string name="QuoteView_media">Media message</string>
|
||||||
<string name="QuoteView_sticker">Sticker</string>
|
<string name="QuoteView_sticker">Sticker</string>
|
||||||
<string name="QuoteView_document">Document</string>
|
<string name="QuoteView_document">Document</string>
|
||||||
<string name="QuoteView_you">You</string>
|
<string name="QuoteView_you">You</string>
|
||||||
|
|
|
@ -142,6 +142,16 @@
|
||||||
<item name="android:lineSpacingMultiplier">1.25</item>
|
<item name="android:lineSpacingMultiplier">1.25</item>
|
||||||
</style>
|
</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 -->
|
<!-- For Holo Light Dialog Activity Styling Emulation -->
|
||||||
|
|
||||||
<style name="Widget.ProgressBar.Horizontal" parent="@android:style/Widget.ProgressBar.Horizontal">
|
<style name="Widget.ProgressBar.Horizontal" parent="@android:style/Widget.ProgressBar.Horizontal">
|
||||||
|
|
|
@ -150,8 +150,14 @@ public class InputPanel extends LinearLayout
|
||||||
composeText.setMediaListener(listener);
|
composeText.setMediaListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setQuote(@NonNull GlideRequests glideRequests, long id, @NonNull Recipient author, @NonNull String body, @NonNull SlideDeck attachments) {
|
public void setQuote(@NonNull GlideRequests glideRequests,
|
||||||
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments);
|
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);
|
this.quoteView.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
if (this.linkPreview.getVisibility() == View.VISIBLE) {
|
if (this.linkPreview.getVisibility() == View.VISIBLE) {
|
||||||
|
|
|
@ -149,7 +149,8 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||||
@NonNull Recipient author,
|
@NonNull Recipient author,
|
||||||
@Nullable String body,
|
@Nullable String body,
|
||||||
boolean originalMissing,
|
boolean originalMissing,
|
||||||
@NonNull SlideDeck attachments)
|
@NonNull SlideDeck attachments,
|
||||||
|
boolean isViewOnce)
|
||||||
{
|
{
|
||||||
if (this.author != null) this.author.removeForeverObserver(this);
|
if (this.author != null) this.author.removeForeverObserver(this);
|
||||||
|
|
||||||
|
@ -160,7 +161,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||||
|
|
||||||
this.author.observeForever(this);
|
this.author.observeForever(this);
|
||||||
setQuoteAuthor(author);
|
setQuoteAuthor(author);
|
||||||
setQuoteText(body, attachments);
|
setQuoteText(body, attachments, isViewOnce);
|
||||||
setQuoteAttachment(glideRequests, attachments);
|
setQuoteAttachment(glideRequests, attachments);
|
||||||
setQuoteMissingFooter(originalMissing);
|
setQuoteMissingFooter(originalMissing);
|
||||||
}
|
}
|
||||||
|
@ -197,7 +198,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||||
mainView.setBackgroundColor(author.getColor().toQuoteBackgroundColor(getContext(), outgoing));
|
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()) {
|
if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
|
||||||
bodyView.setVisibility(VISIBLE);
|
bodyView.setVisibility(VISIBLE);
|
||||||
bodyView.setText(body == null ? "" : body);
|
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();
|
List<Slide> stickerSlides = Stream.of(attachments.getSlides()).filter(Slide::hasSticker).limit(1).toList();
|
||||||
|
|
||||||
// Given that most types have images, we specifically check images last
|
// 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);
|
mediaDescriptionText.setText(R.string.QuoteView_audio);
|
||||||
} else if (!documentSlides.isEmpty()) {
|
} else if (!documentSlides.isEmpty()) {
|
||||||
mediaDescriptionText.setVisibility(GONE);
|
mediaDescriptionText.setVisibility(GONE);
|
||||||
|
|
|
@ -2687,7 +2687,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||||
messageRecord.getDateSent(),
|
messageRecord.getDateSent(),
|
||||||
author,
|
author,
|
||||||
body,
|
body,
|
||||||
slideDeck);
|
slideDeck,
|
||||||
|
messageRecord.isViewOnce());
|
||||||
|
|
||||||
} else if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) {
|
} else if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) {
|
||||||
LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
|
LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
|
||||||
|
@ -2701,7 +2702,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||||
messageRecord.getDateSent(),
|
messageRecord.getDateSent(),
|
||||||
author,
|
author,
|
||||||
messageRecord.getBody(),
|
messageRecord.getBody(),
|
||||||
slideDeck);
|
slideDeck,
|
||||||
|
messageRecord.isViewOnce());
|
||||||
} else {
|
} else {
|
||||||
SlideDeck slideDeck = messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck();
|
SlideDeck slideDeck = messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck();
|
||||||
|
|
||||||
|
@ -2715,7 +2717,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||||
messageRecord.getDateSent(),
|
messageRecord.getDateSent(),
|
||||||
author,
|
author,
|
||||||
messageRecord.getBody(),
|
messageRecord.getBody(),
|
||||||
slideDeck);
|
slideDeck,
|
||||||
|
messageRecord.isViewOnce());
|
||||||
}
|
}
|
||||||
|
|
||||||
inputPanel.clickOnComposeInput();
|
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.");
|
Log.i(TAG, "Copying the view-once photo to temp storage and deleting underlying media.");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
InputStream inputStream = PartAuthority.getAttachmentStream(requireContext(), messageRecord.getSlideDeck().getThumbnailSlide().getUri());
|
Slide thumbnailSlide = messageRecord.getSlideDeck().getThumbnailSlide();
|
||||||
Uri tempUri = BlobProvider.getInstance().forData(inputStream, 0).createForSingleSessionOnDisk(requireContext());
|
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());
|
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) {
|
if (current.isMms() && !current.isMmsNotification() && ((MediaMmsMessageRecord)current).getQuote() != null) {
|
||||||
Quote quote = ((MediaMmsMessageRecord)current).getQuote();
|
Quote quote = ((MediaMmsMessageRecord)current).getQuote();
|
||||||
//noinspection ConstantConditions
|
//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.setVisibility(View.VISIBLE);
|
||||||
quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
|
|
||||||
|
|
|
@ -245,4 +245,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||||
public boolean isUnidentified() {
|
public boolean isUnidentified() {
|
||||||
return unidentified;
|
return unidentified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isViewOnce() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,11 @@ public abstract class MmsMessageRecord extends MessageRecord {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isViewOnce() {
|
||||||
|
return viewOnce;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean containsMediaSlide() {
|
public boolean containsMediaSlide() {
|
||||||
return slideDeck.containsMediaSlide();
|
return slideDeck.containsMediaSlide();
|
||||||
}
|
}
|
||||||
|
@ -80,8 +85,4 @@ public abstract class MmsMessageRecord extends MessageRecord {
|
||||||
public @NonNull List<LinkPreview> getLinkPreviews() {
|
public @NonNull List<LinkPreview> getLinkPreviews() {
|
||||||
return linkPreviews;
|
return linkPreviews;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isViewOnce() {
|
|
||||||
return viewOnce;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,8 +122,8 @@ public class ThreadRecord extends DisplayRecord {
|
||||||
if (TextUtils.isEmpty(getBody())) {
|
if (TextUtils.isEmpty(getBody())) {
|
||||||
if (extra != null && extra.isSticker()) {
|
if (extra != null && extra.isSticker()) {
|
||||||
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_sticker)));
|
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_sticker)));
|
||||||
} else if (extra != null && extra.isRevealable() && MediaUtil.isImageType(contentType)) {
|
} else if (extra != null && extra.isRevealable()) {
|
||||||
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_disappearing_photo)));
|
return new SpannableString(emphasisAdded(getViewOnceDescription(context, contentType)));
|
||||||
} else {
|
} else {
|
||||||
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
|
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
|
||||||
}
|
}
|
||||||
|
@ -144,6 +144,14 @@ public class ThreadRecord extends DisplayRecord {
|
||||||
return spannable;
|
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() {
|
public long getCount() {
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,6 @@ import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||||
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
|
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
|
||||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
||||||
import org.thoughtcrime.securesms.database.Address;
|
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
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.state.SignalProtocolStore;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
|
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.SignalServiceContent;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
|
||||||
|
@ -1177,15 +1177,19 @@ public class PushDecryptJob extends BaseJob {
|
||||||
|
|
||||||
private boolean isInvalidMessage(@NonNull SignalServiceDataMessage message) {
|
private boolean isInvalidMessage(@NonNull SignalServiceDataMessage message) {
|
||||||
if (message.isViewOnce()) {
|
if (message.isViewOnce()) {
|
||||||
return !message.getAttachments().isPresent() ||
|
List<SignalServiceAttachment> attachments = message.getAttachments().or(Collections.emptyList());
|
||||||
message.getAttachments().get().size() != 1 ||
|
|
||||||
!MediaUtil.isImageType(message.getAttachments().get().get(0).getContentType().toLowerCase());
|
|
||||||
|
|
||||||
|
return attachments.size() != 1 ||
|
||||||
|
!isViewOnceSupportedContentType(attachments.get(0).getContentType().toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isViewOnceSupportedContentType(@NonNull String contentType) {
|
||||||
|
return MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType);
|
||||||
|
}
|
||||||
|
|
||||||
private Optional<QuoteModel> getValidatedQuote(Optional<SignalServiceDataMessage.Quote> quote) {
|
private Optional<QuoteModel> getValidatedQuote(Optional<SignalServiceDataMessage.Quote> quote) {
|
||||||
if (!quote.isPresent()) return Optional.absent();
|
if (!quote.isPresent()) return Optional.absent();
|
||||||
|
|
||||||
|
|
|
@ -494,7 +494,8 @@ class MediaSendViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean mediaSupportsRevealableMessage(@NonNull List<Media> media) {
|
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
|
@Override
|
||||||
|
|
|
@ -34,6 +34,7 @@ import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.service.notification.StatusBarNotification;
|
import android.service.notification.StatusBarNotification;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
import androidx.core.app.NotificationManagerCompat;
|
||||||
import android.text.TextUtils;
|
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.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||||
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
@ -466,7 +469,7 @@ public class MessageNotifier {
|
||||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_sticker));
|
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_sticker));
|
||||||
slideDeck = ((MmsMessageRecord) record).getSlideDeck();
|
slideDeck = ((MmsMessageRecord) record).getSlideDeck();
|
||||||
} else if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) {
|
} 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()) {
|
} else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
|
||||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message));
|
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message));
|
||||||
slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
|
slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
|
||||||
|
@ -486,6 +489,24 @@ public class MessageNotifier {
|
||||||
return notificationState;
|
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) {
|
private static void updateBadge(Context context, int count) {
|
||||||
try {
|
try {
|
||||||
if (count == 0) ShortcutBadger.removeCount(context);
|
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.
|
* @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 {
|
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)) {
|
if (isAuthority(uri)) {
|
||||||
StorageType storageType = StorageType.decode(uri.getPathSegments().get(STORAGE_TYPE_PATH_SEGMENT));
|
StorageType storageType = StorageType.decode(uri.getPathSegments().get(STORAGE_TYPE_PATH_SEGMENT));
|
||||||
|
|
||||||
|
@ -100,7 +108,7 @@ public class BlobProvider {
|
||||||
String directory = getDirectory(storageType);
|
String directory = getDirectory(storageType);
|
||||||
File file = new File(getOrCreateCacheDirectory(context, directory), buildFileName(id));
|
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 {
|
} else {
|
||||||
throw new IOException("Provided URI does not match this spec. Uri: " + uri);
|
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.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
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.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
|
@ -15,20 +20,48 @@ import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
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.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 TAG = Log.tag(ViewOnceMessageActivity.class);
|
||||||
|
|
||||||
private static final String KEY_MESSAGE_ID = "message_id";
|
private static final String KEY_MESSAGE_ID = "message_id";
|
||||||
private static final String KEY_URI = "uri";
|
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 ImageView image;
|
||||||
|
private VideoPlayer video;
|
||||||
private View closeButton;
|
private View closeButton;
|
||||||
|
private TextView duration;
|
||||||
private ViewOnceMessageViewModel viewModel;
|
private ViewOnceMessageViewModel viewModel;
|
||||||
private Uri uri;
|
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) {
|
public static Intent getIntent(@NonNull Context context, long messageId, @NonNull Uri uri) {
|
||||||
Intent intent = new Intent(context, ViewOnceMessageActivity.class);
|
Intent intent = new Intent(context, ViewOnceMessageActivity.class);
|
||||||
intent.putExtra(KEY_MESSAGE_ID, messageId);
|
intent.putExtra(KEY_MESSAGE_ID, messageId);
|
||||||
|
@ -42,12 +75,24 @@ public class ViewOnceMessageActivity extends PassphraseRequiredActionBarActivity
|
||||||
setContentView(R.layout.view_once_message_activity);
|
setContentView(R.layout.view_once_message_activity);
|
||||||
|
|
||||||
this.image = findViewById(R.id.view_once_image);
|
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.closeButton = findViewById(R.id.view_once_close_button);
|
||||||
this.uri = getIntent().getParcelableExtra(KEY_URI);
|
this.uri = getIntent().getParcelableExtra(KEY_URI);
|
||||||
|
|
||||||
image.setOnClickListener(v -> finish());
|
ViewOnceGestureListener imageListener = new ViewOnceGestureListener(image);
|
||||||
closeButton.setOnClickListener(v -> finish());
|
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);
|
initViewModel(getIntent().getLongExtra(KEY_MESSAGE_ID, -1), uri);
|
||||||
}
|
}
|
||||||
|
@ -55,10 +100,18 @@ public class ViewOnceMessageActivity extends PassphraseRequiredActionBarActivity
|
||||||
@Override
|
@Override
|
||||||
protected void onStop() {
|
protected void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
|
cancelDurationUpdate();
|
||||||
|
video.cleanup();
|
||||||
BlobProvider.getInstance().delete(this, uri);
|
BlobProvider.getInstance().delete(this, uri);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerReady() {
|
||||||
|
updateCounter = 0;
|
||||||
|
handler.post(durationUpdateRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
private void initViewModel(long messageId, @NonNull Uri uri) {
|
private void initViewModel(long messageId, @NonNull Uri uri) {
|
||||||
ViewOnceMessageRepository repository = new ViewOnceMessageRepository(this);
|
ViewOnceMessageRepository repository = new ViewOnceMessageRepository(this);
|
||||||
|
|
||||||
|
@ -69,13 +122,83 @@ public class ViewOnceMessageActivity extends PassphraseRequiredActionBarActivity
|
||||||
if (message == null) return;
|
if (message == null) return;
|
||||||
|
|
||||||
if (message.isPresent()) {
|
if (message.isPresent()) {
|
||||||
GlideApp.with(this)
|
displayMedia(uri);
|
||||||
.load(new DecryptableUri(uri))
|
|
||||||
.into(image);
|
|
||||||
} else {
|
} else {
|
||||||
image.setImageDrawable(null);
|
image.setImageDrawable(null);
|
||||||
finish();
|
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.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
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.database.model.MmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
||||||
public class ViewOnceMessageView extends LinearLayout {
|
public class ViewOnceMessageView extends LinearLayout {
|
||||||
|
@ -101,12 +104,12 @@ public class ViewOnceMessageView extends LinearLayout {
|
||||||
private void presentText(@NonNull MmsMessageRecord messageRecord) {
|
private void presentText(@NonNull MmsMessageRecord messageRecord) {
|
||||||
if (messageRecord.isOutgoing()) {
|
if (messageRecord.isOutgoing()) {
|
||||||
foregroundColor = openedForegroundColor;
|
foregroundColor = openedForegroundColor;
|
||||||
text.setText(R.string.RevealableMessageView_photo);
|
text.setText(R.string.RevealableMessageView_outgoing_media);
|
||||||
icon.setImageResource(R.drawable.ic_play_outline_24);
|
icon.setImageResource(R.drawable.ic_play_outline_24);
|
||||||
progress.setVisibility(GONE);
|
progress.setVisibility(GONE);
|
||||||
} else if (ViewOnceUtil.isViewable(messageRecord)) {
|
} else if (ViewOnceUtil.isViewable(messageRecord)) {
|
||||||
foregroundColor = unopenedForegroundColor;
|
foregroundColor = unopenedForegroundColor;
|
||||||
text.setText(R.string.RevealableMessageView_view_photo);
|
text.setText(getDescriptionId(messageRecord));
|
||||||
icon.setImageResource(R.drawable.ic_play_solid_24);
|
icon.setImageResource(R.drawable.ic_play_solid_24);
|
||||||
progress.setVisibility(GONE);
|
progress.setVisibility(GONE);
|
||||||
} else if (networkInProgress(messageRecord)) {
|
} else if (networkInProgress(messageRecord)) {
|
||||||
|
@ -146,6 +149,16 @@ public class ViewOnceMessageView extends LinearLayout {
|
||||||
return Util.getPrettyFileSize(size);
|
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)
|
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||||
public void onEventAsync(final PartProgressEvent event) {
|
public void onEventAsync(final PartProgressEvent event) {
|
||||||
if (event.attachment.equals(attachment)) {
|
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.
|
* After a feature has been launched, the flag should be removed.
|
||||||
*/
|
*/
|
||||||
public class FeatureFlags {
|
public class FeatureFlags {
|
||||||
/** Send support for view-once photos. */
|
/** Send support for view-once media. */
|
||||||
public static final boolean VIEW_ONCE_SENDING = false;
|
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.StickerSlide;
|
||||||
import org.thoughtcrime.securesms.mms.TextSlide;
|
import org.thoughtcrime.securesms.mms.TextSlide;
|
||||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||||
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -239,6 +240,10 @@ public class MediaUtil {
|
||||||
return (null != contentType) && contentType.startsWith("video/");
|
return (null != contentType) && contentType.startsWith("video/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isImageOrVideoType(String contentType) {
|
||||||
|
return isImageType(contentType) || isVideoType(contentType);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isLongTextType(String contentType) {
|
public static boolean isLongTextType(String contentType) {
|
||||||
return (null != contentType) && contentType.equals(LONG_TEXT);
|
return (null != contentType) && contentType.equals(LONG_TEXT);
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,7 @@ public class VideoPlayer extends FrameLayout {
|
||||||
private SimpleExoPlayer exoPlayer;
|
private SimpleExoPlayer exoPlayer;
|
||||||
private PlayerControlView exoControls;
|
private PlayerControlView exoControls;
|
||||||
private Window window;
|
private Window window;
|
||||||
|
private PlayerStateCallback playerStateCallback;
|
||||||
|
|
||||||
public VideoPlayer(Context context) {
|
public VideoPlayer(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
|
@ -84,7 +85,7 @@ public class VideoPlayer extends FrameLayout {
|
||||||
LoadControl loadControl = new DefaultLoadControl();
|
LoadControl loadControl = new DefaultLoadControl();
|
||||||
|
|
||||||
exoPlayer = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, loadControl);
|
exoPlayer = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, loadControl);
|
||||||
exoPlayer.addListener(new ExoPlayerListener(window));
|
exoPlayer.addListener(new ExoPlayerListener(window, playerStateCallback));
|
||||||
exoView.setPlayer(exoPlayer);
|
exoView.setPlayer(exoPlayer);
|
||||||
exoControls.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) {
|
public void setWindow(@Nullable Window window) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ExoPlayerListener extends Player.DefaultEventListener {
|
public void setPlayerStateCallbacks(@Nullable PlayerStateCallback playerStateCallback) {
|
||||||
private final Window window;
|
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.window = window;
|
||||||
|
this.playerStateCallback = playerStateCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -146,10 +166,19 @@ public class VideoPlayer extends FrameLayout {
|
||||||
} else {
|
} else {
|
||||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
}
|
}
|
||||||
|
notifyPlayerReady();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
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 com.google.android.exoplayer2.upstream.TransferListener;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -19,12 +20,16 @@ public class AttachmentDataSource implements DataSource {
|
||||||
|
|
||||||
private final DefaultDataSource defaultDataSource;
|
private final DefaultDataSource defaultDataSource;
|
||||||
private final PartDataSource partDataSource;
|
private final PartDataSource partDataSource;
|
||||||
|
private final BlobDataSource blobDataSource;
|
||||||
|
|
||||||
private DataSource dataSource;
|
private DataSource dataSource;
|
||||||
|
|
||||||
public AttachmentDataSource(DefaultDataSource defaultDataSource, PartDataSource partDataSource) {
|
public AttachmentDataSource(DefaultDataSource defaultDataSource,
|
||||||
|
PartDataSource partDataSource,
|
||||||
|
BlobDataSource blobDataSource) {
|
||||||
this.defaultDataSource = defaultDataSource;
|
this.defaultDataSource = defaultDataSource;
|
||||||
this.partDataSource = partDataSource;
|
this.partDataSource = partDataSource;
|
||||||
|
this.blobDataSource = blobDataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -33,8 +38,9 @@ public class AttachmentDataSource implements DataSource {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long open(DataSpec dataSpec) throws IOException {
|
public long open(DataSpec dataSpec) throws IOException {
|
||||||
if (PartAuthority.isLocalUri(dataSpec.uri)) dataSource = partDataSource;
|
if (BlobProvider.isAuthority(dataSpec.uri)) dataSource = blobDataSource;
|
||||||
else dataSource = defaultDataSource;
|
else if (PartAuthority.isLocalUri(dataSpec.uri)) dataSource = partDataSource;
|
||||||
|
else dataSource = defaultDataSource;
|
||||||
|
|
||||||
return dataSource.open(dataSpec);
|
return dataSource.open(dataSpec);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ public class AttachmentDataSourceFactory implements DataSource.Factory {
|
||||||
@Override
|
@Override
|
||||||
public AttachmentDataSource createDataSource() {
|
public AttachmentDataSource createDataSource() {
|
||||||
return new AttachmentDataSource(defaultDataSourceFactory.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