2015-05-29 16:23:47 -07:00
package org.thoughtcrime.securesms.jobs ;
2017-12-08 14:36:36 -08:00
import android.Manifest ;
2015-05-29 16:23:47 -07:00
import android.content.Context ;
import android.content.res.AssetFileDescriptor ;
import android.database.Cursor ;
import android.net.Uri ;
import android.provider.ContactsContract ;
2019-06-05 15:47:14 -04:00
import androidx.annotation.NonNull ;
import androidx.annotation.Nullable ;
2015-05-29 16:23:47 -07:00
2018-07-06 17:28:58 -07:00
import org.thoughtcrime.securesms.ApplicationContext ;
2015-05-29 16:23:47 -07:00
import org.thoughtcrime.securesms.contacts.ContactAccessor ;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData ;
2017-09-12 22:49:30 -07:00
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil ;
2018-05-22 02:13:10 -07:00
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil ;
2017-07-26 09:59:15 -07:00
import org.thoughtcrime.securesms.database.Address ;
2017-06-23 13:57:38 -07:00
import org.thoughtcrime.securesms.database.DatabaseFactory ;
import org.thoughtcrime.securesms.database.IdentityDatabase ;
2015-05-29 16:23:47 -07:00
import org.thoughtcrime.securesms.dependencies.InjectableType ;
2019-03-28 08:56:35 -07:00
import org.thoughtcrime.securesms.jobmanager.Data ;
import org.thoughtcrime.securesms.jobmanager.Job ;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint ;
2018-08-01 11:09:24 -04:00
import org.thoughtcrime.securesms.logging.Log ;
2017-12-08 14:36:36 -08:00
import org.thoughtcrime.securesms.permissions.Permissions ;
2016-08-26 16:53:23 -07:00
import org.thoughtcrime.securesms.recipients.Recipient ;
2016-10-05 16:57:52 -07:00
import org.thoughtcrime.securesms.util.TextSecurePreferences ;
2017-06-23 13:57:38 -07:00
import org.whispersystems.libsignal.IdentityKey ;
2016-03-23 10:34:41 -07:00
import org.whispersystems.libsignal.util.guava.Optional ;
import org.whispersystems.signalservice.api.SignalServiceMessageSender ;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException ;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment ;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream ;
2017-05-11 22:46:35 -07:00
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage ;
2016-03-23 10:34:41 -07:00
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact ;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream ;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage ;
2017-06-23 13:57:38 -07:00
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage ;
2016-03-23 10:34:41 -07:00
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException ;
2017-02-27 12:03:34 +01:00
import org.whispersystems.signalservice.api.util.InvalidNumberException ;
2015-05-29 16:23:47 -07:00
import java.io.ByteArrayInputStream ;
import java.io.File ;
import java.io.FileInputStream ;
import java.io.FileOutputStream ;
import java.io.IOException ;
import java.util.Collection ;
2018-07-06 17:28:58 -07:00
import java.util.concurrent.TimeUnit ;
2015-05-29 16:23:47 -07:00
import javax.inject.Inject ;
2019-03-28 08:56:35 -07:00
public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableType {
2018-08-09 10:15:43 -04:00
2019-03-28 08:56:35 -07:00
public static final String KEY = " MultiDeviceContactUpdateJob " ;
2015-06-24 18:26:51 -07:00
2015-05-29 16:23:47 -07:00
private static final String TAG = MultiDeviceContactUpdateJob . class . getSimpleName ( ) ;
2018-07-06 17:28:58 -07:00
private static final long FULL_SYNC_TIME = TimeUnit . HOURS . toMillis ( 6 ) ;
2018-08-09 10:15:43 -04:00
private static final String KEY_ADDRESS = " address " ;
private static final String KEY_FORCE_SYNC = " force_sync " ;
2019-03-28 08:56:35 -07:00
@Inject SignalServiceMessageSender messageSender ;
2015-05-29 16:23:47 -07:00
2018-08-09 10:15:43 -04:00
private @Nullable String address ;
2016-08-26 16:53:23 -07:00
2018-07-06 17:28:58 -07:00
private boolean forceSync ;
2017-08-01 08:57:26 -07:00
public MultiDeviceContactUpdateJob ( @NonNull Context context ) {
2018-07-06 17:28:58 -07:00
this ( context , false ) ;
}
public MultiDeviceContactUpdateJob ( @NonNull Context context , boolean forceSync ) {
this ( context , null , forceSync ) ;
2016-08-26 16:53:23 -07:00
}
2017-08-01 08:57:26 -07:00
public MultiDeviceContactUpdateJob ( @NonNull Context context , @Nullable Address address ) {
2018-07-06 17:28:58 -07:00
this ( context , address , true ) ;
}
public MultiDeviceContactUpdateJob ( @NonNull Context context , @Nullable Address address , boolean forceSync ) {
2019-03-28 08:56:35 -07:00
this ( new Job . Parameters . Builder ( )
. addConstraint ( NetworkConstraint . KEY )
. setQueue ( " MultiDeviceContactUpdateJob " )
. setLifespan ( TimeUnit . DAYS . toMillis ( 1 ) )
. setMaxAttempts ( Parameters . UNLIMITED )
. build ( ) ,
address ,
forceSync ) ;
}
private MultiDeviceContactUpdateJob ( @NonNull Job . Parameters parameters , @Nullable Address address , boolean forceSync ) {
super ( parameters ) ;
2016-08-26 16:53:23 -07:00
2018-07-06 17:28:58 -07:00
this . forceSync = forceSync ;
2017-08-01 08:57:26 -07:00
if ( address ! = null ) this . address = address . serialize ( ) ;
else this . address = null ;
2015-05-29 16:23:47 -07:00
}
2018-08-09 10:15:43 -04:00
@Override
2019-03-28 08:56:35 -07:00
public @NonNull Data serialize ( ) {
return new Data . Builder ( ) . putString ( KEY_ADDRESS , address )
. putBoolean ( KEY_FORCE_SYNC , forceSync )
. build ( ) ;
2018-08-09 10:15:43 -04:00
}
@Override
2019-03-28 08:56:35 -07:00
public @NonNull String getFactoryKey ( ) {
return KEY ;
2018-08-09 10:15:43 -04:00
}
2015-05-29 16:23:47 -07:00
@Override
2018-11-15 12:05:08 -08:00
public void onRun ( )
2015-05-29 16:23:47 -07:00
throws IOException , UntrustedIdentityException , NetworkException
2016-08-26 16:53:23 -07:00
{
2016-10-05 16:57:52 -07:00
if ( ! TextSecurePreferences . isMultiDevice ( context ) ) {
2018-10-11 16:45:22 -07:00
Log . i ( TAG , " Not multi device, aborting... " ) ;
2016-10-05 16:57:52 -07:00
return ;
}
2017-07-26 09:59:15 -07:00
if ( address = = null ) generateFullContactUpdate ( ) ;
else generateSingleContactUpdate ( Address . fromSerialized ( address ) ) ;
2016-08-26 16:53:23 -07:00
}
2017-07-26 09:59:15 -07:00
private void generateSingleContactUpdate ( @NonNull Address address )
2016-08-26 16:53:23 -07:00
throws IOException , UntrustedIdentityException , NetworkException
{
2017-09-15 22:38:53 -07:00
File contactDataFile = createTempFile ( " multidevice-contact-update " ) ;
2016-08-26 16:53:23 -07:00
try {
2017-06-23 13:57:38 -07:00
DeviceContactsOutputStream out = new DeviceContactsOutputStream ( new FileOutputStream ( contactDataFile ) ) ;
2017-08-21 18:32:38 -07:00
Recipient recipient = Recipient . from ( context , address , false ) ;
2017-07-26 09:59:15 -07:00
Optional < IdentityDatabase . IdentityRecord > identityRecord = DatabaseFactory . getIdentityDatabase ( context ) . getIdentity ( address ) ;
2017-06-23 13:57:38 -07:00
Optional < VerifiedMessage > verifiedMessage = getVerifiedMessage ( recipient , identityRecord ) ;
2016-08-26 16:53:23 -07:00
2017-07-26 09:59:15 -07:00
out . write ( new DeviceContact ( address . toPhoneString ( ) ,
2016-08-26 16:53:23 -07:00
Optional . fromNullable ( recipient . getName ( ) ) ,
getAvatar ( recipient . getContactUri ( ) ) ,
2017-06-23 13:57:38 -07:00
Optional . fromNullable ( recipient . getColor ( ) . serialize ( ) ) ,
2017-08-25 12:00:52 -07:00
verifiedMessage ,
2018-01-18 10:01:41 -08:00
Optional . fromNullable ( recipient . getProfileKey ( ) ) ,
recipient . isBlocked ( ) ,
recipient . getExpireMessages ( ) > 0 ?
Optional . of ( recipient . getExpireMessages ( ) ) :
Optional . absent ( ) ) ) ;
2016-08-26 16:53:23 -07:00
out . close ( ) ;
2017-05-11 22:46:35 -07:00
sendUpdate ( messageSender , contactDataFile , false ) ;
2016-08-26 16:53:23 -07:00
2017-02-27 12:03:34 +01:00
} catch ( InvalidNumberException e ) {
Log . w ( TAG , e ) ;
2016-08-26 16:53:23 -07:00
} finally {
if ( contactDataFile ! = null ) contactDataFile . delete ( ) ;
}
}
private void generateFullContactUpdate ( )
throws IOException , UntrustedIdentityException , NetworkException
2015-05-29 16:23:47 -07:00
{
2017-12-08 14:36:36 -08:00
if ( ! Permissions . hasAny ( context , Manifest . permission . READ_CONTACTS , Manifest . permission . WRITE_CONTACTS ) ) {
Log . w ( TAG , " No contact permissions, skipping multi-device contact update... " ) ;
return ;
}
2018-07-06 17:28:58 -07:00
boolean isAppVisible = ApplicationContext . getInstance ( context ) . isAppVisible ( ) ;
long timeSinceLastSync = System . currentTimeMillis ( ) - TextSecurePreferences . getLastFullContactSyncTime ( context ) ;
Log . d ( TAG , " Requesting a full contact sync. forced = " + forceSync + " , appVisible = " + isAppVisible + " , timeSinceLastSync = " + timeSinceLastSync + " ms " ) ;
if ( ! forceSync & & ! isAppVisible & & timeSinceLastSync < FULL_SYNC_TIME ) {
Log . i ( TAG , " App is backgrounded and the last contact sync was too soon ( " + timeSinceLastSync + " ms ago). Marking that we need a sync. Skipping multi-device contact update... " ) ;
TextSecurePreferences . setNeedsFullContactSync ( context , true ) ;
return ;
}
TextSecurePreferences . setLastFullContactSyncTime ( context , System . currentTimeMillis ( ) ) ;
TextSecurePreferences . setNeedsFullContactSync ( context , false ) ;
2017-09-15 22:38:53 -07:00
File contactDataFile = createTempFile ( " multidevice-contact-update " ) ;
2015-05-29 16:23:47 -07:00
try {
DeviceContactsOutputStream out = new DeviceContactsOutputStream ( new FileOutputStream ( contactDataFile ) ) ;
Collection < ContactData > contacts = ContactAccessor . getInstance ( ) . getContactsWithPush ( context ) ;
for ( ContactData contactData : contacts ) {
2018-01-18 10:01:41 -08:00
Uri contactUri = Uri . withAppendedPath ( ContactsContract . Contacts . CONTENT_URI , String . valueOf ( contactData . id ) ) ;
Address address = Address . fromExternal ( context , contactData . numbers . get ( 0 ) . number ) ;
Recipient recipient = Recipient . from ( context , address , false ) ;
Optional < IdentityDatabase . IdentityRecord > identity = DatabaseFactory . getIdentityDatabase ( context ) . getIdentity ( address ) ;
Optional < VerifiedMessage > verified = getVerifiedMessage ( recipient , identity ) ;
Optional < String > name = Optional . fromNullable ( contactData . name ) ;
Optional < String > color = Optional . of ( recipient . getColor ( ) . serialize ( ) ) ;
Optional < byte [ ] > profileKey = Optional . fromNullable ( recipient . getProfileKey ( ) ) ;
boolean blocked = recipient . isBlocked ( ) ;
Optional < Integer > expireTimer = recipient . getExpireMessages ( ) > 0 ? Optional . of ( recipient . getExpireMessages ( ) ) : Optional . absent ( ) ;
out . write ( new DeviceContact ( address . toPhoneString ( ) , name , getAvatar ( contactUri ) , color , verified , profileKey , blocked , expireTimer ) ) ;
2015-05-29 16:23:47 -07:00
}
2017-09-12 22:49:30 -07:00
if ( ProfileKeyUtil . hasProfileKey ( context ) ) {
2019-02-15 14:08:19 -08:00
Recipient self = Recipient . from ( context , Address . fromSerialized ( TextSecurePreferences . getLocalNumber ( context ) ) , false ) ;
2017-09-12 22:49:30 -07:00
out . write ( new DeviceContact ( TextSecurePreferences . getLocalNumber ( context ) ,
Optional . absent ( ) , Optional . absent ( ) ,
2019-02-15 14:08:19 -08:00
Optional . of ( self . getColor ( ) . serialize ( ) ) , Optional . absent ( ) ,
2018-01-18 10:01:41 -08:00
Optional . of ( ProfileKeyUtil . getProfileKey ( context ) ) ,
2019-02-15 14:08:19 -08:00
false , self . getExpireMessages ( ) > 0 ? Optional . of ( self . getExpireMessages ( ) ) : Optional . absent ( ) ) ) ;
2017-09-12 22:49:30 -07:00
}
2015-05-29 16:23:47 -07:00
out . close ( ) ;
2017-05-11 22:46:35 -07:00
sendUpdate ( messageSender , contactDataFile , true ) ;
2017-02-27 12:03:34 +01:00
} catch ( InvalidNumberException e ) {
Log . w ( TAG , e ) ;
2015-05-29 16:23:47 -07:00
} finally {
if ( contactDataFile ! = null ) contactDataFile . delete ( ) ;
}
}
@Override
2019-05-22 13:51:56 -03:00
public boolean onShouldRetry ( @NonNull Exception exception ) {
2015-06-22 14:49:04 -07:00
if ( exception instanceof PushNetworkException ) return true ;
2015-05-29 16:23:47 -07:00
return false ;
}
@Override
public void onCanceled ( ) {
}
2017-05-11 22:46:35 -07:00
private void sendUpdate ( SignalServiceMessageSender messageSender , File contactsFile , boolean complete )
2015-05-29 16:23:47 -07:00
throws IOException , UntrustedIdentityException , NetworkException
{
2015-10-23 12:51:28 -07:00
if ( contactsFile . length ( ) > 0 ) {
2016-03-23 10:34:41 -07:00
FileInputStream contactsFileStream = new FileInputStream ( contactsFile ) ;
SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment . newStreamBuilder ( )
. withStream ( contactsFileStream )
. withContentType ( " application/octet-stream " )
. withLength ( contactsFile . length ( ) )
. build ( ) ;
2015-05-29 16:23:47 -07:00
2015-10-23 12:51:28 -07:00
try {
2018-05-22 02:13:10 -07:00
messageSender . sendMessage ( SignalServiceSyncMessage . forContacts ( new ContactsMessage ( attachmentStream , complete ) ) ,
UnidentifiedAccessUtil . getAccessForSync ( context ) ) ;
2015-10-23 12:51:28 -07:00
} catch ( IOException ioe ) {
throw new NetworkException ( ioe ) ;
}
2015-05-29 16:23:47 -07:00
}
}
2016-09-19 23:25:15 -07:00
private Optional < SignalServiceAttachmentStream > getAvatar ( @Nullable Uri uri ) throws IOException {
if ( uri = = null ) {
return Optional . absent ( ) ;
}
2019-03-20 15:09:27 -07:00
Uri displayPhotoUri = Uri . withAppendedPath ( uri , ContactsContract . Contacts . Photo . DISPLAY_PHOTO ) ;
2018-09-27 20:01:01 -07:00
2019-03-20 15:09:27 -07:00
try {
AssetFileDescriptor fd = context . getContentResolver ( ) . openAssetFileDescriptor ( displayPhotoUri , " r " ) ;
if ( fd = = null ) {
return Optional . absent ( ) ;
2015-05-29 16:23:47 -07:00
}
2019-03-20 15:09:27 -07:00
return Optional . of ( SignalServiceAttachment . newStreamBuilder ( )
. withStream ( fd . createInputStream ( ) )
. withContentType ( " image/* " )
. withLength ( fd . getLength ( ) )
. build ( ) ) ;
} catch ( IOException e ) {
Log . i ( TAG , " Could not find avatar for URI: " + displayPhotoUri ) ;
2015-05-29 16:23:47 -07:00
}
Uri photoUri = Uri . withAppendedPath ( uri , ContactsContract . Contacts . Photo . CONTENT_DIRECTORY ) ;
if ( photoUri = = null ) {
return Optional . absent ( ) ;
}
Cursor cursor = context . getContentResolver ( ) . query ( photoUri ,
new String [ ] {
ContactsContract . CommonDataKinds . Photo . PHOTO ,
ContactsContract . CommonDataKinds . Phone . MIMETYPE
} , null , null , null ) ;
try {
if ( cursor ! = null & & cursor . moveToNext ( ) ) {
byte [ ] data = cursor . getBlob ( 0 ) ;
if ( data ! = null ) {
2016-03-23 10:34:41 -07:00
return Optional . of ( SignalServiceAttachment . newStreamBuilder ( )
. withStream ( new ByteArrayInputStream ( data ) )
. withContentType ( " image/* " )
. withLength ( data . length )
. build ( ) ) ;
2015-05-29 16:23:47 -07:00
}
}
return Optional . absent ( ) ;
} finally {
if ( cursor ! = null ) {
cursor . close ( ) ;
}
}
}
2017-06-23 13:57:38 -07:00
private Optional < VerifiedMessage > getVerifiedMessage ( Recipient recipient , Optional < IdentityDatabase . IdentityRecord > identity ) throws InvalidNumberException {
if ( ! identity . isPresent ( ) ) return Optional . absent ( ) ;
2017-07-26 09:59:15 -07:00
String destination = recipient . getAddress ( ) . toPhoneString ( ) ;
2017-06-23 13:57:38 -07:00
IdentityKey identityKey = identity . get ( ) . getIdentityKey ( ) ;
VerifiedMessage . VerifiedState state ;
switch ( identity . get ( ) . getVerifiedStatus ( ) ) {
case VERIFIED : state = VerifiedMessage . VerifiedState . VERIFIED ; break ;
case UNVERIFIED : state = VerifiedMessage . VerifiedState . UNVERIFIED ; break ;
case DEFAULT : state = VerifiedMessage . VerifiedState . DEFAULT ; break ;
default : throw new AssertionError ( " Unknown state: " + identity . get ( ) . getVerifiedStatus ( ) ) ;
}
return Optional . of ( new VerifiedMessage ( destination , identityKey , state , System . currentTimeMillis ( ) ) ) ;
}
2015-05-29 16:23:47 -07:00
private File createTempFile ( String prefix ) throws IOException {
File file = File . createTempFile ( prefix , " tmp " , context . getCacheDir ( ) ) ;
file . deleteOnExit ( ) ;
return file ;
}
private static class NetworkException extends Exception {
public NetworkException ( Exception ioe ) {
super ( ioe ) ;
}
}
2019-03-28 08:56:35 -07:00
public static final class Factory implements Job . Factory < MultiDeviceContactUpdateJob > {
@Override
public @NonNull MultiDeviceContactUpdateJob create ( @NonNull Parameters parameters , @NonNull Data data ) {
String serialized = data . getString ( KEY_ADDRESS ) ;
Address address = serialized ! = null ? Address . fromSerialized ( serialized ) : null ;
return new MultiDeviceContactUpdateJob ( parameters , address , data . getBoolean ( KEY_FORCE_SYNC ) ) ;
}
}
2015-05-29 16:23:47 -07:00
}