From efad14fcdc436b8344716bb394fbc1301c95e50c Mon Sep 17 00:00:00 2001 From: Mikunj Date: Thu, 24 Oct 2019 17:16:53 +1100 Subject: [PATCH] Message syncing. --- .../securesms/ApplicationContext.java | 24 +++++ .../SignalCommunicationModule.java | 8 +- .../securesms/jobmanager/Data.java | 22 ++++- .../securesms/jobs/JobManagerFactories.java | 2 + .../securesms/jobs/PushDecryptJob.java | 80 ++++++++++------- .../securesms/jobs/PushMediaSendJob.java | 49 ++++++---- .../securesms/jobs/PushTextSendJob.java | 37 +++++--- .../securesms/loki/LokiMessageSyncEvent.kt | 22 +++++ .../securesms/loki/MultiDeviceUtilities.kt | 65 ++++++++++++++ .../securesms/loki/PushMessageSyncSendJob.kt | 89 +++++++++++++++++++ ...r.java => MessageSenderEventListener.java} | 16 ++-- .../securesms/sms/MessageSender.java | 88 ++++++++++++------ 12 files changed, 403 insertions(+), 99 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/loki/LokiMessageSyncEvent.kt create mode 100644 src/org/thoughtcrime/securesms/loki/PushMessageSyncSendJob.kt rename src/org/thoughtcrime/securesms/push/{SecurityEventListener.java => MessageSenderEventListener.java} (50%) diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 39b6b7bf38..70c76ce3ec 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -20,13 +20,17 @@ import android.annotation.SuppressLint; import android.arch.lifecycle.DefaultLifecycleObserver; import android.arch.lifecycle.LifecycleOwner; import android.arch.lifecycle.ProcessLifecycleOwner; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.database.ContentObserver; import android.os.AsyncTask; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.multidex.MultiDexApplication; +import android.support.v4.content.LocalBroadcastManager; import com.crashlytics.android.Crashlytics; import com.google.android.gms.security.ProviderInstaller; @@ -62,6 +66,7 @@ import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger; import org.thoughtcrime.securesms.loki.BackgroundPollWorker; import org.thoughtcrime.securesms.loki.LokiAPIDatabase; +import org.thoughtcrime.securesms.loki.LokiMessageSyncEvent; import org.thoughtcrime.securesms.loki.LokiPublicChatManager; import org.thoughtcrime.securesms.loki.LokiRSSFeedPoller; import org.thoughtcrime.securesms.loki.LokiUserDatabase; @@ -77,6 +82,7 @@ import org.thoughtcrime.securesms.service.LocalBackupListener; import org.thoughtcrime.securesms.service.RotateSenderCertificateListener; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; import org.thoughtcrime.securesms.service.UpdateApkRefreshListener; +import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper; import org.webrtc.PeerConnectionFactory; @@ -141,6 +147,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc private LokiPublicChatAPI lokiPublicChatAPI = null; public SignalCommunicationModule communicationModule; public MixpanelAPI mixpanel; + private BroadcastReceiver syncMessageEventReceiver; private volatile boolean isAppVisible; @@ -193,6 +200,22 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc if (setUpStorageAPIIfNeeded()) { LokiStorageAPI.Companion.getShared().updateUserDeviceMappings(); } + + // Loki - Event listener + syncMessageEventReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Send the sync message to our devices + long messageID = intent.getLongExtra(LokiMessageSyncEvent.MESSAGE_ID, -1); + long timestamp = intent.getLongExtra(LokiMessageSyncEvent.TIMESTAMP, -1); + byte[] message = intent.getByteArrayExtra(LokiMessageSyncEvent.SYNC_MESSAGE); + int ttl = intent.getIntExtra(LokiMessageSyncEvent.TTL, -1); + if (messageID > 0 && timestamp > 0 && message != null && ttl > 0) { + MessageSender.sendSyncMessageToOurDevices(context, messageID, timestamp, message, ttl); + } + } + }; + LocalBroadcastManager.getInstance(this).registerReceiver(syncMessageEventReceiver, new IntentFilter(LokiMessageSyncEvent.MESSAGE_SYNC_EVENT)); } @Override @@ -220,6 +243,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc @Override public void onTerminate() { stopKovenant(); + LocalBroadcastManager.getInstance(this).unregisterReceiver(syncMessageEventReceiver); super.onTerminate(); } diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index 747c7bfbc1..833c7f895f 100644 --- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -47,8 +47,9 @@ import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob; import org.thoughtcrime.securesms.jobs.TypingSendJob; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.loki.PushMessageSyncSendJob; import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; -import org.thoughtcrime.securesms.push.SecurityEventListener; +import org.thoughtcrime.securesms.push.MessageSenderEventListener; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.service.IncomingMessageObserver; import org.thoughtcrime.securesms.service.WebRtcCallService; @@ -112,7 +113,8 @@ import network.loki.messenger.BuildConfig; StickerPackDownloadJob.class, MultiDeviceStickerPackOperationJob.class, MultiDeviceStickerPackSyncJob.class, - LinkPreviewRepository.class}) + LinkPreviewRepository.class, + PushMessageSyncSendJob.class}) public class SignalCommunicationModule { @@ -151,7 +153,7 @@ public class SignalCommunicationModule { TextSecurePreferences.isMultiDevice(context), Optional.fromNullable(IncomingMessageObserver.getPipe()), Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()), - Optional.of(new SecurityEventListener(context)), + Optional.of(new MessageSenderEventListener(context)), TextSecurePreferences.getLocalNumber(context), DatabaseFactory.getLokiAPIDatabase(context), DatabaseFactory.getLokiThreadDatabase(context), diff --git a/src/org/thoughtcrime/securesms/jobmanager/Data.java b/src/org/thoughtcrime/securesms/jobmanager/Data.java index f7f77b0aa4..a55069ec38 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/Data.java +++ b/src/org/thoughtcrime/securesms/jobmanager/Data.java @@ -24,6 +24,7 @@ public class Data { @JsonProperty private final Map doubleArrays; @JsonProperty private final Map booleans; @JsonProperty private final Map booleanArrays; + @JsonProperty private final Map byteArrays; public Data(@JsonProperty("strings") @NonNull Map strings, @JsonProperty("stringArrays") @NonNull Map stringArrays, @@ -36,7 +37,8 @@ public class Data { @JsonProperty("doubles") @NonNull Map doubles, @JsonProperty("doubleArrays") @NonNull Map doubleArrays, @JsonProperty("booleans") @NonNull Map booleans, - @JsonProperty("booleanArrays") @NonNull Map booleanArrays) + @JsonProperty("booleanArrays") @NonNull Map booleanArrays, + @JsonProperty("byteArrays") @NonNull Map byteArrays) { this.strings = strings; this.stringArrays = stringArrays; @@ -50,6 +52,7 @@ public class Data { this.doubleArrays = doubleArrays; this.booleans = booleans; this.booleanArrays = booleanArrays; + this.byteArrays = byteArrays; } public boolean hasString(@NonNull String key) { @@ -201,6 +204,14 @@ public class Data { return booleanArrays.get(key); } + public boolean hasByteArray(@NonNull String key) { + return byteArrays.containsKey(key); + } + + public byte[] getByteArray(@NonNull String key) { + throwIfAbsent(byteArrays, key); + return byteArrays.get(key); + } private void throwIfAbsent(@NonNull Map map, @NonNull String key) { if (!map.containsKey(key)) { @@ -223,6 +234,7 @@ public class Data { private final Map doubleArrays = new HashMap<>(); private final Map booleans = new HashMap<>(); private final Map booleanArrays = new HashMap<>(); + private final Map byteArrays = new HashMap<>(); public Builder putString(@NonNull String key, @Nullable String value) { strings.put(key, value); @@ -284,6 +296,11 @@ public class Data { return this; } + public Builder putByteArray(@NonNull String key, @NonNull byte[] value) { + byteArrays.put(key, value); + return this; + } + public Data build() { return new Data(strings, stringArrays, @@ -296,7 +313,8 @@ public class Data { doubles, doubleArrays, booleans, - booleanArrays); + booleanArrays, + byteArrays); } } diff --git a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index af769eda20..7309716f9c 100644 --- a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver; +import org.thoughtcrime.securesms.loki.PushMessageSyncSendJob; import java.util.Arrays; import java.util.HashMap; @@ -70,6 +71,7 @@ public final class JobManagerFactories { put(TrimThreadJob.KEY, new TrimThreadJob.Factory()); put(TypingSendJob.KEY, new TypingSendJob.Factory()); put(UpdateApkJob.KEY, new UpdateApkJob.Factory()); + put(PushMessageSyncSendJob.KEY, new PushMessageSyncSendJob.Factory()); }}; } diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index f5604b331d..bccaad08a5 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -821,8 +821,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } private long handleSynchronizeSentExpirationUpdate(@NonNull SentTranscriptMessage message) throws MmsException { - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - Recipient recipient = getSyncMessageDestination(message); + MmsDatabase database = DatabaseFactory.getMmsDatabase(context); + Recipient recipient = getSyncMessagePrimaryDestination(message); OutgoingExpirationUpdateMessage expirationUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, message.getTimestamp(), @@ -842,7 +842,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { throws MmsException { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - Recipient recipients = getSyncMessageDestination(message); + Recipient recipients = getSyncMessagePrimaryDestination(message); Optional quote = getValidatedQuote(message.getMessage().getQuote()); Optional sticker = getStickerAttachment(message.getMessage().getSticker()); Optional> sharedContacts = getContacts(message.getMessage().getSharedContacts()); @@ -1172,7 +1172,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { throws MmsException { - Recipient recipient = getSyncMessageDestination(message); + Recipient recipient = getSyncMessagePrimaryDestination(message); String body = message.getMessage().getBody().or(""); long expiresInMillis = message.getMessage().getExpiresInSeconds() * 1000L; @@ -1523,35 +1523,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } } - private Recipient getMessagePrimaryDestination(SignalServiceContent content, SignalServiceDataMessage message) { - if (message.getGroupInfo().isPresent()) { - return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false)), false); + private Recipient getSyncMessagePrimaryDestination(SentTranscriptMessage message) { + if (message.getMessage().getGroupInfo().isPresent()) { + return getSyncMessageDestination(message); } else { - SettableFuture device = new SettableFuture<>(); - String contentSender = content.getSender(); - - // Get the primary device - LokiStorageAPI.shared.getPrimaryDevicePublicKey(contentSender).success(primaryDevice -> { - String publicKey = (primaryDevice != null) ? primaryDevice : contentSender; - // If our the public key matches our primary device then we need to forward the message to ourselves (Note to self) - String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context); - if (ourPrimaryDevice != null && ourPrimaryDevice.equals(publicKey)) { - publicKey = TextSecurePreferences.getLocalNumber(context); - } - device.set(publicKey); - return Unit.INSTANCE; - }).fail(exception -> { - device.set(contentSender); - return Unit.INSTANCE; - }); - - try { - String primarySender = device.get(); - return Recipient.from(context, Address.fromSerialized(primarySender), false); - } catch (Exception e) { - Log.d("Loki", "Failed to get primary device public key for message. " + e.getMessage()); - return Recipient.from(context, Address.fromSerialized(content.getSender()), false); - } + return getPrimaryDeviceRecipient(message.getDestination().get()); } } @@ -1563,6 +1539,41 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } } + private Recipient getMessagePrimaryDestination(SignalServiceContent content, SignalServiceDataMessage message) { + if (message.getGroupInfo().isPresent()) { + return getMessageDestination(content, message); + } else { + return getPrimaryDeviceRecipient(content.getSender()); + } + } + + private Recipient getPrimaryDeviceRecipient(String recipient) { + SettableFuture device = new SettableFuture<>(); + + // Get the primary device + LokiStorageAPI.shared.getPrimaryDevicePublicKey(recipient).success(primaryDevice -> { + String publicKey = (primaryDevice != null) ? primaryDevice : recipient; + // If our the public key matches our primary device then we need to forward the message to ourselves (Note to self) + String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context); + if (ourPrimaryDevice != null && ourPrimaryDevice.equals(publicKey)) { + publicKey = TextSecurePreferences.getLocalNumber(context); + } + device.set(publicKey); + return Unit.INSTANCE; + }).fail(exception -> { + device.set(recipient); + return Unit.INSTANCE; + }); + + try { + String primarySender = device.get(); + return Recipient.from(context, Address.fromSerialized(primarySender), false); + } catch (Exception e) { + Log.d("Loki", "Failed to get primary device public key for message. " + e.getMessage()); + return Recipient.from(context, Address.fromSerialized(recipient), false); + } + } + private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient conversationRecipient, @NonNull String sender, int device) { Recipient author = Recipient.from(context, Address.fromSerialized(sender), false); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(conversationRecipient); @@ -1609,6 +1620,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } } else if (content.getCallMessage().isPresent() || content.getTypingMessage().isPresent()) { return sender.isBlocked(); + } else if (content.getSyncMessage().isPresent()) { + // We should ignore a sync message if the sender is not one of our devices + boolean isOurDevice = MultiDeviceUtilitiesKt.isOneOfOurDevices(context, sender.getAddress()); + if (!isOurDevice) { Log.w(TAG, "Got a sync message from a device that is not ours!."); } + return !isOurDevice; } return false; diff --git a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java index 263dedc322..6e50e3446d 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.recipients.Recipient; @@ -42,6 +43,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.loki.messaging.LokiSyncMessage; import java.io.FileNotFoundException; import java.io.IOException; @@ -61,6 +63,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { private static final String KEY_DESTINATION = "destination"; private static final String KEY_IS_FRIEND_REQUEST = "is_friend_request"; private static final String KEY_CUSTOM_FR_MESSAGE = "custom_friend_request_message"; + private static final String KEY_SHOULD_SEND_SYNC_MESSAGE = "should_send_sync_message"; @Inject SignalServiceMessageSender messageSender; @@ -71,34 +74,36 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { private Address destination; // Destination to check whether this is another device we're sending to private boolean isFriendRequest; // Whether this is a friend request message private String customFriendRequestMessage; // If this isn't set then we use the message body + private boolean shouldSendSyncMessage; public PushMediaSendJob(long messageId, Address destination) { this(messageId, messageId, destination); } - public PushMediaSendJob(long templateMessageId, long messageId, Address destination) { this(templateMessageId, messageId, destination, false, null); } - public PushMediaSendJob(long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage) { - this(constructParameters(destination), templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage); + public PushMediaSendJob(long templateMessageId, long messageId, Address destination) { this(templateMessageId, messageId, destination, false, null, false); } + public PushMediaSendJob(long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage, boolean shouldSendSyncMessage) { + this(constructParameters(destination), templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage, shouldSendSyncMessage); } - private PushMediaSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage) { + private PushMediaSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage, boolean shouldSendSyncMessage) { super(parameters); this.templateMessageId = templateMessageId; this.messageId = messageId; this.destination = destination; this.isFriendRequest = isFriendRequest; this.customFriendRequestMessage = customFriendRequestMessage; + this.shouldSendSyncMessage = shouldSendSyncMessage; } @WorkerThread - public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination) { - enqueue(context, jobManager, messageId, messageId, destination); + public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination, boolean shouldSendSyncMessage) { + enqueue(context, jobManager, messageId, messageId, destination, shouldSendSyncMessage); } @WorkerThread - public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long templateMessageId, long messageId, @NonNull Address destination) { - enqueue(context, jobManager, templateMessageId, messageId, destination, false, null); + public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long templateMessageId, long messageId, @NonNull Address destination, boolean shouldSendSyncMessage) { + enqueue(context, jobManager, templateMessageId, messageId, destination, false, null, shouldSendSyncMessage); } @WorkerThread - public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long templateMessageId, long messageId, @NonNull Address destination, Boolean isFriendRequest, @Nullable String customFriendRequestMessage) { + public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long templateMessageId, long messageId, @NonNull Address destination, Boolean isFriendRequest, @Nullable String customFriendRequestMessage, boolean shouldSendSyncMessage) { try { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); OutgoingMediaMessage message = database.getOutgoingMessage(messageId); @@ -111,10 +116,10 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { List attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId(), destination)).toList(); if (attachmentJobs.isEmpty()) { - jobManager.add(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage)); + jobManager.add(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage, shouldSendSyncMessage)); } else { jobManager.startChain(attachmentJobs) - .then(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage)) + .then(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage, shouldSendSyncMessage)) .enqueue(); } @@ -128,10 +133,11 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { @Override public @NonNull Data serialize() { Data.Builder builder = new Data.Builder() - .putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId) - .putLong(KEY_MESSAGE_ID, messageId) - .putString(KEY_DESTINATION, destination.serialize()) - .putBoolean(KEY_IS_FRIEND_REQUEST, isFriendRequest); + .putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId) + .putLong(KEY_MESSAGE_ID, messageId) + .putString(KEY_DESTINATION, destination.serialize()) + .putBoolean(KEY_IS_FRIEND_REQUEST, isFriendRequest) + .putBoolean(KEY_SHOULD_SEND_SYNC_MESSAGE, shouldSendSyncMessage); if (customFriendRequestMessage != null) { builder.putString(KEY_CUSTOM_FR_MESSAGE, customFriendRequestMessage); } return builder.build(); @@ -271,7 +277,15 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { messageSender.sendMessage(messageId, syncMessage, syncAccess); return syncAccess.isPresent(); } else { - return messageSender.sendMessage(messageId, address, UnidentifiedAccessUtil.getAccessFor(context, recipient), mediaMessage).getSuccess().isUnidentified(); + LokiSyncMessage syncMessage = null; + if (shouldSendSyncMessage) { + // Set the sync message destination the primary device, this way it will show that we sent a message to the primary device and not a secondary device + String primaryDevice = MultiDeviceUtilitiesKt.getPrimaryDevicePublicKey(address.getNumber()); + SignalServiceAddress primaryAddress = primaryDevice == null ? address : new SignalServiceAddress(primaryDevice); + // We also need to use the original message id and not -1 + syncMessage = new LokiSyncMessage(primaryAddress, templateMessageId); + } + return messageSender.sendMessage(messageId, address, UnidentifiedAccessUtil.getAccessFor(context, recipient), mediaMessage, Optional.fromNullable(syncMessage)).getSuccess().isUnidentified(); } } catch (UnregisteredUserException e) { warn(TAG, e); @@ -292,8 +306,9 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { long messageID = data.getLong(KEY_MESSAGE_ID); Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION)); boolean isFriendRequest = data.getBoolean(KEY_IS_FRIEND_REQUEST); + boolean shouldSendSyncMessage = data.getBoolean(KEY_SHOULD_SEND_SYNC_MESSAGE); String frMessage = data.hasString(KEY_CUSTOM_FR_MESSAGE) ? data.getString(KEY_CUSTOM_FR_MESSAGE) : null; - return new PushMediaSendJob(parameters, templateMessageID, messageID, destination, isFriendRequest, frMessage); + return new PushMediaSendJob(parameters, templateMessageID, messageID, destination, isFriendRequest, frMessage, shouldSendSyncMessage); } } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index db24ce9fe8..99dcb6e78b 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.ExpiringMessageManager; @@ -29,6 +30,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.loki.messaging.LokiSyncMessage; import java.io.IOException; @@ -45,6 +47,7 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { private static final String KEY_DESTINATION = "destination"; private static final String KEY_IS_FRIEND_REQUEST = "is_friend_request"; private static final String KEY_CUSTOM_FR_MESSAGE = "custom_friend_request_message"; + private static final String KEY_SHOULD_SEND_SYNC_MESSAGE = "should_send_sync_message"; @Inject SignalServiceMessageSender messageSender; @@ -55,29 +58,32 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { private Address destination; // Destination to check whether this is another device we're sending to private boolean isFriendRequest; // Whether this is a friend request message private String customFriendRequestMessage; // If this isn't set then we use the message body + private boolean shouldSendSyncMessage; - public PushTextSendJob(long messageId, Address destination) { this(messageId, messageId, destination); } - public PushTextSendJob(long templateMessageId, long messageId, Address destination) { this(templateMessageId, messageId, destination, false, null); } - public PushTextSendJob(long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage) { - this(constructParameters(destination), templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage); + public PushTextSendJob(long messageId, Address destination) { this(messageId, messageId, destination, false); } + public PushTextSendJob(long templateMessageId, long messageId, Address destination, boolean shouldSendSyncMessage) { this(templateMessageId, messageId, destination, false, null, shouldSendSyncMessage); } + public PushTextSendJob(long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage, boolean shouldSendSyncMessage) { + this(constructParameters(destination), templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage, shouldSendSyncMessage); } - private PushTextSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage) { + private PushTextSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage, boolean shouldSendSyncMessage) { super(parameters); this.templateMessageId = templateMessageId; this.messageId = messageId; this.destination = destination; this.isFriendRequest = isFriendRequest; this.customFriendRequestMessage = customFriendRequestMessage; + this.shouldSendSyncMessage = shouldSendSyncMessage; } @Override public @NonNull Data serialize() { Data.Builder builder = new Data.Builder() - .putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId) - .putLong(KEY_MESSAGE_ID, messageId) - .putString(KEY_DESTINATION, destination.serialize()) - .putBoolean(KEY_IS_FRIEND_REQUEST, isFriendRequest); + .putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId) + .putLong(KEY_MESSAGE_ID, messageId) + .putString(KEY_DESTINATION, destination.serialize()) + .putBoolean(KEY_IS_FRIEND_REQUEST, isFriendRequest) + .putBoolean(KEY_SHOULD_SEND_SYNC_MESSAGE, shouldSendSyncMessage); if (customFriendRequestMessage != null) { builder.putString(KEY_CUSTOM_FR_MESSAGE, customFriendRequestMessage); } return builder.build(); @@ -222,7 +228,15 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { messageSender.sendMessage(messageId, syncMessage, syncAccess); return syncAccess.isPresent(); } else { - return messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage).getSuccess().isUnidentified(); + LokiSyncMessage syncMessage = null; + if (shouldSendSyncMessage) { + // Set the sync message destination the primary device, this way it will show that we sent a message to the primary device and not a secondary device + String primaryDevice = MultiDeviceUtilitiesKt.getPrimaryDevicePublicKey(address.getNumber()); + SignalServiceAddress primaryAddress = primaryDevice == null ? address : new SignalServiceAddress(primaryDevice); + // We also need to use the original message id and not -1 + syncMessage = new LokiSyncMessage(primaryAddress, templateMessageId); + } + return messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage, Optional.fromNullable(syncMessage)).getSuccess().isUnidentified(); } } catch (UnregisteredUserException e) { warn(TAG, "Failure", e); @@ -241,7 +255,8 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION)); boolean isFriendRequest = data.getBoolean(KEY_IS_FRIEND_REQUEST); String frMessage = data.hasString(KEY_CUSTOM_FR_MESSAGE) ? data.getString(KEY_CUSTOM_FR_MESSAGE) : null; - return new PushTextSendJob(parameters, templateMessageID, messageID, destination, isFriendRequest, frMessage); + boolean shouldSendSyncMessage = data.getBoolean(KEY_SHOULD_SEND_SYNC_MESSAGE); + return new PushTextSendJob(parameters, templateMessageID, messageID, destination, isFriendRequest, frMessage, shouldSendSyncMessage); } } } diff --git a/src/org/thoughtcrime/securesms/loki/LokiMessageSyncEvent.kt b/src/org/thoughtcrime/securesms/loki/LokiMessageSyncEvent.kt new file mode 100644 index 0000000000..426154a3cd --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/LokiMessageSyncEvent.kt @@ -0,0 +1,22 @@ +package org.thoughtcrime.securesms.loki + +import android.content.Context +import android.content.Intent +import android.support.v4.content.LocalBroadcastManager + +object LokiMessageSyncEvent { + const val MESSAGE_SYNC_EVENT = "com.loki-network.messenger.MESSAGE_SYNC_EVENT" + const val MESSAGE_ID = "message_id" + const val TIMESTAMP = "timestamp" + const val SYNC_MESSAGE = "sync_message" + const val TTL = "ttl" + + fun broadcastSecurityUpdateEvent(context: Context, messageID: Long, timestamp: Long, message: ByteArray, ttl: Int) { + val intent = Intent(MESSAGE_SYNC_EVENT) + intent.putExtra(MESSAGE_ID, messageID) + intent.putExtra(TIMESTAMP, timestamp) + intent.putExtra(SYNC_MESSAGE, message) + intent.putExtra(TTL, ttl) + LocalBroadcastManager.getInstance(context).sendBroadcast(intent) + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt b/src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt index 2410f49d76..d1f696ea94 100644 --- a/src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt @@ -137,4 +137,69 @@ fun signAndSendPairingAuthorisationMessage(context: Context, pairingAuthorisatio LokiStorageAPI.shared.updateUserDeviceMappings().fail { exception -> Log.w("Loki", "Failed to update device mapping") } +} + +fun shouldSendSycMessage(context: Context, address: Address): Boolean { + if (address.isGroup || address.isEmail || address.isMmsGroup) { + return false + } + + // Don't send sync messages if it's our address + val publicKey = address.serialize() + if (publicKey == TextSecurePreferences.getLocalNumber(context)) { + return false + } + + val storageAPI = LokiStorageAPI.shared + val future = SettableFuture() + storageAPI.getPrimaryDevicePublicKey(publicKey).success { primaryDevicePublicKey -> + val isOurPrimaryDevice = primaryDevicePublicKey != null && TextSecurePreferences.getMasterHexEncodedPublicKey(context) == publicKey + // Don't send sync message if the primary device is the same as ours + future.set(!isOurPrimaryDevice) + }.fail { + future.set(false) + } + + return try { + future.get() + } catch (e: Exception) { + false + } +} + +fun isOneOfOurDevices(context: Context, address: Address): Boolean { + if (address.isGroup || address.isEmail || address.isMmsGroup) { + return false + } + + val ourPublicKey = TextSecurePreferences.getLocalNumber(context) + val storageAPI = LokiStorageAPI.shared + val future = SettableFuture() + storageAPI.getAllDevicePublicKeys(ourPublicKey).success { + future.set(it.contains(address.serialize())) + }.fail { + future.set(false) + } + + return try { + future.get() + } catch (e: Exception) { + false + } +} + +fun getPrimaryDevicePublicKey(hexEncodedPublicKey: String): String? { + val storageAPI = LokiStorageAPI.shared + val future = SettableFuture() + storageAPI.getPrimaryDevicePublicKey(hexEncodedPublicKey).success { + future.set(it) + }.fail { + future.set(null) + } + + return try { + future.get() + } catch (e: Exception) { + null + } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/PushMessageSyncSendJob.kt b/src/org/thoughtcrime/securesms/loki/PushMessageSyncSendJob.kt new file mode 100644 index 0000000000..9fb7f9d75e --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/PushMessageSyncSendJob.kt @@ -0,0 +1,89 @@ +package org.thoughtcrime.securesms.loki + +import org.thoughtcrime.securesms.database.Address +import org.thoughtcrime.securesms.dependencies.InjectableType +import org.thoughtcrime.securesms.jobmanager.Data +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.jobs.BaseJob +import org.whispersystems.signalservice.api.SignalServiceMessageSender +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException +import org.whispersystems.signalservice.api.push.SignalServiceAddress +import java.io.IOException +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class PushMessageSyncSendJob private constructor( + parameters: Parameters, + private val messageID: Long, + private val recipient: Address, + private val timestamp: Long, + private val message: ByteArray, + private val ttl: Int +) : BaseJob(parameters), InjectableType { + + companion object { + const val KEY = "PushMessageSyncSendJob" + + private val TAG = PushMessageSyncSendJob::class.java.simpleName + + private val KEY_MESSAGE_ID = "message_id" + private val KEY_RECIPIENT = "recipient" + private val KEY_TIMESTAMP = "timestamp" + private val KEY_MESSAGE = "message" + private val KEY_TTL = "ttl" + } + + @Inject + lateinit var messageSender: SignalServiceMessageSender + + constructor(messageID: Long, recipient: Address, timestamp: Long, message: ByteArray, ttl: Int) : this(Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(3) + .build(), + messageID, recipient, timestamp, message, ttl) + + override fun serialize(): Data { + return Data.Builder() + .putLong(KEY_MESSAGE_ID, messageID) + .putString(KEY_RECIPIENT, recipient.serialize()) + .putLong(KEY_TIMESTAMP, timestamp) + .putByteArray(KEY_MESSAGE, message) + .putInt(KEY_TTL, ttl) + .build() + } + + override fun getFactoryKey(): String { + return KEY + } + + @Throws(IOException::class, UntrustedIdentityException::class) + public override fun onRun() { + // Don't send sync messages to a group + if (recipient.isGroup || recipient.isEmail) { return } + messageSender.lokiSendSyncMessage(messageID, SignalServiceAddress(recipient.toPhoneString()), timestamp, message, ttl) + } + + public override fun onShouldRetry(e: Exception): Boolean { + // Loki - Disable since we have our own retrying when sending messages + return false + } + + override fun onCanceled() {} + + class Factory : Job.Factory { + override fun create(parameters: Parameters, data: Data): PushMessageSyncSendJob { + try { + return PushMessageSyncSendJob(parameters, + data.getLong(KEY_MESSAGE_ID), + Address.fromSerialized(data.getString(KEY_RECIPIENT)), + data.getLong(KEY_TIMESTAMP), + data.getByteArray(KEY_MESSAGE), + data.getInt(KEY_TTL)) + } catch (e: IOException) { + throw AssertionError(e) + } + } + } +} diff --git a/src/org/thoughtcrime/securesms/push/SecurityEventListener.java b/src/org/thoughtcrime/securesms/push/MessageSenderEventListener.java similarity index 50% rename from src/org/thoughtcrime/securesms/push/SecurityEventListener.java rename to src/org/thoughtcrime/securesms/push/MessageSenderEventListener.java index f876a6a91b..e8babe29f5 100644 --- a/src/org/thoughtcrime/securesms/push/SecurityEventListener.java +++ b/src/org/thoughtcrime/securesms/push/MessageSenderEventListener.java @@ -3,20 +3,17 @@ package org.thoughtcrime.securesms.push; import android.content.Context; import org.thoughtcrime.securesms.crypto.SecurityEvent; -import org.thoughtcrime.securesms.database.Address; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.loki.LokiMessageSyncEvent; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -public class SecurityEventListener implements SignalServiceMessageSender.EventListener { +public class MessageSenderEventListener implements SignalServiceMessageSender.EventListener { - private static final String TAG = SecurityEventListener.class.getSimpleName(); + private static final String TAG = MessageSenderEventListener.class.getSimpleName(); private final Context context; - public SecurityEventListener(Context context) { + public MessageSenderEventListener(Context context) { this.context = context.getApplicationContext(); } @@ -24,4 +21,9 @@ public class SecurityEventListener implements SignalServiceMessageSender.EventLi public void onSecurityEvent(SignalServiceAddress textSecureAddress) { SecurityEvent.broadcastSecurityUpdateEvent(context); } + + @Override + public void onSyncEvent(long messageID, long timestamp, byte[] message, int ttl) { + LokiMessageSyncEvent.INSTANCE.broadcastSecurityUpdateEvent(context, messageID, timestamp, message, ttl); + } } diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java index 195db0e6d9..de26203e64 100644 --- a/src/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java @@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.loki.GeneralUtilitiesKt; import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt; +import org.thoughtcrime.securesms.loki.PushMessageSyncSendJob; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.push.AccountManagerFactory; @@ -149,6 +150,28 @@ public class MessageSender { return allocatedThreadId; } + public static void sendSyncMessageToOurDevices(final Context context, + final long messageID, + final long timestamp, + final byte[] message, + final int ttl) { + String ourPublicKey = TextSecurePreferences.getLocalNumber(context); + LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared(); + JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); + + storageAPI.getAllDevicePublicKeys(ourPublicKey).success(devices -> { + for (String device : devices) { + // Don't send to ourselves + if (device.equals(ourPublicKey)) { continue; } + + // Create a send job for our device + Address address = Address.fromSerialized(device); + jobManager.add(new PushMessageSyncSendJob(messageID, address, timestamp, message, ttl)); + } + return Unit.INSTANCE; + }); + } + public static void resendGroupMessage(Context context, MessageRecord messageRecord, Address filterAddress) { if (!messageRecord.isMms()) throw new AssertionError("Not Group"); sendGroupPush(context, messageRecord.getRecipient(), messageRecord.getId(), filterAddress); @@ -204,22 +227,27 @@ public class MessageSender { jobManager.add(new PushTextSendJob(messageId, recipient.getAddress())); return; } + boolean[] hasSentSyncMessage = { false }; MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, recipientPublicKey, storageAPI, (devicePublicKey, isFriend, friendCount) -> { - Address address = Address.fromSerialized(devicePublicKey); - long messageIDToUse = recipientPublicKey.equals(devicePublicKey) ? messageId : -1L; - - if (isFriend) { - // Send a normal message if the user is friends with the recipient - jobManager.add(new PushTextSendJob(messageId, messageIDToUse, address)); - } else { - // Send friend requests to non friends. If the user is friends with any - // of the devices then send out a default friend request message. - boolean isFriendsWithAny = (friendCount > 0); - String defaultFriendRequestMessage = isFriendsWithAny ? "Accept this friend request to enable messages to be synced across devices" : null; - jobManager.add(new PushTextSendJob(messageId, messageIDToUse, address, true, defaultFriendRequestMessage)); - } + Util.runOnMain(() -> { + Address address = Address.fromSerialized(devicePublicKey); + long messageIDToUse = recipientPublicKey.equals(devicePublicKey) ? messageId : -1L; + if (isFriend) { + // Send a normal message if the user is friends with the recipient + // We should also send a sync message if we haven't already sent one + boolean shouldSendSyncMessage = !hasSentSyncMessage[0] && MultiDeviceUtilitiesKt.shouldSendSycMessage(context, address); + jobManager.add(new PushTextSendJob(messageId, messageIDToUse, address, shouldSendSyncMessage)); + hasSentSyncMessage[0] = shouldSendSyncMessage; + } else { + // Send friend requests to non friends. If the user is friends with any + // of the devices then send out a default friend request message. + boolean isFriendsWithAny = (friendCount > 0); + String defaultFriendRequestMessage = isFriendsWithAny ? "Accept this friend request to enable messages to be synced across devices" : null; + jobManager.add(new PushTextSendJob(messageId, messageIDToUse, address, true, defaultFriendRequestMessage, false)); + } + }); return Unit.INSTANCE; }); } @@ -231,25 +259,31 @@ public class MessageSender { // Just send the message normally if it's a group message String recipientPublicKey = recipient.getAddress().serialize(); if (GeneralUtilitiesKt.isPublicChat(context, recipientPublicKey)) { - PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress()); + PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress(), false); return; } + boolean[] hasSentSyncMessage = { false }; + MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, recipientPublicKey, storageAPI, (devicePublicKey, isFriend, friendCount) -> { - Address address = Address.fromSerialized(devicePublicKey); - long messageIDToUse = recipientPublicKey.equals(devicePublicKey) ? messageId : -1L; - - if (isFriend) { - // Send a normal message if the user is friends with the recipient - PushMediaSendJob.enqueue(context, jobManager, messageId, messageIDToUse, address); - } else { - // Send friend requests to non friends. If the user is friends with any - // of the devices then send out a default friend request message. - boolean isFriendsWithAny = friendCount > 0; - String defaultFriendRequestMessage = isFriendsWithAny ? "Accept this friend request to enable messages to be synced across devices" : null; - PushMediaSendJob.enqueue(context, jobManager, messageId, messageIDToUse, address, true, defaultFriendRequestMessage); - } + Util.runOnMain(() -> { + Address address = Address.fromSerialized(devicePublicKey); + long messageIDToUse = recipientPublicKey.equals(devicePublicKey) ? messageId : -1L; + if (isFriend) { + // Send a normal message if the user is friends with the recipient + // We should also send a sync message if we haven't already sent one + boolean shouldSendSyncMessage = !hasSentSyncMessage[0] && MultiDeviceUtilitiesKt.shouldSendSycMessage(context, address); + PushMediaSendJob.enqueue(context, jobManager, messageId, messageIDToUse, address, shouldSendSyncMessage); + hasSentSyncMessage[0] = shouldSendSyncMessage; + } else { + // Send friend requests to non friends. If the user is friends with any + // of the devices then send out a default friend request message. + boolean isFriendsWithAny = friendCount > 0; + String defaultFriendRequestMessage = isFriendsWithAny ? "Accept this friend request to enable messages to be synced across devices" : null; + PushMediaSendJob.enqueue(context, jobManager, messageId, messageIDToUse, address, true, defaultFriendRequestMessage, false); + } + }); return Unit.INSTANCE; });