session-android/src/org/thoughtcrime/securesms/jobs/PushSendJob.java

292 lines
14 KiB
Java
Raw Normal View History

package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.support.annotation.NonNull;
2018-12-08 03:31:39 +01:00
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.annimon.stream.Stream;
2017-02-18 05:27:11 +01:00
import org.greenrobot.eventbus.EventBus;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.libsignal.metadata.certificate.SenderCertificate;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.TextSecureExpiredException;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.events.PartProgressEvent;
2019-03-28 16:56:35 +01:00
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
2018-08-01 17:09:24 +02:00
import org.thoughtcrime.securesms.logging.Log;
2018-02-07 23:01:37 +01:00
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
2014-12-12 10:03:24 +01:00
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
2018-12-08 03:31:39 +01:00
import org.thoughtcrime.securesms.util.Base64;
2018-02-07 23:01:37 +01:00
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
2018-12-08 03:31:39 +01:00
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
2018-12-08 03:31:39 +01:00
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
2018-02-07 23:01:37 +01:00
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
2019-01-15 09:41:05 +01:00
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
2018-02-07 23:01:37 +01:00
import java.io.ByteArrayInputStream;
2014-12-12 10:03:24 +01:00
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public abstract class PushSendJob extends SendJob {
private static final String TAG = PushSendJob.class.getSimpleName();
private static final long CERTIFICATE_EXPIRATION_BUFFER = TimeUnit.DAYS.toMillis(1);
2019-03-28 16:56:35 +01:00
protected PushSendJob(Job.Parameters parameters) {
super(parameters);
}
2019-03-28 16:56:35 +01:00
protected static Job.Parameters constructParameters(Address destination) {
return new Parameters.Builder()
.setQueue(destination.serialize())
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build();
}
@Override
protected final void onSend() throws Exception {
if (TextSecurePreferences.getSignedPreKeyFailureCount(context) > 5) {
ApplicationContext.getInstance(context)
.getJobManager()
2019-03-28 16:56:35 +01:00
.add(new RotateSignedPreKeyJob());
throw new TextSecureExpiredException("Too many signed prekey rotation failures");
}
2018-02-02 04:22:48 +01:00
onPushSend();
}
@Override
public void onRetry() {
super.onRetry();
Log.i(TAG, "onRetry()");
2019-03-28 16:56:35 +01:00
if (getRunAttempt() > 1) {
Log.i(TAG, "Scheduling service outage detection job.");
2019-03-28 16:56:35 +01:00
ApplicationContext.getInstance(context).getJobManager().add(new ServiceOutageDetectionJob());
}
}
protected Optional<byte[]> getProfileKey(@NonNull Recipient recipient) {
if (!recipient.resolve().isSystemContact() && !recipient.resolve().isProfileSharing()) {
return Optional.absent();
}
return Optional.of(ProfileKeyUtil.getProfileKey(context));
}
protected SignalServiceAddress getPushAddress(Address address) {
String relay = null;
return new SignalServiceAddress(address.toPhoneString(), Optional.fromNullable(relay));
}
protected List<SignalServiceAttachment> getAttachmentsFor(List<Attachment> parts) {
List<SignalServiceAttachment> attachments = new LinkedList<>();
for (final Attachment attachment : parts) {
SignalServiceAttachment converted = getAttachmentFor(attachment);
if (converted != null) {
attachments.add(converted);
}
}
return attachments;
}
protected SignalServiceAttachment getAttachmentFor(Attachment attachment) {
try {
if (attachment.getDataUri() == null || attachment.getSize() == 0) throw new IOException("Assertion failed, outgoing attachment has no data!");
InputStream is = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
return SignalServiceAttachment.newStreamBuilder()
.withStream(is)
.withContentType(attachment.getContentType())
.withLength(attachment.getSize())
.withFileName(attachment.getFileName())
.withVoiceNote(attachment.isVoiceNote())
.withWidth(attachment.getWidth())
.withHeight(attachment.getHeight())
2018-11-09 08:33:37 +01:00
.withCaption(attachment.getCaption())
.withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)))
.build();
} catch (IOException ioe) {
Log.w(TAG, "Couldn't open attachment", ioe);
}
return null;
}
protected @NonNull List<SignalServiceAttachment> getAttachmentPointersFor(List<Attachment> attachments) {
2018-12-08 03:31:39 +01:00
return Stream.of(attachments).map(this::getAttachmentPointerFor).filter(a -> a != null).toList();
}
protected @Nullable SignalServiceAttachment getAttachmentPointerFor(Attachment attachment) {
if (TextUtils.isEmpty(attachment.getLocation())) {
Log.w(TAG, "empty content id");
return null;
}
if (TextUtils.isEmpty(attachment.getKey())) {
Log.w(TAG, "empty encrypted key");
return null;
}
try {
long id = Long.parseLong(attachment.getLocation());
byte[] key = Base64.decode(attachment.getKey());
return new SignalServiceAttachmentPointer(id,
attachment.getContentType(),
key,
Optional.of(Util.toIntExact(attachment.getSize())),
Optional.absent(),
attachment.getWidth(),
attachment.getHeight(),
Optional.fromNullable(attachment.getDigest()),
Optional.fromNullable(attachment.getFileName()),
attachment.isVoiceNote(),
Optional.fromNullable(attachment.getCaption()));
} catch (IOException | ArithmeticException e) {
Log.w(TAG, e);
return null;
}
}
protected static void notifyMediaMessageDeliveryFailed(Context context, long messageId) {
long threadId = DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId);
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
if (threadId != -1 && recipient != null) {
MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId);
}
}
2018-02-07 23:01:37 +01:00
protected Optional<SignalServiceDataMessage.Quote> getQuoteFor(OutgoingMediaMessage message) {
if (message.getOutgoingQuote() == null) return Optional.absent();
long quoteId = message.getOutgoingQuote().getId();
String quoteBody = message.getOutgoingQuote().getText();
Address quoteAuthor = message.getOutgoingQuote().getAuthor();
List<SignalServiceDataMessage.Quote.QuotedAttachment> quoteAttachments = new LinkedList<>();
2018-02-07 23:01:37 +01:00
for (Attachment attachment : message.getOutgoingQuote().getAttachments()) {
BitmapUtil.ScaleResult thumbnailData = null;
SignalServiceAttachment thumbnail = null;
2018-02-07 23:01:37 +01:00
try {
if (MediaUtil.isImageType(attachment.getContentType()) && attachment.getDataUri() != null) {
thumbnailData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getDataUri()), 100, 100, 500 * 1024);
2018-02-07 23:01:37 +01:00
} else if (MediaUtil.isVideoType(attachment.getContentType()) && attachment.getThumbnailUri() != null) {
thumbnailData = BitmapUtil.createScaledBytes(context, new DecryptableStreamUriLoader.DecryptableUri(attachment.getThumbnailUri()), 100, 100, 500 * 1024);
2018-02-07 23:01:37 +01:00
}
if (thumbnailData != null) {
thumbnail = SignalServiceAttachment.newStreamBuilder()
.withContentType("image/jpeg")
.withWidth(thumbnailData.getWidth())
.withHeight(thumbnailData.getHeight())
.withLength(thumbnailData.getBitmap().length)
.withStream(new ByteArrayInputStream(thumbnailData.getBitmap()))
.build();
2018-02-07 23:01:37 +01:00
}
quoteAttachments.add(new SignalServiceDataMessage.Quote.QuotedAttachment(attachment.getContentType(),
attachment.getFileName(),
thumbnail));
2018-02-07 23:01:37 +01:00
} catch (BitmapDecodingException e) {
Log.w(TAG, e);
}
}
return Optional.of(new SignalServiceDataMessage.Quote(quoteId, new SignalServiceAddress(quoteAuthor.serialize()), quoteBody, quoteAttachments));
}
List<SharedContact> getSharedContactsFor(OutgoingMediaMessage mediaMessage) {
List<SharedContact> sharedContacts = new LinkedList<>();
for (Contact contact : mediaMessage.getSharedContacts()) {
SharedContact.Builder builder = ContactModelMapper.localToRemoteBuilder(contact);
SharedContact.Avatar avatar = null;
if (contact.getAvatar() != null && contact.getAvatar().getAttachment() != null) {
avatar = SharedContact.Avatar.newBuilder().withAttachment(getAttachmentFor(contact.getAvatarAttachment()))
.withProfileFlag(contact.getAvatar().isProfile())
.build();
}
builder.setAvatar(avatar);
sharedContacts.add(builder.build());
}
return sharedContacts;
}
2018-02-07 23:01:37 +01:00
2019-01-15 09:41:05 +01:00
List<Preview> getPreviewsFor(OutgoingMediaMessage mediaMessage) {
return Stream.of(mediaMessage.getLinkPreviews()).map(lp -> {
SignalServiceAttachment attachment = lp.getThumbnail().isPresent() ? getAttachmentPointerFor(lp.getThumbnail().get()) : null;
return new Preview(lp.getUrl(), lp.getTitle(), Optional.fromNullable(attachment));
}).toList();
}
protected void rotateSenderCertificateIfNecessary() throws IOException {
try {
byte[] certificateBytes = TextSecurePreferences.getUnidentifiedAccessCertificate(context);
if (certificateBytes == null) {
throw new InvalidCertificateException("No certificate was present.");
}
SenderCertificate certificate = new SenderCertificate(certificateBytes);
if (System.currentTimeMillis() > (certificate.getExpiration() - CERTIFICATE_EXPIRATION_BUFFER)) {
throw new InvalidCertificateException("Certificate is expired, or close to it. Expires on: " + certificate.getExpiration() + ", currently: " + System.currentTimeMillis());
}
Log.d(TAG, "Certificate is valid.");
} catch (InvalidCertificateException e) {
Log.w(TAG, "Certificate was invalid at send time. Fetching a new one.", e);
2018-11-27 21:34:42 +01:00
RotateCertificateJob certificateJob = new RotateCertificateJob(context);
ApplicationContext.getInstance(context).injectDependencies(certificateJob);
certificateJob.onRun();
}
}
protected SignalServiceSyncMessage buildSelfSendSyncMessage(@NonNull Context context, @NonNull SignalServiceDataMessage message, Optional<UnidentifiedAccessPair> syncAccess) {
String localNumber = TextSecurePreferences.getLocalNumber(context);
SentTranscriptMessage transcript = new SentTranscriptMessage(localNumber,
message.getTimestamp(),
message,
message.getExpiresInSeconds(),
Collections.singletonMap(localNumber, syncAccess.isPresent()));
return SignalServiceSyncMessage.forSentTranscript(transcript);
}
2018-02-02 04:22:48 +01:00
protected abstract void onPushSend() throws Exception;
}