Permanent attachment failure.

Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
Alex Hart
2022-10-27 17:33:33 -03:00
committed by GitHub
parent 9ef58516e2
commit 6f46e9000b
14 changed files with 377 additions and 43 deletions

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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();
}
}
}

View File

@@ -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));
}

View File

@@ -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);}

View File

@@ -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 {

View File

@@ -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)
}
}
}