Revert "Instant Video Playback UI"

This reverts commit f8283acfae.
This commit is contained in:
Alex Hart 2023-09-22 16:13:19 -03:00
parent 15c6c372ba
commit 45583ea469
25 changed files with 399 additions and 856 deletions

View file

@ -172,11 +172,7 @@ public abstract class Attachment {
@Nullable
public byte[] getIncrementalDigest() {
if (incrementalDigest != null && incrementalDigest.length > 0) {
return incrementalDigest;
} else {
return null;
}
return incrementalDigest;
}
@Nullable

View file

@ -12,7 +12,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
@ -55,7 +54,7 @@ public class AlbumThumbnailView extends FrameLayout {
inflate(getContext(), R.layout.album_thumbnail_view, this);
albumCellContainer = findViewById(R.id.album_cell_container);
transferControls = new Stub<>(findViewById(R.id.album_transfer_controls_stub));
transferControls = new Stub<>(findViewById(R.id.album_transfer_controls_stub));
}
public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides, boolean showControls) {
@ -65,13 +64,12 @@ public class AlbumThumbnailView extends FrameLayout {
if (showControls) {
transferControls.get().setShowDownloadText(true);
transferControls.get().setSlides(slides);
transferControls.get().setDownloadClickListener(v -> {
if (downloadClickListener != null) {
downloadClickListener.onClick(v, slides);
}
});
transferControls.get().setSlides(slides);
transferControls.setVisibility(VISIBLE);
} else {
if (transferControls.resolved()) {
transferControls.get().setVisibility(GONE);
@ -87,7 +85,6 @@ public class AlbumThumbnailView extends FrameLayout {
showSlides(glideRequests, slides);
applyCorners();
forceLayout();
}
public void setCellBackgroundColor(@ColorInt int color) {
@ -120,25 +117,22 @@ public class AlbumThumbnailView extends FrameLayout {
private void inflateLayout(int sizeClass) {
albumCellContainer.removeAllViews();
int resId = switch (sizeClass) {
case 2 -> R.layout.album_thumbnail_2;
case 3 -> R.layout.album_thumbnail_3;
case 4 -> R.layout.album_thumbnail_4;
case 5 -> R.layout.album_thumbnail_5;
default -> R.layout.album_thumbnail_many;
};
inflate(getContext(), resId, albumCellContainer);
if (transferControls.resolved()) {
int size = switch (sizeClass) {
case 2 -> R.dimen.album_2_total_height;
case 3 -> R.dimen.album_3_total_height;
case 4 -> R.dimen.album_4_total_height;
default -> R.dimen.album_5_total_height;
};
final ViewGroup.LayoutParams params = transferControls.get().getLayoutParams();
params.height = getContext().getResources().getDimensionPixelSize(size);
transferControls.get().setLayoutParams(params);
switch (sizeClass) {
case 2:
inflate(getContext(), R.layout.album_thumbnail_2, albumCellContainer);
break;
case 3:
inflate(getContext(), R.layout.album_thumbnail_3, albumCellContainer);
break;
case 4:
inflate(getContext(), R.layout.album_thumbnail_4, albumCellContainer);
break;
case 5:
inflate(getContext(), R.layout.album_thumbnail_5, albumCellContainer);
break;
default:
inflate(getContext(), R.layout.album_thumbnail_many, albumCellContainer);
break;
}
}

View file

@ -255,17 +255,9 @@ class ConversationItemThumbnail @JvmOverloads constructor(
state.applyState(thumbnail, album)
}
fun setPlayVideoClickListener(listener: SlideClickListener?) {
fun setProgressWheelClickListener(listener: SlideClickListener?) {
state = state.copy(
thumbnailViewState = state.thumbnailViewState.copy(playVideoClickListener = listener)
)
state.applyState(thumbnail, album)
}
fun setCancelDownloadClickListener(listener: SlidesClickedListener?) {
state = state.copy(
thumbnailViewState = state.thumbnailViewState.copy(cancelDownloadClickListener = listener)
thumbnailViewState = state.thumbnailViewState.copy(progressWheelClickListener = listener)
)
state.applyState(thumbnail, album)

View file

@ -31,9 +31,7 @@ data class ConversationItemThumbnailState(
@IgnoredOnParcel
private val downloadClickListener: SlidesClickedListener? = null,
@IgnoredOnParcel
private val cancelDownloadClickListener: SlidesClickedListener? = null,
@IgnoredOnParcel
private val playVideoClickListener: SlideClickListener? = null,
private val progressWheelClickListener: SlideClickListener? = null,
@IgnoredOnParcel
private val longClickListener: OnLongClickListener? = null,
private val visibility: Int = View.GONE,
@ -59,8 +57,7 @@ data class ConversationItemThumbnailState(
thumbnailView.get().setRadii(cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft)
thumbnailView.get().setThumbnailClickListener(clickListener)
thumbnailView.get().setDownloadClickListener(downloadClickListener)
thumbnailView.get().setCancelDownloadClickListener(cancelDownloadClickListener)
thumbnailView.get().setPlayVideoClickListener(playVideoClickListener)
thumbnailView.get().setProgressWheelClickListener(progressWheelClickListener)
thumbnailView.get().setOnLongClickListener(longClickListener)
thumbnailView.get().setBounds(minWidth, maxWidth, minHeight, maxHeight)
}

View file

@ -31,7 +31,6 @@ import org.signal.core.util.logging.Log;
import org.signal.glide.transforms.SignalDownsampleStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequest;
@ -42,6 +41,7 @@ import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
@ -80,13 +80,12 @@ public class ThumbnailView extends FrameLayout {
private final CornerMask cornerMask;
private ThumbnailViewTransferControlsState transferControlsState = new ThumbnailViewTransferControlsState();
private ThumbnailViewTransferControlsState transferControlsState = new ThumbnailViewTransferControlsState();
private Stub<TransferControlView> transferControlViewStub;
private SlideClickListener thumbnailClickListener = null;
private SlidesClickedListener downloadClickListener = null;
private SlidesClickedListener cancelDownloadClickListener = null;
private SlideClickListener playVideoClickListener = null;
private Slide slide = null;
private SlideClickListener thumbnailClickListener = null;
private SlidesClickedListener downloadClickListener = null;
private SlideClickListener progressWheelClickListener = null;
private Slide slide = null;
public ThumbnailView(Context context) {
@ -368,11 +367,10 @@ public class ThumbnailView extends FrameLayout {
}
transferControlsState = transferControlsState.withSlide(slide)
.withDownloadClickListener(new DownloadClickDispatcher())
.withCancelDownloadClickListener(new CancelClickDispatcher());
.withDownloadClickListener(new DownloadClickDispatcher());
if (MediaUtil.isInstantVideoSupported(slide)) {
transferControlsState = transferControlsState.withInstantPlaybackClickListener(new ProgressWheelClickDispatcher());
if (FeatureFlags.instantVideoPlayback()) {
transferControlsState = transferControlsState.withProgressWheelClickListener(new ProgressWheelClickDispatcher());
}
transferControlsState.applyState(transferControlViewStub);
@ -527,12 +525,8 @@ public class ThumbnailView extends FrameLayout {
this.downloadClickListener = listener;
}
public void setCancelDownloadClickListener(SlidesClickedListener listener) {
this.cancelDownloadClickListener = listener;
}
public void setPlayVideoClickListener(SlideClickListener listener) {
this.playVideoClickListener = listener;
public void setProgressWheelClickListener(SlideClickListener listener) {
this.progressWheelClickListener = listener;
}
public void clear(GlideRequests glideRequests) {
@ -574,9 +568,9 @@ public class ThumbnailView extends FrameLayout {
private GlideRequest<Drawable> buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
GlideRequest<Drawable> request = applySizing(glideRequests.load(new DecryptableUri(Objects.requireNonNull(slide.getUri())))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
.transition(withCrossFade()));
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
.transition(withCrossFade()));
boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23;
@ -631,7 +625,7 @@ public class ThumbnailView extends FrameLayout {
if (Util.equals(slide, other)) {
if (slide != null && other != null) {
byte[] digestLeft = slide.asAttachment().getDigest();
byte[] digestLeft = slide.asAttachment().getDigest();
byte[] digestRight = other.asAttachment().getDigest();
return Arrays.equals(digestLeft, digestRight);
@ -676,26 +670,14 @@ public class ThumbnailView extends FrameLayout {
}
}
private class CancelClickDispatcher implements View.OnClickListener {
@Override
public void onClick(View view) {
Log.i(TAG, "onClick() for cancel button");
if (cancelDownloadClickListener != null && slide != null) {
cancelDownloadClickListener.onClick(view, Collections.singletonList(slide));
} else {
Log.w(TAG, "Received a cancel button click, but unable to execute it. slide: " + slide + " cancelDownloadClickListener: " + cancelDownloadClickListener);
}
}
}
private class ProgressWheelClickDispatcher implements View.OnClickListener {
@Override
public void onClick(View view) {
Log.i(TAG, "onClick() for instant video playback");
if (playVideoClickListener != null && slide != null) {
playVideoClickListener.onClick(view, slide);
Log.i(TAG, "onClick() for progress wheel");
if (progressWheelClickListener != null && slide != null) {
progressWheelClickListener.onClick(view, slide);
} else {
Log.w(TAG, "Received an instant video click, but unable to execute it. slide: " + slide + " progressWheelClickListener: " + playVideoClickListener);
Log.w(TAG, "Received a progress wheel click, but unable to execute it. slide: " + slide + " progressWheelClickListener: " + progressWheelClickListener);
}
}
}

View file

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.components
import android.view.View.OnClickListener
import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.util.views.Stub
@ -13,8 +12,7 @@ data class ThumbnailViewTransferControlsState(
val isClickable: Boolean = true,
val slide: Slide? = null,
val downloadClickedListener: OnClickListener? = null,
val cancelDownloadClickedListener: OnClickListener? = null,
val instantPlaybackClickListener: OnClickListener? = null,
val progressWheelClickedListener: OnClickListener? = null,
val showDownloadText: Boolean = true
) {
@ -22,8 +20,7 @@ data class ThumbnailViewTransferControlsState(
fun withClickable(isClickable: Boolean): ThumbnailViewTransferControlsState = copy(isClickable = isClickable)
fun withSlide(slide: Slide?): ThumbnailViewTransferControlsState = copy(slide = slide)
fun withDownloadClickListener(downloadClickedListener: OnClickListener): ThumbnailViewTransferControlsState = copy(downloadClickedListener = downloadClickedListener)
fun withCancelDownloadClickListener(cancelClickListener: OnClickListener): ThumbnailViewTransferControlsState = copy(cancelDownloadClickedListener = cancelClickListener)
fun withInstantPlaybackClickListener(instantPlaybackClickListener: OnClickListener): ThumbnailViewTransferControlsState = copy(instantPlaybackClickListener = instantPlaybackClickListener)
fun withProgressWheelClickListener(progressWheelClickedListener: OnClickListener): ThumbnailViewTransferControlsState = copy(progressWheelClickedListener = progressWheelClickedListener)
fun withDownloadText(showDownloadText: Boolean): ThumbnailViewTransferControlsState = copy(showDownloadText = showDownloadText)
fun applyState(transferControlView: Stub<TransferControlView>) {
@ -34,9 +31,8 @@ data class ThumbnailViewTransferControlsState(
transferControlView.get().setSlide(slide)
}
transferControlView.get().setDownloadClickListener(downloadClickedListener)
transferControlView.get().setProgressWheelClickListener(progressWheelClickedListener)
transferControlView.get().setShowDownloadText(showDownloadText)
transferControlView.get().setCancelClickListener(cancelDownloadClickedListener)
transferControlView.get().setInstantPlaybackClickListener(instantPlaybackClickListener)
}
}
}

View file

@ -0,0 +1,273 @@
package org.thoughtcrime.securesms.components;
import android.animation.LayoutTransition;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.annimon.stream.Stream;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.mms.Slide;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public final class TransferControlView extends FrameLayout {
private static final String TAG = "TransferControlView";
private static final int UPLOAD_TASK_WEIGHT = 1;
/**
* A weighting compared to {@link #UPLOAD_TASK_WEIGHT}
*/
private static final int COMPRESSION_TASK_WEIGHT = 3;
@Nullable private List<Slide> slides;
@Nullable private View current;
private final ProgressWheel progressWheel;
private final View downloadDetails;
private final TextView downloadDetailsText;
private final Map<Attachment, Float> networkProgress;
private final Map<Attachment, Float> compresssionProgress;
public TransferControlView(Context context) {
this(context, null);
}
public TransferControlView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TransferControlView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.transfer_controls_view, this);
setLongClickable(false);
setBackground(ContextCompat.getDrawable(context, R.drawable.transfer_controls_background));
setVisibility(GONE);
setLayoutTransition(new LayoutTransition());
this.networkProgress = new HashMap<>();
this.compresssionProgress = new HashMap<>();
this.progressWheel = findViewById(R.id.progress_wheel);
this.downloadDetails = findViewById(R.id.download_details);
this.downloadDetailsText = findViewById(R.id.download_details_text);
}
@Override
public void setFocusable(boolean focusable) {
super.setFocusable(focusable);
downloadDetails.setFocusable(focusable);
}
@Override
public void setClickable(boolean clickable) {
super.setClickable(clickable);
downloadDetails.setClickable(clickable);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
EventBus.getDefault().unregister(this);
}
public void setSlide(final @NonNull Slide slides) {
setSlides(Collections.singletonList(slides));
}
public void setSlides(final @NonNull List<Slide> slides) {
if (slides.isEmpty()) {
throw new IllegalArgumentException("Must provide at least one slide.");
}
this.slides = slides;
if (!isUpdateToExistingSet(slides)) {
networkProgress.clear();
compresssionProgress.clear();
Stream.of(slides).forEach(s -> networkProgress.put(s.asAttachment(), 0f));
}
for (Slide slide : slides) {
if (slide.asAttachment().getTransferState() == AttachmentTable.TRANSFER_PROGRESS_DONE) {
networkProgress.put(slide.asAttachment(), 1f);
}
}
switch (getTransferState(slides)) {
case AttachmentTable.TRANSFER_PROGRESS_STARTED:
showProgressSpinner(calculateProgress(networkProgress, compresssionProgress));
break;
case AttachmentTable.TRANSFER_PROGRESS_PENDING:
case AttachmentTable.TRANSFER_PROGRESS_FAILED:
String downloadText = getDownloadText(this.slides);
if (!Objects.equals(downloadText, downloadDetailsText.getText().toString())) {
downloadDetailsText.setText(getDownloadText(this.slides));
}
display(downloadDetails);
break;
default:
display(null);
break;
}
}
public void showProgressSpinner() {
showProgressSpinner(calculateProgress(networkProgress, compresssionProgress));
}
public void showProgressSpinner(float progress) {
if (progress == 0) {
progressWheel.spin();
} else {
progressWheel.setInstantProgress(progress);
}
display(progressWheel);
}
public void setDownloadClickListener(final @Nullable OnClickListener listener) {
downloadDetails.setOnClickListener(listener);
}
public void setProgressWheelClickListener(final @Nullable OnClickListener listener) {
progressWheel.setOnClickListener(listener);
}
public void clear() {
clearAnimation();
setVisibility(GONE);
if (current != null) {
current.clearAnimation();
current.setVisibility(GONE);
}
current = null;
slides = null;
}
public void setShowDownloadText(boolean showDownloadText) {
downloadDetailsText.setVisibility(showDownloadText ? VISIBLE : GONE);
forceLayout();
}
private boolean isUpdateToExistingSet(@NonNull List<Slide> slides) {
if (slides.size() != networkProgress.size()) {
return false;
}
for (Slide slide : slides) {
if (!networkProgress.containsKey(slide.asAttachment())) {
return false;
}
}
return true;
}
static int getTransferState(@NonNull List<Slide> slides) {
int transferState = AttachmentTable.TRANSFER_PROGRESS_DONE;
boolean allFailed = true;
for (Slide slide : slides) {
if (slide.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE) {
allFailed = false;
if (slide.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_PENDING && transferState == AttachmentTable.TRANSFER_PROGRESS_DONE) {
transferState = slide.getTransferState();
} else {
transferState = Math.max(transferState, slide.getTransferState());
}
}
}
return allFailed ? AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE : transferState;
}
private String getDownloadText(@NonNull List<Slide> slides) {
if (slides.size() == 1) {
return slides.get(0).getContentDescription(getContext());
} else {
int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_DONE ? count + 1 : count);
return getContext().getResources().getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount);
}
}
private void display(@Nullable final View view) {
if (current == view) {
return;
}
if (current != null) {
current.setVisibility(GONE);
}
if (view != null) {
view.setVisibility(VISIBLE);
setVisibility(VISIBLE);
} else {
setVisibility(GONE);
}
current = view;
}
private static float calculateProgress(@NonNull Map<Attachment, Float> uploadDownloadProgress, Map<Attachment, Float> compresssionProgress) {
float totalDownloadProgress = 0;
float totalCompressionProgress = 0;
for (float progress : uploadDownloadProgress.values()) {
totalDownloadProgress += progress;
}
for (float progress : compresssionProgress.values()) {
totalCompressionProgress += progress;
}
float weightedProgress = UPLOAD_TASK_WEIGHT * totalDownloadProgress + COMPRESSION_TASK_WEIGHT * totalCompressionProgress;
float weightedTotal = UPLOAD_TASK_WEIGHT * uploadDownloadProgress.size() + COMPRESSION_TASK_WEIGHT * compresssionProgress.size();
return weightedProgress / weightedTotal;
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventAsync(final PartProgressEvent event) {
final Attachment attachment = event.attachment;
if (networkProgress.containsKey(attachment)) {
float proportionCompleted = ((float) event.progress) / event.total;
if (event.type == PartProgressEvent.Type.COMPRESSION) {
compresssionProgress.put(attachment, proportionCompleted);
} else {
networkProgress.put(attachment, proportionCompleted);
}
progressWheel.setInstantProgress(calculateProgress(networkProgress, compresssionProgress));
}
}
}

View file

@ -1,289 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.transfercontrols
import android.animation.LayoutTransition
import android.annotation.SuppressLint
import android.content.Context
import android.text.format.Formatter
import android.util.AttributeSet
import android.view.View
import android.widget.Space
import android.widget.TextView
import androidx.appcompat.widget.AppCompatImageView
import androidx.constraintlayout.widget.ConstraintLayout
import com.annimon.stream.Stream
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.events.PartProgressEvent
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.ThrottledDebouncer
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.visible
class TransferControlView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
private var slides: List<Slide> = emptyList()
private var current: MutableSet<View> = HashSet()
private var playableWhileDownloading = false
private var showDownloadText = true
private val downloadDetails: View
private val downloadDetailsText: TextView
private val primaryDetailsText: TextView
private val secondaryViewSpace: Space
private val playVideoButton: AppCompatImageView
private val primaryProgressView: TransferProgressView
private val secondaryProgressView: TransferProgressView
private val networkProgress: MutableMap<Attachment, Progress>
private val compressionProgress: MutableMap<Attachment, Progress>
private val debouncer: ThrottledDebouncer = ThrottledDebouncer(8) // frame time for 120 Hz
init {
inflate(context, R.layout.transfer_controls_view, this)
isLongClickable = false
visibility = GONE
layoutTransition = LayoutTransition()
networkProgress = HashMap()
compressionProgress = HashMap()
primaryProgressView = findViewById(R.id.primary_progress_view)
secondaryProgressView = findViewById(R.id.secondary_progress_view)
playVideoButton = findViewById(R.id.play_video_button)
downloadDetails = findViewById(R.id.secondary_background)
downloadDetailsText = findViewById(R.id.download_details_text)
secondaryViewSpace = findViewById(R.id.secondary_view_space)
primaryDetailsText = findViewById(R.id.primary_details_text)
}
override fun setFocusable(focusable: Boolean) {
super.setFocusable(focusable)
progressView.isFocusable = focusable
if (playVideoButton.visibility == VISIBLE) {
playVideoButton.isFocusable = focusable
}
}
override fun setClickable(clickable: Boolean) {
super.setClickable(clickable)
secondaryProgressView.isClickable = secondaryProgressView.visible && clickable
primaryProgressView.isClickable = primaryProgressView.visible && clickable
playVideoButton.isClickable = playVideoButton.visible && clickable
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
EventBus.getDefault().unregister(this)
}
fun setSlide(slides: Slide) {
setSlides(listOf(slides))
}
fun setSlides(slides: List<Slide>) {
require(slides.isNotEmpty()) { "Must provide at least one slide." }
this.slides = slides
if (!isUpdateToExistingSet(slides)) {
networkProgress.clear()
compressionProgress.clear()
slides.forEach { networkProgress[it.asAttachment()] = Progress(0L, it.fileSize) }
}
var allStreamableOrDone = true
for (slide in slides) {
val attachment = slide.asAttachment()
if (attachment.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE) {
networkProgress[attachment] = Progress(1L, attachment.size)
} else if (!MediaUtil.isInstantVideoSupported(slide)) {
allStreamableOrDone = false
}
}
playableWhileDownloading = allStreamableOrDone
setPlayableWhileDownloading(playableWhileDownloading)
val uploading = slides.any { it.asAttachment().uploadTimestamp == 0L }
when (getTransferState(slides)) {
AttachmentTable.TRANSFER_PROGRESS_STARTED -> showProgressSpinner(calculateProgress(), uploading)
AttachmentTable.TRANSFER_PROGRESS_PENDING -> {
updateDownloadText()
progressView.setStopped(false)
this.visible = true
}
AttachmentTable.TRANSFER_PROGRESS_FAILED -> {
downloadDetailsText.setText(R.string.NetworkFailure__retry)
progressView.setStopped(false)
this.visible = true
}
else -> this.visible = false
}
}
private val progressView: TransferProgressView
get() = if (playableWhileDownloading) {
secondaryProgressView
} else {
primaryProgressView
}
@JvmOverloads
fun showProgressSpinner(progress: Float = calculateProgress(), uploading: Boolean = false) {
if (uploading || progress == 0f) {
progressView.setUploading(progress)
} else {
progressView.setDownloading(progress)
}
}
fun setDownloadClickListener(listener: OnClickListener?) {
primaryProgressView.startClickListener = listener
secondaryProgressView.startClickListener = listener
}
fun setCancelClickListener(listener: OnClickListener?) {
primaryProgressView.cancelClickListener = listener
secondaryProgressView.cancelClickListener = listener
}
fun setInstantPlaybackClickListener(onPlayClickedListener: OnClickListener?) {
playVideoButton.setOnClickListener(onPlayClickedListener)
}
fun clear() {
clearAnimation()
visibility = GONE
if (current.isNotEmpty()) {
for (v in current) {
v.clearAnimation()
v.visibility = GONE
}
}
current.clear()
slides = emptyList()
}
fun setShowDownloadText(showDownloadText: Boolean) {
this.showDownloadText = showDownloadText
updateDownloadText()
}
private fun isUpdateToExistingSet(slides: List<Slide>): Boolean {
if (slides.size != networkProgress.size) {
return false
}
for (slide in slides) {
if (!networkProgress.containsKey(slide.asAttachment())) {
return false
}
}
return true
}
private fun updateDownloadText() {
val byteCount = slides.sumOf { it.asAttachment().size }
downloadDetailsText.text = Formatter.formatShortFileSize(context, byteCount)
downloadDetailsText.invalidate()
if (slides.size > 1) {
val downloadCount = Stream.of(slides).reduce(0) { count: Int, slide: Slide -> if (slide.transferState != AttachmentTable.TRANSFER_PROGRESS_DONE) count + 1 else count }
primaryDetailsText.text = context.resources.getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount)
primaryDetailsText.visible = showDownloadText
} else {
primaryDetailsText.text = ""
primaryDetailsText.visible = false
}
}
@SuppressLint("SetTextI18n")
private fun updateDownloadProgressText(isCompression: Boolean) {
val context = context
val progress = if (isCompression) compressionProgress.values.sumOf { it.completed } else networkProgress.values.sumOf { it.completed }
val total = if (isCompression) compressionProgress.values.sumOf { it.total } else networkProgress.values.sumOf { it.total }
val progressText = Formatter.formatShortFileSize(context, progress)
val totalText = Formatter.formatShortFileSize(context, total)
downloadDetailsText.text = "$progressText/$totalText"
downloadDetailsText.invalidate()
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
fun onEventAsync(event: PartProgressEvent) {
val attachment = event.attachment
if (networkProgress.containsKey(attachment)) {
val proportionCompleted = event.progress.toFloat() / event.total
if (event.type == PartProgressEvent.Type.COMPRESSION) {
compressionProgress[attachment] = Progress.fromEvent(event)
} else {
networkProgress[attachment] = Progress.fromEvent(event)
}
debouncer.publish {
val progress = calculateProgress()
if (attachment.uploadTimestamp == 0L) {
progressView.setUploading(progress)
} else {
progressView.setDownloading(progress)
}
updateDownloadProgressText(event.type == PartProgressEvent.Type.COMPRESSION)
}
}
}
private fun setPlayableWhileDownloading(playableWhileDownloading: Boolean) {
playVideoButton.visible = playableWhileDownloading
secondaryProgressView.visible = playableWhileDownloading
secondaryViewSpace.visible = !playableWhileDownloading // exists because constraint layout was being very weird about margins and this was the only way
primaryProgressView.visibility = if (playableWhileDownloading) INVISIBLE else VISIBLE
val textPadding = if (playableWhileDownloading) 0 else context.resources.getDimensionPixelSize(R.dimen.transfer_control_view_progressbar_to_textview_margin)
ViewUtil.setPaddingStart(downloadDetailsText, textPadding)
}
private fun calculateProgress(): Float {
val totalDownloadProgress: Float = networkProgress.values.map { it.completed.toFloat() / it.total }.sum()
val totalCompressionProgress: Float = compressionProgress.values.map { it.completed.toFloat() / it.total }.sum()
val weightedProgress = UPLOAD_TASK_WEIGHT * totalDownloadProgress + COMPRESSION_TASK_WEIGHT * totalCompressionProgress
val weightedTotal = (UPLOAD_TASK_WEIGHT * networkProgress.size + COMPRESSION_TASK_WEIGHT * compressionProgress.size).toFloat()
return weightedProgress / weightedTotal
}
companion object {
private const val TAG = "TransferControlView"
private const val UPLOAD_TASK_WEIGHT = 1
/**
* A weighting compared to [.UPLOAD_TASK_WEIGHT]
*/
private const val COMPRESSION_TASK_WEIGHT = 3
@JvmStatic
fun getTransferState(slides: List<Slide>): Int {
var transferState = AttachmentTable.TRANSFER_PROGRESS_DONE
var allFailed = true
for (slide in slides) {
if (slide.transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE) {
allFailed = false
transferState = if (slide.transferState == AttachmentTable.TRANSFER_PROGRESS_PENDING && transferState == AttachmentTable.TRANSFER_PROGRESS_DONE) {
slide.transferState
} else {
transferState.coerceAtLeast(slide.transferState)
}
}
}
return if (allFailed) AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE else transferState
}
}
data class Progress(val completed: Long, val total: Long) {
companion object {
fun fromEvent(event: PartProgressEvent): Progress {
return Progress(event.progress, event.total)
}
}
}
}

View file

@ -1,169 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.transfercontrols
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.graphics.withTranslation
import org.signal.core.util.dp
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import kotlin.math.roundToInt
class TransferProgressView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
companion object {
const val TAG = "TransferProgressView"
private const val PROGRESS_ARC_STROKE_WIDTH = 2f
private const val ICON_INSET_PERCENT = 0.2f
}
private val progressRect = RectF()
private val stopIconRect = RectF()
private val progressPaint = progressPaint()
private val stopIconPaint = stopIconPaint()
private val trackPaint = trackPaint()
private var progressPercent = 0f
private var currentState = State.READY_TO_DOWNLOAD
private val downloadDrawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_down_24)
private val uploadDrawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_up_16)
var startClickListener: OnClickListener? = null
var cancelClickListener: OnClickListener? = null
init {
val tint = ContextCompat.getColor(context, R.color.signal_colorOnCustom)
val filter = PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_ATOP)
downloadDrawable?.colorFilter = filter
uploadDrawable?.colorFilter = filter
}
override fun requestLayout() {
super.requestLayout()
Log.d(TAG, "Requesting new layout.", Exception())
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
when (currentState) {
State.IN_PROGRESS_CANCELABLE, State.IN_PROGRESS_NON_CANCELABLE -> drawProgress(canvas, progressPercent)
State.READY_TO_UPLOAD -> sizeAndDrawDrawable(canvas, uploadDrawable)
State.READY_TO_DOWNLOAD -> sizeAndDrawDrawable(canvas, downloadDrawable)
}
}
fun setDownloading(progress: Float) {
if (currentState != State.IN_PROGRESS_CANCELABLE) {
currentState = State.IN_PROGRESS_CANCELABLE
setOnClickListener(cancelClickListener)
}
progressPercent = progress
}
fun setUploading(progress: Float) {
if (currentState != State.IN_PROGRESS_NON_CANCELABLE) {
currentState = State.IN_PROGRESS_NON_CANCELABLE
setOnClickListener(null)
}
progressPercent = progress
}
fun setStopped(isUpload: Boolean) {
val newState = if (isUpload) State.READY_TO_UPLOAD else State.READY_TO_DOWNLOAD
if (currentState != newState) {
currentState = newState
setOnClickListener(startClickListener)
}
progressPercent = 0f
}
private fun drawProgress(canvas: Canvas, progressPercent: Float) {
if (currentState == State.IN_PROGRESS_CANCELABLE) {
val miniIcon = height < 32.dp
val stopIconCornerRadius = if (miniIcon) 1f.dp else 4f.dp
val iconSize: Float = if (miniIcon) 6.6f.dp else 16f.dp
stopIconRect.set(0f, 0f, iconSize, iconSize)
canvas.withTranslation(width / 2 - (iconSize / 2), height / 2 - (iconSize / 2)) {
drawRoundRect(stopIconRect, stopIconCornerRadius, stopIconCornerRadius, stopIconPaint)
}
}
val widthDp = PROGRESS_ARC_STROKE_WIDTH.dp
val inset = 2.dp
progressRect.top = widthDp + inset
progressRect.left = widthDp + inset
progressRect.right = (width - widthDp) - inset
progressRect.bottom = (height - widthDp) - inset
canvas.drawArc(progressRect, 0f, 360f, false, trackPaint)
canvas.drawArc(progressRect, 270f, 360f * progressPercent, false, progressPaint)
}
private fun stopIconPaint(): Paint {
val stopIconPaint = Paint()
stopIconPaint.color = ContextCompat.getColor(context, R.color.signal_colorOnCustom)
stopIconPaint.isAntiAlias = true
stopIconPaint.style = Paint.Style.FILL
return stopIconPaint
}
private fun trackPaint(): Paint {
val trackPaint = Paint()
trackPaint.color = ContextCompat.getColor(context, R.color.signal_colorTransparent2)
trackPaint.isAntiAlias = true
trackPaint.style = Paint.Style.STROKE
trackPaint.strokeWidth = PROGRESS_ARC_STROKE_WIDTH.dp
return trackPaint
}
private fun progressPaint(): Paint {
val progressPaint = Paint()
progressPaint.color = ContextCompat.getColor(context, R.color.signal_colorOnCustom)
progressPaint.isAntiAlias = true
progressPaint.style = Paint.Style.STROKE
progressPaint.strokeWidth = PROGRESS_ARC_STROKE_WIDTH.dp
return progressPaint
}
private fun sizeAndDrawDrawable(canvas: Canvas, drawable: Drawable?) {
if (drawable == null) {
Log.w(TAG, "Could not load icon for $currentState")
return
}
drawable.setBounds(
(width * ICON_INSET_PERCENT).roundToInt(),
(height * ICON_INSET_PERCENT).roundToInt(),
(width * (1 - ICON_INSET_PERCENT)).roundToInt(),
(height * (1 - ICON_INSET_PERCENT)).roundToInt()
)
drawable.draw(canvas)
}
private enum class State {
IN_PROGRESS_CANCELABLE,
IN_PROGRESS_NON_CANCELABLE,
READY_TO_UPLOAD,
READY_TO_DOWNLOAD
}
}

View file

@ -63,16 +63,12 @@ import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.common.collect.Sets;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.DimensionUnit;
import org.signal.core.util.StringUtil;
import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.badges.BadgeImageView;
import org.thoughtcrime.securesms.badges.gifts.GiftMessageView;
@ -110,10 +106,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
import org.thoughtcrime.securesms.jobs.MmsSendJob;
@ -247,8 +241,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
private final PlayVideoClickListener playVideoClickListener = new PlayVideoClickListener();
private final AttachmentCancelClickListener attachmentCancelClickListener = new AttachmentCancelClickListener();
private final ProgressWheelClickListener progressWheelClickListener = new ProgressWheelClickListener();
private final SlideClickPassthroughListener singleDownloadClickListener = new SlideClickPassthroughListener(downloadClickListener);
private final SharedContactEventListener sharedContactEventListener = new SharedContactEventListener();
private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
@ -1179,8 +1172,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
mediaThumbnailStub.require().setImageResource(glideRequests, Collections.singletonList(new ImageSlide(linkPreview.getThumbnail().get())), showControls, false);
mediaThumbnailStub.require().setThumbnailClickListener(new LinkPreviewThumbnailClickListener());
mediaThumbnailStub.require().setDownloadClickListener(downloadClickListener);
mediaThumbnailStub.require().setCancelDownloadClickListener(attachmentCancelClickListener);
mediaThumbnailStub.require().setPlayVideoClickListener(playVideoClickListener);
mediaThumbnailStub.require().setProgressWheelClickListener(progressWheelClickListener);
mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener);
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, false);
@ -1320,8 +1312,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
false);
mediaThumbnailStub.require().setThumbnailClickListener(new ThumbnailClickListener());
mediaThumbnailStub.require().setDownloadClickListener(downloadClickListener);
mediaThumbnailStub.require().setCancelDownloadClickListener(attachmentCancelClickListener);
mediaThumbnailStub.require().setPlayVideoClickListener(playVideoClickListener);
mediaThumbnailStub.require().setProgressWheelClickListener(progressWheelClickListener);
mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener);
mediaThumbnailStub.require().setOnClickListener(passthroughClickListener);
mediaThumbnailStub.require().showShade(messageRecord.isDisplayBodyEmpty(getContext()) && !hasExtraText(messageRecord));
@ -2452,84 +2443,18 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
private class AttachmentCancelClickListener implements SlidesClickedListener {
@Override
public void onClick(View v, List<Slide> slides) {
Log.i(TAG, "onClick() for attachment cancellation");
final JobManager jobManager = ApplicationDependencies.getJobManager();
if (messageRecord.isMmsNotification()) {
Log.i(TAG, "Canceling MMS attachments download");
jobManager.cancel("mms-operation");
} else {
Log.i(TAG, "Canceling push attachment downloads for " + slides.size() + " items");
for (Slide slide : slides) {
final String queue = AttachmentDownloadJob.constructQueueString(((DatabaseAttachment) slide.asAttachment()).getAttachmentId());
jobManager.cancelAllInQueue(queue);
}
}
}
}
private class PlayVideoClickListener implements SlideClickListener {
private static final float MINIMUM_DOWNLOADED_THRESHOLD = 0.05f;
private View parentView;
private Slide activeSlide;
private class ProgressWheelClickListener implements SlideClickListener {
@Override
public void onClick(View v, Slide slide) {
if (messageRecord.isOutgoing()) {
Log.d(TAG, "Video player button for outgoing slide clicked.");
return;
}
if (MediaUtil.isInstantVideoSupported(slide)) {
final DatabaseAttachment databaseAttachment = (DatabaseAttachment) slide.asAttachment();
if (databaseAttachment.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_STARTED) {
final AttachmentId attachmentId = databaseAttachment.getAttachmentId();
final JobManager jobManager = ApplicationDependencies.getJobManager();
final String queue = AttachmentDownloadJob.constructQueueString(attachmentId);
setup(v, slide);
jobManager.add(new AttachmentDownloadJob(messageRecord.getId(),
attachmentId,
true));
jobManager.addListener(queue, (job, jobState) -> {
if (jobState.isComplete()) {
cleanup();
}
});
} else {
launchMediaPreview(v, slide);
cleanup();
}
final boolean isIncremental = slide.asAttachment().getIncrementalDigest() != null;
final boolean contentTypeSupported = MediaUtil.isVideoType(slide.getContentType());
if (FeatureFlags.instantVideoPlayback() && isIncremental && contentTypeSupported) {
launchMediaPreview(v, slide);
} else {
Log.d(TAG, "Non-eligible slide clicked.");
Log.d(TAG, "Non-eligible slide clicked: " + "\tisIncremental: " + isIncremental + "\tcontentTypeSupported: " + contentTypeSupported);
}
}
private void setup(View v, Slide slide) {
parentView = v;
activeSlide = slide;
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this);
}
private void cleanup() {
parentView = null;
activeSlide = null;
if (EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().unregister(this);
}
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventAsync(PartProgressEvent event) {
float progressPercent = ((float) event.progress) / event.total;
final View currentParentView = parentView;
final Slide currentActiveSlide = activeSlide;
if (progressPercent >= MINIMUM_DOWNLOADED_THRESHOLD && currentParentView != null && currentActiveSlide != null) {
cleanup();
launchMediaPreview(currentParentView, currentActiveSlide);
}
}
}
private class SlideClickPassthroughListener implements SlideClickListener {

View file

@ -19,9 +19,9 @@ import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobLogger;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.notifications.v2.ConversationId;
@ -33,7 +33,6 @@ import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
@ -70,11 +69,11 @@ public final class AttachmentDownloadJob extends BaseJob {
public AttachmentDownloadJob(long messageId, AttachmentId attachmentId, boolean manual) {
this(new Job.Parameters.Builder()
.setQueue(constructQueueString(attachmentId))
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build(),
.setQueue("AttachmentDownloadJob" + attachmentId.getRowId() + "-" + attachmentId.getUniqueId())
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build(),
messageId,
attachmentId,
manual);
@ -110,8 +109,8 @@ public final class AttachmentDownloadJob extends BaseJob {
final AttachmentTable database = SignalDatabase.attachments();
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
final DatabaseAttachment attachment = database.getAttachment(attachmentId);
final boolean pending = attachment != null && attachment.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_DONE
&& attachment.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE;
final boolean pending = attachment != null && attachment.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_DONE
&& attachment.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE;
if (pending && (manual || AttachmentUtil.isAutoDownloadPermitted(context, attachment))) {
Log.i(TAG, "onAdded() Marking attachment progress as 'started'");
@ -131,8 +130,8 @@ public final class AttachmentDownloadJob extends BaseJob {
public void doWork() throws IOException, RetryLaterException {
Log.i(TAG, "onRun() messageId: " + messageId + " partRowId: " + partRowId + " partUniqueId: " + partUniqueId + " manual: " + manual);
final AttachmentTable database = SignalDatabase.attachments();
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
final AttachmentTable database = SignalDatabase.attachments();
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
final DatabaseAttachment attachment = database.getAttachment(attachmentId);
if (attachment == null) {
@ -196,20 +195,11 @@ public final class AttachmentDownloadJob extends BaseJob {
}
SignalServiceMessageReceiver messageReceiver = ApplicationDependencies.getSignalServiceMessageReceiver();
SignalServiceAttachmentPointer pointer = createAttachmentPointer(attachment);
InputStream stream = messageReceiver.retrieveAttachment(pointer,
attachmentFile,
maxReceiveSize,
new SignalServiceAttachment.ProgressListener() {
@Override
public void onAttachmentProgress(long total, long progress) {
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress));
}
InputStream stream = messageReceiver.retrieveAttachment(pointer,
attachmentFile,
maxReceiveSize,
(total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress)));
@Override
public boolean shouldCancel() {
return isCanceled();
}
});
database.insertAttachmentsForPlaceholder(messageId, attachmentId, stream);
} catch (RangeException e) {
Log.w(TAG, "Range exception, file size " + attachmentFile.length(), e);
@ -308,13 +298,8 @@ public final class AttachmentDownloadJob extends BaseJob {
}
}
public static String constructQueueString(AttachmentId attachmentId) {
return "AttachmentDownloadJob" + attachmentId.getRowId() + "-" + attachmentId.getUniqueId();
}
@VisibleForTesting static class InvalidPartException extends Exception {
InvalidPartException(String s) {super(s);}
InvalidPartException(Exception e) {super(e);}
}

View file

@ -213,18 +213,10 @@ public final class AttachmentUploadJob extends BaseJob {
.withCaption(attachment.getCaption())
.withCancelationSignal(this::isCanceled)
.withResumableUploadSpec(resumableUploadSpec)
.withListener(new SignalServiceAttachment.ProgressListener() {
@Override
public void onAttachmentProgress(long total, long progress) {
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress));
if (notification != null) {
notification.setProgress(total, progress);
}
}
@Override
public boolean shouldCancel() {
return isCanceled();
.withListener((total, progress) -> {
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress));
if (notification != null) {
notification.setProgress(total, progress);
}
});
if (MediaUtil.isImageType(attachment.getContentType())) {

View file

@ -200,17 +200,7 @@ public abstract class PushSendJob extends SendJob {
.withWidth(attachment.getWidth())
.withHeight(attachment.getHeight())
.withCaption(attachment.getCaption())
.withListener(new SignalServiceAttachment.ProgressListener() {
@Override
public void onAttachmentProgress(long total, long progress) {
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress));
}
@Override
public boolean shouldCancel() {
return isCanceled();
}
})
.withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress)))
.build();
} catch (IOException ioe) {
Log.w(TAG, "Couldn't open attachment", ioe);

View file

@ -468,15 +468,6 @@ public class MediaUtil {
return mediaMetadataRetriever.getFrameAtTime(timeUs);
}
public static boolean isInstantVideoSupported(Slide slide) {
if (!FeatureFlags.instantVideoPlayback()) {
return false;
}
final boolean isIncremental = slide.asAttachment().getIncrementalDigest() != null;
final boolean contentTypeSupported = isVideoType(slide.getContentType());
return isIncremental && contentTypeSupported;
}
public static @Nullable String getDiscreteMimeType(@NonNull String mimeType) {
final String[] sections = mimeType.split("/", 2);
return sections.length > 1 ? sections[0] : null;

View file

@ -4,6 +4,6 @@
android:shape="rectangle">
<corners android:radius="16dp" />
<solid android:color="@color/signal_colorTransparentInverse4" />
<solid android:color="@color/core_grey_02" />
</shape>

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="24dp" />
<solid android:color="@color/signal_colorTransparentInverse4" />
</shape>

View file

@ -1,8 +0,0 @@
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="1dp" />
<solid android:color="@color/signal_light_colorOnCustom" />
<size android:width="6dp" android:height="6dp" />
</shape>

View file

@ -15,8 +15,8 @@
<ViewStub
android:id="@+id/album_transfer_controls_stub"
android:layout_width="@dimen/album_total_width"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout="@layout/transfer_controls_stub" />

View file

@ -71,10 +71,9 @@
<ViewStub
android:id="@+id/transfer_controls_stub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@color/transparent"
android:layout="@layout/transfer_controls_stub" />
</merge>

View file

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.transfercontrols.TransferControlView
<org.thoughtcrime.securesms.components.TransferControlView
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/transfer_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/transparent"
android:elevation="2dp"
android:orientation="vertical" />

View file

@ -1,135 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
tools:viewBindingIgnore="true"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:parentTag="org.thoughtcrime.securesms.components.transfercontrols.TransferControlView"
tools:viewBindingIgnore="true">
tools:parentTag="org.thoughtcrime.securesms.components.TransferControlView">
<View
android:id="@+id/secondary_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/transfer_controls_background"
android:longClickable="false"
app:layout_constraintBottom_toBottomOf="@+id/download_details_text"
app:layout_constraintEnd_toEndOf="@+id/download_details_text"
app:layout_constraintStart_toStartOf="@+id/secondary_progress_view"
app:layout_constraintTop_toTopOf="@+id/download_details_text" />
<View
android:id="@+id/primary_background"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:background="@drawable/transfer_controls_play_background"
android:longClickable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/primary_details_text"
app:layout_constraintStart_toStartOf="@+id/primary_progress_view"
app:layout_constraintTop_toTopOf="parent" />
<org.thoughtcrime.securesms.components.transfercontrols.TransferProgressView
android:id="@+id/secondary_progress_view"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:paddingStart="4dp"
android:paddingEnd="-4dp"
app:layout_constraintStart_toStartOf="@+id/vertical_guideline"
app:layout_constraintTop_toTopOf="@+id/horizontal_guideline" />
<TextView
android:id="@+id/download_details_text"
style="@style/Signal.Text.Preview"
android:layout_width="0dp"
<com.pnikosis.materialishprogress.ProgressWheel
android:id="@+id/progress_wheel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/transfer_control_view_progressbar_to_textview_margin"
android:layout_marginEnd="4dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:longClickable="false"
android:paddingStart="4dp"
android:paddingTop="2dp"
android:paddingEnd="8dp"
android:paddingBottom="2dp"
android:textColor="@color/signal_colorOnCustom"
app:layout_constrainedWidth="true"
app:layout_constraintStart_toEndOf="@+id/secondary_view_space"
app:layout_constraintTop_toTopOf="@+id/secondary_progress_view" />
<Space
android:id="@+id/secondary_view_space"
android:layout_width="@dimen/transfer_control_view_progressbar_to_textview_margin"
android:layout_height="0dp"
app:layout_constraintStart_toEndOf="@+id/secondary_progress_view"
/>
<org.thoughtcrime.securesms.components.transfercontrols.TransferProgressView
android:id="@+id/primary_progress_view"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:visibility="gone"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@+id/primary_details_text"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:matProg_barColor="@color/core_ultramarine"
app:matProg_rimColor="@color/core_grey_05"
app:matProg_linearProgress="true"
app:matProg_spinSpeed="0.2"
app:matProg_barWidth="2dp"
app:matProg_rimWidth="2dp"
app:matProg_circleRadius="24dp"
tools:visibility="visible"/>
<TextView
android:id="@+id/primary_details_text"
style="@style/Signal.Text.Body"
<LinearLayout
android:id="@+id/download_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="4dp"
android:gravity="center"
android:longClickable="false"
android:paddingStart="0dp"
android:paddingTop="4dp"
android:paddingEnd="16dp"
android:paddingBottom="4dp"
android:textColor="@color/signal_colorOnCustom"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="@+id/primary_progress_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/primary_progress_view"
app:layout_constraintTop_toTopOf="@+id/primary_progress_view" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/play_video_button"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="12dp"
android:contentDescription="@string/ThumbnailView_Play_video_description"
android:paddingLeft="5dp"
android:scaleType="fitXY"
android:tint="@color/signal_colorOnCustom"
app:layout_constraintBottom_toBottomOf="@+id/primary_progress_view"
app:layout_constraintEnd_toEndOf="@+id/primary_progress_view"
app:layout_constraintStart_toStartOf="@+id/primary_progress_view"
app:layout_constraintTop_toTopOf="@+id/primary_progress_view"
app:srcCompat="@drawable/triangle_right"
tools:ignore="RtlHardcoded,RtlSymmetry"
tools:visibility="gone" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/vertical_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="8dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/horizontal_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="8dp" />
android:visibility="gone"
tools:visibility="visible">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:tint="@color/core_grey_60"
app:srcCompat="@drawable/ic_arrow_down_circle_filled" />
<TextView
android:id="@+id/download_details_text"
style="@style/Signal.Text.Preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:longClickable="false"
android:textColor="@color/black" />
</LinearLayout>
</merge>

View file

@ -153,7 +153,6 @@
<dimen name="transfer_top_padding">64dp</dimen>
<dimen name="transfer_split_top_padding">32dp</dimen>
<dimen name="transfer_item_spacing">24dp</dimen>
<dimen name="transfer_control_view_progressbar_to_textview_margin">4dp</dimen>
<dimen name="create_payment_key_width">80dp</dimen>
<dimen name="create_payment_key_height">70dp</dimen>

View file

@ -176,6 +176,5 @@ public abstract class SignalServiceAttachment {
* @param progress The amount that has been transmitted/received in bytes thus far
*/
public void onAttachmentProgress(long total, long progress);
boolean shouldCancel();
}
}

View file

@ -1445,7 +1445,7 @@ public class PushServiceSocket {
}
}
private void downloadFromCdn(File destination, int cdnNumber, String path, long maxSizeBytes, ProgressListener listener)
private void downloadFromCdn(File destination, int cdnNumber, String path, long maxSizeBytes, ProgressListener listener)
throws IOException, MissingConfigurationException
{
try (FileOutputStream outputStream = new FileOutputStream(destination, true)) {
@ -1502,9 +1502,6 @@ public class PushServiceSocket {
if (listener != null) {
listener.onAttachmentProgress(body.contentLength() + offset, totalRead);
if (listener.shouldCancel()) {
call.cancel();
}
}
}
} else if (response.code() == 416) {

View file

@ -3,7 +3,6 @@ package org.whispersystems.signalservice.internal.push.http;
import org.junit.Test;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayInputStream;
@ -71,15 +70,6 @@ public class DigestingRequestBodyTest {
}
private DigestingRequestBody getBody(long contentStart) {
return new DigestingRequestBody(new ByteArrayInputStream(input), outputStreamFactory, "application/octet", CONTENT_LENGTH, new SignalServiceAttachment.ProgressListener() {
@Override
public void onAttachmentProgress(long total, long progress) {
// no-op
}
@Override public boolean shouldCancel() {
return false;
}
}, () -> false, contentStart);
return new DigestingRequestBody(new ByteArrayInputStream(input), outputStreamFactory, "application/octet", CONTENT_LENGTH, (a, b) -> {}, () -> false, contentStart);
}
}