mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-28 05:35:44 +00:00
New attachment download UI.
This commit is contained in:
committed by
Nicholas Tinsley
parent
1f41b9e481
commit
82956c4149
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.attachments;
|
||||
|
||||
import android.net.Uri;
|
||||
@@ -244,7 +249,11 @@ public abstract class Attachment implements Parcelable {
|
||||
|
||||
@Nullable
|
||||
public byte[] getIncrementalDigest() {
|
||||
return incrementalDigest;
|
||||
if (incrementalDigest != null && incrementalDigest.length > 0) {
|
||||
return incrementalDigest;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
@@ -12,10 +17,12 @@ 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;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
|
||||
import java.util.List;
|
||||
@@ -24,13 +31,15 @@ public class AlbumThumbnailView extends FrameLayout {
|
||||
|
||||
private @Nullable SlideClickListener thumbnailClickListener;
|
||||
private @Nullable SlidesClickedListener downloadClickListener;
|
||||
private @Nullable SlidesClickedListener cancelDownloadClickListener;
|
||||
private @Nullable SlideClickListener playVideoClickListener;
|
||||
|
||||
private int currentSizeClass;
|
||||
|
||||
private final int[] corners = new int[4];
|
||||
|
||||
private ViewGroup albumCellContainer;
|
||||
private Stub<TransferControlView> transferControls;
|
||||
private final ViewGroup albumCellContainer;
|
||||
private final Stub<TransferControlView> transferControlsStub;
|
||||
|
||||
private final SlideClickListener defaultThumbnailClickListener = (v, slide) -> {
|
||||
if (thumbnailClickListener != null) {
|
||||
@@ -42,19 +51,18 @@ public class AlbumThumbnailView extends FrameLayout {
|
||||
|
||||
public AlbumThumbnailView(@NonNull Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
inflate(getContext(), R.layout.album_thumbnail_view, this);
|
||||
|
||||
albumCellContainer = findViewById(R.id.album_cell_container);
|
||||
transferControlsStub = new Stub<>(findViewById(R.id.album_transfer_controls_stub));
|
||||
}
|
||||
|
||||
public AlbumThumbnailView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
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));
|
||||
albumCellContainer = findViewById(R.id.album_cell_container);
|
||||
transferControlsStub = new Stub<>(findViewById(R.id.album_transfer_controls_stub));
|
||||
}
|
||||
|
||||
public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides, boolean showControls) {
|
||||
@@ -63,16 +71,17 @@ 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);
|
||||
}
|
||||
});
|
||||
transferControlsStub.get().setShowSecondaryText(true);
|
||||
transferControlsStub.get().setDownloadClickListener(
|
||||
v -> {
|
||||
if (downloadClickListener != null) {
|
||||
downloadClickListener.onClick(v, slides);
|
||||
}
|
||||
});
|
||||
transferControlsStub.get().setSlides(slides);
|
||||
} else {
|
||||
if (transferControls.resolved()) {
|
||||
transferControls.get().setVisibility(GONE);
|
||||
if (transferControlsStub.resolved()) {
|
||||
transferControlsStub.get().setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +94,7 @@ public class AlbumThumbnailView extends FrameLayout {
|
||||
|
||||
showSlides(glideRequests, slides);
|
||||
applyCorners();
|
||||
forceLayout();
|
||||
}
|
||||
|
||||
public void setCellBackgroundColor(@ColorInt int color) {
|
||||
@@ -101,10 +111,19 @@ public class AlbumThumbnailView extends FrameLayout {
|
||||
thumbnailClickListener = listener;
|
||||
}
|
||||
|
||||
public void setDownloadClickListener(@Nullable SlidesClickedListener listener) {
|
||||
downloadClickListener = listener;
|
||||
public void setDownloadClickListener(SlidesClickedListener listener) {
|
||||
this.downloadClickListener = listener;
|
||||
}
|
||||
|
||||
public void setCancelDownloadClickListener(SlidesClickedListener listener) {
|
||||
this.cancelDownloadClickListener = listener;
|
||||
}
|
||||
|
||||
public void setPlayVideoClickListener(SlideClickListener listener) {
|
||||
this.playVideoClickListener = listener;
|
||||
}
|
||||
|
||||
|
||||
public void setRadii(int topLeft, int topRight, int bottomRight, int bottomLeft) {
|
||||
corners[0] = topLeft;
|
||||
corners[1] = topRight;
|
||||
@@ -117,23 +136,46 @@ public class AlbumThumbnailView extends FrameLayout {
|
||||
private void inflateLayout(int sizeClass) {
|
||||
albumCellContainer.removeAllViews();
|
||||
|
||||
int resId;
|
||||
switch (sizeClass) {
|
||||
case 2:
|
||||
inflate(getContext(), R.layout.album_thumbnail_2, albumCellContainer);
|
||||
resId = R.layout.album_thumbnail_2;
|
||||
break;
|
||||
case 3:
|
||||
inflate(getContext(), R.layout.album_thumbnail_3, albumCellContainer);
|
||||
resId = R.layout.album_thumbnail_3;
|
||||
break;
|
||||
case 4:
|
||||
inflate(getContext(), R.layout.album_thumbnail_4, albumCellContainer);
|
||||
resId = R.layout.album_thumbnail_4;
|
||||
break;
|
||||
case 5:
|
||||
inflate(getContext(), R.layout.album_thumbnail_5, albumCellContainer);
|
||||
resId = R.layout.album_thumbnail_5;
|
||||
break;
|
||||
default:
|
||||
inflate(getContext(), R.layout.album_thumbnail_many, albumCellContainer);
|
||||
resId = R.layout.album_thumbnail_many;
|
||||
break;
|
||||
}
|
||||
|
||||
inflate(getContext(), resId, albumCellContainer);
|
||||
if (transferControlsStub.resolved()) {
|
||||
int size;
|
||||
switch (sizeClass) {
|
||||
case 2:
|
||||
size = R.dimen.album_2_total_height;
|
||||
break;
|
||||
case 3:
|
||||
size = R.dimen.album_3_total_height;
|
||||
break;
|
||||
case 4:
|
||||
size = R.dimen.album_4_total_height;
|
||||
break;
|
||||
default:
|
||||
size = R.dimen.album_5_total_height;
|
||||
break;
|
||||
}
|
||||
ViewGroup.LayoutParams params = transferControlsStub.get().getLayoutParams();
|
||||
params.height = getContext().getResources().getDimensionPixelSize(size);
|
||||
transferControlsStub.get().setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyCorners() {
|
||||
@@ -214,19 +256,20 @@ public class AlbumThumbnailView extends FrameLayout {
|
||||
}
|
||||
|
||||
private void showSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides) {
|
||||
setSlide(glideRequests, slides.get(0), R.id.album_cell_1);
|
||||
setSlide(glideRequests, slides.get(1), R.id.album_cell_2);
|
||||
boolean showControls = TransferControlView.containsPlayableSlides(slides);
|
||||
setSlide(glideRequests, slides.get(0), R.id.album_cell_1, showControls);
|
||||
setSlide(glideRequests, slides.get(1), R.id.album_cell_2, showControls);
|
||||
|
||||
if (slides.size() >= 3) {
|
||||
setSlide(glideRequests, slides.get(2), R.id.album_cell_3);
|
||||
setSlide(glideRequests, slides.get(2), R.id.album_cell_3, showControls);
|
||||
}
|
||||
|
||||
if (slides.size() >= 4) {
|
||||
setSlide(glideRequests, slides.get(3), R.id.album_cell_4);
|
||||
setSlide(glideRequests, slides.get(3), R.id.album_cell_4, showControls);
|
||||
}
|
||||
|
||||
if (slides.size() >= 5) {
|
||||
setSlide(glideRequests, slides.get(4), R.id.album_cell_5);
|
||||
setSlide(glideRequests, slides.get(4), R.id.album_cell_5, showControls && slides.size() == 5);
|
||||
}
|
||||
|
||||
if (slides.size() > 5) {
|
||||
@@ -235,11 +278,17 @@ public class AlbumThumbnailView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide, @IdRes int id) {
|
||||
private void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide, @IdRes int id, boolean showControls) {
|
||||
ThumbnailView cell = findViewById(id);
|
||||
cell.setImageResource(glideRequests, slide, false, false);
|
||||
cell.showSecondaryText(false);
|
||||
cell.setThumbnailClickListener(defaultThumbnailClickListener);
|
||||
cell.setDownloadClickListener(downloadClickListener);
|
||||
cell.setCancelDownloadClickListener(cancelDownloadClickListener);
|
||||
if (MediaUtil.isInstantVideoSupported(slide)) {
|
||||
cell.setPlayVideoClickListener(playVideoClickListener);
|
||||
}
|
||||
cell.setOnLongClickListener(defaultLongClickListener);
|
||||
cell.setImageResource(glideRequests, slide, showControls, false);
|
||||
}
|
||||
|
||||
private int sizeClass(int size) {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
@@ -86,7 +91,7 @@ class ConversationItemThumbnail @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable? {
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
val root = super.onSaveInstanceState()
|
||||
return bundleOf(
|
||||
STATE_ROOT to root,
|
||||
@@ -255,9 +260,19 @@ class ConversationItemThumbnail @JvmOverloads constructor(
|
||||
state.applyState(thumbnail, album)
|
||||
}
|
||||
|
||||
fun setProgressWheelClickListener(listener: SlideClickListener?) {
|
||||
fun setPlayVideoClickListener(listener: SlideClickListener?) {
|
||||
state = state.copy(
|
||||
thumbnailViewState = state.thumbnailViewState.copy(progressWheelClickListener = listener)
|
||||
thumbnailViewState = state.thumbnailViewState.copy(playVideoClickListener = listener),
|
||||
albumViewState = state.albumViewState.copy(playVideoClickListener = listener)
|
||||
)
|
||||
|
||||
state.applyState(thumbnail, album)
|
||||
}
|
||||
|
||||
fun setCancelDownloadClickListener(listener: SlidesClickedListener?) {
|
||||
state = state.copy(
|
||||
thumbnailViewState = state.thumbnailViewState.copy(cancelDownloadClickListener = listener),
|
||||
albumViewState = state.albumViewState.copy(cancelDownloadClickListener = listener)
|
||||
)
|
||||
|
||||
state.applyState(thumbnail, album)
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.graphics.Color
|
||||
@@ -31,7 +36,9 @@ data class ConversationItemThumbnailState(
|
||||
@IgnoredOnParcel
|
||||
private val downloadClickListener: SlidesClickedListener? = null,
|
||||
@IgnoredOnParcel
|
||||
private val progressWheelClickListener: SlideClickListener? = null,
|
||||
private val cancelDownloadClickListener: SlidesClickedListener? = null,
|
||||
@IgnoredOnParcel
|
||||
private val playVideoClickListener: SlideClickListener? = null,
|
||||
@IgnoredOnParcel
|
||||
private val longClickListener: OnLongClickListener? = null,
|
||||
private val visibility: Int = View.GONE,
|
||||
@@ -57,7 +64,8 @@ data class ConversationItemThumbnailState(
|
||||
thumbnailView.get().setRadii(cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft)
|
||||
thumbnailView.get().setThumbnailClickListener(clickListener)
|
||||
thumbnailView.get().setDownloadClickListener(downloadClickListener)
|
||||
thumbnailView.get().setProgressWheelClickListener(progressWheelClickListener)
|
||||
thumbnailView.get().setCancelDownloadClickListener(cancelDownloadClickListener)
|
||||
thumbnailView.get().setPlayVideoClickListener(playVideoClickListener)
|
||||
thumbnailView.get().setOnLongClickListener(longClickListener)
|
||||
thumbnailView.get().setBounds(minWidth, maxWidth, minHeight, maxHeight)
|
||||
}
|
||||
@@ -72,6 +80,10 @@ data class ConversationItemThumbnailState(
|
||||
@IgnoredOnParcel
|
||||
private val downloadClickListener: SlidesClickedListener? = null,
|
||||
@IgnoredOnParcel
|
||||
private val cancelDownloadClickListener: SlidesClickedListener? = null,
|
||||
@IgnoredOnParcel
|
||||
private val playVideoClickListener: SlideClickListener? = null,
|
||||
@IgnoredOnParcel
|
||||
private val longClickListener: OnLongClickListener? = null,
|
||||
private val visibility: Int = View.GONE,
|
||||
private val cellBackgroundColor: Int = Color.TRANSPARENT,
|
||||
@@ -92,6 +104,8 @@ data class ConversationItemThumbnailState(
|
||||
albumView.get().setRadii(cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft)
|
||||
albumView.get().setThumbnailClickListener(clickListener)
|
||||
albumView.get().setDownloadClickListener(downloadClickListener)
|
||||
albumView.get().setCancelDownloadClickListener(cancelDownloadClickListener)
|
||||
albumView.get().setPlayVideoClickListener(playVideoClickListener)
|
||||
albumView.get().setOnLongClickListener(longClickListener)
|
||||
albumView.get().setCellBackgroundColor(cellBackgroundColor)
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
thumbnailState.applyState(thumbnail);
|
||||
thumbnail.get().setImageResource(glideRequests, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION && !scheduleMessageMode, false);
|
||||
thumbnail.get().showDownloadText(false);
|
||||
thumbnail.get().showSecondaryText(false);
|
||||
} else if (callLinkRootKey != null) {
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
thumbnailState.applyState(thumbnail);
|
||||
@@ -228,7 +228,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
.asDrawable(getContext(),
|
||||
AvatarColorHash.forCallLink(callLinkRootKey.getKeyBytes()))
|
||||
);
|
||||
thumbnail.get().showDownloadText(false);
|
||||
thumbnail.get().showSecondaryText(false);
|
||||
} else {
|
||||
thumbnail.setVisibility(GONE);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
@@ -31,6 +36,7 @@ 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;
|
||||
@@ -41,7 +47,6 @@ 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;
|
||||
@@ -50,6 +55,7 @@ import org.thoughtcrime.securesms.util.views.Stub;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@@ -80,12 +86,12 @@ public class ThumbnailView extends FrameLayout {
|
||||
|
||||
private final CornerMask cornerMask;
|
||||
|
||||
private ThumbnailViewTransferControlsState transferControlsState = new ThumbnailViewTransferControlsState();
|
||||
private Stub<TransferControlView> transferControlViewStub;
|
||||
private SlideClickListener thumbnailClickListener = null;
|
||||
private SlidesClickedListener downloadClickListener = null;
|
||||
private SlideClickListener progressWheelClickListener = null;
|
||||
private Slide slide = null;
|
||||
private final Stub<TransferControlView> transferControlViewStub;
|
||||
private SlideClickListener thumbnailClickListener = null;
|
||||
private SlidesClickedListener downloadClickListener = null;
|
||||
private SlidesClickedListener cancelDownloadClickListener = null;
|
||||
private SlideClickListener playVideoClickListener = null;
|
||||
private Slide slide = null;
|
||||
|
||||
|
||||
public ThumbnailView(Context context) {
|
||||
@@ -278,15 +284,13 @@ public class ThumbnailView extends FrameLayout {
|
||||
@Override
|
||||
public void setFocusable(boolean focusable) {
|
||||
super.setFocusable(focusable);
|
||||
transferControlsState = transferControlsState.withFocusable(focusable);
|
||||
transferControlsState.applyState(transferControlViewStub);
|
||||
transferControlViewStub.get().setFocusable(focusable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClickable(boolean clickable) {
|
||||
super.setClickable(clickable);
|
||||
transferControlsState = transferControlsState.withClickable(clickable);
|
||||
transferControlsState.applyState(transferControlViewStub);
|
||||
transferControlViewStub.get().setClickable(clickable);
|
||||
}
|
||||
|
||||
public @Nullable Drawable getImageDrawable() {
|
||||
@@ -359,24 +363,15 @@ public class ThumbnailView extends FrameLayout {
|
||||
}
|
||||
|
||||
if (showControls) {
|
||||
int transferState = TransferControlView.getTransferState(Collections.singletonList(slide));
|
||||
if (transferState == AttachmentTable.TRANSFER_PROGRESS_DONE || transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE) {
|
||||
transferControlViewStub.setVisibility(View.GONE);
|
||||
} else {
|
||||
transferControlViewStub.setVisibility(View.VISIBLE);
|
||||
transferControlViewStub.get().setDownloadClickListener(new DownloadClickDispatcher());
|
||||
transferControlViewStub.get().setCancelClickListener(new CancelClickDispatcher());
|
||||
if (MediaUtil.isInstantVideoSupported(slide)) {
|
||||
transferControlViewStub.get().setInstantPlaybackClickListener(new InstantVideoClickDispatcher());
|
||||
}
|
||||
|
||||
transferControlsState = transferControlsState.withSlide(slide)
|
||||
.withDownloadClickListener(new DownloadClickDispatcher());
|
||||
|
||||
if (FeatureFlags.instantVideoPlayback()) {
|
||||
transferControlsState = transferControlsState.withProgressWheelClickListener(new ProgressWheelClickDispatcher());
|
||||
}
|
||||
|
||||
transferControlsState.applyState(transferControlViewStub);
|
||||
} else {
|
||||
transferControlViewStub.setVisibility(View.GONE);
|
||||
transferControlViewStub.get().setSlides(List.of(slide));
|
||||
}
|
||||
int transferState = TransferControlView.getTransferState(List.of(slide));
|
||||
transferControlViewStub.get().setVisible(showControls && transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE);
|
||||
|
||||
if (slide.getUri() != null && slide.hasPlayOverlay() &&
|
||||
(slide.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_DONE || isPreview))
|
||||
@@ -525,8 +520,41 @@ public class ThumbnailView extends FrameLayout {
|
||||
this.downloadClickListener = listener;
|
||||
}
|
||||
|
||||
public void setProgressWheelClickListener(SlideClickListener listener) {
|
||||
this.progressWheelClickListener = listener;
|
||||
public void setCancelDownloadClickListener(SlidesClickedListener listener) {
|
||||
this.cancelDownloadClickListener = listener;
|
||||
}
|
||||
|
||||
public void setPlayVideoClickListener(SlideClickListener listener) {
|
||||
this.playVideoClickListener = listener;
|
||||
}
|
||||
|
||||
private static boolean hasSameContents(@Nullable Slide slide, @Nullable Slide other) {
|
||||
if (Util.equals(slide, other)) {
|
||||
|
||||
if (slide != null && other != null) {
|
||||
byte[] digestLeft = slide.asAttachment().getDigest();
|
||||
byte[] digestRight = other.asAttachment().getDigest();
|
||||
|
||||
return Arrays.equals(digestLeft, digestRight);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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()));
|
||||
|
||||
boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23;
|
||||
|
||||
if (slide.isInProgress() || doNotShowMissingThumbnailImage) {
|
||||
return request;
|
||||
} else {
|
||||
return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture));
|
||||
}
|
||||
}
|
||||
|
||||
public void clear(GlideRequests glideRequests) {
|
||||
@@ -543,13 +571,12 @@ public class ThumbnailView extends FrameLayout {
|
||||
slide = null;
|
||||
}
|
||||
|
||||
public void showDownloadText(boolean showDownloadText) {
|
||||
transferControlsState = transferControlsState.withDownloadText(showDownloadText);
|
||||
transferControlsState.applyState(transferControlViewStub);
|
||||
public void showSecondaryText(boolean showSecondaryText) {
|
||||
transferControlViewStub.get().setShowSecondaryText(showSecondaryText);
|
||||
}
|
||||
|
||||
public void showProgressSpinner() {
|
||||
transferControlViewStub.get().showProgressSpinner();
|
||||
transferControlViewStub.get().setVisible(true);
|
||||
}
|
||||
|
||||
public void setScaleType(@NonNull ImageView.ScaleType scaleType) {
|
||||
@@ -566,20 +593,6 @@ public class ThumbnailView extends FrameLayout {
|
||||
invalidate();
|
||||
}
|
||||
|
||||
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()));
|
||||
|
||||
boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23;
|
||||
|
||||
if (slide.isInProgress() || doNotShowMissingThumbnailImage) {
|
||||
return request;
|
||||
} else {
|
||||
return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture));
|
||||
}
|
||||
}
|
||||
|
||||
private RequestBuilder<Bitmap> buildPlaceholderGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
|
||||
GlideRequest<Bitmap> bitmap = glideRequests.asBitmap();
|
||||
@@ -621,19 +634,6 @@ public class ThumbnailView extends FrameLayout {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static boolean hasSameContents(@Nullable Slide slide, @Nullable Slide other) {
|
||||
if (Util.equals(slide, other)) {
|
||||
|
||||
if (slide != null && other != null) {
|
||||
byte[] digestLeft = slide.asAttachment().getDigest();
|
||||
byte[] digestRight = other.asAttachment().getDigest();
|
||||
|
||||
return Arrays.equals(digestLeft, digestRight);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public interface ThumbnailRequestListener extends RequestListener<Drawable> {
|
||||
void onLoadCanceled();
|
||||
@@ -670,14 +670,26 @@ public class ThumbnailView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private class ProgressWheelClickDispatcher implements View.OnClickListener {
|
||||
private class CancelClickDispatcher implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Log.i(TAG, "onClick() for progress wheel");
|
||||
if (progressWheelClickListener != null && slide != null) {
|
||||
progressWheelClickListener.onClick(view, slide);
|
||||
Log.i(TAG, "onClick() for cancel button");
|
||||
if (cancelDownloadClickListener != null && slide != null) {
|
||||
cancelDownloadClickListener.onClick(view, Collections.singletonList(slide));
|
||||
} else {
|
||||
Log.w(TAG, "Received a progress wheel click, but unable to execute it. slide: " + slide + " progressWheelClickListener: " + progressWheelClickListener);
|
||||
Log.w(TAG, "Received a cancel button click, but unable to execute it. slide: " + slide + " cancelDownloadClickListener: " + cancelDownloadClickListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class InstantVideoClickDispatcher 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);
|
||||
} else {
|
||||
Log.w(TAG, "Received an instant video click, but unable to execute it. slide: " + slide + " playVideoClickListener: " + playVideoClickListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.view.View.OnClickListener
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
import org.thoughtcrime.securesms.util.views.Stub
|
||||
|
||||
/**
|
||||
* State object for transfer controls.
|
||||
*/
|
||||
data class ThumbnailViewTransferControlsState(
|
||||
val isFocusable: Boolean = true,
|
||||
val isClickable: Boolean = true,
|
||||
val slide: Slide? = null,
|
||||
val downloadClickedListener: OnClickListener? = null,
|
||||
val progressWheelClickedListener: OnClickListener? = null,
|
||||
val showDownloadText: Boolean = true
|
||||
) {
|
||||
|
||||
fun withFocusable(isFocusable: Boolean): ThumbnailViewTransferControlsState = copy(isFocusable = isFocusable)
|
||||
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 withProgressWheelClickListener(progressWheelClickedListener: OnClickListener): ThumbnailViewTransferControlsState = copy(progressWheelClickedListener = progressWheelClickedListener)
|
||||
fun withDownloadText(showDownloadText: Boolean): ThumbnailViewTransferControlsState = copy(showDownloadText = showDownloadText)
|
||||
|
||||
fun applyState(transferControlView: Stub<TransferControlView>) {
|
||||
if (transferControlView.resolved()) {
|
||||
transferControlView.get().isFocusable = isFocusable
|
||||
transferControlView.get().isClickable = isClickable
|
||||
if (slide != null) {
|
||||
transferControlView.get().setSlide(slide)
|
||||
}
|
||||
transferControlView.get().setDownloadClickListener(downloadClickedListener)
|
||||
transferControlView.get().setProgressWheelClickListener(progressWheelClickedListener)
|
||||
transferControlView.get().setShowDownloadText(showDownloadText)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,629 @@
|
||||
/*
|
||||
* 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.os.Build
|
||||
import android.text.format.Formatter
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.children
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.databinding.TransferControlsViewBinding
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
class TransferControlView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||
private val binding: TransferControlsViewBinding
|
||||
private val store = RxStore(TransferControlViewState())
|
||||
private val disposables = CompositeDisposable().apply {
|
||||
add(store)
|
||||
}
|
||||
|
||||
private var previousState = TransferControlViewState()
|
||||
|
||||
init {
|
||||
binding = TransferControlsViewBinding.inflate(LayoutInflater.from(context), this)
|
||||
visibility = GONE
|
||||
isLongClickable = false
|
||||
layoutTransition = LayoutTransition()
|
||||
disposables += store.stateFlowable.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe {
|
||||
applyState(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
EventBus.getDefault().unregister(this)
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
private fun applyState(currentState: TransferControlViewState) {
|
||||
when (deriveMode(currentState)) {
|
||||
Mode.PENDING_GALLERY -> displayPendingGallery(currentState)
|
||||
Mode.PENDING_GALLERY_CONTAINS_PLAYABLE -> displayPendingGalleryWithPlayable(currentState)
|
||||
Mode.PENDING_SINGLE_ITEM -> displayPendingSingleItem(currentState)
|
||||
Mode.PENDING_VIDEO_PLAYABLE -> displayPendingPlayableVideo(currentState)
|
||||
Mode.DOWNLOADING_GALLERY -> displayDownloadingGallery(currentState)
|
||||
Mode.DOWNLOADING_SINGLE_ITEM -> displayDownloadingSingleItem(currentState)
|
||||
Mode.DOWNLOADING_VIDEO_PLAYABLE -> displayDownloadingPlayableVideo(currentState)
|
||||
Mode.UPLOADING_GALLERY -> displayUploadingGallery(currentState)
|
||||
Mode.UPLOADING_SINGLE_ITEM -> displayUploadingSingleItem(currentState)
|
||||
Mode.RETRY_DOWNLOADING -> displayRetry(currentState, false)
|
||||
Mode.RETRY_UPLOADING -> displayRetry(currentState, true)
|
||||
Mode.GONE -> displayChildrenAsGone(currentState, previousState)
|
||||
}
|
||||
|
||||
previousState = currentState
|
||||
}
|
||||
|
||||
private fun deriveMode(currentState: TransferControlViewState): Mode {
|
||||
if (currentState.slides.isEmpty()) {
|
||||
return Mode.GONE
|
||||
}
|
||||
|
||||
if (currentState.slides.all { it.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE }) {
|
||||
return Mode.GONE
|
||||
}
|
||||
|
||||
if (currentState.isVisible) {
|
||||
if (currentState.slides.size == 1) {
|
||||
val slide = currentState.slides.first()
|
||||
if (slide.hasVideo()) {
|
||||
if (currentState.isOutgoing) {
|
||||
return if (slide.transferState == AttachmentTable.TRANSFER_PROGRESS_STARTED) {
|
||||
Mode.UPLOADING_SINGLE_ITEM
|
||||
} else {
|
||||
Mode.RETRY_UPLOADING
|
||||
}
|
||||
} else {
|
||||
return if (slide.transferState == AttachmentTable.TRANSFER_PROGRESS_STARTED) {
|
||||
if (currentState.playableWhileDownloading) {
|
||||
Mode.DOWNLOADING_VIDEO_PLAYABLE
|
||||
} else {
|
||||
Mode.DOWNLOADING_SINGLE_ITEM
|
||||
}
|
||||
} else if (slide.transferState == AttachmentTable.TRANSFER_PROGRESS_FAILED) {
|
||||
Mode.RETRY_DOWNLOADING
|
||||
} else {
|
||||
if (currentState.playableWhileDownloading) {
|
||||
Mode.PENDING_VIDEO_PLAYABLE
|
||||
} else {
|
||||
Mode.PENDING_SINGLE_ITEM
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return if (currentState.isOutgoing) {
|
||||
if (slide.transferState == AttachmentTable.TRANSFER_PROGRESS_STARTED) {
|
||||
Mode.UPLOADING_SINGLE_ITEM
|
||||
} else {
|
||||
Mode.RETRY_UPLOADING
|
||||
}
|
||||
} else {
|
||||
return when (slide.transferState) {
|
||||
AttachmentTable.TRANSFER_PROGRESS_STARTED -> {
|
||||
Mode.DOWNLOADING_SINGLE_ITEM
|
||||
}
|
||||
|
||||
AttachmentTable.TRANSFER_PROGRESS_FAILED -> {
|
||||
Mode.RETRY_DOWNLOADING
|
||||
}
|
||||
|
||||
else -> {
|
||||
Mode.PENDING_SINGLE_ITEM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when (getTransferState(currentState.slides)) {
|
||||
AttachmentTable.TRANSFER_PROGRESS_STARTED -> {
|
||||
return if (currentState.isOutgoing) {
|
||||
Mode.UPLOADING_GALLERY
|
||||
} else {
|
||||
Mode.DOWNLOADING_GALLERY
|
||||
}
|
||||
}
|
||||
|
||||
AttachmentTable.TRANSFER_PROGRESS_PENDING -> {
|
||||
return if (containsPlayableSlides(currentState.slides)) {
|
||||
Mode.PENDING_GALLERY_CONTAINS_PLAYABLE
|
||||
} else {
|
||||
Mode.PENDING_GALLERY
|
||||
}
|
||||
}
|
||||
|
||||
AttachmentTable.TRANSFER_PROGRESS_FAILED -> {
|
||||
return if (currentState.isOutgoing) {
|
||||
Mode.RETRY_UPLOADING
|
||||
} else {
|
||||
Mode.RETRY_DOWNLOADING
|
||||
}
|
||||
}
|
||||
|
||||
AttachmentTable.TRANSFER_PROGRESS_DONE -> {
|
||||
return Mode.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Mode.GONE
|
||||
}
|
||||
|
||||
Log.i(TAG, "Hit default mode case, this should not happen.")
|
||||
return Mode.GONE
|
||||
}
|
||||
|
||||
private fun displayPendingGallery(currentState: TransferControlViewState) {
|
||||
binding.primaryProgressView.startClickListener = currentState.downloadClickedListener
|
||||
applyFocusableAndClickable(currentState, listOf(binding.primaryProgressView, binding.primaryDetailsText), listOf(binding.secondaryProgressView, binding.playVideoButton))
|
||||
binding.primaryProgressView.setStopped(false)
|
||||
showAllViews(
|
||||
playVideoButton = false,
|
||||
secondaryProgressView = false,
|
||||
secondaryDetailsText = currentState.showSecondaryText
|
||||
)
|
||||
|
||||
val remainingSlides = currentState.slides.filterNot { it.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE }
|
||||
val downloadCount = remainingSlides.size
|
||||
binding.primaryDetailsText.text = context.resources.getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount)
|
||||
val byteCount = remainingSlides.sumOf { it.asAttachment().size }
|
||||
binding.secondaryDetailsText.text = Formatter.formatShortFileSize(context, byteCount)
|
||||
binding.secondaryDetailsText.invalidate()
|
||||
binding.secondaryDetailsText.forceLayout()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
private fun displayPendingGalleryWithPlayable(currentState: TransferControlViewState) {
|
||||
binding.secondaryProgressView.startClickListener = currentState.downloadClickedListener
|
||||
super.setClickable(false)
|
||||
binding.secondaryProgressView.isClickable = currentState.showSecondaryText
|
||||
binding.secondaryProgressView.isFocusable = currentState.showSecondaryText
|
||||
binding.primaryProgressView.isClickable = false
|
||||
binding.primaryProgressView.isFocusable = false
|
||||
showAllViews(
|
||||
playVideoButton = false,
|
||||
primaryProgressView = false,
|
||||
primaryDetailsText = false,
|
||||
secondaryProgressView = currentState.showSecondaryText,
|
||||
secondaryDetailsText = currentState.showSecondaryText
|
||||
)
|
||||
|
||||
binding.secondaryProgressView.setStopped(false)
|
||||
|
||||
val byteCount = currentState.slides.sumOf { it.asAttachment().size }
|
||||
binding.secondaryDetailsText.text = Formatter.formatShortFileSize(context, byteCount)
|
||||
binding.secondaryDetailsText.invalidate()
|
||||
binding.secondaryDetailsText.forceLayout()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
private fun displayPendingSingleItem(currentState: TransferControlViewState) {
|
||||
binding.primaryProgressView.startClickListener = currentState.downloadClickedListener
|
||||
applyFocusableAndClickable(currentState, listOf(binding.primaryProgressView), listOf(binding.secondaryProgressView, binding.playVideoButton))
|
||||
binding.primaryProgressView.setStopped(false)
|
||||
showAllViews(
|
||||
playVideoButton = false,
|
||||
primaryDetailsText = false,
|
||||
secondaryProgressView = false,
|
||||
secondaryDetailsText = currentState.showSecondaryText
|
||||
)
|
||||
val byteCount = currentState.slides.sumOf { it.asAttachment().size }
|
||||
binding.secondaryDetailsText.text = Formatter.formatShortFileSize(context, byteCount)
|
||||
binding.secondaryDetailsText.invalidate()
|
||||
binding.secondaryDetailsText.forceLayout()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
private fun displayPendingPlayableVideo(currentState: TransferControlViewState) {
|
||||
binding.secondaryProgressView.startClickListener = currentState.downloadClickedListener
|
||||
binding.playVideoButton.setOnClickListener(currentState.instantPlaybackClickListener)
|
||||
applyFocusableAndClickable(currentState, listOf(binding.secondaryProgressView, binding.playVideoButton), listOf(binding.primaryProgressView))
|
||||
binding.secondaryProgressView.setStopped(false)
|
||||
showAllViews(
|
||||
primaryProgressView = false,
|
||||
primaryDetailsText = false,
|
||||
secondaryDetailsText = currentState.showSecondaryText,
|
||||
secondaryProgressView = currentState.showSecondaryText
|
||||
)
|
||||
val byteCount = currentState.slides.sumOf { it.asAttachment().size }
|
||||
binding.secondaryDetailsText.text = Formatter.formatShortFileSize(context, byteCount)
|
||||
binding.secondaryDetailsText.invalidate()
|
||||
binding.secondaryDetailsText.forceLayout()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
private fun displayDownloadingGallery(currentState: TransferControlViewState) {
|
||||
applyFocusableAndClickable(currentState, listOf(binding.secondaryProgressView), listOf(binding.primaryProgressView, binding.playVideoButton))
|
||||
showAllViews(
|
||||
playVideoButton = false,
|
||||
primaryProgressView = false,
|
||||
primaryDetailsText = false,
|
||||
secondaryDetailsText = currentState.showSecondaryText
|
||||
)
|
||||
|
||||
val progress = calculateProgress(currentState)
|
||||
if (progress == 0f) {
|
||||
binding.secondaryProgressView.setUploading(progress)
|
||||
} else {
|
||||
binding.secondaryProgressView.cancelClickListener = currentState.cancelDownloadClickedListener
|
||||
binding.secondaryProgressView.setDownloading(progress)
|
||||
}
|
||||
|
||||
binding.secondaryDetailsText.text = deriveSecondaryDetailsText(currentState)
|
||||
binding.secondaryDetailsText.invalidate()
|
||||
binding.secondaryDetailsText.forceLayout()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
private fun displayDownloadingSingleItem(currentState: TransferControlViewState) {
|
||||
binding.primaryProgressView.cancelClickListener = currentState.cancelDownloadClickedListener
|
||||
applyFocusableAndClickable(currentState, listOf(binding.primaryProgressView), listOf(binding.secondaryProgressView, binding.playVideoButton))
|
||||
showAllViews(
|
||||
playVideoButton = false,
|
||||
primaryDetailsText = false,
|
||||
secondaryProgressView = false,
|
||||
secondaryDetailsText = currentState.showSecondaryText
|
||||
)
|
||||
|
||||
val progress = calculateProgress(currentState)
|
||||
if (progress == 0f) {
|
||||
binding.primaryProgressView.setUploading(progress)
|
||||
} else {
|
||||
binding.primaryProgressView.setDownloading(progress)
|
||||
}
|
||||
|
||||
binding.secondaryDetailsText.text = deriveSecondaryDetailsText(currentState)
|
||||
binding.secondaryDetailsText.invalidate()
|
||||
binding.secondaryDetailsText.forceLayout()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
private fun displayDownloadingPlayableVideo(currentState: TransferControlViewState) {
|
||||
binding.secondaryProgressView.cancelClickListener = currentState.cancelDownloadClickedListener
|
||||
applyFocusableAndClickable(currentState, listOf(binding.secondaryProgressView, binding.playVideoButton), listOf(binding.primaryProgressView))
|
||||
showAllViews(
|
||||
primaryDetailsText = false,
|
||||
secondaryProgressView = currentState.showSecondaryText,
|
||||
secondaryDetailsText = currentState.showSecondaryText
|
||||
)
|
||||
|
||||
binding.playVideoButton.setOnClickListener(currentState.instantPlaybackClickListener)
|
||||
|
||||
val progress = calculateProgress(currentState)
|
||||
if (progress == 0f) {
|
||||
binding.secondaryProgressView.setUploading(progress)
|
||||
} else {
|
||||
binding.secondaryProgressView.setDownloading(progress)
|
||||
}
|
||||
binding.secondaryDetailsText.text = deriveSecondaryDetailsText(currentState)
|
||||
binding.secondaryDetailsText.invalidate()
|
||||
binding.secondaryDetailsText.forceLayout()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
private fun displayUploadingSingleItem(currentState: TransferControlViewState) {
|
||||
binding.secondaryProgressView.startClickListener = currentState.downloadClickedListener
|
||||
applyFocusableAndClickable(currentState, listOf(binding.secondaryProgressView), listOf(binding.primaryProgressView, binding.playVideoButton))
|
||||
showAllViews(
|
||||
playVideoButton = false,
|
||||
primaryProgressView = false,
|
||||
primaryDetailsText = false,
|
||||
secondaryDetailsText = currentState.showSecondaryText
|
||||
)
|
||||
|
||||
val progress = calculateProgress(currentState)
|
||||
binding.secondaryProgressView.setUploading(progress)
|
||||
|
||||
binding.secondaryDetailsText.text = deriveSecondaryDetailsText(currentState)
|
||||
binding.secondaryDetailsText.invalidate()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
private fun displayUploadingGallery(currentState: TransferControlViewState) {
|
||||
binding.secondaryProgressView.startClickListener = currentState.downloadClickedListener
|
||||
applyFocusableAndClickable(currentState, listOf(binding.secondaryProgressView), listOf(binding.primaryProgressView, binding.playVideoButton))
|
||||
showAllViews(
|
||||
playVideoButton = false,
|
||||
primaryProgressView = false,
|
||||
primaryDetailsText = false
|
||||
)
|
||||
|
||||
val progress = calculateProgress(currentState)
|
||||
binding.secondaryProgressView.setUploading(progress)
|
||||
|
||||
binding.secondaryDetailsText.text = deriveSecondaryDetailsText(currentState)
|
||||
binding.secondaryDetailsText.invalidate()
|
||||
binding.secondaryDetailsText.forceLayout()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
private fun displayRetry(currentState: TransferControlViewState, isUploading: Boolean) {
|
||||
binding.secondaryProgressView.startClickListener = currentState.downloadClickedListener
|
||||
applyFocusableAndClickable(currentState, listOf(binding.secondaryProgressView), listOf(binding.primaryProgressView, binding.playVideoButton))
|
||||
showAllViews(
|
||||
playVideoButton = false,
|
||||
primaryProgressView = false,
|
||||
primaryDetailsText = false,
|
||||
secondaryDetailsText = currentState.showSecondaryText
|
||||
)
|
||||
|
||||
binding.secondaryProgressView.setStopped(isUploading)
|
||||
binding.secondaryDetailsText.text = resources.getString(R.string.NetworkFailure__retry)
|
||||
binding.secondaryDetailsText.invalidate()
|
||||
binding.secondaryDetailsText.forceLayout()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
private fun displayChildrenAsGone(currentState: TransferControlViewState, prevState: TransferControlViewState) {
|
||||
children.forEach {
|
||||
it.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows all views by defaults, but allows individual views to be overridden to not be shown.
|
||||
*
|
||||
* @param root
|
||||
* @param playVideoButton
|
||||
* @param primaryProgressView
|
||||
* @param primaryDetailsText
|
||||
* @param secondaryProgressView
|
||||
* @param secondaryDetailsText
|
||||
*/
|
||||
private fun showAllViews(
|
||||
root: Boolean = true,
|
||||
playVideoButton: Boolean = true,
|
||||
primaryProgressView: Boolean = true,
|
||||
primaryDetailsText: Boolean = true,
|
||||
secondaryProgressView: Boolean = true,
|
||||
secondaryDetailsText: Boolean = true
|
||||
) {
|
||||
this.visible = root
|
||||
binding.playVideoButton.visible = playVideoButton
|
||||
binding.primaryProgressView.visibility = if (primaryProgressView) View.VISIBLE else View.INVISIBLE
|
||||
binding.primaryDetailsText.visible = primaryDetailsText
|
||||
binding.primaryBackground.visible = primaryProgressView || primaryDetailsText || playVideoButton
|
||||
binding.secondaryProgressView.visible = secondaryProgressView
|
||||
binding.secondaryDetailsText.visible = secondaryDetailsText
|
||||
binding.secondaryBackground.visible = secondaryProgressView || secondaryDetailsText
|
||||
val textPadding = if (secondaryProgressView) 0 else context.resources.getDimensionPixelSize(R.dimen.transfer_control_view_progressbar_to_textview_margin)
|
||||
ViewUtil.setPaddingStart(binding.secondaryDetailsText, textPadding)
|
||||
if (ViewUtil.isLtr(binding.secondaryDetailsText)) {
|
||||
(binding.secondaryDetailsText.layoutParams as MarginLayoutParams).leftMargin = textPadding
|
||||
} else {
|
||||
(binding.secondaryDetailsText.layoutParams as MarginLayoutParams).rightMargin = textPadding
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyFocusableAndClickable(currentState: TransferControlViewState, activeViews: List<View>, inactiveViews: List<View>) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val focusIntDef = if (currentState.isFocusable) View.FOCUSABLE else View.NOT_FOCUSABLE
|
||||
activeViews.forEach { it.focusable = focusIntDef }
|
||||
inactiveViews.forEach { it.focusable = View.NOT_FOCUSABLE }
|
||||
}
|
||||
activeViews.forEach { it.isClickable = currentState.isClickable }
|
||||
inactiveViews.forEach { it.isClickable = false }
|
||||
}
|
||||
|
||||
override fun setFocusable(focusable: Boolean) {
|
||||
super.setFocusable(false)
|
||||
store.update { it.copy(isFocusable = focusable) }
|
||||
}
|
||||
|
||||
override fun setClickable(clickable: Boolean) {
|
||||
super.setClickable(false)
|
||||
store.update { it.copy(isClickable = clickable) }
|
||||
}
|
||||
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
fun onEventAsync(event: PartProgressEvent) {
|
||||
val attachment = event.attachment
|
||||
store.update {
|
||||
if (!it.networkProgress.containsKey(attachment)) {
|
||||
return@update it
|
||||
}
|
||||
|
||||
if (event.type == PartProgressEvent.Type.COMPRESSION) {
|
||||
val mutableMap = it.compressionProgress.toMutableMap()
|
||||
mutableMap[attachment] = Progress.fromEvent(event)
|
||||
return@update it.copy(compressionProgress = mutableMap.toMap())
|
||||
} else {
|
||||
val mutableMap = it.networkProgress.toMutableMap()
|
||||
mutableMap[attachment] = Progress.fromEvent(event)
|
||||
return@update it.copy(networkProgress = mutableMap.toMap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setSlides(slides: List<Slide>) {
|
||||
require(slides.isNotEmpty()) { "Must provide at least one slide." }
|
||||
store.update { state ->
|
||||
val isNewSlideSet = !isUpdateToExistingSet(state, slides)
|
||||
val networkProgress: MutableMap<Attachment, Progress> = if (isNewSlideSet) HashMap() else state.networkProgress.toMutableMap()
|
||||
if (isNewSlideSet) {
|
||||
slides.forEach { networkProgress[it.asAttachment()] = Progress(0L, it.fileSize) }
|
||||
}
|
||||
val compressionProgress: MutableMap<Attachment, Progress> = if (isNewSlideSet) HashMap() else state.compressionProgress.toMutableMap()
|
||||
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
|
||||
}
|
||||
}
|
||||
val playableWhileDownloading = allStreamableOrDone
|
||||
val isOutgoing = slides.any { it.asAttachment().uploadTimestamp == 0L }
|
||||
|
||||
state.copy(
|
||||
slides = slides,
|
||||
networkProgress = networkProgress,
|
||||
compressionProgress = compressionProgress,
|
||||
playableWhileDownloading = playableWhileDownloading,
|
||||
isOutgoing = isOutgoing
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isUpdateToExistingSet(currentState: TransferControlViewState, slides: List<Slide>): Boolean {
|
||||
if (slides.size != currentState.networkProgress.size) {
|
||||
return false
|
||||
}
|
||||
for (slide in slides) {
|
||||
if (!currentState.networkProgress.containsKey(slide.asAttachment())) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun setDownloadClickListener(listener: OnClickListener) {
|
||||
store.update {
|
||||
it.copy(downloadClickedListener = listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun setCancelClickListener(listener: OnClickListener) {
|
||||
store.update {
|
||||
it.copy(cancelDownloadClickedListener = listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun setInstantPlaybackClickListener(listener: OnClickListener) {
|
||||
store.update {
|
||||
it.copy(instantPlaybackClickListener = listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
clearAnimation()
|
||||
visibility = GONE
|
||||
store.update { TransferControlViewState() }
|
||||
}
|
||||
|
||||
fun setShowSecondaryText(showSecondaryText: Boolean) {
|
||||
store.update {
|
||||
it.copy(showSecondaryText = showSecondaryText)
|
||||
}
|
||||
}
|
||||
|
||||
fun setVisible(isVisible: Boolean) {
|
||||
store.update {
|
||||
it.copy(isVisible = isVisible)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCompressing(state: TransferControlViewState): Boolean {
|
||||
// We never get a completion event so it never actually reaches 100%
|
||||
return state.compressionProgress.sumTotal() > 0 && state.compressionProgress.values.map { it.completed.toFloat() / it.total }.sum() < 0.99f
|
||||
}
|
||||
|
||||
private fun calculateProgress(state: TransferControlViewState): Float {
|
||||
val totalCompressionProgress: Float = state.compressionProgress.values.map { it.completed.toFloat() / it.total }.sum()
|
||||
val totalDownloadProgress: Float = state.networkProgress.values.map { it.completed.toFloat() / it.total }.sum()
|
||||
val weightedProgress = UPLOAD_TASK_WEIGHT * totalDownloadProgress + COMPRESSION_TASK_WEIGHT * totalCompressionProgress
|
||||
val weightedTotal = (UPLOAD_TASK_WEIGHT * state.networkProgress.size + COMPRESSION_TASK_WEIGHT * state.compressionProgress.size).toFloat()
|
||||
return weightedProgress / weightedTotal
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun deriveSecondaryDetailsText(currentState: TransferControlViewState): String {
|
||||
return if (isCompressing(currentState)) {
|
||||
return context.getString(R.string.TransferControlView__processing)
|
||||
} else {
|
||||
val progressText = Formatter.formatShortFileSize(context, currentState.networkProgress.sumCompleted())
|
||||
val totalText = Formatter.formatShortFileSize(context, currentState.networkProgress.sumTotal())
|
||||
"$progressText/$totalText"
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun containsPlayableSlides(slides: List<Slide>): Boolean {
|
||||
return slides.any { MediaUtil.isInstantVideoSupported(it) }
|
||||
}
|
||||
}
|
||||
|
||||
data class Progress(val completed: Long, val total: Long) {
|
||||
companion object {
|
||||
fun fromEvent(event: PartProgressEvent): Progress {
|
||||
return Progress(event.progress, event.total)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Map<Attachment, Progress>.sumCompleted(): Long {
|
||||
return this.values.sumOf { it.completed }
|
||||
}
|
||||
|
||||
private fun Map<Attachment, Progress>.sumTotal(): Long {
|
||||
return this.values.sumOf { it.total }
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
PENDING_GALLERY,
|
||||
PENDING_GALLERY_CONTAINS_PLAYABLE,
|
||||
PENDING_SINGLE_ITEM,
|
||||
PENDING_VIDEO_PLAYABLE,
|
||||
DOWNLOADING_GALLERY,
|
||||
DOWNLOADING_SINGLE_ITEM,
|
||||
DOWNLOADING_VIDEO_PLAYABLE,
|
||||
UPLOADING_GALLERY,
|
||||
UPLOADING_SINGLE_ITEM,
|
||||
RETRY_DOWNLOADING,
|
||||
RETRY_UPLOADING,
|
||||
GONE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.transfercontrols
|
||||
|
||||
import android.view.View
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
|
||||
data class TransferControlViewState(
|
||||
val isVisible: Boolean = true,
|
||||
val isFocusable: Boolean = true,
|
||||
val isClickable: Boolean = true,
|
||||
val slides: List<Slide> = emptyList(),
|
||||
val downloadClickedListener: View.OnClickListener? = null,
|
||||
val cancelDownloadClickedListener: View.OnClickListener? = null,
|
||||
val instantPlaybackClickListener: View.OnClickListener? = null,
|
||||
val showSecondaryText: Boolean = true,
|
||||
val networkProgress: Map<Attachment, TransferControlView.Progress> = HashMap(),
|
||||
val compressionProgress: Map<Attachment, TransferControlView.Progress> = HashMap(),
|
||||
val playableWhileDownloading: Boolean = false,
|
||||
val isOutgoing: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
/**
|
||||
* This displays a circular progress around an icon. The icon is either an upload arrow, a download arrow, or a rectangular stop button.
|
||||
*/
|
||||
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 = 1.5f
|
||||
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.UNINITIALIZED
|
||||
|
||||
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 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)
|
||||
State.UNINITIALIZED -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun setDownloading(progress: Float) {
|
||||
currentState = State.IN_PROGRESS_CANCELABLE
|
||||
if (cancelClickListener == null) {
|
||||
Log.i(TAG, "Illegal click listener attached.")
|
||||
} else {
|
||||
setOnClickListener(cancelClickListener)
|
||||
}
|
||||
progressPercent = progress
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun setUploading(progress: Float) {
|
||||
currentState = State.IN_PROGRESS_NON_CANCELABLE
|
||||
setOnClickListener { Log.d(TAG, "Not allowed to click an upload.") }
|
||||
progressPercent = progress
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun setStopped(isUpload: Boolean) {
|
||||
val newState = if (isUpload) State.READY_TO_UPLOAD else State.READY_TO_DOWNLOAD
|
||||
currentState = newState
|
||||
if (startClickListener == null) {
|
||||
Log.i(TAG, "Illegal click listener attached.")
|
||||
} else {
|
||||
setOnClickListener(startClickListener)
|
||||
}
|
||||
progressPercent = 0f
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private fun drawProgress(canvas: Canvas, progressPercent: Float) {
|
||||
val miniIcon = height < 32.dp
|
||||
val stopIconCornerRadius = if (miniIcon) 1f.dp else 4f.dp
|
||||
val iconSize: Float = if (miniIcon) 5.5f.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,
|
||||
UNINITIALIZED
|
||||
}
|
||||
}
|
||||
@@ -63,12 +63,16 @@ 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;
|
||||
@@ -106,8 +110,10 @@ 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;
|
||||
@@ -197,42 +203,42 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
private Optional<MessageRecord> previousMessage;
|
||||
private ConversationItemDisplayMode displayMode;
|
||||
|
||||
protected ConversationItemBodyBubble bodyBubble;
|
||||
protected View reply;
|
||||
protected View replyIcon;
|
||||
@Nullable protected ViewGroup contactPhotoHolder;
|
||||
@Nullable private QuoteView quoteView;
|
||||
private EmojiTextView bodyText;
|
||||
private ConversationItemFooter footer;
|
||||
@Nullable private ConversationItemFooter stickerFooter;
|
||||
@Nullable private TextView groupSender;
|
||||
@Nullable private View groupSenderHolder;
|
||||
private AvatarImageView contactPhoto;
|
||||
private AlertView alertView;
|
||||
protected ReactionsConversationView reactionsView;
|
||||
protected BadgeImageView badgeImageView;
|
||||
private View storyReactionLabelWrapper;
|
||||
private TextView storyReactionLabel;
|
||||
protected View quotedIndicator;
|
||||
protected View scheduledIndicator;
|
||||
private ConversationItemBodyBubble bodyBubble;
|
||||
private View reply;
|
||||
private View replyIcon;
|
||||
@Nullable private ViewGroup contactPhotoHolder;
|
||||
@Nullable private QuoteView quoteView;
|
||||
private EmojiTextView bodyText;
|
||||
private ConversationItemFooter footer;
|
||||
@Nullable private ConversationItemFooter stickerFooter;
|
||||
@Nullable private TextView groupSender;
|
||||
@Nullable private View groupSenderHolder;
|
||||
private AvatarImageView contactPhoto;
|
||||
private AlertView alertView;
|
||||
private ReactionsConversationView reactionsView;
|
||||
private BadgeImageView badgeImageView;
|
||||
private View storyReactionLabelWrapper;
|
||||
private TextView storyReactionLabel;
|
||||
private View quotedIndicator;
|
||||
private View scheduledIndicator;
|
||||
|
||||
private @NonNull Set<MultiselectPart> batchSelected = new HashSet<>();
|
||||
private @NonNull Outliner outliner = new Outliner();
|
||||
private @NonNull Outliner pulseOutliner = new Outliner();
|
||||
private @NonNull List<Outliner> outliners = new ArrayList<>(2);
|
||||
private LiveRecipient conversationRecipient;
|
||||
private NullableStub<ConversationItemThumbnail> mediaThumbnailStub;
|
||||
private Stub<AudioView> audioViewStub;
|
||||
private Stub<DocumentView> documentViewStub;
|
||||
private Stub<SharedContactView> sharedContactStub;
|
||||
private Stub<LinkPreviewView> linkPreviewStub;
|
||||
private Stub<BorderlessImageView> stickerStub;
|
||||
private Stub<ViewOnceMessageView> revealableStub;
|
||||
private Stub<CallLinkJoinButton> joinCallLinkStub;
|
||||
private Stub<Button> callToActionStub;
|
||||
private Stub<GiftMessageView> giftViewStub;
|
||||
private Stub<PaymentMessageView> paymentViewStub;
|
||||
private @Nullable EventListener eventListener;
|
||||
private @NonNull Set<MultiselectPart> batchSelected = new HashSet<>();
|
||||
private final @NonNull Outliner outliner = new Outliner();
|
||||
private final @NonNull Outliner pulseOutliner = new Outliner();
|
||||
private final @NonNull List<Outliner> outliners = new ArrayList<>(2);
|
||||
private LiveRecipient conversationRecipient;
|
||||
private NullableStub<ConversationItemThumbnail> mediaThumbnailStub;
|
||||
private Stub<AudioView> audioViewStub;
|
||||
private Stub<DocumentView> documentViewStub;
|
||||
private Stub<SharedContactView> sharedContactStub;
|
||||
private Stub<LinkPreviewView> linkPreviewStub;
|
||||
private Stub<BorderlessImageView> stickerStub;
|
||||
private Stub<ViewOnceMessageView> revealableStub;
|
||||
private Stub<CallLinkJoinButton> joinCallLinkStub;
|
||||
private Stub<Button> callToActionStub;
|
||||
private Stub<GiftMessageView> giftViewStub;
|
||||
private Stub<PaymentMessageView> paymentViewStub;
|
||||
private @Nullable EventListener eventListener;
|
||||
|
||||
private int defaultBubbleColor;
|
||||
private int defaultBubbleColorForWallpaper;
|
||||
@@ -241,7 +247,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
|
||||
private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
|
||||
private final ProgressWheelClickListener progressWheelClickListener = new ProgressWheelClickListener();
|
||||
private final PlayVideoClickListener playVideoClickListener = new PlayVideoClickListener();
|
||||
private final AttachmentCancelClickListener attachmentCancelClickListener = new AttachmentCancelClickListener();
|
||||
private final SlideClickPassthroughListener singleDownloadClickListener = new SlideClickPassthroughListener(downloadClickListener);
|
||||
private final SharedContactEventListener sharedContactEventListener = new SharedContactEventListener();
|
||||
private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
|
||||
@@ -256,14 +263,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
private final Context context;
|
||||
|
||||
private MediaItem mediaItem;
|
||||
private boolean canPlayContent;
|
||||
private Projection.Corners bodyBubbleCorners;
|
||||
private Colorizer colorizer;
|
||||
private boolean hasWallpaper;
|
||||
private float lastYDownRelativeToThis;
|
||||
private ProjectionList colorizerProjections = new ProjectionList(3);
|
||||
private boolean isBound = false;
|
||||
private MediaItem mediaItem;
|
||||
private boolean canPlayContent;
|
||||
private Projection.Corners bodyBubbleCorners;
|
||||
private Colorizer colorizer;
|
||||
private boolean hasWallpaper;
|
||||
private float lastYDownRelativeToThis;
|
||||
private final ProjectionList colorizerProjections = new ProjectionList(3);
|
||||
private boolean isBound = false;
|
||||
|
||||
private final Runnable shrinkBubble = new Runnable() {
|
||||
@Override
|
||||
@@ -709,6 +716,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
bodyBubble.setVideoPlayerProjection(null);
|
||||
bodyBubble.setQuoteViewProjection(null);
|
||||
|
||||
playVideoClickListener.cleanup();
|
||||
|
||||
glideRequests = null;
|
||||
}
|
||||
|
||||
@@ -1170,7 +1179,8 @@ 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().setProgressWheelClickListener(progressWheelClickListener);
|
||||
mediaThumbnailStub.require().setCancelDownloadClickListener(attachmentCancelClickListener);
|
||||
mediaThumbnailStub.require().setPlayVideoClickListener(playVideoClickListener);
|
||||
mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener);
|
||||
|
||||
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, false);
|
||||
@@ -1304,17 +1314,18 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
: R.dimen.media_bubble_min_width_with_content));
|
||||
mediaThumbnailStub.require().setMaximumThumbnailHeight(readDimen(isContentCondensed() ? R.dimen.media_bubble_max_height_condensed
|
||||
: R.dimen.media_bubble_max_height));
|
||||
|
||||
mediaThumbnailStub.require().setThumbnailClickListener(new ThumbnailClickListener());
|
||||
mediaThumbnailStub.require().setDownloadClickListener(downloadClickListener);
|
||||
mediaThumbnailStub.require().setCancelDownloadClickListener(attachmentCancelClickListener);
|
||||
mediaThumbnailStub.require().setPlayVideoClickListener(playVideoClickListener);
|
||||
mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener);
|
||||
mediaThumbnailStub.require().setOnClickListener(passthroughClickListener);
|
||||
mediaThumbnailStub.require().showShade(messageRecord.isDisplayBodyEmpty(getContext()) && !hasExtraText(messageRecord));
|
||||
mediaThumbnailStub.require().setImageResource(glideRequests,
|
||||
thumbnailSlides,
|
||||
showControls,
|
||||
false);
|
||||
mediaThumbnailStub.require().setThumbnailClickListener(new ThumbnailClickListener());
|
||||
mediaThumbnailStub.require().setDownloadClickListener(downloadClickListener);
|
||||
mediaThumbnailStub.require().setProgressWheelClickListener(progressWheelClickListener);
|
||||
mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener);
|
||||
mediaThumbnailStub.require().setOnClickListener(passthroughClickListener);
|
||||
mediaThumbnailStub.require().showShade(messageRecord.isDisplayBodyEmpty(getContext()) && !hasExtraText(messageRecord));
|
||||
|
||||
if (!messageRecord.isOutgoing()) {
|
||||
mediaThumbnailStub.require().setConversationColor(getDefaultBubbleColor(hasWallpaper));
|
||||
} else {
|
||||
@@ -2441,16 +2452,82 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
}
|
||||
|
||||
private class ProgressWheelClickListener implements SlideClickListener {
|
||||
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;
|
||||
|
||||
@Override
|
||||
public void onClick(View v, Slide slide) {
|
||||
final boolean isIncremental = slide.asAttachment().getIncrementalDigest() != null;
|
||||
final boolean contentTypeSupported = MediaUtil.isVideoType(slide.getContentType());
|
||||
if (FeatureFlags.instantVideoPlayback() && isIncremental && contentTypeSupported) {
|
||||
launchMediaPreview(v, 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();
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Non-eligible slide clicked: " + "\tisIncremental: " + isIncremental + "\tcontentTypeSupported: " + contentTypeSupported);
|
||||
Log.d(TAG, "Non-eligible slide clicked.");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2592,7 +2669,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
private class ClickListener implements View.OnClickListener {
|
||||
private OnClickListener parent;
|
||||
private final OnClickListener parent;
|
||||
|
||||
ClickListener(@Nullable OnClickListener parent) {
|
||||
this.parent = parent;
|
||||
@@ -2724,8 +2801,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
if (message > -1) builder.setMessage(message);
|
||||
|
||||
builder.setPositiveButton(R.string.yes, (dialogInterface, i) -> {
|
||||
MessageTable db = messageRecord.isMms() ? SignalDatabase.messages()
|
||||
: SignalDatabase.messages();
|
||||
MessageTable db = SignalDatabase.messages();
|
||||
|
||||
db.markAsInsecure(messageRecord.getId());
|
||||
db.markAsOutbox(messageRecord.getId());
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.text.TextUtils;
|
||||
@@ -19,9 +24,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,6 +38,7 @@ 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;
|
||||
@@ -62,18 +68,18 @@ public final class AttachmentDownloadJob extends BaseJob {
|
||||
private static final String KEY_PAR_UNIQUE_ID = "part_unique_id";
|
||||
private static final String KEY_MANUAL = "part_manual";
|
||||
|
||||
private long messageId;
|
||||
private long partRowId;
|
||||
private long partUniqueId;
|
||||
private boolean manual;
|
||||
private final long messageId;
|
||||
private final long partRowId;
|
||||
private final long partUniqueId;
|
||||
private final boolean manual;
|
||||
|
||||
public AttachmentDownloadJob(long messageId, AttachmentId attachmentId, boolean manual) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue("AttachmentDownloadJob" + attachmentId.getRowId() + "-" + attachmentId.getUniqueId())
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
.setQueue(constructQueueString(attachmentId))
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
messageId,
|
||||
attachmentId,
|
||||
manual);
|
||||
@@ -102,6 +108,10 @@ public final class AttachmentDownloadJob extends BaseJob {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
public static String constructQueueString(AttachmentId attachmentId) {
|
||||
return "AttachmentDownloadJob" + attachmentId.getRowId() + "-" + attachmentId.getUniqueId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdded() {
|
||||
Log.i(TAG, "onAdded() messageId: " + messageId + " partRowId: " + partRowId + " partUniqueId: " + partUniqueId + " manual: " + manual);
|
||||
@@ -109,8 +119,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'");
|
||||
@@ -130,8 +140,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) {
|
||||
@@ -195,11 +205,20 @@ public final class AttachmentDownloadJob extends BaseJob {
|
||||
}
|
||||
SignalServiceMessageReceiver messageReceiver = ApplicationDependencies.getSignalServiceMessageReceiver();
|
||||
SignalServiceAttachmentPointer pointer = createAttachmentPointer(attachment);
|
||||
InputStream stream = messageReceiver.retrieveAttachment(pointer,
|
||||
attachmentFile,
|
||||
maxReceiveSize,
|
||||
(total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress)));
|
||||
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));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldCancel() {
|
||||
return isCanceled();
|
||||
}
|
||||
});
|
||||
database.insertAttachmentsForPlaceholder(messageId, attachmentId, stream);
|
||||
} catch (RangeException e) {
|
||||
Log.w(TAG, "Range exception, file size " + attachmentFile.length(), e);
|
||||
@@ -299,7 +318,8 @@ public final class AttachmentDownloadJob extends BaseJob {
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting static class InvalidPartException extends Exception {
|
||||
@VisibleForTesting
|
||||
static class InvalidPartException extends Exception {
|
||||
InvalidPartException(String s) {super(s);}
|
||||
InvalidPartException(Exception e) {super(e);}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
@@ -213,10 +218,18 @@ public final class AttachmentUploadJob extends BaseJob {
|
||||
.withCaption(attachment.getCaption())
|
||||
.withCancelationSignal(this::isCanceled)
|
||||
.withResumableUploadSpec(resumableUploadSpec)
|
||||
.withListener((total, progress) -> {
|
||||
EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress));
|
||||
if (notification != null) {
|
||||
notification.setProgress(total, progress);
|
||||
.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();
|
||||
}
|
||||
});
|
||||
if (MediaUtil.isImageType(attachment.getContentType())) {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
@@ -200,7 +205,17 @@ public abstract class PushSendJob extends SendJob {
|
||||
.withWidth(attachment.getWidth())
|
||||
.withHeight(attachment.getHeight())
|
||||
.withCaption(attachment.getCaption())
|
||||
.withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, PartProgressEvent.Type.NETWORK, total, progress)))
|
||||
.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();
|
||||
}
|
||||
})
|
||||
.build();
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Couldn't open attachment", ioe);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
@@ -148,12 +153,10 @@ public class MediaUtil {
|
||||
if (fileExtension == null) {
|
||||
return mimeType;
|
||||
}
|
||||
switch (fileExtension.toLowerCase()) {
|
||||
case "m4a":
|
||||
return safeMimeTypeOverride(mimeType, AUDIO_MP4);
|
||||
default:
|
||||
return mimeType;
|
||||
if (fileExtension.toLowerCase().equals("m4a")) {
|
||||
return safeMimeTypeOverride(mimeType, AUDIO_MP4);
|
||||
}
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public static @Nullable String getCorrectedMimeType(@Nullable String mimeType) {
|
||||
@@ -401,11 +404,7 @@ public class MediaUtil {
|
||||
} else if (uri.toString().startsWith("file://") &&
|
||||
MediaUtil.isVideo(URLConnection.guessContentTypeFromName(uri.toString()))) {
|
||||
return true;
|
||||
} else if (PartAuthority.isAttachmentUri(uri) && MediaUtil.isVideoType(PartAuthority.getAttachmentContentType(context, uri))) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else return PartAuthority.isAttachmentUri(uri) && MediaUtil.isVideoType(PartAuthority.getAttachmentContentType(context, uri));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -468,6 +467,17 @@ public class MediaUtil {
|
||||
return mediaMetadataRetriever.getFrameAtTime(timeUs);
|
||||
}
|
||||
|
||||
public static boolean isInstantVideoSupported(Slide slide) {
|
||||
if (!FeatureFlags.instantVideoPlayback()) {
|
||||
return false;
|
||||
}
|
||||
final Attachment attachment = slide.asAttachment();
|
||||
final boolean isIncremental = attachment.getIncrementalDigest() != null;
|
||||
final boolean hasIncrementalMacChunkSizeDefined = attachment.getIncrementalMacChunkSize() > 0;
|
||||
final boolean contentTypeSupported = isVideoType(slide.getContentType());
|
||||
return isIncremental && contentTypeSupported && hasIncrementalMacChunkSizeDefined;
|
||||
}
|
||||
|
||||
public static @Nullable String getDiscreteMimeType(@NonNull String mimeType) {
|
||||
final String[] sections = mimeType.split("/", 2);
|
||||
return sections.length > 1 ? sections[0] : null;
|
||||
@@ -476,7 +486,7 @@ public class MediaUtil {
|
||||
public static class ThumbnailData implements AutoCloseable {
|
||||
|
||||
@NonNull private final Bitmap bitmap;
|
||||
private final float aspectRatio;
|
||||
private final float aspectRatio;
|
||||
|
||||
public ThumbnailData(@NonNull Bitmap bitmap) {
|
||||
this.bitmap = bitmap;
|
||||
|
||||
Reference in New Issue
Block a user