Permanent attachment failure.

Co-authored-by: Cody Henthorne <cody@signal.org>
main
Alex Hart 2022-10-27 17:33:33 -03:00 zatwierdzone przez GitHub
rodzic 9ef58516e2
commit 6f46e9000b
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
14 zmienionych plików z 377 dodań i 43 usunięć

Wyświetl plik

@ -0,0 +1,170 @@
package org.thoughtcrime.securesms.conversation
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.ThreadUtil
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import java.util.Optional
/**
* Helper test for rendering conversation items for preview.
*/
@RunWith(AndroidJUnit4::class)
@Ignore("For testing/previewing manually, no assertions")
class ConversationItemPreviewer {
@get:Rule
val harness = SignalActivityRule(othersCount = 10)
@Test
fun testShowLongName() {
val other: Recipient = Recipient.resolved(harness.others.first())
SignalDatabase.recipients.setProfileName(other.id, ProfileName.fromParts("Seef", "$$$"))
insertFailedMediaMessage(other = other, attachmentCount = 1)
insertFailedMediaMessage(other = other, attachmentCount = 2)
insertFailedMediaMessage(other = other, body = "Test", attachmentCount = 1)
// insertFailedOutgoingMediaMessage(other = other, body = "Test", attachmentCount = 1)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
val scenario: ActivityScenario<ConversationActivity> = harness.launchActivity { putExtra("recipient_id", other.id.serialize()) }
scenario.onActivity {
}
// Uncomment to make dialog stay on screen, otherwise will show/dismiss immediately
// ThreadUtil.sleep(45000)
}
private fun insertMediaMessage(other: Recipient, body: String? = null, attachmentCount: Int = 1) {
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
attachment()
}
val message = IncomingMediaMessage(
from = other.id,
body = body,
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = System.currentTimeMillis(),
receivedTimeMillis = System.currentTimeMillis(),
attachments = PointerAttachment.forPointers(Optional.of(attachments)),
)
SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
ThreadUtil.sleep(1)
}
private fun insertFailedMediaMessage(other: Recipient, body: String? = null, attachmentCount: Int = 1) {
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
attachment()
}
val message = IncomingMediaMessage(
from = other.id,
body = body,
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = System.currentTimeMillis(),
receivedTimeMillis = System.currentTimeMillis(),
attachments = PointerAttachment.forPointers(Optional.of(attachments)),
)
val insert = SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
SignalDatabase.attachments.getAttachmentsForMessage(insert.messageId).forEachIndexed { index, attachment ->
// if (index != 1) {
SignalDatabase.attachments.setTransferProgressPermanentFailure(attachment.attachmentId, insert.messageId)
// } else {
// SignalDatabase.attachments.setTransferState(insert.messageId, attachment, TRANSFER_PROGRESS_STARTED)
// }
}
ThreadUtil.sleep(1)
}
private fun insertFailedOutgoingMediaMessage(other: Recipient, body: String? = null, attachmentCount: Int = 1) {
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
attachment()
}
val message = OutgoingMediaMessage(
other,
body,
PointerAttachment.forPointers(Optional.of(attachments)),
System.currentTimeMillis(),
-1,
0,
false,
ThreadDatabase.DistributionTypes.DEFAULT,
StoryType.NONE,
null,
false,
null,
emptyList(),
emptyList(),
emptyList(),
emptySet(),
emptySet(),
null
)
val insert = SignalDatabase.mms.insertMessageOutbox(
OutgoingSecureMediaMessage(message),
SignalDatabase.threads.getOrCreateThreadIdFor(other),
false,
null
)
SignalDatabase.attachments.getAttachmentsForMessage(insert).forEachIndexed { index, attachment ->
SignalDatabase.attachments.setTransferProgressPermanentFailure(attachment.attachmentId, insert)
}
ThreadUtil.sleep(1)
}
private fun attachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
ReleaseChannel.CDN_NUMBER,
SignalServiceAttachmentRemoteId.from(""),
"image/webp",
null,
Optional.empty(),
Optional.empty(),
1024,
1024,
Optional.empty(),
Optional.of("/not-there.jpg"),
false,
false,
false,
Optional.empty(),
Optional.empty(),
System.currentTimeMillis()
)
}
}

Wyświetl plik

@ -110,6 +110,7 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), IdentityKeyUtil.generateIdentityKeyPair().publicKey)
others += recipientId
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M2.844,6.849C2.829,6.967 2.817,7.088 2.807,7.212C2.75,7.909 2.75,8.775 2.75,9.867V14.133C2.75,15.225 2.75,16.091 2.807,16.788C2.865,17.502 2.987,18.105 3.268,18.656C3.723,19.55 4.45,20.277 5.344,20.732C5.895,21.013 6.498,21.135 7.212,21.193C7.909,21.25 8.775,21.25 9.867,21.25H14.133C15.225,21.25 16.091,21.25 16.788,21.193C16.912,21.183 17.033,21.171 17.151,21.156L15.735,19.74C15.277,19.75 14.741,19.75 14.1,19.75H9.9C8.768,19.75 7.963,19.749 7.334,19.698C6.713,19.647 6.329,19.551 6.025,19.396C5.413,19.084 4.916,18.587 4.604,17.976C4.449,17.671 4.353,17.287 4.302,16.666C4.291,16.528 4.282,16.381 4.275,16.224L7.297,12.984C7.659,12.596 8.207,12.492 8.668,12.673L7.253,11.258C6.865,11.402 6.503,11.636 6.2,11.961L4.25,14.052V9.9C4.25,9.259 4.25,8.723 4.26,8.265L2.844,6.849ZM13.202,11.197L14.251,10.071C15.338,8.903 17.188,8.903 18.276,10.071L19.75,11.653V9.9C19.75,8.768 19.749,7.963 19.698,7.334C19.647,6.713 19.551,6.329 19.396,6.025C19.084,5.413 18.587,4.916 17.976,4.604C17.671,4.449 17.287,4.353 16.666,4.302C16.037,4.251 15.233,4.25 14.1,4.25H9.9C8.768,4.25 7.963,4.251 7.334,4.302C6.967,4.332 6.682,4.378 6.448,4.443L5.297,3.292C5.312,3.284 5.328,3.276 5.344,3.268C5.895,2.987 6.498,2.865 7.212,2.807C7.909,2.75 8.775,2.75 9.867,2.75H14.133C15.225,2.75 16.091,2.75 16.788,2.807C17.502,2.865 18.105,2.987 18.656,3.268C19.55,3.723 20.277,4.45 20.732,5.344C21.013,5.895 21.135,6.498 21.193,7.212C21.25,7.909 21.25,8.775 21.25,9.867V14.133C21.25,15.225 21.25,16.091 21.193,16.788C21.135,17.502 21.013,18.105 20.732,18.656C20.724,18.672 20.716,18.688 20.708,18.703L19.557,17.552C19.622,17.318 19.668,17.033 19.698,16.666C19.749,16.037 19.75,15.233 19.75,14.1V13.854L17.178,11.093C16.684,10.562 15.843,10.562 15.349,11.093L14.263,12.258L13.202,11.197Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M1.47,2.47C1.763,2.177 2.237,2.177 2.53,2.47L21.53,21.47C21.823,21.763 21.823,22.237 21.53,22.53C21.237,22.823 20.763,22.823 20.47,22.53L1.47,3.53C1.177,3.237 1.177,2.763 1.47,2.47Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

Wyświetl plik

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M2.813,7.141C2.811,7.165 2.809,7.188 2.807,7.212C2.75,7.909 2.75,8.775 2.75,9.867V14.133C2.75,15.225 2.75,16.091 2.807,16.788C2.865,17.502 2.987,18.105 3.268,18.656C3.723,19.55 4.45,20.277 5.344,20.732C5.895,21.013 6.498,21.135 7.212,21.193C7.909,21.25 8.775,21.25 9.867,21.25H14.133C15.225,21.25 16.091,21.25 16.788,21.193C16.812,21.191 16.835,21.189 16.859,21.187L15.417,19.745C15.033,19.75 14.598,19.75 14.1,19.75H9.9C8.768,19.75 7.963,19.749 7.334,19.698C6.713,19.647 6.329,19.551 6.025,19.396C5.413,19.084 4.916,18.587 4.604,17.976C4.449,17.671 4.353,17.287 4.302,16.666C4.251,16.037 4.25,15.233 4.25,14.1V9.9C4.25,9.402 4.25,8.967 4.255,8.583L2.813,7.141ZM8.625,12.953V15.031C8.625,15.705 9.354,16.126 9.938,15.789L10.903,15.231L8.625,12.953ZM14.489,13.161L9.424,8.096C9.594,8.081 9.772,8.115 9.938,8.211L15.188,11.242C15.771,11.579 15.771,12.421 15.188,12.758L14.489,13.161ZM19.364,18.036C19.375,18.016 19.385,17.996 19.396,17.976C19.551,17.671 19.647,17.287 19.698,16.666C19.749,16.037 19.75,15.233 19.75,14.1V9.9C19.75,8.768 19.749,7.963 19.698,7.334C19.647,6.713 19.551,6.329 19.396,6.025C19.084,5.413 18.587,4.916 17.976,4.604C17.671,4.449 17.287,4.353 16.666,4.302C16.037,4.251 15.233,4.25 14.1,4.25H9.9C8.768,4.25 7.963,4.251 7.334,4.302C6.713,4.353 6.329,4.449 6.025,4.604C6.004,4.615 5.984,4.625 5.964,4.636L4.872,3.543C5.023,3.443 5.181,3.351 5.344,3.268C5.895,2.987 6.498,2.865 7.212,2.807C7.909,2.75 8.775,2.75 9.867,2.75H14.133C15.225,2.75 16.091,2.75 16.788,2.807C17.502,2.865 18.105,2.987 18.656,3.268C19.55,3.723 20.277,4.45 20.732,5.344C21.013,5.895 21.135,6.498 21.193,7.212C21.25,7.909 21.25,8.775 21.25,9.867V14.133C21.25,15.225 21.25,16.091 21.193,16.788C21.135,17.502 21.013,18.105 20.732,18.656C20.649,18.819 20.557,18.977 20.457,19.128L19.364,18.036Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M1.47,2.97C1.763,2.677 2.237,2.677 2.53,2.97L21.03,21.47C21.323,21.763 21.323,22.237 21.03,22.53C20.737,22.823 20.263,22.823 19.97,22.53L1.47,4.03C1.177,3.737 1.177,3.263 1.47,2.97Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

Wyświetl plik

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
tools:viewBindingIgnore="true"
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
tools:parentTag="android.widget.FrameLayout"
tools:viewBindingIgnore="true">
<ImageView
android:id="@+id/thumbnail_blurhash"
@ -11,9 +11,9 @@
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:clickable="false"
android:contentDescription="@string/conversation_item__mms_image_description"
android:longClickable="false"
android:scaleType="fitCenter"
android:contentDescription="@string/conversation_item__mms_image_description" />
android:scaleType="fitCenter" />
<ImageView
android:id="@+id/thumbnail_image"
@ -21,26 +21,37 @@
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:clickable="false"
android:contentDescription="@string/conversation_item__mms_image_description"
android:longClickable="false"
android:scaleType="fitCenter"
android:contentDescription="@string/conversation_item__mms_image_description" />
android:scaleType="fitCenter" />
<ImageView
android:id="@+id/thumbnail_caption_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/ThumbnailView_Has_a_caption_description"
android:padding="6dp"
android:src="@drawable/ic_caption_28"
android:visibility="gone"
android:contentDescription="@string/ThumbnailView_Has_a_caption_description"
tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/thumbnail_error"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center"
android:longClickable="false"
android:visibility="gone"
app:srcCompat="@drawable/ic_error_outline_24"
app:tint="@color/signal_colorOnSurface"
tools:visibility="visible" />
<FrameLayout
android:id="@+id/play_overlay"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/circle_white"
android:layout_gravity="center"
android:background="@drawable/circle_white"
android:longClickable="false"
android:visibility="gone"
tools:visibility="visible">

Wyświetl plik

@ -297,6 +297,18 @@
<string name="ConversationItem_pending">&#160; Pending</string>
<string name="ConversationItem_this_message_was_deleted">This message was deleted.</string>
<string name="ConversationItem_you_deleted_this_message">You deleted this message.</string>
<!-- Dialog error message shown when user can't download a message from someone else due to a permanent failure (e.g., unable to decrypt), placeholder is other's name -->
<string name="ConversationItem_cant_download_message_s_will_need_to_send_it_again">Can\'t download message. %1$s will need to send it again.</string>
<!-- Dialog error message shown when user can't download an image message from someone else due to a permanent failure (e.g., unable to decrypt), placeholder is other's name -->
<string name="ConversationItem_cant_download_image_s_will_need_to_send_it_again">Can\'t download image. %1$s will need to send it again.</string>
<!-- Dialog error message shown when user can't download a video message from someone else due to a permanent failure (e.g., unable to decrypt), placeholder is other's name -->
<string name="ConversationItem_cant_download_video_s_will_need_to_send_it_again">Can\'t download video. %1$s will need to send it again.</string>
<!-- Dialog error message shown when user can't download a their own message via a linked device due to a permanent failure (e.g., unable to decrypt) -->
<string name="ConversationItem_cant_download_message_you_will_need_to_send_it_again">Can\'t download message. You will need to send it again.</string>
<!-- Dialog error message shown when user can't download a their own image message via a linked device due to a permanent failure (e.g., unable to decrypt) -->
<string name="ConversationItem_cant_download_image_you_will_need_to_send_it_again">Can\'t download image. You will need to send it again.</string>
<!-- Dialog error message shown when user can't download a their own video message via a linked device due to a permanent failure (e.g., unable to decrypt) -->
<string name="ConversationItem_cant_download_video_you_will_need_to_send_it_again">Can\'t download video. You will need to send it again.</string>
<!-- ConversationActivity -->
<string name="ConversationActivity_add_attachment">Add attachment</string>
@ -5093,6 +5105,8 @@
<string name="StoryDirectReplyDialogFragment__sending_reply">Sending reply…</string>
<!-- Displayed in the viewer when a story is no longer available -->
<string name="StorySlateView__this_story_is_no_longer_available">This story is no longer available.</string>
<!-- Displayed in the viewer when a story has permanently failed to download. -->
<string name="StorySlateView__cant_download_story_s_will_need_to_share_it_again">Can\'t download story. %1$s will need to share it again.</string>
<!-- Displayed in the viewer when the network is not available -->
<string name="StorySlateView__no_internet_connection">No Internet Connection</string>
<!-- Displayed in the viewer when network is available but content could not be downloaded -->