Allow canceling media attachment send.

This commit is contained in:
Nicholas Tinsley
2024-01-05 13:36:31 -05:00
committed by Alex Hart
parent deacf28d77
commit 5022d81d9a
17 changed files with 278 additions and 133 deletions

View File

@@ -30,9 +30,9 @@ import java.util.List;
public class AlbumThumbnailView extends FrameLayout {
private @Nullable SlideClickListener thumbnailClickListener;
private @Nullable SlidesClickedListener downloadClickListener;
private @Nullable SlidesClickedListener cancelDownloadClickListener;
private @Nullable SlideClickListener playVideoClickListener;
private @Nullable SlidesClickedListener startTransferClickListener;
private @Nullable SlidesClickedListener cancelTransferClickListener;
private @Nullable SlideClickListener playVideoClickListener;
private int currentSizeClass;
@@ -72,16 +72,16 @@ public class AlbumThumbnailView extends FrameLayout {
if (showControls) {
transferControlsStub.get().setShowSecondaryText(true);
transferControlsStub.get().setDownloadClickListener(
transferControlsStub.get().setTransferClickListener(
v -> {
if (downloadClickListener != null) {
downloadClickListener.onClick(v, slides);
if (startTransferClickListener != null) {
startTransferClickListener.onClick(v, slides);
}
});
transferControlsStub.get().setCancelClickListener(
v -> {
if (cancelDownloadClickListener != null) {
cancelDownloadClickListener.onClick(v, slides);
if (cancelTransferClickListener != null) {
cancelTransferClickListener.onClick(v, slides);
}
});
transferControlsStub.get().setSlides(slides);
@@ -117,12 +117,12 @@ public class AlbumThumbnailView extends FrameLayout {
thumbnailClickListener = listener;
}
public void setDownloadClickListener(SlidesClickedListener listener) {
this.downloadClickListener = listener;
public void setStartTransferClickListener(SlidesClickedListener listener) {
this.startTransferClickListener = listener;
}
public void setCancelDownloadClickListener(SlidesClickedListener listener) {
this.cancelDownloadClickListener = listener;
public void setCancelTransferClickListener(SlidesClickedListener listener) {
this.cancelTransferClickListener = listener;
}
public void setPlayVideoClickListener(SlideClickListener listener) {
@@ -288,8 +288,8 @@ public class AlbumThumbnailView extends FrameLayout {
ThumbnailView cell = findViewById(id);
cell.showSecondaryText(false);
cell.setThumbnailClickListener(defaultThumbnailClickListener);
cell.setDownloadClickListener(downloadClickListener);
cell.setCancelDownloadClickListener(cancelDownloadClickListener);
cell.setStartTransferClickListener(startTransferClickListener);
cell.setCancelTransferClickListener(cancelTransferClickListener);
if (MediaUtil.isInstantVideoSupported(slide)) {
cell.setPlayVideoClickListener(playVideoClickListener);
}

View File

@@ -9,9 +9,6 @@ import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.CenterInside;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
@@ -74,6 +71,6 @@ public class BorderlessImageView extends FrameLayout {
}
public void setDownloadClickListener(@NonNull SlidesClickedListener listener) {
image.setDownloadClickListener(listener);
image.setStartTransferClickListener(listener);
}
}

View File

@@ -31,8 +31,8 @@ import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode;
import org.thoughtcrime.securesms.conversation.v2.computed.FormattedDate;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -302,7 +302,9 @@ public class ConversationItemFooter extends ConstraintLayout {
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale, @NonNull ConversationItemDisplayMode displayMode) {
dateView.forceLayout();
if (messageRecord.isFailed()) {
if (messageRecord.isMediaPending()) {
dateView.setText(null);
} else if (messageRecord.isFailed()) {
int errorMsg;
if (messageRecord.hasFailedWithNetworkFailures()) {
errorMsg = R.string.ConversationItem_error_network_not_delivered;

View File

@@ -251,10 +251,10 @@ class ConversationItemThumbnail @JvmOverloads constructor(
state.applyState(thumbnail, album)
}
fun setDownloadClickListener(listener: SlidesClickedListener?) {
fun setStartTransferClickListener(listener: SlidesClickedListener?) {
state = state.copy(
thumbnailViewState = state.thumbnailViewState.copy(downloadClickListener = listener),
albumViewState = state.albumViewState.copy(downloadClickListener = listener)
thumbnailViewState = state.thumbnailViewState.copy(startTransferClickListener = listener),
albumViewState = state.albumViewState.copy(startTransferClickListener = listener)
)
state.applyState(thumbnail, album)
@@ -269,10 +269,10 @@ class ConversationItemThumbnail @JvmOverloads constructor(
state.applyState(thumbnail, album)
}
fun setCancelDownloadClickListener(listener: SlidesClickedListener?) {
fun setCancelTransferClickListener(listener: SlidesClickedListener?) {
state = state.copy(
thumbnailViewState = state.thumbnailViewState.copy(cancelDownloadClickListener = listener),
albumViewState = state.albumViewState.copy(cancelDownloadClickListener = listener)
thumbnailViewState = state.thumbnailViewState.copy(cancelTransferClickListener = listener),
albumViewState = state.albumViewState.copy(cancelTransferClickListener = listener)
)
state.applyState(thumbnail, album)

View File

@@ -34,9 +34,9 @@ data class ConversationItemThumbnailState(
@IgnoredOnParcel
private val clickListener: SlideClickListener? = null,
@IgnoredOnParcel
private val downloadClickListener: SlidesClickedListener? = null,
private val startTransferClickListener: SlidesClickedListener? = null,
@IgnoredOnParcel
private val cancelDownloadClickListener: SlidesClickedListener? = null,
private val cancelTransferClickListener: SlidesClickedListener? = null,
@IgnoredOnParcel
private val playVideoClickListener: SlideClickListener? = null,
@IgnoredOnParcel
@@ -63,8 +63,8 @@ data class ConversationItemThumbnailState(
thumbnailView.get().isClickable = clickable
thumbnailView.get().setRadii(cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft)
thumbnailView.get().setThumbnailClickListener(clickListener)
thumbnailView.get().setDownloadClickListener(downloadClickListener)
thumbnailView.get().setCancelDownloadClickListener(cancelDownloadClickListener)
thumbnailView.get().setStartTransferClickListener(startTransferClickListener)
thumbnailView.get().setCancelTransferClickListener(cancelTransferClickListener)
thumbnailView.get().setPlayVideoClickListener(playVideoClickListener)
thumbnailView.get().setOnLongClickListener(longClickListener)
thumbnailView.get().setBounds(minWidth, maxWidth, minHeight, maxHeight)
@@ -78,9 +78,9 @@ data class ConversationItemThumbnailState(
@IgnoredOnParcel
private val clickListener: SlideClickListener? = null,
@IgnoredOnParcel
private val downloadClickListener: SlidesClickedListener? = null,
private val startTransferClickListener: SlidesClickedListener? = null,
@IgnoredOnParcel
private val cancelDownloadClickListener: SlidesClickedListener? = null,
private val cancelTransferClickListener: SlidesClickedListener? = null,
@IgnoredOnParcel
private val playVideoClickListener: SlideClickListener? = null,
@IgnoredOnParcel
@@ -103,8 +103,8 @@ data class ConversationItemThumbnailState(
albumView.get().isClickable = clickable
albumView.get().setRadii(cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft)
albumView.get().setThumbnailClickListener(clickListener)
albumView.get().setDownloadClickListener(downloadClickListener)
albumView.get().setCancelDownloadClickListener(cancelDownloadClickListener)
albumView.get().setStartTransferClickListener(startTransferClickListener)
albumView.get().setCancelTransferClickListener(cancelTransferClickListener)
albumView.get().setPlayVideoClickListener(playVideoClickListener)
albumView.get().setOnLongClickListener(longClickListener)
albumView.get().setCellBackgroundColor(cellBackgroundColor)

View File

@@ -22,7 +22,7 @@ data class LinkPreviewViewThumbnailState(
fun applyState(thumbnail: Stub<OutlinedThumbnailView>) {
if (thumbnail.resolved()) {
thumbnail.get().setCorners(cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft)
thumbnail.get().setDownloadClickListener(downloadListener)
thumbnail.get().setStartTransferClickListener(downloadListener)
}
}
}

View File

@@ -86,12 +86,12 @@ public class ThumbnailView extends FrameLayout {
private final CornerMask cornerMask;
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;
private final Stub<TransferControlView> transferControlViewStub;
private SlideClickListener thumbnailClickListener = null;
private SlidesClickedListener startTransferClickListener = null;
private SlidesClickedListener cancelTransferClickListener = null;
private SlideClickListener playVideoClickListener = null;
private Slide slide = null;
public ThumbnailView(Context context) {
@@ -363,7 +363,7 @@ public class ThumbnailView extends FrameLayout {
}
if (showControls) {
transferControlViewStub.get().setDownloadClickListener(new DownloadClickDispatcher());
transferControlViewStub.get().setTransferClickListener(new DownloadClickDispatcher());
transferControlViewStub.get().setCancelClickListener(new CancelClickDispatcher());
if (MediaUtil.isInstantVideoSupported(slide)) {
transferControlViewStub.get().setInstantPlaybackClickListener(new InstantVideoClickDispatcher());
@@ -516,12 +516,12 @@ public class ThumbnailView extends FrameLayout {
this.thumbnailClickListener = listener;
}
public void setDownloadClickListener(SlidesClickedListener listener) {
this.downloadClickListener = listener;
public void setStartTransferClickListener(SlidesClickedListener listener) {
this.startTransferClickListener = listener;
}
public void setCancelDownloadClickListener(SlidesClickedListener listener) {
this.cancelDownloadClickListener = listener;
public void setCancelTransferClickListener(SlidesClickedListener listener) {
this.cancelTransferClickListener = listener;
}
public void setPlayVideoClickListener(SlideClickListener listener) {
@@ -667,10 +667,10 @@ public class ThumbnailView extends FrameLayout {
@Override
public void onClick(View view) {
Log.i(TAG, "onClick() for download button");
if (downloadClickListener != null && slide != null) {
downloadClickListener.onClick(view, Collections.singletonList(slide));
if (startTransferClickListener != null && slide != null) {
startTransferClickListener.onClick(view, Collections.singletonList(slide));
} else {
Log.w(TAG, "Received a download button click, but unable to execute it. slide: " + slide + " downloadClickListener: " + downloadClickListener);
Log.w(TAG, "Received a download button click, but unable to execute it. slide: " + slide + " downloadClickListener: " + startTransferClickListener);
}
}
}
@@ -679,10 +679,10 @@ public class ThumbnailView extends FrameLayout {
@Override
public void onClick(View view) {
Log.i(TAG, "onClick() for cancel button");
if (cancelDownloadClickListener != null && slide != null) {
cancelDownloadClickListener.onClick(view, Collections.singletonList(slide));
if (cancelTransferClickListener != null && slide != null) {
cancelTransferClickListener.onClick(view, Collections.singletonList(slide));
} else {
Log.w(TAG, "Received a cancel button click, but unable to execute it. slide: " + slide + " cancelDownloadClickListener: " + cancelDownloadClickListener);
Log.w(TAG, "Received a cancel button click, but unable to execute it. slide: " + slide + " cancelDownloadClickListener: " + cancelTransferClickListener);
}
}
}

View File

@@ -215,7 +215,7 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
}
private fun displayPendingGallery(currentState: TransferControlViewState) {
binding.primaryProgressView.startClickListener = currentState.downloadClickedListener
binding.primaryProgressView.startClickListener = currentState.startTransferClickListener
applyFocusableAndClickable(
currentState,
listOf(binding.primaryProgressView, binding.primaryDetailsText, binding.primaryBackground),
@@ -228,8 +228,8 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
secondaryDetailsText = currentState.showSecondaryText
)
binding.primaryDetailsText.setOnClickListener(currentState.downloadClickedListener)
binding.primaryBackground.setOnClickListener(currentState.downloadClickedListener)
binding.primaryDetailsText.setOnClickListener(currentState.startTransferClickListener)
binding.primaryBackground.setOnClickListener(currentState.startTransferClickListener)
binding.primaryDetailsText.translationX = if (ViewUtil.isLtr(this)) {
ViewUtil.dpToPx(-PRIMARY_TEXT_OFFSET_DP).toFloat()
@@ -244,7 +244,7 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
}
private fun displayPendingGalleryWithPlayable(currentState: TransferControlViewState) {
binding.secondaryProgressView.startClickListener = currentState.downloadClickedListener
binding.secondaryProgressView.startClickListener = currentState.startTransferClickListener
super.setClickable(false)
binding.secondaryProgressView.isClickable = currentState.showSecondaryText
binding.secondaryProgressView.isFocusable = currentState.showSecondaryText
@@ -269,7 +269,7 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
}
private fun displayPendingSingleItem(currentState: TransferControlViewState) {
binding.primaryProgressView.startClickListener = currentState.downloadClickedListener
binding.primaryProgressView.startClickListener = currentState.startTransferClickListener
applyFocusableAndClickable(currentState, listOf(binding.primaryProgressView), listOf(binding.secondaryProgressView, binding.playVideoButton))
binding.primaryProgressView.setStopped(false)
showAllViews(
@@ -284,9 +284,9 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
}
private fun displayPendingPlayableVideo(currentState: TransferControlViewState) {
binding.secondaryProgressView.startClickListener = currentState.downloadClickedListener
binding.secondaryDetailsText.setOnClickListener(currentState.downloadClickedListener)
binding.secondaryBackground.setOnClickListener(currentState.downloadClickedListener)
binding.secondaryProgressView.startClickListener = currentState.startTransferClickListener
binding.secondaryDetailsText.setOnClickListener(currentState.startTransferClickListener)
binding.secondaryBackground.setOnClickListener(currentState.startTransferClickListener)
binding.playVideoButton.setOnClickListener(currentState.instantPlaybackClickListener)
applyFocusableAndClickable(
currentState,
@@ -320,17 +320,17 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
val progress = calculateProgress(currentState)
if (progress == 0f) {
binding.secondaryProgressView.setUploading(progress)
binding.secondaryProgressView.setProgress(progress)
} else {
binding.secondaryProgressView.cancelClickListener = currentState.cancelDownloadClickedListener
binding.secondaryProgressView.setDownloading(progress)
binding.secondaryProgressView.cancelClickListener = currentState.cancelTransferClickedListener
binding.secondaryProgressView.setProgress(progress)
}
binding.secondaryDetailsText.translationX = 0f
binding.secondaryDetailsText.text = deriveSecondaryDetailsText(currentState)
}
private fun displayDownloadingSingleItem(currentState: TransferControlViewState) {
binding.primaryProgressView.cancelClickListener = currentState.cancelDownloadClickedListener
binding.primaryProgressView.cancelClickListener = currentState.cancelTransferClickedListener
applyFocusableAndClickable(currentState, listOf(binding.primaryProgressView), listOf(binding.secondaryProgressView, binding.playVideoButton))
showAllViews(
playVideoButton = false,
@@ -341,16 +341,16 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
val progress = calculateProgress(currentState)
if (progress == 0f) {
binding.primaryProgressView.setUploading(progress)
binding.primaryProgressView.setProgress(progress)
} else {
binding.primaryProgressView.setDownloading(progress)
binding.primaryProgressView.setProgress(progress)
}
binding.secondaryDetailsText.translationX = 0f
binding.secondaryDetailsText.text = deriveSecondaryDetailsText(currentState)
}
private fun displayDownloadingPlayableVideo(currentState: TransferControlViewState) {
binding.secondaryProgressView.cancelClickListener = currentState.cancelDownloadClickedListener
binding.secondaryProgressView.cancelClickListener = currentState.cancelTransferClickedListener
applyFocusableAndClickable(currentState, listOf(binding.secondaryProgressView, binding.playVideoButton), listOf(binding.primaryProgressView))
showAllViews(
primaryDetailsText = false,
@@ -362,16 +362,16 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
val progress = calculateProgress(currentState)
if (progress == 0f) {
binding.secondaryProgressView.setUploading(progress)
binding.secondaryProgressView.setProgress(progress)
} else {
binding.secondaryProgressView.setDownloading(progress)
binding.secondaryProgressView.setProgress(progress)
}
binding.secondaryDetailsText.translationX = 0f
binding.secondaryDetailsText.text = deriveSecondaryDetailsText(currentState)
}
private fun displayUploadingSingleItem(currentState: TransferControlViewState) {
binding.secondaryProgressView.startClickListener = currentState.downloadClickedListener
binding.secondaryProgressView.cancelClickListener = currentState.cancelTransferClickedListener
applyFocusableAndClickable(currentState, listOf(binding.secondaryProgressView), listOf(binding.primaryProgressView, binding.playVideoButton))
showAllViews(
playVideoButton = false,
@@ -381,18 +381,14 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
)
val progress = calculateProgress(currentState)
binding.secondaryProgressView.setUploading(progress)
binding.secondaryProgressView.setProgress(progress)
binding.secondaryDetailsText.translationX = 0f
binding.secondaryDetailsText.text = deriveSecondaryDetailsText(currentState)
}
private fun displayUploadingGallery(currentState: TransferControlViewState) {
if (currentState.downloadClickedListener == null) {
Log.w(TAG, "No click listener set for retry!")
}
binding.secondaryProgressView.startClickListener = currentState.downloadClickedListener
binding.secondaryProgressView.cancelClickListener = currentState.cancelTransferClickedListener
applyFocusableAndClickable(currentState, listOf(binding.secondaryProgressView), listOf(binding.primaryProgressView, binding.playVideoButton))
showAllViews(
playVideoButton = false,
@@ -401,18 +397,18 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
)
val progress = calculateProgress(currentState)
binding.secondaryProgressView.setUploading(progress)
binding.secondaryProgressView.setProgress(progress)
binding.secondaryDetailsText.translationX = 0f
binding.secondaryDetailsText.text = deriveSecondaryDetailsText(currentState)
}
private fun displayRetry(currentState: TransferControlViewState, isUploading: Boolean) {
if (currentState.downloadClickedListener == null) {
if (currentState.startTransferClickListener == null) {
Log.w(TAG, "No click listener set for retry!")
}
binding.secondaryProgressView.startClickListener = currentState.downloadClickedListener
binding.secondaryProgressView.startClickListener = currentState.startTransferClickListener
applyFocusableAndClickable(
currentState,
listOf(binding.secondaryProgressView, binding.secondaryDetailsText, binding.secondaryBackground),
@@ -429,8 +425,8 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
} else {
ViewUtil.dpToPx(RETRY_SECONDARY_TEXT_OFFSET_DP).toFloat()
}
binding.secondaryBackground.setOnClickListener(currentState.downloadClickedListener)
binding.secondaryDetailsText.setOnClickListener(currentState.downloadClickedListener)
binding.secondaryBackground.setOnClickListener(currentState.startTransferClickListener)
binding.secondaryDetailsText.setOnClickListener(currentState.startTransferClickListener)
binding.secondaryProgressView.setStopped(isUploading)
binding.secondaryDetailsText.text = resources.getString(R.string.NetworkFailure__retry)
}
@@ -594,11 +590,11 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
return true
}
fun setDownloadClickListener(listener: OnClickListener) {
verboseLog("downloadClickListener update")
fun setTransferClickListener(listener: OnClickListener) {
verboseLog("transferClickListener update")
updateState {
it.copy(
downloadClickedListener = listener
startTransferClickListener = listener
)
}
}
@@ -607,7 +603,7 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
verboseLog("cancelClickListener update")
updateState {
it.copy(
cancelDownloadClickedListener = listener
cancelTransferClickedListener = listener
)
}
}
@@ -660,7 +656,7 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
@SuppressLint("SetTextI18n")
private fun deriveSecondaryDetailsText(currentState: TransferControlViewState): String {
return if (isCompressing(currentState)) {
return if (currentState.networkProgress.sumCompleted() == 0L || isCompressing(currentState)) {
return context.getString(R.string.TransferControlView__processing)
} else {
val progressMiB = currentState.networkProgress.sumCompleted() / MEBIBYTE

View File

@@ -14,8 +14,8 @@ data class TransferControlViewState(
val isFocusable: Boolean = true,
val isClickable: Boolean = true,
val slides: List<Slide> = emptyList(),
val downloadClickedListener: View.OnClickListener? = null,
val cancelDownloadClickedListener: View.OnClickListener? = null,
val startTransferClickListener: View.OnClickListener? = null,
val cancelTransferClickedListener: View.OnClickListener? = null,
val instantPlaybackClickListener: View.OnClickListener? = null,
val showSecondaryText: Boolean = true,
val networkProgress: Map<Attachment, TransferControlView.Progress> = HashMap(),

View File

@@ -97,7 +97,7 @@ class TransferProgressView @JvmOverloads constructor(
}
}
fun setDownloading(progress: Float) {
fun setProgress(progress: Float) {
currentState = State.IN_PROGRESS_CANCELABLE
if (cancelClickListener == null) {
Log.i(TAG, "Illegal click listener attached.")
@@ -108,13 +108,6 @@ class TransferProgressView @JvmOverloads constructor(
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

View File

@@ -95,6 +95,8 @@ import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.clicklisteners.AttachmentCancelClickListener;
import org.thoughtcrime.securesms.conversation.clicklisteners.ResendClickListener;
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectCollection;
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
@@ -1083,7 +1085,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
boolean messageRequestAccepted,
boolean allowedToPlayInline)
{
boolean showControls = !messageRecord.isFailed() && !MessageRecordUtil.isScheduled(messageRecord);
boolean showControls = messageRecord.isMediaPending() || (!messageRecord.isFailed() && !MessageRecordUtil.isScheduled(messageRecord));
ViewUtil.setTopMargin(bodyText, readDimen(R.dimen.message_bubble_top_padding));
@@ -1174,8 +1176,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
mediaThumbnailStub.require().setMaximumThumbnailHeight(readDimen(R.dimen.media_bubble_max_height));
mediaThumbnailStub.require().setImageResource(glideRequests, Collections.singletonList(new ImageSlide(linkPreview.getThumbnail().get())), showControls, false);
mediaThumbnailStub.require().setThumbnailClickListener(new LinkPreviewThumbnailClickListener());
mediaThumbnailStub.require().setDownloadClickListener(downloadClickListener);
mediaThumbnailStub.require().setCancelDownloadClickListener(attachmentCancelClickListener);
mediaThumbnailStub.require().setStartTransferClickListener(downloadClickListener);
mediaThumbnailStub.require().setCancelTransferClickListener(attachmentCancelClickListener);
mediaThumbnailStub.require().setPlayVideoClickListener(playVideoClickListener);
mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener);
@@ -1312,8 +1314,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
: R.dimen.media_bubble_max_height));
mediaThumbnailStub.require().setThumbnailClickListener(new ThumbnailClickListener());
mediaThumbnailStub.require().setDownloadClickListener(downloadClickListener);
mediaThumbnailStub.require().setCancelDownloadClickListener(attachmentCancelClickListener);
mediaThumbnailStub.require().setCancelTransferClickListener(attachmentCancelClickListener);
mediaThumbnailStub.require().setPlayVideoClickListener(playVideoClickListener);
mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener);
mediaThumbnailStub.require().setOnClickListener(passthroughClickListener);
@@ -1324,8 +1325,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
false);
if (!messageRecord.isOutgoing()) {
mediaThumbnailStub.require().setConversationColor(getDefaultBubbleColor(hasWallpaper));
mediaThumbnailStub.require().setStartTransferClickListener(downloadClickListener);
} else {
mediaThumbnailStub.require().setConversationColor(Color.TRANSPARENT);
mediaThumbnailStub.require().setStartTransferClickListener(new ResendClickListener(messageRecord));
}
mediaThumbnailStub.require().setBorderless(false);
@@ -1566,7 +1569,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private void setStatusIcons(MessageRecord messageRecord, boolean hasWallpaper) {
bodyText.setCompoundDrawablesWithIntrinsicBounds(0, 0, messageRecord.isKeyExchange() ? R.drawable.ic_menu_login : 0, 0);
if (messageRecord.isFailed()) {
if (!messageRecord.isMediaPending() && messageRecord.isFailed()) {
alertView.setFailed();
} else if (messageRecord.isPendingInsecureSmsFallback()) {
alertView.setPendingApproval();
@@ -2445,25 +2448,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
private class AttachmentCancelClickListener implements SlidesClickedListener {
@Override
public void onClick(View v, List<Slide> slides) {
Log.i(TAG, "onClick() for attachment cancellation");
final JobManager jobManager = ApplicationDependencies.getJobManager();
if (messageRecord.isMmsNotification()) {
Log.i(TAG, "Canceling MMS attachments download");
jobManager.cancel("mms-operation");
} else {
Log.i(TAG, "Canceling push attachment downloads for " + slides.size() + " items");
for (Slide slide : slides) {
final String queue = AttachmentDownloadJob.constructQueueString(((DatabaseAttachment) slide.asAttachment()).attachmentId);
jobManager.cancelAllInQueue(queue);
}
}
}
}
private class PlayVideoClickListener implements SlideClickListener {
private static final float MINIMUM_DOWNLOADED_THRESHOLD = 0.05f;
private View parentView;

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.clicklisteners
import android.view.View
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.AttachmentCompressionJob
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlidesClickedListener
internal class AttachmentCancelClickListener : SlidesClickedListener {
override fun onClick(v: View, slides: List<Slide>) {
Log.i(TAG, "Canceling compression/upload/download jobs for ${slides.size} items")
val jobManager = ApplicationDependencies.getJobManager()
var cancelCount = 0
for (slide in slides) {
val attachmentId = (slide.asAttachment() as DatabaseAttachment).attachmentId
val jobsToCancel = jobManager.find {
when (it.factoryKey) {
AttachmentDownloadJob.KEY -> AttachmentDownloadJob.jobSpecMatchesAttachmentId(it, attachmentId)
AttachmentCompressionJob.KEY -> AttachmentCompressionJob.jobSpecMatchesAttachmentId(it, attachmentId)
AttachmentUploadJob.KEY -> AttachmentUploadJob.jobSpecMatchesAttachmentId(it, attachmentId)
else -> false
}
}
jobsToCancel.forEach {
jobManager.cancel(it.id)
cancelCount++
}
}
Log.i(TAG, "Canceled $cancelCount jobs.")
}
companion object {
private val TAG = Log.tag(AttachmentCancelClickListener::class.java)
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.clicklisteners
import android.view.View
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlidesClickedListener
import org.thoughtcrime.securesms.sms.MessageSender
class ResendClickListener(private val messageRecord: MessageRecord) : SlidesClickedListener {
override fun onClick(v: View?, slides: MutableList<Slide>?) {
if (v == null) {
Log.w(TAG, "Could not resend message, view was null!")
return
}
MessageSender.resend(v.context, messageRecord)
}
companion object {
private val TAG = Log.tag(ResendClickListener::class.java)
}
}

View File

@@ -7,7 +7,6 @@ import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.media3.common.MimeTypes;
import org.greenrobot.eventbus.EventBus;
@@ -23,9 +22,10 @@ import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.MediaStream;
@@ -124,6 +124,21 @@ public final class AttachmentCompressionJob extends BaseJob {
return true;
}
@Override
public void onAdded() {
Log.i(TAG, "onAdded() " + attachmentId.toString());
final AttachmentTable database = SignalDatabase.attachments();
final DatabaseAttachment attachment = database.getAttachment(attachmentId);
final boolean pending = attachment != null && attachment.transferState != AttachmentTable.TRANSFER_PROGRESS_DONE
&& attachment.transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE;
if (pending) {
Log.i(TAG, "onAdded() Marking attachment progress as 'started'");
database.setTransferState(attachment.mmsId, attachmentId, AttachmentTable.TRANSFER_PROGRESS_STARTED);
}
}
@Override
public void onRun() throws Exception {
Log.d(TAG, "Running for: " + attachmentId);
@@ -147,7 +162,20 @@ public final class AttachmentCompressionJob extends BaseJob {
}
@Override
public void onFailure() { }
public void onFailure() {
AttachmentTable database = SignalDatabase.attachments();
DatabaseAttachment databaseAttachment = database.getAttachment(attachmentId);
if (databaseAttachment == null) {
Log.i(TAG, "Could not find attachment in DB for compression job upon failure.");
return;
}
try {
database.setTransferProgressFailed(attachmentId, databaseAttachment.mmsId);
} catch (MmsException e) {
Log.w(TAG, "Error marking attachment as failed upon failed compression.", e);
}
}
@Override
protected boolean onShouldRetry(@NonNull Exception exception) {
@@ -352,6 +380,21 @@ public final class AttachmentCompressionJob extends BaseJob {
result.getHeight());
}
public static boolean jobSpecMatchesAttachmentId(@NonNull JobSpec jobSpec, @NonNull AttachmentId attachmentId) {
if (!KEY.equals(jobSpec.getFactoryKey())) {
return false;
}
final byte[] serializedData = jobSpec.getSerializedData();
if (serializedData == null) {
return false;
}
JsonJobData data = JsonJobData.deserialize(serializedData);
final AttachmentId parsed = new AttachmentId(data.getLong(KEY_ROW_ID), data.getLong(KEY_UNIQUE_ID));
return attachmentId.equals(parsed);
}
public static final class Factory implements Job.Factory<AttachmentCompressionJob> {
@Override
public @NonNull AttachmentCompressionJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) {

View File

@@ -28,6 +28,7 @@ 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.jobmanager.persistence.JobSpec;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.notifications.v2.ConversationId;
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel;
@@ -63,10 +64,10 @@ public final class AttachmentDownloadJob extends BaseJob {
private static final String TAG = Log.tag(AttachmentDownloadJob.class);
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_PART_ROW_ID = "part_row_id";
private static final String KEY_PAR_UNIQUE_ID = "part_unique_id";
private static final String KEY_MANUAL = "part_manual";
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_ROW_ID = "part_row_id";
private static final String KEY_UNIQUE_ID = "part_unique_id";
private static final String KEY_MANUAL = "part_manual";
private final long messageId;
private final long partRowId;
@@ -97,8 +98,8 @@ public final class AttachmentDownloadJob extends BaseJob {
@Override
public @Nullable byte[] serialize() {
return new JsonJobData.Builder().putLong(KEY_MESSAGE_ID, messageId)
.putLong(KEY_PART_ROW_ID, partRowId)
.putLong(KEY_PAR_UNIQUE_ID, partUniqueId)
.putLong(KEY_ROW_ID, partRowId)
.putLong(KEY_UNIQUE_ID, partUniqueId)
.putBoolean(KEY_MANUAL, manual)
.serialize();
}
@@ -318,6 +319,21 @@ public final class AttachmentDownloadJob extends BaseJob {
}
}
public static boolean jobSpecMatchesAttachmentId(@NonNull JobSpec jobSpec, @NonNull AttachmentId attachmentId) {
if (!KEY.equals(jobSpec.getFactoryKey())) {
return false;
}
final byte[] serializedData = jobSpec.getSerializedData();
if (serializedData == null) {
return false;
}
JsonJobData data = JsonJobData.deserialize(serializedData);
final AttachmentId parsed = new AttachmentId(data.getLong(KEY_ROW_ID), data.getLong(KEY_UNIQUE_ID));
return attachmentId.equals(parsed);
}
@VisibleForTesting
static class InvalidPartException extends Exception {
InvalidPartException(String s) {super(s);}
@@ -331,7 +347,7 @@ public final class AttachmentDownloadJob extends BaseJob {
return new AttachmentDownloadJob(parameters,
data.getLong(KEY_MESSAGE_ID),
new AttachmentId(data.getLong(KEY_PART_ROW_ID), data.getLong(KEY_PAR_UNIQUE_ID)),
new AttachmentId(data.getLong(KEY_ROW_ID), data.getLong(KEY_UNIQUE_ID)),
data.getBoolean(KEY_MANUAL));
}
}

View File

@@ -17,12 +17,15 @@ import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.blurhash.BlurHashEncoder
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.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec
import org.thoughtcrime.securesms.jobs.protos.AttachmentUploadJobData
import org.thoughtcrime.securesms.mms.MmsException
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.net.NotPushRegisteredException
import org.thoughtcrime.securesms.recipients.Recipient
@@ -71,6 +74,17 @@ class AttachmentUploadJob private constructor(
val maxPaddedSize = AttachmentCipherStreamUtil.getPlaintextLength(maxCipherTextSize)
return PaddingInputStream.getMaxUnpaddedSize(maxPaddedSize)
}
@JvmStatic
fun jobSpecMatchesAttachmentId(jobSpec: JobSpec, attachmentId: AttachmentId): Boolean {
if (KEY != jobSpec.factoryKey) {
return false
}
val serializedData = jobSpec.serializedData ?: return false
val data = AttachmentUploadJobData.ADAPTER.decode(serializedData)
val parsed = AttachmentId(data.attachmentRowId, data.attachmentUniqueId)
return attachmentId == parsed
}
}
constructor(attachmentId: AttachmentId) : this(
@@ -95,6 +109,25 @@ class AttachmentUploadJob private constructor(
override fun shouldTrace(): Boolean = true
override fun onAdded() {
Log.i(TAG, "onAdded() $attachmentId")
val database = SignalDatabase.attachments
val attachment = database.getAttachment(attachmentId)
if (attachment == null) {
Log.w(TAG, "Could not fetch attachment from database.")
return
}
val pending = attachment.transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && attachment.transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE
if (pending) {
Log.i(TAG, "onAdded() Marking attachment progress as 'started'")
database.setTransferState(attachment.mmsId, attachmentId, AttachmentTable.TRANSFER_PROGRESS_STARTED)
}
}
@Throws(Exception::class)
public override fun onRun() {
if (!Recipient.self().isRegistered) {
@@ -152,8 +185,17 @@ class AttachmentUploadJob private constructor(
}
override fun onFailure() {
if (isCanceled) {
SignalDatabase.attachments.deleteAttachment(attachmentId)
val database = SignalDatabase.attachments
val databaseAttachment = database.getAttachment(attachmentId)
if (databaseAttachment == null) {
Log.i(TAG, "Could not find attachment in DB for upload job upon failure/cancellation.")
return
}
try {
database.setTransferProgressFailed(attachmentId, databaseAttachment.mmsId)
} catch (e: MmsException) {
Log.w(TAG, "Error marking attachment as failed upon failed/canceled upload.", e)
}
}

View File

@@ -161,6 +161,7 @@ public class MediaUploadRepository {
if (result != null) {
Stream.of(result.getJobIds()).forEach(jobManager::cancel);
uploadResults.remove(media);
SignalDatabase.attachments().deleteAttachment(result.getAttachmentId());
}
}