mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 09:49:30 +01:00
Permanent attachment failure.
Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
@@ -119,7 +119,12 @@ public abstract class Attachment {
|
||||
|
||||
public boolean isInProgress() {
|
||||
return transferState != AttachmentDatabase.TRANSFER_PROGRESS_DONE &&
|
||||
transferState != AttachmentDatabase.TRANSFER_PROGRESS_FAILED;
|
||||
transferState != AttachmentDatabase.TRANSFER_PROGRESS_FAILED &&
|
||||
transferState != AttachmentDatabase.TRANSFER_PROGRESS_PERMANENT_FAILURE;
|
||||
}
|
||||
|
||||
public boolean isPermanentlyFailed() {
|
||||
return transferState == AttachmentDatabase.TRANSFER_PROGRESS_PERMANENT_FAILURE;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Px;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.appcompat.widget.AppCompatImageView;
|
||||
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
@@ -34,9 +35,11 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
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.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -62,10 +65,12 @@ public class ThumbnailView extends FrameLayout {
|
||||
private static final int MIN_HEIGHT = 2;
|
||||
private static final int MAX_HEIGHT = 3;
|
||||
|
||||
private ImageView image;
|
||||
private ImageView blurhash;
|
||||
private View playOverlay;
|
||||
private View captionIcon;
|
||||
private final ImageView image;
|
||||
private final ImageView blurhash;
|
||||
private final View playOverlay;
|
||||
private final View captionIcon;
|
||||
private final AppCompatImageView errorImage;
|
||||
|
||||
private OnClickListener parentClickListener;
|
||||
|
||||
private final int[] dimens = new int[2];
|
||||
@@ -97,6 +102,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
this.blurhash = findViewById(R.id.thumbnail_blurhash);
|
||||
this.playOverlay = findViewById(R.id.play_overlay);
|
||||
this.captionIcon = findViewById(R.id.thumbnail_caption_icon);
|
||||
this.errorImage = findViewById(R.id.thumbnail_error);
|
||||
|
||||
super.setOnClickListener(new ThumbnailClickDispatcher());
|
||||
|
||||
@@ -302,6 +308,34 @@ public class ThumbnailView extends FrameLayout {
|
||||
boolean showControls, boolean isPreview,
|
||||
int naturalWidth, int naturalHeight)
|
||||
{
|
||||
if (slide.asAttachment().isPermanentlyFailed()) {
|
||||
this.slide = slide;
|
||||
|
||||
transferControls.ifPresent(c -> c.setVisibility(View.GONE));
|
||||
playOverlay.setVisibility(View.GONE);
|
||||
|
||||
glideRequests.clear(blurhash);
|
||||
blurhash.setImageDrawable(null);
|
||||
|
||||
glideRequests.clear(image);
|
||||
image.setImageDrawable(null);
|
||||
|
||||
int errorImageResource;
|
||||
if (slide instanceof ImageSlide) {
|
||||
errorImageResource = R.drawable.ic_photo_slash_outline_24;
|
||||
} else if (slide instanceof VideoSlide) {
|
||||
errorImageResource = R.drawable.ic_video_slash_outline_24;
|
||||
} else {
|
||||
errorImageResource = R.drawable.ic_error_outline_24;
|
||||
}
|
||||
errorImage.setImageResource(errorImageResource);
|
||||
errorImage.setVisibility(View.VISIBLE);
|
||||
|
||||
return new SettableFuture<>(true);
|
||||
} else {
|
||||
errorImage.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (showControls) {
|
||||
getTransferControls().setSlide(slide);
|
||||
getTransferControls().setDownloadClickListener(new DownloadClickDispatcher());
|
||||
@@ -532,11 +566,13 @@ public class ThumbnailView extends FrameLayout {
|
||||
private class ThumbnailClickDispatcher implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (thumbnailClickListener != null &&
|
||||
slide != null &&
|
||||
slide.asAttachment().getUri() != null &&
|
||||
slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE)
|
||||
{
|
||||
boolean validThumbnail = slide != null &&
|
||||
slide.asAttachment().getUri() != null &&
|
||||
slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE;
|
||||
|
||||
boolean permanentFailure = slide != null && slide.asAttachment().isPermanentlyFailed();
|
||||
|
||||
if (thumbnailClickListener != null && (validThumbnail || permanentFailure)) {
|
||||
thumbnailClickListener.onClick(view, slide);
|
||||
} else if (parentClickListener != null) {
|
||||
parentClickListener.onClick(view);
|
||||
|
||||
@@ -183,15 +183,20 @@ public final class TransferControlView extends FrameLayout {
|
||||
}
|
||||
|
||||
private int getTransferState(@NonNull List<Slide> slides) {
|
||||
int transferState = AttachmentDatabase.TRANSFER_PROGRESS_DONE;
|
||||
int transferState = AttachmentDatabase.TRANSFER_PROGRESS_DONE;
|
||||
boolean allFailed = true;
|
||||
|
||||
for (Slide slide : slides) {
|
||||
if (slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_PENDING && transferState == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
|
||||
transferState = slide.getTransferState();
|
||||
} else {
|
||||
transferState = Math.max(transferState, slide.getTransferState());
|
||||
if (slide.getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_PERMANENT_FAILURE) {
|
||||
allFailed = false;
|
||||
if (slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_PENDING && transferState == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
|
||||
transferState = slide.getTransferState();
|
||||
} else {
|
||||
transferState = Math.max(transferState, slide.getTransferState());
|
||||
}
|
||||
}
|
||||
}
|
||||
return transferState;
|
||||
return allFailed ? AttachmentDatabase.TRANSFER_PROGRESS_PERMANENT_FAILURE : transferState;
|
||||
}
|
||||
|
||||
private String getDownloadText(@NonNull List<Slide> slides) {
|
||||
|
||||
@@ -64,6 +64,7 @@ import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
@@ -2348,6 +2349,25 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
Log.w(TAG, "No activity existed to view the media.");
|
||||
Toast.makeText(context, R.string.ConversationItem_unable_to_open_media, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
} else if (slide.asAttachment().isPermanentlyFailed()) {
|
||||
String failedMessage;
|
||||
|
||||
if (slide instanceof ImageSlide) {
|
||||
failedMessage = messageRecord.isOutgoing() ? context.getString(R.string.ConversationItem_cant_download_image_you_will_need_to_send_it_again)
|
||||
: context.getString(R.string.ConversationItem_cant_download_image_s_will_need_to_send_it_again, messageRecord.getIndividualRecipient().getShortDisplayName(context));
|
||||
} else if (slide instanceof VideoSlide) {
|
||||
failedMessage = messageRecord.isOutgoing() ? context.getString(R.string.ConversationItem_cant_download_video_you_will_need_to_send_it_again)
|
||||
: context.getString(R.string.ConversationItem_cant_download_video_s_will_need_to_send_it_again, messageRecord.getIndividualRecipient().getShortDisplayName(context));
|
||||
} else {
|
||||
failedMessage = messageRecord.isOutgoing() ? context.getString(R.string.ConversationItem_cant_download_message_you_will_need_to_send_it_again)
|
||||
: context.getString(R.string.ConversationItem_cant_download_message_s_will_need_to_send_it_again, messageRecord.getIndividualRecipient().getShortDisplayName(context));
|
||||
}
|
||||
|
||||
new MaterialAlertDialogBuilder(getContext())
|
||||
.setMessage(failedMessage)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,10 +127,11 @@ public class AttachmentDatabase extends Database {
|
||||
|
||||
private static final String DIRECTORY = "parts";
|
||||
|
||||
public static final int TRANSFER_PROGRESS_DONE = 0;
|
||||
public static final int TRANSFER_PROGRESS_STARTED = 1;
|
||||
public static final int TRANSFER_PROGRESS_PENDING = 2;
|
||||
public static final int TRANSFER_PROGRESS_FAILED = 3;
|
||||
public static final int TRANSFER_PROGRESS_DONE = 0;
|
||||
public static final int TRANSFER_PROGRESS_STARTED = 1;
|
||||
public static final int TRANSFER_PROGRESS_PENDING = 2;
|
||||
public static final int TRANSFER_PROGRESS_FAILED = 3;
|
||||
public static final int TRANSFER_PROGRESS_PERMANENT_FAILURE = 4;
|
||||
|
||||
public static final long PREUPLOAD_MESSAGE_ID = -8675309;
|
||||
|
||||
@@ -233,6 +234,17 @@ public class AttachmentDatabase extends Database {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_FAILED);
|
||||
|
||||
database.update(TABLE_NAME, values, PART_ID_WHERE + " AND " + TRANSFER_STATE + " < " + TRANSFER_PROGRESS_PERMANENT_FAILURE, attachmentId.toStrings());
|
||||
notifyConversationListeners(SignalDatabase.mms().getThreadIdForMessage(mmsId));
|
||||
}
|
||||
|
||||
public void setTransferProgressPermanentFailure(AttachmentId attachmentId, long mmsId)
|
||||
throws MmsException
|
||||
{
|
||||
SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(TRANSFER_STATE, TRANSFER_PROGRESS_PERMANENT_FAILURE);
|
||||
|
||||
database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings());
|
||||
notifyConversationListeners(SignalDatabase.mms().getThreadIdForMessage(mmsId));
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.annotation.VisibleForTesting;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.Hex;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.InvalidMacException;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
@@ -107,7 +108,8 @@ public final class AttachmentDownloadJob extends BaseJob {
|
||||
final AttachmentDatabase database = SignalDatabase.attachments();
|
||||
final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId);
|
||||
final DatabaseAttachment attachment = database.getAttachment(attachmentId);
|
||||
final boolean pending = attachment != null && attachment.getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_DONE;
|
||||
final boolean pending = attachment != null && attachment.getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_DONE
|
||||
&& attachment.getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_PERMANENT_FAILURE;
|
||||
|
||||
if (pending && (manual || AttachmentUtil.isAutoDownloadPermitted(context, attachment))) {
|
||||
Log.i(TAG, "onAdded() Marking attachment progress as 'started'");
|
||||
@@ -136,6 +138,11 @@ public final class AttachmentDownloadJob extends BaseJob {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attachment.isPermanentlyFailed()) {
|
||||
Log.w(TAG, "Attachment was marked as a permanent failure. Refusing to download.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!attachment.isInProgress()) {
|
||||
Log.w(TAG, "Attachment was already downloaded.");
|
||||
return;
|
||||
@@ -194,9 +201,17 @@ public final class AttachmentDownloadJob extends BaseJob {
|
||||
} else {
|
||||
throw new IOException("Failed to delete temp download file following range exception");
|
||||
}
|
||||
} catch (InvalidPartException | NonSuccessfulResponseCodeException | InvalidMessageException | MmsException | MissingConfigurationException e) {
|
||||
} catch (InvalidPartException | NonSuccessfulResponseCodeException | MmsException | MissingConfigurationException e) {
|
||||
Log.w(TAG, "Experienced exception while trying to download an attachment.", e);
|
||||
markFailed(messageId, attachmentId);
|
||||
} catch (InvalidMessageException e) {
|
||||
Log.w(TAG, "Experienced an InvalidMessageException while trying to download an attachment.", e);
|
||||
if (e.getCause() instanceof InvalidMacException) {
|
||||
Log.w(TAG, "Detected an invalid mac. Treating as a permanent failure.");
|
||||
markPermanentlyFailed(messageId, attachmentId);
|
||||
} else {
|
||||
markFailed(messageId, attachmentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,6 +277,15 @@ public final class AttachmentDownloadJob extends BaseJob {
|
||||
}
|
||||
}
|
||||
|
||||
private void markPermanentlyFailed(long messageId, AttachmentId attachmentId) {
|
||||
try {
|
||||
AttachmentDatabase database = SignalDatabase.attachments();
|
||||
database.setTransferProgressPermanentFailure(attachmentId, messageId);
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting static class InvalidPartException extends Exception {
|
||||
InvalidPartException(String s) {super(s);}
|
||||
InvalidPartException(Exception e) {super(e);}
|
||||
|
||||
@@ -8,12 +8,12 @@ import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
@@ -24,10 +24,6 @@ class StorySlateView @JvmOverloads constructor(
|
||||
attrs: AttributeSet? = null
|
||||
) : FrameLayout(context, attrs) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(StorySlateView::class.java)
|
||||
}
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
var state: State = State.HIDDEN
|
||||
@@ -43,10 +39,10 @@ class StorySlateView @JvmOverloads constructor(
|
||||
private val loadingSpinner: View = findViewById(R.id.loading_spinner)
|
||||
private val errorCircle: View = findViewById(R.id.error_circle)
|
||||
private val errorBackground: View = findViewById(R.id.stories_error_background)
|
||||
private val unavailableText: View = findViewById(R.id.unavailable)
|
||||
private val unavailableText: TextView = findViewById(R.id.unavailable)
|
||||
private val errorText: TextView = findViewById(R.id.error_text)
|
||||
|
||||
fun moveToState(state: State, postId: Long) {
|
||||
fun moveToState(state: State, postId: Long, sender: Recipient? = null) {
|
||||
if (this.state == state && this.postId == postId) {
|
||||
return
|
||||
}
|
||||
@@ -61,7 +57,7 @@ class StorySlateView @JvmOverloads constructor(
|
||||
State.LOADING -> moveToProgressState(State.LOADING)
|
||||
State.ERROR -> moveToErrorState()
|
||||
State.RETRY -> moveToProgressState(State.RETRY)
|
||||
State.NOT_FOUND -> moveToNotFoundState()
|
||||
State.NOT_FOUND, State.FAILED -> moveToNotFoundState(state, sender)
|
||||
State.HIDDEN -> moveToHiddenState()
|
||||
}
|
||||
|
||||
@@ -106,8 +102,8 @@ class StorySlateView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun moveToNotFoundState() {
|
||||
state = State.NOT_FOUND
|
||||
private fun moveToNotFoundState(state: State, sender: Recipient?) {
|
||||
this.state = state
|
||||
visible = true
|
||||
background.visible = true
|
||||
loadingSpinner.visible = false
|
||||
@@ -115,6 +111,12 @@ class StorySlateView @JvmOverloads constructor(
|
||||
errorBackground.visible = false
|
||||
unavailableText.visible = true
|
||||
errorText.visible = false
|
||||
|
||||
if (state == State.FAILED && sender != null) {
|
||||
unavailableText.text = context.getString(R.string.StorySlateView__cant_download_story_s_will_need_to_share_it_again, sender.getShortDisplayName(context))
|
||||
} else {
|
||||
unavailableText.setText(R.string.StorySlateView__this_story_is_no_longer_available)
|
||||
}
|
||||
}
|
||||
|
||||
private fun moveToHiddenState() {
|
||||
@@ -155,7 +157,8 @@ class StorySlateView @JvmOverloads constructor(
|
||||
ERROR(1, true),
|
||||
RETRY(2, true),
|
||||
NOT_FOUND(3, false),
|
||||
HIDDEN(4, false);
|
||||
HIDDEN(4, false),
|
||||
FAILED(5, false);
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: Int): State {
|
||||
|
||||
@@ -736,6 +736,11 @@ class StoryViewerPageFragment :
|
||||
sharedViewModel.setContentIsReady()
|
||||
viewModel.setIsDisplayingSlate(true)
|
||||
}
|
||||
AttachmentDatabase.TRANSFER_PROGRESS_PERMANENT_FAILURE -> {
|
||||
storySlate.moveToState(StorySlateView.State.FAILED, post.id, post.sender)
|
||||
sharedViewModel.setContentIsReady()
|
||||
viewModel.setIsDisplayingSlate(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user