Message syncing.

This commit is contained in:
Mikunj 2019-10-24 17:16:53 +11:00
parent 98cfd93b97
commit efad14fcdc
12 changed files with 403 additions and 99 deletions

View File

@ -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();
}

View File

@ -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),

View File

@ -24,6 +24,7 @@ public class Data {
@JsonProperty private final Map<String, double[]> doubleArrays;
@JsonProperty private final Map<String, Boolean> booleans;
@JsonProperty private final Map<String, boolean[]> booleanArrays;
@JsonProperty private final Map<String, byte[]> byteArrays;
public Data(@JsonProperty("strings") @NonNull Map<String, String> strings,
@JsonProperty("stringArrays") @NonNull Map<String, String[]> stringArrays,
@ -36,7 +37,8 @@ public class Data {
@JsonProperty("doubles") @NonNull Map<String, Double> doubles,
@JsonProperty("doubleArrays") @NonNull Map<String, double[]> doubleArrays,
@JsonProperty("booleans") @NonNull Map<String, Boolean> booleans,
@JsonProperty("booleanArrays") @NonNull Map<String, boolean[]> booleanArrays)
@JsonProperty("booleanArrays") @NonNull Map<String, boolean[]> booleanArrays,
@JsonProperty("byteArrays") @NonNull Map<String, byte[]> 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<String, double[]> doubleArrays = new HashMap<>();
private final Map<String, Boolean> booleans = new HashMap<>();
private final Map<String, boolean[]> booleanArrays = new HashMap<>();
private final Map<String, byte[]> 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);
}
}

View File

@ -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());
}};
}

View File

@ -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<QuoteModel> quote = getValidatedQuote(message.getMessage().getQuote());
Optional<Attachment> sticker = getStickerAttachment(message.getMessage().getSticker());
Optional<List<Contact>> 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<String> 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<String> 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;

View File

@ -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<AttachmentUploadJob> 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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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)
}
}

View File

@ -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<Boolean>()
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<Boolean>()
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<String?>()
storageAPI.getPrimaryDevicePublicKey(hexEncodedPublicKey).success {
future.set(it)
}.fail {
future.set(null)
}
return try {
future.get()
} catch (e: Exception) {
null
}
}

View File

@ -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<PushMessageSyncSendJob> {
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)
}
}
}
}

View File

@ -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);
}
}

View File

@ -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;
});