2014-11-08 19:35:58 +00:00
package org.thoughtcrime.securesms.jobs ;
import android.content.Context ;
2019-04-17 14:21:30 +00:00
import android.graphics.Bitmap ;
2017-08-22 17:44:04 +00:00
import android.support.annotation.NonNull ;
2018-12-08 02:31:39 +00:00
import android.support.annotation.Nullable ;
import android.text.TextUtils ;
import com.annimon.stream.Stream ;
2014-11-08 19:35:58 +00:00
2017-02-18 04:27:11 +00:00
import org.greenrobot.eventbus.EventBus ;
2018-11-22 01:26:06 +00:00
import org.signal.libsignal.metadata.certificate.InvalidCertificateException ;
import org.signal.libsignal.metadata.certificate.SenderCertificate ;
2017-01-06 17:19:58 +00:00
import org.thoughtcrime.securesms.ApplicationContext ;
import org.thoughtcrime.securesms.TextSecureExpiredException ;
2015-10-13 01:25:05 +00:00
import org.thoughtcrime.securesms.attachments.Attachment ;
2018-04-27 00:03:54 +00:00
import org.thoughtcrime.securesms.contactshare.Contact ;
import org.thoughtcrime.securesms.contactshare.ContactModelMapper ;
2017-08-25 19:00:52 +00:00
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil ;
2017-07-26 16:59:15 +00:00
import org.thoughtcrime.securesms.database.Address ;
2014-11-08 19:35:58 +00:00
import org.thoughtcrime.securesms.database.DatabaseFactory ;
2015-11-02 22:32:02 +00:00
import org.thoughtcrime.securesms.events.PartProgressEvent ;
2019-03-28 15:56:35 +00:00
import org.thoughtcrime.securesms.jobmanager.Job ;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint ;
2018-08-01 15:09:24 +00:00
import org.thoughtcrime.securesms.logging.Log ;
2018-02-07 22:01:37 +00:00
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader ;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage ;
2014-12-12 09:03:24 +00:00
import org.thoughtcrime.securesms.mms.PartAuthority ;
2014-11-08 19:35:58 +00:00
import org.thoughtcrime.securesms.notifications.MessageNotifier ;
2017-08-01 15:56:00 +00:00
import org.thoughtcrime.securesms.recipients.Recipient ;
2018-12-08 02:31:39 +00:00
import org.thoughtcrime.securesms.util.Base64 ;
2018-02-07 22:01:37 +00:00
import org.thoughtcrime.securesms.util.BitmapDecodingException ;
import org.thoughtcrime.securesms.util.BitmapUtil ;
2019-04-17 14:21:30 +00:00
import org.thoughtcrime.securesms.util.Hex ;
2018-02-07 22:01:37 +00:00
import org.thoughtcrime.securesms.util.MediaUtil ;
2017-01-06 17:19:58 +00:00
import org.thoughtcrime.securesms.util.TextSecurePreferences ;
2018-12-08 02:31:39 +00:00
import org.thoughtcrime.securesms.util.Util ;
2016-03-23 17:34:41 +00:00
import org.whispersystems.libsignal.util.guava.Optional ;
2019-01-14 07:30:54 +00:00
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair ;
2016-03-23 17:34:41 +00:00
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment ;
2018-12-08 02:31:39 +00:00
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer ;
2018-02-07 22:01:37 +00:00
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage ;
2019-01-15 08:41:05 +00:00
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview ;
2019-01-14 07:30:54 +00:00
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage ;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage ;
2018-04-27 00:03:54 +00:00
import org.whispersystems.signalservice.api.messages.shared.SharedContact ;
2016-03-23 17:34:41 +00:00
import org.whispersystems.signalservice.api.push.SignalServiceAddress ;
2014-11-08 19:35:58 +00:00
2018-02-07 22:01:37 +00:00
import java.io.ByteArrayInputStream ;
2014-12-12 09:03:24 +00:00
import java.io.IOException ;
import java.io.InputStream ;
2019-01-14 07:30:54 +00:00
import java.util.Collections ;
2014-11-08 19:35:58 +00:00
import java.util.LinkedList ;
import java.util.List ;
2018-06-20 02:22:39 +00:00
import java.util.concurrent.TimeUnit ;
2014-11-08 19:35:58 +00:00
2015-01-12 04:27:34 +00:00
public abstract class PushSendJob extends SendJob {
2014-11-08 19:35:58 +00:00
2018-11-22 01:26:06 +00:00
private static final String TAG = PushSendJob . class . getSimpleName ( ) ;
private static final long CERTIFICATE_EXPIRATION_BUFFER = TimeUnit . DAYS . toMillis ( 1 ) ;
2014-11-08 19:35:58 +00:00
2019-03-28 15:56:35 +00:00
protected PushSendJob ( Job . Parameters parameters ) {
super ( parameters ) ;
2014-11-08 19:35:58 +00:00
}
2019-03-28 15:56:35 +00: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 ( ) ;
2014-11-08 19:35:58 +00:00
}
2017-01-06 17:19:58 +00:00
@Override
2018-11-15 20:05:08 +00:00
protected final void onSend ( ) throws Exception {
2017-01-06 17:19:58 +00:00
if ( TextSecurePreferences . getSignedPreKeyFailureCount ( context ) > 5 ) {
ApplicationContext . getInstance ( context )
. getJobManager ( )
2019-03-28 15:56:35 +00:00
. add ( new RotateSignedPreKeyJob ( ) ) ;
2017-01-06 17:19:58 +00:00
throw new TextSecureExpiredException ( " Too many signed prekey rotation failures " ) ;
}
2018-02-02 03:22:48 +00:00
onPushSend ( ) ;
2017-01-06 17:19:58 +00:00
}
2018-06-11 16:37:01 +00:00
@Override
2018-06-20 20:56:05 +00:00
public void onRetry ( ) {
super . onRetry ( ) ;
2018-08-02 13:50:36 +00:00
Log . i ( TAG , " onRetry() " ) ;
2018-06-20 20:56:05 +00:00
2019-03-28 15:56:35 +00:00
if ( getRunAttempt ( ) > 1 ) {
2018-08-02 13:50:36 +00:00
Log . i ( TAG , " Scheduling service outage detection job. " ) ;
2019-03-28 15:56:35 +00:00
ApplicationContext . getInstance ( context ) . getJobManager ( ) . add ( new ServiceOutageDetectionJob ( ) ) ;
2018-06-20 20:56:05 +00:00
}
2018-06-11 16:37:01 +00:00
}
2017-08-22 17:44:04 +00:00
protected Optional < byte [ ] > getProfileKey ( @NonNull Recipient recipient ) {
2017-08-25 19:00:52 +00:00
if ( ! recipient . resolve ( ) . isSystemContact ( ) & & ! recipient . resolve ( ) . isProfileSharing ( ) ) {
return Optional . absent ( ) ;
2017-08-15 01:11:13 +00:00
}
2017-08-25 19:00:52 +00:00
return Optional . of ( ProfileKeyUtil . getProfileKey ( context ) ) ;
2017-08-15 01:11:13 +00:00
}
2017-07-26 16:59:15 +00:00
protected SignalServiceAddress getPushAddress ( Address address ) {
2017-08-07 21:24:53 +00:00
String relay = null ;
2017-07-26 16:59:15 +00:00
return new SignalServiceAddress ( address . toPhoneString ( ) , Optional . fromNullable ( relay ) ) ;
2014-11-08 19:35:58 +00:00
}
2018-01-25 03:17:44 +00:00
protected List < SignalServiceAttachment > getAttachmentsFor ( List < Attachment > parts ) {
2016-03-23 17:34:41 +00:00
List < SignalServiceAttachment > attachments = new LinkedList < > ( ) ;
2014-11-08 19:35:58 +00:00
2015-10-13 01:25:05 +00:00
for ( final Attachment attachment : parts ) {
2018-04-27 00:03:54 +00:00
SignalServiceAttachment converted = getAttachmentFor ( attachment ) ;
if ( converted ! = null ) {
attachments . add ( converted ) ;
2014-11-08 19:35:58 +00:00
}
}
return attachments ;
}
2018-04-27 00:03:54 +00:00
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 07:33:37 +00:00
. withCaption ( attachment . getCaption ( ) )
2018-04-27 00:03:54 +00:00
. 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 ;
}
2019-01-15 20:43:38 +00:00
protected @NonNull List < SignalServiceAttachment > getAttachmentPointersFor ( List < Attachment > attachments ) {
2018-12-08 02:31:39 +00: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 ) {
2017-08-01 15:56:00 +00:00
long threadId = DatabaseFactory . getMmsDatabase ( context ) . getThreadIdForMessage ( messageId ) ;
Recipient recipient = DatabaseFactory . getThreadDatabase ( context ) . getRecipientForThreadId ( threadId ) ;
2014-11-08 19:35:58 +00:00
2017-08-01 15:56:00 +00:00
if ( threadId ! = - 1 & & recipient ! = null ) {
MessageNotifier . notifyMessageDeliveryFailed ( context , recipient , threadId ) ;
2015-04-14 17:01:33 +00:00
}
2014-11-08 19:35:58 +00:00
}
2017-01-06 17:19:58 +00:00
2018-02-07 22:01:37 +00:00
protected Optional < SignalServiceDataMessage . Quote > getQuoteFor ( OutgoingMediaMessage message ) {
if ( message . getOutgoingQuote ( ) = = null ) return Optional . absent ( ) ;
2018-04-02 23:17:32 +00:00
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 22:01:37 +00:00
for ( Attachment attachment : message . getOutgoingQuote ( ) . getAttachments ( ) ) {
2018-04-02 23:17:32 +00:00
BitmapUtil . ScaleResult thumbnailData = null ;
SignalServiceAttachment thumbnail = null ;
2019-04-17 14:21:30 +00:00
String thumbnailType = MediaUtil . IMAGE_JPEG ;
2018-02-07 22:01:37 +00:00
try {
if ( MediaUtil . isImageType ( attachment . getContentType ( ) ) & & attachment . getDataUri ( ) ! = null ) {
2019-04-17 14:21:30 +00:00
Bitmap . CompressFormat format = BitmapUtil . getCompressFormatForContentType ( attachment . getContentType ( ) ) ;
thumbnailData = BitmapUtil . createScaledBytes ( context , new DecryptableStreamUriLoader . DecryptableUri ( attachment . getDataUri ( ) ) , 100 , 100 , 500 * 1024 , format ) ;
thumbnailType = attachment . getContentType ( ) ;
2018-02-07 22:01:37 +00:00
} else if ( MediaUtil . isVideoType ( attachment . getContentType ( ) ) & & attachment . getThumbnailUri ( ) ! = null ) {
2018-04-02 23:17:32 +00:00
thumbnailData = BitmapUtil . createScaledBytes ( context , new DecryptableStreamUriLoader . DecryptableUri ( attachment . getThumbnailUri ( ) ) , 100 , 100 , 500 * 1024 ) ;
2018-02-07 22:01:37 +00:00
}
2018-04-02 23:17:32 +00:00
if ( thumbnailData ! = null ) {
thumbnail = SignalServiceAttachment . newStreamBuilder ( )
2019-04-17 14:21:30 +00:00
. withContentType ( thumbnailType )
2018-04-02 23:17:32 +00:00
. withWidth ( thumbnailData . getWidth ( ) )
. withHeight ( thumbnailData . getHeight ( ) )
. withLength ( thumbnailData . getBitmap ( ) . length )
. withStream ( new ByteArrayInputStream ( thumbnailData . getBitmap ( ) ) )
. build ( ) ;
2018-02-07 22:01:37 +00:00
}
2018-04-02 23:17:32 +00:00
quoteAttachments . add ( new SignalServiceDataMessage . Quote . QuotedAttachment ( attachment . getContentType ( ) ,
attachment . getFileName ( ) ,
thumbnail ) ) ;
2018-02-07 22:01:37 +00:00
} catch ( BitmapDecodingException e ) {
Log . w ( TAG , e ) ;
}
}
return Optional . of ( new SignalServiceDataMessage . Quote ( quoteId , new SignalServiceAddress ( quoteAuthor . serialize ( ) ) , quoteBody , quoteAttachments ) ) ;
}
2019-04-17 14:21:30 +00:00
protected Optional < SignalServiceDataMessage . Sticker > getStickerFor ( OutgoingMediaMessage message ) {
Attachment stickerAttachment = Stream . of ( message . getAttachments ( ) ) . filter ( Attachment : : isSticker ) . findFirst ( ) . orElse ( null ) ;
if ( stickerAttachment = = null ) {
return Optional . absent ( ) ;
}
try {
byte [ ] packId = Hex . fromStringCondensed ( stickerAttachment . getSticker ( ) . getPackId ( ) ) ;
byte [ ] packKey = Hex . fromStringCondensed ( stickerAttachment . getSticker ( ) . getPackKey ( ) ) ;
int stickerId = stickerAttachment . getSticker ( ) . getStickerId ( ) ;
SignalServiceAttachment attachment = getAttachmentPointerFor ( stickerAttachment ) ;
return Optional . of ( new SignalServiceDataMessage . Sticker ( packId , packKey , stickerId , attachment ) ) ;
} catch ( IOException e ) {
Log . w ( TAG , " Failed to decode sticker id/key " , e ) ;
return Optional . absent ( ) ;
}
}
2018-04-27 00:03:54 +00:00
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 22:01:37 +00:00
2019-01-15 08:41:05 +00: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 ( ) ;
}
2018-11-22 01:26:06 +00:00
protected void rotateSenderCertificateIfNecessary ( ) throws IOException {
try {
2018-12-03 21:16:29 +00:00
byte [ ] certificateBytes = TextSecurePreferences . getUnidentifiedAccessCertificate ( context ) ;
if ( certificateBytes = = null ) {
throw new InvalidCertificateException ( " No certificate was present. " ) ;
}
SenderCertificate certificate = new SenderCertificate ( certificateBytes ) ;
2018-11-22 01:26:06 +00:00
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 20:34:42 +00:00
RotateCertificateJob certificateJob = new RotateCertificateJob ( context ) ;
2018-11-22 01:26:06 +00:00
ApplicationContext . getInstance ( context ) . injectDependencies ( certificateJob ) ;
certificateJob . onRun ( ) ;
}
}
2019-01-14 07:30:54 +00:00
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 03:22:48 +00:00
protected abstract void onPushSend ( ) throws Exception ;
2014-11-08 19:35:58 +00:00
}