2014-11-08 11:35:58 -08:00
package org.thoughtcrime.securesms.jobs ;
import android.content.Context ;
2017-08-22 10:44:04 -07:00
import android.support.annotation.NonNull ;
2018-12-07 18:31:39 -08:00
import android.support.annotation.Nullable ;
import android.text.TextUtils ;
import com.annimon.stream.Stream ;
2014-11-08 11:35:58 -08:00
2017-02-17 20:27:11 -08:00
import org.greenrobot.eventbus.EventBus ;
2018-11-21 17:26:06 -08:00
import org.signal.libsignal.metadata.certificate.InvalidCertificateException ;
import org.signal.libsignal.metadata.certificate.SenderCertificate ;
2017-01-06 09:19:58 -08:00
import org.thoughtcrime.securesms.ApplicationContext ;
import org.thoughtcrime.securesms.TextSecureExpiredException ;
2015-10-12 18:25:05 -07:00
import org.thoughtcrime.securesms.attachments.Attachment ;
2018-04-26 17:03:54 -07:00
import org.thoughtcrime.securesms.contactshare.Contact ;
import org.thoughtcrime.securesms.contactshare.ContactModelMapper ;
2017-08-25 12:00:52 -07:00
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil ;
2017-07-26 09:59:15 -07:00
import org.thoughtcrime.securesms.database.Address ;
2014-11-08 11:35:58 -08:00
import org.thoughtcrime.securesms.database.DatabaseFactory ;
2015-11-02 14:32:02 -08:00
import org.thoughtcrime.securesms.events.PartProgressEvent ;
2018-06-18 12:27:04 -07:00
import org.thoughtcrime.securesms.jobmanager.JobParameters ;
2018-08-01 11:09:24 -04:00
import org.thoughtcrime.securesms.logging.Log ;
2018-02-07 14:01:37 -08:00
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader ;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage ;
2014-12-12 01:03:24 -08:00
import org.thoughtcrime.securesms.mms.PartAuthority ;
2014-11-08 11:35:58 -08:00
import org.thoughtcrime.securesms.notifications.MessageNotifier ;
2017-08-01 08:56:00 -07:00
import org.thoughtcrime.securesms.recipients.Recipient ;
2018-12-07 18:31:39 -08:00
import org.thoughtcrime.securesms.util.Base64 ;
2018-02-07 14:01:37 -08:00
import org.thoughtcrime.securesms.util.BitmapDecodingException ;
import org.thoughtcrime.securesms.util.BitmapUtil ;
import org.thoughtcrime.securesms.util.MediaUtil ;
2017-01-06 09:19:58 -08:00
import org.thoughtcrime.securesms.util.TextSecurePreferences ;
2018-12-07 18:31:39 -08:00
import org.thoughtcrime.securesms.util.Util ;
2016-03-23 10:34:41 -07:00
import org.whispersystems.libsignal.util.guava.Optional ;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment ;
2018-12-07 18:31:39 -08:00
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer ;
2018-02-07 14:01:37 -08:00
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage ;
2018-04-26 17:03:54 -07:00
import org.whispersystems.signalservice.api.messages.shared.SharedContact ;
2016-03-23 10:34:41 -07:00
import org.whispersystems.signalservice.api.push.SignalServiceAddress ;
2014-11-08 11:35:58 -08:00
2018-02-07 14:01:37 -08:00
import java.io.ByteArrayInputStream ;
2014-12-12 01:03:24 -08:00
import java.io.IOException ;
import java.io.InputStream ;
2014-11-08 11:35:58 -08:00
import java.util.LinkedList ;
import java.util.List ;
2018-06-19 19:22:39 -07:00
import java.util.concurrent.TimeUnit ;
2014-11-08 11:35:58 -08:00
2018-11-27 12:34:42 -08:00
import androidx.work.WorkerParameters ;
2015-01-11 20:27:34 -08:00
public abstract class PushSendJob extends SendJob {
2014-11-08 11:35:58 -08:00
2018-11-21 17:26:06 -08:00
private static final long serialVersionUID = 5906098204770900739L ;
private static final String TAG = PushSendJob . class . getSimpleName ( ) ;
private static final long CERTIFICATE_EXPIRATION_BUFFER = TimeUnit . DAYS . toMillis ( 1 ) ;
2014-11-08 11:35:58 -08:00
2018-11-27 12:34:42 -08:00
protected PushSendJob ( @NonNull Context context , @NonNull WorkerParameters workerParameters ) {
super ( context , workerParameters ) ;
}
2014-11-08 11:35:58 -08:00
protected PushSendJob ( Context context , JobParameters parameters ) {
super ( context , parameters ) ;
}
2018-08-09 10:15:43 -04:00
protected static JobParameters constructParameters ( Address destination ) {
2014-11-08 11:35:58 -08:00
JobParameters . Builder builder = JobParameters . newBuilder ( ) ;
2017-07-26 09:59:15 -07:00
builder . withGroupId ( destination . serialize ( ) ) ;
2018-08-09 10:15:43 -04:00
builder . withNetworkRequirement ( ) ;
2018-06-19 19:22:39 -07:00
builder . withRetryDuration ( TimeUnit . DAYS . toMillis ( 1 ) ) ;
2014-11-08 11:35:58 -08:00
return builder . create ( ) ;
}
2017-01-06 09:19:58 -08:00
@Override
2018-11-15 12:05:08 -08:00
protected final void onSend ( ) throws Exception {
2017-01-06 09:19:58 -08:00
if ( TextSecurePreferences . getSignedPreKeyFailureCount ( context ) > 5 ) {
ApplicationContext . getInstance ( context )
. getJobManager ( )
. add ( new RotateSignedPreKeyJob ( context ) ) ;
throw new TextSecureExpiredException ( " Too many signed prekey rotation failures " ) ;
}
2018-02-01 19:22:48 -08:00
onPushSend ( ) ;
2017-01-06 09:19:58 -08:00
}
2018-06-11 09:37:01 -07:00
@Override
2018-06-20 13:56:05 -07:00
public void onRetry ( ) {
super . onRetry ( ) ;
2018-08-02 09:50:36 -04:00
Log . i ( TAG , " onRetry() " ) ;
2018-06-20 13:56:05 -07:00
2018-08-09 10:15:43 -04:00
if ( getRunAttemptCount ( ) > 1 ) {
2018-08-02 09:50:36 -04:00
Log . i ( TAG , " Scheduling service outage detection job. " ) ;
2018-06-20 13:56:05 -07:00
ApplicationContext . getInstance ( context ) . getJobManager ( ) . add ( new ServiceOutageDetectionJob ( context ) ) ;
}
2018-06-11 09:37:01 -07:00
}
2017-08-22 10:44:04 -07:00
protected Optional < byte [ ] > getProfileKey ( @NonNull Recipient recipient ) {
2017-08-25 12:00:52 -07:00
if ( ! recipient . resolve ( ) . isSystemContact ( ) & & ! recipient . resolve ( ) . isProfileSharing ( ) ) {
return Optional . absent ( ) ;
2017-08-14 18:11:13 -07:00
}
2017-08-25 12:00:52 -07:00
return Optional . of ( ProfileKeyUtil . getProfileKey ( context ) ) ;
2017-08-14 18:11:13 -07:00
}
2017-07-26 09:59:15 -07:00
protected SignalServiceAddress getPushAddress ( Address address ) {
2017-08-07 14:24:53 -07:00
String relay = null ;
2017-07-26 09:59:15 -07:00
return new SignalServiceAddress ( address . toPhoneString ( ) , Optional . fromNullable ( relay ) ) ;
2014-11-08 11:35:58 -08:00
}
2018-01-24 19:17:44 -08:00
protected List < SignalServiceAttachment > getAttachmentsFor ( List < Attachment > parts ) {
2016-03-23 10:34:41 -07:00
List < SignalServiceAttachment > attachments = new LinkedList < > ( ) ;
2014-11-08 11:35:58 -08:00
2015-10-12 18:25:05 -07:00
for ( final Attachment attachment : parts ) {
2018-04-26 17:03:54 -07:00
SignalServiceAttachment converted = getAttachmentFor ( attachment ) ;
if ( converted ! = null ) {
attachments . add ( converted ) ;
2014-11-08 11:35:58 -08:00
}
}
return attachments ;
}
2018-04-26 17:03:54 -07: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-08 23:33:37 -08:00
. withCaption ( attachment . getCaption ( ) )
2018-04-26 17:03:54 -07: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 ;
}
2018-12-07 18:31:39 -08:00
protected @Nullable List < SignalServiceAttachment > getAttachmentPointersFor ( List < Attachment > attachments ) {
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 08:56:00 -07:00
long threadId = DatabaseFactory . getMmsDatabase ( context ) . getThreadIdForMessage ( messageId ) ;
Recipient recipient = DatabaseFactory . getThreadDatabase ( context ) . getRecipientForThreadId ( threadId ) ;
2014-11-08 11:35:58 -08:00
2017-08-01 08:56:00 -07:00
if ( threadId ! = - 1 & & recipient ! = null ) {
MessageNotifier . notifyMessageDeliveryFailed ( context , recipient , threadId ) ;
2015-04-14 10:01:33 -07:00
}
2014-11-08 11:35:58 -08:00
}
2017-01-06 09:19:58 -08:00
2018-02-07 14:01:37 -08:00
protected Optional < SignalServiceDataMessage . Quote > getQuoteFor ( OutgoingMediaMessage message ) {
if ( message . getOutgoingQuote ( ) = = null ) return Optional . absent ( ) ;
2018-04-02 16:17:32 -07: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 14:01:37 -08:00
for ( Attachment attachment : message . getOutgoingQuote ( ) . getAttachments ( ) ) {
2018-04-02 16:17:32 -07:00
BitmapUtil . ScaleResult thumbnailData = null ;
SignalServiceAttachment thumbnail = null ;
2018-02-07 14:01:37 -08:00
try {
if ( MediaUtil . isImageType ( attachment . getContentType ( ) ) & & attachment . getDataUri ( ) ! = null ) {
2018-04-02 16:17:32 -07:00
thumbnailData = BitmapUtil . createScaledBytes ( context , new DecryptableStreamUriLoader . DecryptableUri ( attachment . getDataUri ( ) ) , 100 , 100 , 500 * 1024 ) ;
2018-02-07 14:01:37 -08:00
} else if ( MediaUtil . isVideoType ( attachment . getContentType ( ) ) & & attachment . getThumbnailUri ( ) ! = null ) {
2018-04-02 16:17:32 -07:00
thumbnailData = BitmapUtil . createScaledBytes ( context , new DecryptableStreamUriLoader . DecryptableUri ( attachment . getThumbnailUri ( ) ) , 100 , 100 , 500 * 1024 ) ;
2018-02-07 14:01:37 -08:00
}
2018-04-02 16:17:32 -07: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 14:01:37 -08:00
}
2018-04-02 16:17:32 -07:00
quoteAttachments . add ( new SignalServiceDataMessage . Quote . QuotedAttachment ( attachment . getContentType ( ) ,
attachment . getFileName ( ) ,
thumbnail ) ) ;
2018-02-07 14:01:37 -08:00
} catch ( BitmapDecodingException e ) {
Log . w ( TAG , e ) ;
}
}
return Optional . of ( new SignalServiceDataMessage . Quote ( quoteId , new SignalServiceAddress ( quoteAuthor . serialize ( ) ) , quoteBody , quoteAttachments ) ) ;
}
2018-04-26 17:03:54 -07: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 14:01:37 -08:00
2018-11-21 17:26:06 -08:00
protected void rotateSenderCertificateIfNecessary ( ) throws IOException {
try {
2018-12-03 13:16:29 -08:00
byte [ ] certificateBytes = TextSecurePreferences . getUnidentifiedAccessCertificate ( context ) ;
if ( certificateBytes = = null ) {
throw new InvalidCertificateException ( " No certificate was present. " ) ;
}
SenderCertificate certificate = new SenderCertificate ( certificateBytes ) ;
2018-11-21 17:26:06 -08: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 12:34:42 -08:00
RotateCertificateJob certificateJob = new RotateCertificateJob ( context ) ;
2018-11-21 17:26:06 -08:00
ApplicationContext . getInstance ( context ) . injectDependencies ( certificateJob ) ;
certificateJob . setContext ( context ) ;
certificateJob . onRun ( ) ;
}
}
2018-02-01 19:22:48 -08:00
protected abstract void onPushSend ( ) throws Exception ;
2014-11-08 11:35:58 -08:00
}