diff --git a/res/layout/activity_seed.xml b/res/layout/activity_seed.xml
index cd79c01125..1ac6b7e57a 100644
--- a/res/layout/activity_seed.xml
+++ b/res/layout/activity_seed.xml
@@ -123,8 +123,7 @@
android:layout_height="50dp"
android:background="@color/transparent"
android:textColor="@color/signal_primary"
- android:text="Link Device (Coming Soon)"
- android:alpha="0.24"
+ android:text="Link Device"
android:elevation="0dp"
android:stateListAnimator="@null" />
diff --git a/res/layout/profile_preference_view.xml b/res/layout/profile_preference_view.xml
index 6df515f480..620b984f88 100644
--- a/res/layout/profile_preference_view.xml
+++ b/res/layout/profile_preference_view.xml
@@ -30,6 +30,14 @@
android:layout_height="wrap_content"
tools:text="+14151231234"/>
+
+
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 51065f8cc1..307c000c62 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1572,6 +1572,7 @@
Looks like you don\'t have any conversations yet. Get started by messaging a friend.
+ Secondary device
Copied to clipboard
Share Public Key
Show QR Code
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
index 666ee32098..270c9254d3 100644
--- a/res/xml/preferences.xml
+++ b/res/xml/preferences.xml
@@ -42,7 +42,7 @@
android:icon="@drawable/icon_qr_code"/>
MultiDeviceUtilities.signAndSendPairingAuthorisationMessage(context, pairingAuthorisation));
+ }
+ @Override public void handleDeviceLinkAuthorized(@NotNull PairingAuthorisation pairingAuthorisation) {}
+ @Override public void handleDeviceLinkingDialogDismissed() {}
}
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java
index 3858149e94..57fc652324 100644
--- a/src/org/thoughtcrime/securesms/ConversationListActivity.java
+++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java
@@ -41,6 +41,7 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.components.RatingManager;
import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
+import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
@@ -58,6 +59,7 @@ import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
+import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import java.util.List;
@@ -195,6 +197,13 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
outline.setOval(0, 0, view.getWidth(), view.getHeight());
}
});
+
+ // Display the correct identicon if we're a secondary device
+ String currentUser = TextSecurePreferences.getLocalNumber(this);
+ String recipientAddress = recipient.getAddress().serialize();
+ String primaryAddress = TextSecurePreferences.getMasterHexEncodedPublicKey(this);
+ String profileAddress = (recipientAddress.equalsIgnoreCase(currentUser) && primaryAddress != null) ? primaryAddress : recipientAddress;
+
profilePictureImageView.setClipToOutline(true);
profilePictureImageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@@ -204,7 +213,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
int height = profilePictureImageView.getHeight();
if (width == 0 || height == 0) return true;
profilePictureImageView.getViewTreeObserver().removeOnPreDrawListener(this);
- JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, recipient.getAddress().serialize().toLowerCase());
+ JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, profileAddress.toLowerCase());
profilePictureImageView.setImageDrawable(identicon);
return true;
}
diff --git a/src/org/thoughtcrime/securesms/components/AvatarImageView.java b/src/org/thoughtcrime/securesms/components/AvatarImageView.java
index 9158ab0689..6eae6b42aa 100644
--- a/src/org/thoughtcrime/securesms/components/AvatarImageView.java
+++ b/src/org/thoughtcrime/securesms/components/AvatarImageView.java
@@ -107,8 +107,9 @@ public class AvatarImageView extends AppCompatImageView {
if (w == 0 || h == 0 || recipient == null) { return; }
Drawable image;
+ Context context = this.getContext();
if (recipient.isGroupRecipient()) {
- Context context = this.getContext();
+
String name = Optional.fromNullable(recipient.getName()).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context))).or("");
MaterialColor fallbackColor = recipient.getColor();
@@ -119,7 +120,12 @@ public class AvatarImageView extends AppCompatImageView {
image = new GeneratedContactPhoto(name, R.drawable.ic_profile_default).asDrawable(context, fallbackColor.toAvatarColor(context));
} else {
- image = new JazzIdenticonDrawable(w, h, recipient.getAddress().serialize().toLowerCase());
+ // Default to primary device image
+ String ourPublicKey = TextSecurePreferences.getLocalNumber(context);
+ String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
+ String recipientAddress = recipient.getAddress().serialize();
+ String profileAddress = (ourPrimaryDevice != null && ourPublicKey.equals(recipientAddress)) ? ourPrimaryDevice : recipientAddress;
+ image = new JazzIdenticonDrawable(w, h, profileAddress.toLowerCase());
}
setImageDrawable(image);
}
diff --git a/src/org/thoughtcrime/securesms/components/TypingStatusSender.java b/src/org/thoughtcrime/securesms/components/TypingStatusSender.java
index b7526f1065..927b158e42 100644
--- a/src/org/thoughtcrime/securesms/components/TypingStatusSender.java
+++ b/src/org/thoughtcrime/securesms/components/TypingStatusSender.java
@@ -9,13 +9,14 @@ import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobs.TypingSendJob;
-import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
+import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import java.util.HashMap;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
import kotlin.Unit;
@@ -90,12 +91,13 @@ public class TypingStatusSender {
ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(threadId, typingStarted));
return;
}
-
- MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, recipient.getAddress().serialize(), storageAPI, (devicePublicKey, isFriend, friendCount) -> {
- Recipient device = Recipient.from(context, Address.fromSerialized(devicePublicKey), false);
- long deviceThreadID = threadDatabase.getThreadIdIfExistsFor(device);
- if (deviceThreadID > -1) {
- ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(deviceThreadID, typingStarted));
+ LokiStorageAPI.shared.getAllDevicePublicKeys(recipient.getAddress().serialize()).success(devices -> {
+ for (String device : devices) {
+ Recipient deviceRecipient = Recipient.from(context, Address.fromSerialized(device), false);
+ long deviceThreadID = threadDatabase.getThreadIdIfExistsFor(deviceRecipient);
+ if (deviceThreadID > -1) {
+ ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(deviceThreadID, typingStarted));
+ }
}
return Unit.INSTANCE;
});
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index 25ee20998d..6e761f90bc 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -38,7 +38,6 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
-import android.os.Handler;
import android.os.Vibrator;
import android.provider.Browser;
import android.provider.ContactsContract;
@@ -158,9 +157,11 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.FriendRequestViewDelegate;
import org.thoughtcrime.securesms.loki.LokiAPIUtilities;
import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
+import org.thoughtcrime.securesms.loki.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.LokiThreadDatabaseDelegate;
import org.thoughtcrime.securesms.loki.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.MentionCandidateSelectionView;
+import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
import org.thoughtcrime.securesms.mms.AttachmentManager;
@@ -225,10 +226,8 @@ import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional;
-import org.whispersystems.signalservice.api.SignalServiceMessageSender;
-import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.LokiAPI;
+import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus;
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.messaging.Mention;
@@ -249,6 +248,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import kotlin.Unit;
import network.loki.messenger.R;
+import static nl.komponents.kovenant.KovenantApi.task;
import static org.thoughtcrime.securesms.TransportOption.Type;
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
@@ -353,6 +353,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private ArrayList mentions = new ArrayList<>();
private String oldText = "";
+ // Multi Device
+ private boolean isFriendsWithAnyDevice = false;
@Override
protected void onPreCreate() {
@@ -719,7 +721,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (isSingleConversation() && getRecipient().getContactUri() == null) {
inflater.inflate(R.menu.conversation_add_to_contacts, menu);
}
- */
+
if (recipient != null && recipient.isLocalNumber()) {
if (isSecureText) menu.findItem(R.id.menu_call_secure).setVisible(false);
@@ -731,6 +733,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
muteItem.setVisible(false);
}
}
+ */
searchViewItem = menu.findItem(R.id.menu_search);
@@ -2183,21 +2186,74 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void handleThreadFriendRequestStatusChanged(long threadID) {
- if (threadID != this.threadId) { return; }
- new Handler(getMainLooper()).post(this::updateInputPanel);
+ if (threadID != this.threadId) {
+ Recipient threadRecipient = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadID);
+ if (threadRecipient != null && !threadRecipient.isGroupRecipient()) {
+ LokiStorageAPI.shared.getAllDevicePublicKeys(threadRecipient.getAddress().serialize()).success(devices -> {
+ // We should update our input if this thread is a part of the other threads device
+ if (devices.contains(recipient.getAddress().serialize())) {
+ this.updateInputPanel();
+ }
+ return Unit.INSTANCE;
+ });
+ }
+ return;
+ }
+
+ this.updateInputPanel();
}
private void updateInputPanel() {
- boolean hasPendingFriendRequest = !recipient.isGroupRecipient() && DatabaseFactory.getLokiThreadDatabase(this).hasPendingFriendRequest(threadId);
- updateToggleButtonState();
- inputPanel.setEnabled(!hasPendingFriendRequest);
- int hintID = hasPendingFriendRequest ? R.string.activity_conversation_pending_friend_request_hint : R.string.activity_conversation_default_hint;
- inputPanel.setHint(getResources().getString(hintID));
- if (!hasPendingFriendRequest) {
- inputPanel.composeText.requestFocus();
- InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
- inputMethodManager.showSoftInput(inputPanel.composeText, 0);
+ /*
+ isFriendsWithAnyDevice caches whether we are friends with any of the other users device.
+
+ This stops the case where the input panel disables and enables rapidly.
+ - This can occur when we are not friends with the current thread BUT multi-device tells us that we are friends with another one of their devices.
+ */
+ if (recipient.isGroupRecipient() || isNoteToSelf() || isFriendsWithAnyDevice) {
+ setInputPanelEnabled(true);
+ return;
}
+
+ // It could take a while before our promise resolves, so we assume the best case
+ LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(this).getFriendRequestStatus(threadId);
+ boolean isPending = friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_SENDING || friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_SENT || friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_RECEIVED;
+ setInputPanelEnabled(!isPending);
+
+ // We should always have the input panel enabled if we are friends with the current user
+ isFriendsWithAnyDevice = friendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS;
+
+ // Multi-device input logic
+ if (!isFriendsWithAnyDevice) {
+ // We should enable the input if we don't have any pending friend requests OR we are friends with a linked device
+ MultiDeviceUtilities.hasPendingFriendRequestWithAnyLinkedDevice(this, recipient).success(hasPendingRequests -> {
+ if (!hasPendingRequests) {
+ setInputPanelEnabled(true);
+ } else {
+ MultiDeviceUtilities.isFriendsWithAnyLinkedDevice(this, recipient).success(isFriends -> {
+ // If we are friend with any of the other devices then we want to make sure the input panel is always enabled for the duration of this conversation
+ isFriendsWithAnyDevice = isFriends;
+ setInputPanelEnabled(isFriends);
+ return Unit.INSTANCE;
+ });
+ }
+ return Unit.INSTANCE;
+ });
+ }
+ }
+
+ private void setInputPanelEnabled(boolean enabled) {
+ Util.runOnMain(() -> {
+ updateToggleButtonState();
+ int hintID = enabled ? R.string.activity_conversation_default_hint : R.string.activity_conversation_pending_friend_request_hint;
+ inputPanel.setHint(getResources().getString(hintID));
+ inputPanel.setEnabled(enabled);
+ if (enabled) {
+ inputPanel.composeText.requestFocus();
+ InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
+ inputMethodManager.showSoftInput(inputPanel.composeText, 0);
+ }
+ });
}
private void sendMessage() {
@@ -2401,9 +2457,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void updateToggleButtonState() {
- // Don't allow attachments if we're not friends
- LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(this).getFriendRequestStatus(threadId);
- if (!recipient.isGroupRecipient() && friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) {
+ // Don't allow attachments if we're not friends with any device
+ if (!isNoteToSelf() && !recipient.isGroupRecipient() && !isFriendsWithAnyDevice) {
buttonToggle.display(sendButton);
quickAttachmentToggle.hide();
inlineAttachmentToggle.hide();
@@ -2988,27 +3043,34 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
// region Loki
@Override
public void acceptFriendRequest(@NotNull MessageRecord friendRequest) {
- String contactID = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(this.threadId).getAddress().toString();
- SignalServiceMessageSender messageSender = ApplicationContext.getInstance(this).communicationModule.provideSignalMessageSender();
- SignalServiceAddress address = new SignalServiceAddress(contactID);
- SignalServiceDataMessage message = new SignalServiceDataMessage(System.currentTimeMillis(), "");
- Context context = this;
- AsyncTask.execute(() -> {
- try {
- messageSender.sendMessage(0, address, Optional.absent(), message); // The message ID doesn't matter
- DatabaseFactory.getLokiThreadDatabase(context).setFriendRequestStatus(this.threadId, LokiThreadFriendRequestStatus.FRIENDS);
- DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(friendRequest.id, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
- } catch (Exception e) {
- Log.d("Loki", "Failed to send background message to: " + contactID + ".");
- }
- });
+ // Send the accept to the original friend request thread id
+ LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(this);
+ long originalThreadID = lokiMessageDatabase.getOriginalThreadID(friendRequest.id);
+ long threadId = originalThreadID < 0 ? this.threadId : originalThreadID;
+
+ Address contact = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadId).getAddress();
+ String contactPubKey = contact.toString();
+ DatabaseFactory.getLokiThreadDatabase(this).setFriendRequestStatus(threadId, LokiThreadFriendRequestStatus.FRIENDS);
+ lokiMessageDatabase.setFriendRequestStatus(friendRequest.id, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
+ MessageSender.sendBackgroundMessageToAllDevices(this, contactPubKey);
+ MessageSender.syncContact(this, contact);
+ updateInputPanel();
}
@Override
public void rejectFriendRequest(@NotNull MessageRecord friendRequest) {
- DatabaseFactory.getLokiThreadDatabase(this).setFriendRequestStatus(this.threadId, LokiThreadFriendRequestStatus.NONE);
- String contactID = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(this.threadId).getAddress().toString();
+ LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(this);
+ long originalThreadID = lokiMessageDatabase.getOriginalThreadID(friendRequest.id);
+ long threadId = originalThreadID < 0 ? this.threadId : originalThreadID;
+
+ DatabaseFactory.getLokiThreadDatabase(this).setFriendRequestStatus(threadId, LokiThreadFriendRequestStatus.NONE);
+ String contactID = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadId).getAddress().toString();
DatabaseFactory.getLokiPreKeyBundleDatabase(this).removePreKeyBundle(contactID);
+ updateInputPanel();
+ }
+
+ public boolean isNoteToSelf() {
+ return TextSecurePreferences.getLocalNumber(this).equals(recipient.getAddress().serialize());
}
// endregion
}
diff --git a/src/org/thoughtcrime/securesms/database/Address.java b/src/org/thoughtcrime/securesms/database/Address.java
index ed20c6fbbb..d063025cfa 100644
--- a/src/org/thoughtcrime/securesms/database/Address.java
+++ b/src/org/thoughtcrime/securesms/database/Address.java
@@ -61,7 +61,7 @@ public class Address implements Parcelable, Comparable {
private Address(@NonNull String address, Boolean isPublicChat) {
if (address == null) throw new AssertionError(address);
- this.address = address;
+ this.address = address.toLowerCase();
this.isPublicChat = isPublicChat;
}
diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java
index 4fdcfdcc6a..112ddc463b 100644
--- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java
@@ -47,6 +47,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.security.SecureRandom;
+import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -193,6 +194,23 @@ public class SmsDatabase extends MessagingDatabase {
return -1;
}
+ public Set getAllMessageIDs(long threadID) {
+ SQLiteDatabase database = databaseHelper.getReadableDatabase();
+ Cursor cursor = null;
+ Set messageIDs = new HashSet<>();
+ try {
+ cursor = database.query(TABLE_NAME, null, THREAD_ID + " = ?", new String[] { threadID + "" }, null, null, null);
+ while (cursor != null && cursor.moveToNext()) {
+ messageIDs.add(cursor.getLong(0));
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return messageIDs;
+ }
+
public void markAsEndSession(long id) {
updateTypeBitmask(id, Types.KEY_EXCHANGE_MASK, Types.END_SESSION_BIT);
}
diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
index 9216a2314b..115cd1090d 100644
--- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
+++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
@@ -70,6 +70,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV1 = 22;
private static final int lokiV2 = 23;
private static final int lokiV3 = 24;
+ private static final int lokiV4 = 25;
private static final int DATABASE_VERSION = lokiV3; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final String DATABASE_NAME = "signal.db";
@@ -128,7 +129,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand());
db.execSQL(LokiPreKeyBundleDatabase.getCreateTableCommand());
db.execSQL(LokiPreKeyRecordDatabase.getCreateTableCommand());
- db.execSQL(LokiMessageDatabase.getCreateTableCommand());
+ db.execSQL(LokiMessageDatabase.getCreateMessageFriendRequestTableCommand());
+ db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand());
db.execSQL(LokiThreadDatabase.getCreateFriendRequestTableCommand());
db.execSQL(LokiThreadDatabase.getCreateSessionResetTableCommand());
db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand());
@@ -504,6 +506,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE part ADD COLUMN url TEXT");
}
+ if (oldVersion < lokiV4) {
+ db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand());
+ }
+
db.setTransactionSuccessful();
} finally {
db.endTransaction();
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/AttachmentUploadJob.java b/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java
index 9a091150ce..dce85f83e6 100644
--- a/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.java
@@ -27,6 +27,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import java.io.IOException;
import java.io.InputStream;
@@ -84,14 +85,17 @@ public class AttachmentUploadJob extends BaseJob implements InjectableType {
if (databaseAttachment == null) {
throw new IllegalStateException("Cannot find the specified attachment.");
}
+
+ // Only upload attachment if necessary
+ if (databaseAttachment.getUrl().isEmpty()) {
+ MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
+ Attachment scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment);
+ SignalServiceAttachment localAttachment = getAttachmentFor(scaledAttachment);
+ SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream(), databaseAttachment.isSticker(), new SignalServiceAddress(destination.serialize()));
+ Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.getFastPreflightId()).get();
- MediaConstraints mediaConstraints = MediaConstraints.getPushMediaConstraints();
- Attachment scaledAttachment = scaleAndStripExif(database, mediaConstraints, databaseAttachment);
- SignalServiceAttachment localAttachment = getAttachmentFor(scaledAttachment);
- SignalServiceAttachmentPointer remoteAttachment = messageSender.uploadAttachment(localAttachment.asStream(), databaseAttachment.isSticker(), new SignalServiceAddress(destination.serialize()));
- Attachment attachment = PointerAttachment.forPointer(Optional.of(remoteAttachment), null, databaseAttachment.getFastPreflightId()).get();
-
- database.updateAttachmentAfterUpload(databaseAttachment.getAttachmentId(), attachment);
+ database.updateAttachmentAfterUpload(databaseAttachment.getAttachmentId(), attachment);
+ }
}
@Override
diff --git a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
index af769eda20..cfa9aa7beb 100644
--- a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
+++ b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
@@ -13,6 +13,8 @@ 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.PushBackgroundMessageSendJob;
+import org.thoughtcrime.securesms.loki.PushMessageSyncSendJob;
import java.util.Arrays;
import java.util.HashMap;
@@ -70,6 +72,8 @@ 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());
+ put(PushBackgroundMessageSendJob.KEY, new PushBackgroundMessageSendJob.Factory());
}};
}
diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java
index 953e6b623c..6479ef0b64 100644
--- a/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.Address;
+import org.thoughtcrime.securesms.database.Database;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.dependencies.InjectableType;
@@ -36,15 +37,19 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
+import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
@@ -58,41 +63,57 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
private static final long FULL_SYNC_TIME = TimeUnit.HOURS.toMillis(6);
private static final String KEY_ADDRESS = "address";
+ private static final String KEY_RECIPIENT = "recipient";
private static final String KEY_FORCE_SYNC = "force_sync";
@Inject SignalServiceMessageSender messageSender;
private @Nullable String address;
+ // The recipient of this sync message. If null then we send to all devices
+ private @Nullable String recipient;
+
private boolean forceSync;
+ /**
+ * Create a full contact sync job which syncs across to all other devices
+ */
public MultiDeviceContactUpdateJob(@NonNull Context context) {
this(context, false);
}
+ public MultiDeviceContactUpdateJob(@NonNull Context context, boolean forceSync) { this(context, null, forceSync); }
- public MultiDeviceContactUpdateJob(@NonNull Context context, boolean forceSync) {
- this(context, null, forceSync);
+ /**
+ * Create a full contact sync job which only gets sent to `recipient`
+ */
+ public MultiDeviceContactUpdateJob(@NonNull Context context, @Nullable Address recipient, boolean forceSync) {
+ this(context, recipient, null, forceSync);
}
+ /**
+ * Create a single contact sync job which syncs across `address` to the all other devices
+ */
public MultiDeviceContactUpdateJob(@NonNull Context context, @Nullable Address address) {
- this(context, address, true);
+ this(context, null, address, true);
}
- public MultiDeviceContactUpdateJob(@NonNull Context context, @Nullable Address address, boolean forceSync) {
+ private MultiDeviceContactUpdateJob(@NonNull Context context, @Nullable Address recipient, @Nullable Address address, boolean forceSync) {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setQueue("MultiDeviceContactUpdateJob")
.setLifespan(TimeUnit.DAYS.toMillis(1))
- .setMaxAttempts(Parameters.UNLIMITED)
+ .setMaxAttempts(1)
.build(),
+ recipient,
address,
forceSync);
}
- private MultiDeviceContactUpdateJob(@NonNull Job.Parameters parameters, @Nullable Address address, boolean forceSync) {
+ private MultiDeviceContactUpdateJob(@NonNull Job.Parameters parameters, @Nullable Address recipient, @Nullable Address address, boolean forceSync) {
super(parameters);
this.forceSync = forceSync;
+ this.recipient = (recipient != null) ? recipient.serialize() : null;
if (address != null) this.address = address.serialize();
else this.address = null;
@@ -102,6 +123,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
public @NonNull Data serialize() {
return new Data.Builder().putString(KEY_ADDRESS, address)
.putBoolean(KEY_FORCE_SYNC, forceSync)
+ .putString(KEY_RECIPIENT, recipient)
.build();
}
@@ -120,12 +142,15 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
}
if (address == null) generateFullContactUpdate();
- else generateSingleContactUpdate(Address.fromSerialized(address));
+ else if (!address.equals(TextSecurePreferences.getMasterHexEncodedPublicKey(context))) generateSingleContactUpdate(Address.fromSerialized(address));
}
private void generateSingleContactUpdate(@NonNull Address address)
throws IOException, UntrustedIdentityException, NetworkException
{
+ // Loki - Only sync regular contacts
+ if (!address.isPhone()) { return; }
+
File contactDataFile = createTempFile("multidevice-contact-update");
try {
@@ -134,16 +159,19 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
Optional identityRecord = DatabaseFactory.getIdentityDatabase(context).getIdentity(address);
Optional verifiedMessage = getVerifiedMessage(recipient, identityRecord);
- out.write(new DeviceContact(address.toPhoneString(),
- Optional.fromNullable(recipient.getName()),
- getAvatar(recipient.getContactUri()),
- Optional.fromNullable(recipient.getColor().serialize()),
- verifiedMessage,
- Optional.fromNullable(recipient.getProfileKey()),
- recipient.isBlocked(),
- recipient.getExpireMessages() > 0 ?
- Optional.of(recipient.getExpireMessages()) :
- Optional.absent()));
+ // Loki - Only sync contacts we are friends with
+ if (getFriendRequestStatus(recipient) == LokiThreadFriendRequestStatus.FRIENDS) {
+ out.write(new DeviceContact(address.toPhoneString(),
+ Optional.fromNullable(recipient.getName()),
+ getAvatar(recipient.getContactUri()),
+ Optional.fromNullable(recipient.getColor().serialize()),
+ verifiedMessage,
+ Optional.fromNullable(recipient.getProfileKey()),
+ recipient.isBlocked(),
+ recipient.getExpireMessages() > 0 ?
+ Optional.of(recipient.getExpireMessages()) :
+ Optional.absent()));
+ }
out.close();
sendUpdate(messageSender, contactDataFile, false);
@@ -158,12 +186,6 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
private void generateFullContactUpdate()
throws IOException, UntrustedIdentityException, NetworkException
{
- /* Loki - Disabled
- if (!Permissions.hasAny(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
- Log.w(TAG, "No contact permissions, skipping multi-device contact update...");
- return;
- }
-
boolean isAppVisible = ApplicationContext.getInstance(context).isAppVisible();
long timeSinceLastSync = System.currentTimeMillis() - TextSecurePreferences.getLastFullContactSyncTime(context);
@@ -181,8 +203,8 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
File contactDataFile = createTempFile("multidevice-contact-update");
try {
- DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactDataFile));
- Collection contacts = ContactAccessor.getInstance().getContactsWithPush(context);
+ DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactDataFile));
+ List contacts = getAllContacts();
for (ContactData contactData : contacts) {
Uri contactUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, String.valueOf(contactData.id));
@@ -196,7 +218,10 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
boolean blocked = recipient.isBlocked();
Optional expireTimer = recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages()) : Optional.absent();
- out.write(new DeviceContact(address.toPhoneString(), name, getAvatar(contactUri), color, verified, profileKey, blocked, expireTimer));
+ // Loki - Only sync contacts we are friends with
+ if (getFriendRequestStatus(recipient) == LokiThreadFriendRequestStatus.FRIENDS) {
+ out.write(new DeviceContact(address.toPhoneString(), name, getAvatar(contactUri), color, verified, profileKey, blocked, expireTimer));
+ }
}
if (ProfileKeyUtil.hasProfileKey(context)) {
@@ -215,12 +240,31 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
} finally {
if (contactDataFile != null) contactDataFile.delete();
}
- */
+ }
+
+ private List getAllContacts() {
+ List contactAddresses = DatabaseFactory.getRecipientDatabase(context).getRegistered();
+ List contacts = new ArrayList<>(contactAddresses.size());
+ for (Address address : contactAddresses) {
+ if (!address.isPhone()) { continue; }
+ long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, address, false));
+ String name = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(address.serialize());
+ ContactData contactData = new ContactData(threadId, name);
+ contactData.numbers.add(new ContactAccessor.NumberData("TextSecure", address.serialize()));
+ contacts.add(contactData);
+ }
+ return contacts;
+ }
+
+ private LokiThreadFriendRequestStatus getFriendRequestStatus(Recipient recipient) {
+ long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient);
+ return DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId);
}
@Override
public boolean onShouldRetry(@NonNull Exception exception) {
- if (exception instanceof PushNetworkException) return true;
+ // Loki - Disabled because we have our own retrying
+ // if (exception instanceof PushNetworkException) return true;
return false;
}
@@ -240,10 +284,10 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
.withLength(contactsFile.length())
.build();
+ SignalServiceAddress messageRecipient = recipient != null ? new SignalServiceAddress(recipient) : null;
+
try {
- // TODO: Message ID
- messageSender.sendMessage(0, SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, complete)),
- UnidentifiedAccessUtil.getAccessForSync(context));
+ messageSender.sendMessage(0, SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, complete)), messageRecipient);
} catch (IOException ioe) {
throw new NetworkException(ioe);
}
@@ -251,6 +295,9 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
}
private Optional getAvatar(@Nullable Uri uri) throws IOException {
+ return Optional.absent();
+
+ /* Loki - Disabled until we support custom avatars. This will need to be reworked
if (uri == null) {
return Optional.absent();
}
@@ -304,6 +351,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
cursor.close();
}
}
+ */
}
private Optional getVerifiedMessage(Recipient recipient, Optional identity) throws InvalidNumberException {
@@ -344,7 +392,10 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
String serialized = data.getString(KEY_ADDRESS);
Address address = serialized != null ? Address.fromSerialized(serialized) : null;
- return new MultiDeviceContactUpdateJob(parameters, address, data.getBoolean(KEY_FORCE_SYNC));
+ String recipientSerialized = data.getString(KEY_RECIPIENT);
+ Address recipient = recipientSerialized != null ? Address.fromSerialized(recipientSerialized) : null;
+
+ return new MultiDeviceContactUpdateJob(parameters, recipient, address, data.getBoolean(KEY_FORCE_SYNC));
}
}
}
diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
index 625efd9455..ff57377e16 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
@@ -14,6 +14,7 @@ import android.util.Pair;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
+import com.google.android.gms.common.util.IOUtils;
import org.signal.libsignal.metadata.InvalidMetadataMessageException;
import org.signal.libsignal.metadata.InvalidMetadataVersionException;
@@ -66,12 +67,13 @@ import org.thoughtcrime.securesms.linkpreview.Link;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.loki.FriendRequestHandler;
import org.thoughtcrime.securesms.loki.LokiAPIUtilities;
import org.thoughtcrime.securesms.loki.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase;
import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase;
import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
-import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
+import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
@@ -87,6 +89,7 @@ import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
+import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
@@ -114,6 +117,9 @@ import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
+import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
+import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
@@ -131,7 +137,10 @@ import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestS
import org.whispersystems.signalservice.loki.messaging.LokiServiceMessage;
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.messaging.LokiThreadSessionResetStatus;
+import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
+import java.io.IOException;
+import java.io.InputStream;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.ArrayList;
@@ -303,6 +312,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Optional rawSenderDisplayName = content.senderDisplayName;
if (rawSenderDisplayName.isPresent() && rawSenderDisplayName.get().length() > 0) {
setDisplayName(envelope.getSource(), rawSenderDisplayName.get());
+
+ // If we got a name from our primary device then we also set that
+ String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
+ if (ourPrimaryDevice != null && envelope.getSource().equals(ourPrimaryDevice)) {
+ TextSecurePreferences.setProfileName(context, rawSenderDisplayName.get());
+ }
}
// TODO: Deleting the display name
@@ -343,6 +358,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
else if (syncMessage.getRead().isPresent()) handleSynchronizeReadMessage(syncMessage.getRead().get(), content.getTimestamp());
else if (syncMessage.getVerified().isPresent()) handleSynchronizeVerifiedMessage(syncMessage.getVerified().get());
else if (syncMessage.getStickerPackOperations().isPresent()) handleSynchronizeStickerPackOperation(syncMessage.getStickerPackOperations().get());
+ else if (syncMessage.getContacts().isPresent()) handleSynchronizeContactMessage(syncMessage.getContacts().get());
else Log.w(TAG, "Contains no known sync types...");
} else if (content.getCallMessage().isPresent()) {
Log.i(TAG, "Got call message...");
@@ -516,7 +532,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Log.d("Loki", "Sending a ping back to " + content.getSender() + ".");
String contactID = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId).getAddress().toString();
- sendBackgroundMessage(contactID);
+ MessageSender.sendBackgroundMessage(context, contactID);
SecurityEvent.broadcastSecurityUpdateEvent(context);
MessageNotifier.updateNotification(context, threadId);
@@ -632,6 +648,48 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
}
+ private void handleSynchronizeContactMessage(@NonNull ContactsMessage contactsMessage) {
+ if (contactsMessage.getContactsStream().isStream()) {
+ Log.d("Loki", "Received contact sync message");
+
+ try {
+ InputStream in = contactsMessage.getContactsStream().asStream().getInputStream();
+ DeviceContactsInputStream contactsInputStream = new DeviceContactsInputStream(in);
+ List devices = contactsInputStream.readAll();
+ for (DeviceContact deviceContact : devices) {
+ // Check if we have the contact as a friend and that we're not trying to sync our own device
+ String pubKey = deviceContact.getNumber();
+ Address address = Address.fromSerialized(pubKey);
+ if (!address.isPhone() || address.toPhoneString().equals(TextSecurePreferences.getLocalNumber(context))) { continue; }
+
+ /*
+ If we're not friends with the contact we received or our friend request expired then we should send them a friend request
+ otherwise if we have received a friend request with from them then we should automatically accept the friend request
+ */
+ Recipient recipient = Recipient.from(context, address, false);
+ long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
+ LokiThreadFriendRequestStatus status = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId);
+ if (status == LokiThreadFriendRequestStatus.NONE || status == LokiThreadFriendRequestStatus.REQUEST_EXPIRED) {
+ MessageSender.sendBackgroundFriendRequest(context, pubKey, "Please accept to enable messages to be synced across devices");
+ Log.d("Loki", "Sent friend request to " + pubKey);
+ } else if (status == LokiThreadFriendRequestStatus.REQUEST_RECEIVED) {
+ // Accept the incoming friend request
+ becomeFriendsWithContact(pubKey, false);
+ // Send them an accept message back
+ MessageSender.sendBackgroundMessage(context, pubKey);
+ Log.d("Loki", "Became friends with " + deviceContact.getNumber());
+ }
+
+ // TODO: Handle blocked - If user is not blocked then we should do the friend request logic otherwise add them to our block list
+ // TODO: Handle expiration timer - Update expiration timer?
+ // TODO: Handle avatar - Download and set avatar?
+ }
+ } catch (Exception e) {
+ Log.d("Loki", "Failed to sync contact: " + e);
+ }
+ }
+ }
+
private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content,
@NonNull SentTranscriptMessage message)
throws StorageFailedException
@@ -749,13 +807,19 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
@NonNull Optional messageServerIDOrNull)
throws StorageFailedException
{
- notifyTypingStoppedFromIncomingMessage(getMessageDestination(content, message), content.getSender(), content.getSenderDevice());
+ Recipient originalRecipient = getMessageDestination(content, message);
+ Recipient primaryDeviceRecipient = getMessagePrimaryDestination(content, message);
+
+ notifyTypingStoppedFromIncomingMessage(primaryDeviceRecipient, content.getSender(), content.getSenderDevice());
Optional quote = getValidatedQuote(message.getQuote());
Optional> sharedContacts = getContacts(message.getSharedContacts());
Optional> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
Optional sticker = getStickerAttachment(message.getSticker());
- IncomingMediaMessage mediaMessage = new IncomingMediaMessage(Address.fromExternal(context, content.getSender()), message.getTimestamp(), -1,
+
+ // If message is from group then we need to map it to the correct sender
+ Address sender = message.isGroupUpdate() ? Address.fromSerialized(content.getSender()) : primaryDeviceRecipient.getAddress();
+ IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender, message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(),
quote, sharedContacts, linkPreviews, sticker);
@@ -798,14 +862,20 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Store message server ID
updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);
+ // Loki - Update mapping of message to original thread id
if (insertResult.isPresent()) {
MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
+
+ ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
+ LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
+ long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient);
+ lokiMessageDatabase.setOriginalThreadID(insertResult.get().getMessageId(), originalThreadId);
}
}
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(),
@@ -821,11 +891,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
return threadId;
}
- private long handleSynchronizeSentMediaMessage(@NonNull SentTranscriptMessage message)
+ public long handleSynchronizeSentMediaMessage(@NonNull SentTranscriptMessage message)
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());
@@ -857,6 +927,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
try {
long messageId = database.insertMessageOutbox(mediaMessage, threadId, false, null);
+ if (message.messageServerID >= 0) { DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageId, message.messageServerID); }
if (recipients.getAddress().isGroup()) {
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
@@ -912,20 +983,23 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
{
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
String body = message.getBody().isPresent() ? message.getBody().get() : "";
- Recipient recipient = getMessageDestination(content, message);
+ Recipient originalRecipient = getMessageDestination(content, message);
+ Recipient primaryDeviceRecipient = getMessagePrimaryDestination(content, message);
- if (message.getExpiresInSeconds() != recipient.getExpireMessages()) {
+ if (message.getExpiresInSeconds() != originalRecipient.getExpireMessages()) {
handleExpirationUpdate(content, message, Optional.absent());
}
- Long threadId;
+ Long threadId = null;
if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) {
threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second;
} else {
- notifyTypingStoppedFromIncomingMessage(recipient, content.getSender(), content.getSenderDevice());
+ notifyTypingStoppedFromIncomingMessage(primaryDeviceRecipient, content.getSender(), content.getSenderDevice());
- IncomingTextMessage _textMessage = new IncomingTextMessage(Address.fromSerialized(content.getSender()),
+ // If message is from group then we need to map it to the correct sender
+ Address sender = message.isGroupUpdate() ? Address.fromSerialized(content.getSender()) : primaryDeviceRecipient.getAddress();
+ IncomingTextMessage _textMessage = new IncomingTextMessage(sender,
content.getSenderDevice(),
message.getTimestamp(), body,
message.getGroupInfo(),
@@ -940,8 +1014,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Insert the message into the database
Optional insertResult = database.insertMessageInbox(textMessage);
- if (insertResult.isPresent()) threadId = insertResult.get().getThreadId();
- else threadId = null;
+ Long messageId = null;
+ if (insertResult.isPresent()) {
+ threadId = insertResult.get().getThreadId();
+ messageId = insertResult.get().getMessageId();
+ }
if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get());
@@ -954,6 +1031,14 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Store message server ID
updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);
+ // Loki - Update mapping of message to original thread id
+ if (messageId != null) {
+ ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
+ LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
+ long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient);
+ lokiMessageDatabase.setOriginalThreadID(messageId, originalThreadId);
+ }
+
boolean isGroupMessage = message.getGroupInfo().isPresent();
if (threadId != null && !isGroupMessage) {
MessageNotifier.updateNotification(context, threadId);
@@ -984,13 +1069,13 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void handlePairingMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
if (authorisation.getType() == PairingAuthorisation.Type.REQUEST) {
- handlePairingRequestMessage(authorisation, envelope);
+ handlePairingRequestMessage(authorisation);
} else if (authorisation.getSecondaryDevicePublicKey().equals(userHexEncodedPublicKey)) {
handlePairingAuthorisationMessage(authorisation, envelope, content);
}
}
- private void handlePairingRequestMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceEnvelope envelope) {
+ private void handlePairingRequestMessage(@NonNull PairingAuthorisation authorisation) {
boolean isValid = isValidPairingMessage(authorisation);
DeviceLinkingSession linkingSession = DeviceLinkingSession.Companion.getShared();
if (isValid && linkingSession.isListeningForLinkingRequests()) {
@@ -1023,8 +1108,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
DatabaseFactory.getLokiAPIDatabase(context).removePairingAuthorisations(userHexEncodedPublicKey);
DatabaseFactory.getLokiAPIDatabase(context).insertOrUpdatePairingAuthorisation(authorisation);
TextSecurePreferences.setMasterHexEncodedPublicKey(context, authorisation.getPrimaryDevicePublicKey());
+ TextSecurePreferences.setMultiDevice(context, true);
// Send a background message to the primary device
- sendBackgroundMessage(authorisation.getPrimaryDevicePublicKey());
+ MessageSender.sendBackgroundMessage(context, authorisation.getPrimaryDevicePublicKey());
// Propagate the updates to the file server
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
storageAPI.updateUserDeviceMappings();
@@ -1032,6 +1118,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (content.senderDisplayName.isPresent() && content.senderDisplayName.get().length() > 0) {
setDisplayName(envelope.getSource(), content.senderDisplayName.get());
}
+
+ // Contact sync
+ if (content.getSyncMessage().isPresent() && content.getSyncMessage().get().getContacts().isPresent()) {
+ handleSynchronizeContactMessage(content.getSyncMessage().get().getContacts().get());
+ }
}
private void setDisplayName(String hexEncodedPublicKey, String profileName) {
@@ -1049,103 +1140,94 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void acceptFriendRequestIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
// If we get anything other than a friend request, we can assume that we have a session with the other user
- if (envelope.isFriendRequest()) { return; }
- becomeFriendsWithContact(content.getSender());
+ if (envelope.isFriendRequest() || isGroupChatMessage(content)) { return; }
+ becomeFriendsWithContact(content.getSender(), true);
}
- private void becomeFriendsWithContact(String pubKey) {
+ private void becomeFriendsWithContact(String pubKey, boolean syncContact) {
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
Recipient contactID = Recipient.from(context, Address.fromSerialized(pubKey), false);
+ if (contactID.isGroupRecipient()) return;
+
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(contactID);
LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS) { return; }
// If the thread's friend request status is not `FRIENDS`, but we're receiving a message,
// it must be a friend request accepted message. Declining a friend request doesn't send a message.
lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS);
- // Update the last message if needed
- SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
- LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
- int messageCount = smsDatabase.getMessageCountForThread(threadID);
- long messageID = smsDatabase.getIDForMessageAtIndex(threadID, messageCount - 1);
- if (messageID > -1 && lokiMessageDatabase.getFriendRequestStatus(messageID) != LokiMessageFriendRequestStatus.REQUEST_ACCEPTED) {
- lokiMessageDatabase.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
+ // Send out a contact sync message
+ if (syncContact) {
+ MessageSender.syncContact(context, contactID.getAddress());
}
- }
-
- private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
- if (!envelope.isFriendRequest()) { return; }
- // This handles the case where another user sends us a regular message without authorisation
- MultiDeviceUtilitiesKt.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context).success(becomeFriends -> {
- if (becomeFriends) {
- // Become friends AND update the message they sent
- becomeFriendsWithContact(content.getSender());
- // Send them an accept message back
- sendBackgroundMessage(content.getSender());
- } else {
- // Do regular friend request logic checks
- Recipient contactID = getMessageDestination(content, message);
- LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
- long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(contactID);
- LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
- SmsDatabase smsMessageDatabase = DatabaseFactory.getSmsDatabase(context);
- MmsDatabase mmsMessageDatabase = DatabaseFactory.getMmsDatabase(context);
- LokiMessageDatabase lokiMessageDatabase= DatabaseFactory.getLokiMessageDatabase(context);
- int messageCount = smsMessageDatabase.getMessageCountForThread(threadID);
- if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_SENT) {
- // This can happen if Alice sent Bob a friend request, Bob declined, but then Bob changed his
- // mind and sent a friend request to Alice. In this case we want Alice to auto-accept the request
- // and send a friend request accepted message back to Bob. We don't check that sending the
- // friend request accepted message succeeded. Even if it doesn't, the thread's current friend
- // request status will be set to `FRIENDS` for Alice making it possible
- // for Alice to send messages to Bob. When Bob receives a message, his thread's friend request status
- // will then be set to `FRIENDS`. If we do check for a successful send
- // before updating Alice's thread's friend request status to `FRIENDS`,
- // we can end up in a deadlock where both users' threads' friend request statuses are
- // `REQUEST_SENT`.
- lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS);
- long messageID = smsMessageDatabase.getIDForMessageAtIndex(threadID, messageCount - 2); // The message before the one that was just received
- // TODO: MMS
- lokiMessageDatabase.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
- // Accept the friend request
- sendBackgroundMessage(content.getSender());
- } else if (threadFriendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) {
- // Checking that the sender of the message isn't already a friend is necessary because otherwise
- // the following situation can occur: Alice and Bob are friends. Bob loses his database and his
- // friend request status is reset to `NONE`. Bob now sends Alice a friend
- // request. Alice's thread's friend request status is reset to
- // `REQUEST_RECEIVED`.
- lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_RECEIVED);
- long messageID = smsMessageDatabase.getIDForMessageAtIndex(threadID, messageCount - 1); // The message that was just received
- if (messageID != -1) {
- lokiMessageDatabase.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_PENDING);
- } else {
- // TODO: The code below is ugly due to Java limitations
- lokiMessageDatabase.setFriendRequestStatus(mmsMessageDatabase.getIDForMessageAtIndex(threadID, 0), LokiMessageFriendRequestStatus.REQUEST_PENDING);
- }
- }
- }
+ // Update the last message if needed
+ LokiStorageAPI.shared.getPrimaryDevicePublicKey(pubKey).success(primaryDevice -> {
+ Util.runOnMain(() -> {
+ long primaryDeviceThreadID = primaryDevice == null ? threadID : DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(primaryDevice), false));
+ FriendRequestHandler.updateLastFriendRequestMessage(context, primaryDeviceThreadID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
+ });
return Unit.INSTANCE;
});
}
- private void sendBackgroundMessage(String contactHexEncodedPublicKey) {
- Util.runOnMain(() -> {
- SignalServiceMessageSender messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender();
- SignalServiceAddress address = new SignalServiceAddress(contactHexEncodedPublicKey);
- SignalServiceDataMessage message = new SignalServiceDataMessage(System.currentTimeMillis(), "");
- try {
- messageSender.sendMessage(0, address, Optional.absent(), message); // The message ID doesn't matter
- } catch (Exception e) {
- Log.d("Loki", "Failed to send background message to: " + contactHexEncodedPublicKey + ".");
+ private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
+ if (!envelope.isFriendRequest() || message.isGroupUpdate()) { return; }
+ // This handles the case where another user sends us a regular message without authorisation
+ boolean shouldBecomeFriends = PromiseUtil.get(MultiDeviceUtilities.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context), false);
+ if (shouldBecomeFriends) {
+ // Become friends AND update the message they sent
+ becomeFriendsWithContact(content.getSender(), true);
+ // Send them an accept message back
+ MessageSender.sendBackgroundMessage(context, content.getSender());
+ } else {
+ // Do regular friend request logic checks
+ Recipient originalRecipient = getMessageDestination(content, message);
+ Recipient primaryDeviceRecipient = getMessagePrimaryDestination(content, message);
+ LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
+
+ // Loki - Friend requests only work in direct chats
+ if (!originalRecipient.getAddress().isPhone()) { return; }
+
+ long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(originalRecipient);
+ long primaryDeviceThreadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(primaryDeviceRecipient);
+ LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
+
+ if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_SENT) {
+ // This can happen if Alice sent Bob a friend request, Bob declined, but then Bob changed his
+ // mind and sent a friend request to Alice. In this case we want Alice to auto-accept the request
+ // and send a friend request accepted message back to Bob. We don't check that sending the
+ // friend request accepted message succeeded. Even if it doesn't, the thread's current friend
+ // request status will be set to `FRIENDS` for Alice making it possible
+ // for Alice to send messages to Bob. When Bob receives a message, his thread's friend request status
+ // will then be set to `FRIENDS`. If we do check for a successful send
+ // before updating Alice's thread's friend request status to `FRIENDS`,
+ // we can end up in a deadlock where both users' threads' friend request statuses are
+ // `REQUEST_SENT`.
+ lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS);
+ // Since messages are forwarded to the primary device thread, we need to update it there
+ FriendRequestHandler.updateLastFriendRequestMessage(context, primaryDeviceThreadID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
+ // Accept the friend request
+ MessageSender.sendBackgroundMessage(context, content.getSender());
+ // Send contact sync message
+ MessageSender.syncContact(context, originalRecipient.getAddress());
+ } else if (threadFriendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) {
+ // Checking that the sender of the message isn't already a friend is necessary because otherwise
+ // the following situation can occur: Alice and Bob are friends. Bob loses his database and his
+ // friend request status is reset to `NONE`. Bob now sends Alice a friend
+ // request. Alice's thread's friend request status is reset to
+ // `REQUEST_RECEIVED`.
+ lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_RECEIVED);
+
+ // Since messages are forwarded to the primary device thread, we need to update it there
+ FriendRequestHandler.receivedIncomingFriendRequestMessage(context, primaryDeviceThreadID);
}
- });
+ }
}
- private long handleSynchronizeSentTextMessage(@NonNull SentTranscriptMessage message)
+ public long handleSynchronizeSentTextMessage(@NonNull SentTranscriptMessage message)
throws MmsException
{
- Recipient recipient = getSyncMessageDestination(message);
+ Recipient recipient = getSyncMessagePrimaryDestination(message);
String body = message.getMessage().getBody().or("");
long expiresInMillis = message.getMessage().getExpiresInSeconds() * 1000L;
@@ -1164,6 +1246,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
outgoingMediaMessage = new OutgoingSecureMediaMessage(outgoingMediaMessage);
messageId = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMediaMessage, threadId, false, null);
+ if (message.messageServerID >= 0) { DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageId, message.messageServerID); }
+
database = DatabaseFactory.getMmsDatabase(context);
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
@@ -1308,10 +1392,17 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void handleDeliveryReceipt(@NonNull SignalServiceContent content,
@NonNull SignalServiceReceiptMessage message)
{
+ // Redirect message to primary device conversation
+ Address sender = Address.fromSerialized(content.getSender());
+ if (sender.isPhone()) {
+ Recipient primaryDevice = getPrimaryDeviceRecipient(content.getSender());
+ sender = primaryDevice.getAddress();
+ }
+
for (long timestamp : message.getTimestamps()) {
Log.i(TAG, String.format("Received encrypted delivery receipt: (XXXXX, %d)", timestamp));
DatabaseFactory.getMmsSmsDatabase(context)
- .incrementDeliveryReceiptCount(new SyncMessageId(Address.fromSerialized(content.getSender()), timestamp), System.currentTimeMillis());
+ .incrementDeliveryReceiptCount(new SyncMessageId(sender, timestamp), System.currentTimeMillis());
}
}
@@ -1320,11 +1411,19 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
@NonNull SignalServiceReceiptMessage message)
{
if (TextSecurePreferences.isReadReceiptsEnabled(context)) {
+
+ // Redirect message to primary device conversation
+ Address sender = Address.fromSerialized(content.getSender());
+ if (sender.isPhone()) {
+ Recipient primaryDevice = getPrimaryDeviceRecipient(content.getSender());
+ sender = primaryDevice.getAddress();
+ }
+
for (long timestamp : message.getTimestamps()) {
Log.i(TAG, String.format("Received encrypted read receipt: (XXXXX, %d)", timestamp));
DatabaseFactory.getMmsSmsDatabase(context)
- .incrementReadReceiptCount(new SyncMessageId(Address.fromSerialized(content.getSender()), timestamp), content.getTimestamp());
+ .incrementReadReceiptCount(new SyncMessageId(sender, timestamp), content.getTimestamp());
}
}
}
@@ -1346,6 +1445,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
} else {
+ // See if we need to redirect the message
+ author = getPrimaryDeviceRecipient(content.getSender());
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(author);
}
@@ -1479,8 +1580,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
private Optional insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp) {
+ Recipient primaryDevice = getPrimaryDeviceRecipient(sender);
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
- IncomingTextMessage textMessage = new IncomingTextMessage(Address.fromSerialized(sender),
+ IncomingTextMessage textMessage = new IncomingTextMessage(primaryDevice.getAddress(),
senderDevice, timestamp, "",
Optional.absent(), 0, false);
@@ -1496,6 +1598,14 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
}
+ private Recipient getSyncMessagePrimaryDestination(SentTranscriptMessage message) {
+ if (message.getMessage().getGroupInfo().isPresent()) {
+ return getSyncMessageDestination(message);
+ } else {
+ return getPrimaryDeviceRecipient(message.getDestination().get());
+ }
+ }
+
private Recipient getMessageDestination(SignalServiceContent content, SignalServiceDataMessage message) {
if (message.getGroupInfo().isPresent()) {
return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false)), false);
@@ -1504,6 +1614,37 @@ 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());
+ }
+ }
+
+ /**
+ * Get the primary device recipient of the passed in device.
+ *
+ * If the device doesn't have a primary device then it will return the same device.
+ * If the device is our primary device then it will return our current device.
+ * Otherwise it will return the primary device.
+ */
+ private Recipient getPrimaryDeviceRecipient(String pubKey) {
+ try {
+ String primaryDevice = LokiStorageAPI.shared.getPrimaryDevicePublicKey(pubKey).get();
+ String publicKey = (primaryDevice != null) ? primaryDevice : pubKey;
+ // If 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);
+ }
+ return Recipient.from(context, Address.fromSerialized(publicKey), false);
+ } catch (Exception e) {
+ Log.d("Loki", "Failed to get primary device public key for message. " + e.getMessage());
+ return Recipient.from(context, Address.fromSerialized(pubKey), 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);
@@ -1522,7 +1663,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Recipient sender = Recipient.from(context, Address.fromSerialized(content.getSender()), false);
- if (content.getDataMessage().isPresent()) {
+ if (content.getPairingAuthorisation().isPresent()) {
+ return false;
+ } else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
Recipient conversation = getMessageDestination(content, message);
@@ -1550,11 +1693,26 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
} else if (content.getCallMessage().isPresent() || content.getTypingMessage().isPresent()) {
return sender.isBlocked();
+ } else if (content.getSyncMessage().isPresent()) {
+ try {
+ // We should ignore a sync message if the sender is not one of our devices
+ boolean isOurDevice = MultiDeviceUtilities.isOneOfOurDevices(context, sender.getAddress()).get();
+ if (!isOurDevice) {
+ Log.w(TAG, "Got a sync message from a device that is not ours!.");
+ }
+ return !isOurDevice;
+ } catch (Exception e) {
+ return true;
+ }
}
return false;
}
+ private boolean isGroupChatMessage(SignalServiceContent content) {
+ return content.getDataMessage().isPresent() && content.getDataMessage().get().isGroupUpdate();
+ }
+
private void resetRecipientToPush(@NonNull Recipient recipient) {
if (recipient.isForceSmsSelection()) {
DatabaseFactory.getRecipientDatabase(context).setForceSmsSelection(recipient, false);
diff --git a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java
index 263dedc322..ae133a0394 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.MultiDeviceUtilities;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -42,9 +43,14 @@ 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.api.LokiStorageAPI;
+import org.whispersystems.signalservice.loki.messaging.LokiSyncMessage;
+import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -61,6 +67,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,53 +78,47 @@ 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;
+ }
+
+ public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination, boolean shouldSendSyncMessage) {
+ enqueue(context, jobManager, messageId, messageId, destination, false, null, shouldSendSyncMessage);
+ }
+
+ public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long templateMessageId, long messageId, @NonNull Address destination, Boolean isFriendRequest, @Nullable String customFriendRequestMessage, boolean shouldSendSyncMessage) {
+ enqueue(context, jobManager, Collections.singletonList(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage, shouldSendSyncMessage)));
}
@WorkerThread
- public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination) {
- enqueue(context, jobManager, messageId, messageId, destination);
- }
-
- @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);
- }
-
- @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, List jobs) {
+ if (jobs.size() == 0) { return; }
+ PushMediaSendJob first = jobs.get(0);
+ long messageId = first.templateMessageId;
try {
- MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
- OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
- List attachments = new LinkedList<>();
-
- attachments.addAll(message.getAttachments());
- attachments.addAll(Stream.of(message.getLinkPreviews()).filter(p -> p.getThumbnail().isPresent()).map(p -> p.getThumbnail().get()).toList());
- attachments.addAll(Stream.of(message.getSharedContacts()).filter(c -> c.getAvatar() != null).map(c -> c.getAvatar().getAttachment()).withoutNulls().toList());
-
- List attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId(), destination)).toList();
+ List attachmentJobs = getAttachmentUploadJobs(context, messageId, first.destination);
if (attachmentJobs.isEmpty()) {
- jobManager.add(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage));
+ for (PushMediaSendJob job : jobs) { jobManager.add(job); }
} else {
jobManager.startChain(attachmentJobs)
- .then(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage))
- .enqueue();
+ .then((List)(List)jobs)
+ .enqueue();
}
-
} catch (NoSuchMessageException | MmsException e) {
Log.w(TAG, "Failed to enqueue message.", e);
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
@@ -125,13 +126,28 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
}
}
+ public static List getAttachmentUploadJobs(@NonNull Context context, long messageId, @NonNull Address destination)
+ throws NoSuchMessageException, MmsException
+ {
+ MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
+ OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
+ List attachments = new LinkedList<>();
+
+ attachments.addAll(message.getAttachments());
+ attachments.addAll(Stream.of(message.getLinkPreviews()).filter(p -> p.getThumbnail().isPresent()).map(p -> p.getThumbnail().get()).toList());
+ attachments.addAll(Stream.of(message.getSharedContacts()).filter(c -> c.getAvatar() != null).map(c -> c.getAvatar().getAttachment()).withoutNulls().toList());
+
+ return Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId(), destination)).toList();
+ }
+
@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();
@@ -211,8 +227,10 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
}
} catch (UntrustedIdentityException uie) {
warn(TAG, "Failure", uie);
- database.addMismatchedIdentity(messageId, Address.fromSerialized(uie.getE164Number()), uie.getIdentityKey());
- database.markAsSentFailed(messageId);
+ if (messageId >= 0) {
+ database.addMismatchedIdentity(messageId, Address.fromSerialized(uie.getE164Number()), uie.getIdentityKey());
+ database.markAsSentFailed(messageId);
+ }
}
}
@@ -268,10 +286,18 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, mediaMessage, syncAccess);
- messageSender.sendMessage(messageId, syncMessage, syncAccess);
+ messageSender.sendMessage(templateMessageId, 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 = PromiseUtil.get(LokiStorageAPI.shared.getPrimaryDevicePublicKey(address.getNumber()), null);
+ 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 +318,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/PushSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
index aaedf8f25a..526264ef67 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java
@@ -303,7 +303,8 @@ public abstract class PushSendJob extends SendJob {
}
protected SignalServiceSyncMessage buildSelfSendSyncMessage(@NonNull Context context, @NonNull SignalServiceDataMessage message, Optional syncAccess) {
- String localNumber = TextSecurePreferences.getLocalNumber(context);
+ String primary = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
+ String localNumber = primary != null ? primary : TextSecurePreferences.getLocalNumber(context);
SentTranscriptMessage transcript = new SentTranscriptMessage(localNumber,
message.getTimestamp(),
message,
diff --git a/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java
index db24ce9fe8..a8a084a447 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.MultiDeviceUtilities;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
@@ -29,6 +30,9 @@ 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.api.LokiStorageAPI;
+import org.whispersystems.signalservice.loki.messaging.LokiSyncMessage;
+import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
import java.io.IOException;
@@ -45,6 +49,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 +60,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();
@@ -151,14 +159,18 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
} catch (InsecureFallbackApprovalException e) {
warn(TAG, "Failure", e);
- database.markAsPendingInsecureSmsFallback(record.getId());
- MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipient(), record.getThreadId());
- ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(false));
+ if (messageId >= 0) {
+ database.markAsPendingInsecureSmsFallback(record.getId());
+ MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipient(), record.getThreadId());
+ ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(false));
+ }
} catch (UntrustedIdentityException e) {
warn(TAG, "Failure", e);
- database.addMismatchedIdentity(record.getId(), Address.fromSerialized(e.getE164Number()), e.getIdentityKey());
- database.markAsSentFailed(record.getId());
- database.markAsPush(record.getId());
+ if (messageId >= 0) {
+ database.addMismatchedIdentity(record.getId(), Address.fromSerialized(e.getE164Number()), e.getIdentityKey());
+ database.markAsSentFailed(record.getId());
+ database.markAsPush(record.getId());
+ }
}
}
@@ -219,10 +231,18 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, textSecureMessage, syncAccess);
- messageSender.sendMessage(messageId, syncMessage, syncAccess);
+ messageSender.sendMessage(templateMessageId, 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 to the primary device, this way it will show that we sent a message to the primary device and not a secondary device
+ String primaryDevice = PromiseUtil.get(LokiStorageAPI.shared.getPrimaryDevicePublicKey(address.getNumber()), null);
+ 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 +261,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/jobs/TypingSendJob.java b/src/org/thoughtcrime/securesms/jobs/TypingSendJob.java
index d1dc6e53fb..babe7fe04e 100644
--- a/src/org/thoughtcrime/securesms/jobs/TypingSendJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/TypingSendJob.java
@@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -19,6 +20,7 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage.Action;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
import java.util.Collections;
import java.util.List;
@@ -96,9 +98,11 @@ public class TypingSendJob extends BaseJob implements InjectableType {
List> unidentifiedAccess = Stream.of(recipients).map(r -> UnidentifiedAccessUtil.getAccessFor(context, r)).toList();
SignalServiceTypingMessage typingMessage = new SignalServiceTypingMessage(typing ? Action.STARTED : Action.STOPPED, System.currentTimeMillis(), groupId);
- // Loki - Don't send typing indicators in group chats
- if (!recipient.isGroupRecipient()) {
- // TODO: Message ID
+ // Loki - Don't send typing indicators in group chats or to ourselves
+ if (recipient.isGroupRecipient()) { return; }
+
+ boolean isOurDevice = PromiseUtil.get(MultiDeviceUtilities.isOneOfOurDevices(context, recipient.getAddress()), false);
+ if (!isOurDevice) {
messageSender.sendTyping(0, addresses, unidentifiedAccess, typingMessage);
}
}
diff --git a/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt b/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt
new file mode 100644
index 0000000000..050f9a1bb6
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt
@@ -0,0 +1,100 @@
+package org.thoughtcrime.securesms.loki
+
+import android.content.Context
+import nl.komponents.kovenant.ui.successUi
+import org.thoughtcrime.securesms.database.DatabaseFactory
+import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus
+import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
+
+object FriendRequestHandler {
+ enum class ActionType { Sending, Sent, Failed }
+
+ @JvmStatic
+ fun updateFriendRequestState(context: Context, type: ActionType, messageId: Long, threadId: Long) {
+ if (threadId < 0) return
+ val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId) ?: return
+ if (!recipient.address.isPhone) { return }
+
+ val currentFriendStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId)
+ // Update thread status if we haven't sent a friend request before
+ if (currentFriendStatus != LokiThreadFriendRequestStatus.REQUEST_RECEIVED &&
+ currentFriendStatus != LokiThreadFriendRequestStatus.REQUEST_SENT &&
+ currentFriendStatus != LokiThreadFriendRequestStatus.FRIENDS
+ ) {
+ val threadFriendStatus = when (type) {
+ ActionType.Sending -> LokiThreadFriendRequestStatus.REQUEST_SENDING
+ ActionType.Failed -> LokiThreadFriendRequestStatus.NONE
+ ActionType.Sent -> LokiThreadFriendRequestStatus.REQUEST_SENT
+ }
+ DatabaseFactory.getLokiThreadDatabase(context).setFriendRequestStatus(threadId, threadFriendStatus)
+ }
+
+ // Update message status
+ if (messageId >= 0) {
+ val messageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
+ val friendRequestStatus = messageDatabase.getFriendRequestStatus(messageId)
+ if (type == ActionType.Sending) {
+ // We only want to update message status if we aren't friends with another of their devices
+ // This avoids spam in the ui where it would keep telling the user that they sent a friend request on every single message
+ isFriendsWithAnyLinkedDevice(context, recipient).successUi { isFriends ->
+ if (!isFriends && friendRequestStatus == LokiMessageFriendRequestStatus.NONE) {
+ messageDatabase.setFriendRequestStatus(messageId, LokiMessageFriendRequestStatus.REQUEST_SENDING)
+ }
+ }
+ } else if (friendRequestStatus != LokiMessageFriendRequestStatus.NONE) {
+ // Update the friend request status of the message if we have it
+ val messageFriendRequestStatus = when (type) {
+ ActionType.Failed -> LokiMessageFriendRequestStatus.REQUEST_FAILED
+ ActionType.Sent -> LokiMessageFriendRequestStatus.REQUEST_PENDING
+ else -> throw IllegalStateException()
+ }
+ messageDatabase.setFriendRequestStatus(messageId, messageFriendRequestStatus)
+ }
+ }
+ }
+
+ @JvmStatic
+ fun updateLastFriendRequestMessage(context: Context, threadId: Long, status: LokiMessageFriendRequestStatus) {
+ if (threadId < 0) { return }
+
+ val messages = DatabaseFactory.getSmsDatabase(context).getAllMessageIDs(threadId)
+ val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
+ val lastMessage = messages.find {
+ val friendRequestStatus = lokiMessageDatabase.getFriendRequestStatus(it)
+ friendRequestStatus == LokiMessageFriendRequestStatus.REQUEST_PENDING
+ } ?: return
+
+ DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(lastMessage, status)
+ }
+
+ @JvmStatic
+ fun receivedIncomingFriendRequestMessage(context: Context, threadId: Long) {
+ val smsMessageDatabase = DatabaseFactory.getSmsDatabase(context)
+
+ // We only want to update the last message status if we're not friends with any of their linked devices
+ // This ensures that we don't spam the UI with accept/decline messages
+ val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId) ?: return
+ if (!recipient.address.isPhone) { return }
+
+ isFriendsWithAnyLinkedDevice(context, recipient).successUi { isFriends ->
+ if (isFriends) { return@successUi }
+
+ // Since messages are forwarded to the primary device thread, we need to update it there
+ val messageCount = smsMessageDatabase.getMessageCountForThread(threadId)
+ val messageID = smsMessageDatabase.getIDForMessageAtIndex(threadId, messageCount - 1) // The message that was just received
+ if (messageID < 0) { return@successUi }
+
+ val messageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
+
+ // We need to go through and set all messages which are REQUEST_PENDING to NONE
+ smsMessageDatabase.getAllMessageIDs(threadId)
+ .filter { messageDatabase.getFriendRequestStatus(it) == LokiMessageFriendRequestStatus.REQUEST_PENDING }
+ .forEach {
+ messageDatabase.setFriendRequestStatus(it, LokiMessageFriendRequestStatus.NONE)
+ }
+
+ // Set the last message to pending
+ messageDatabase.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_PENDING)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/LokiMessageDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiMessageDatabase.kt
index 26d93228e4..817b76f5dd 100644
--- a/src/org/thoughtcrime/securesms/loki/LokiMessageDatabase.kt
+++ b/src/org/thoughtcrime/securesms/loki/LokiMessageDatabase.kt
@@ -12,11 +12,14 @@ import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestS
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
companion object {
- private val tableName = "loki_message_friend_request_database"
+ private val messageFriendRequestTableName = "loki_message_friend_request_database"
+ private val messageThreadMappingTableName = "loki_message_thread_mapping_database"
private val messageID = "message_id"
private val serverID = "server_id"
private val friendRequestStatus = "friend_request_status"
- @JvmStatic val createTableCommand = "CREATE TABLE $tableName ($messageID INTEGER PRIMARY KEY, $serverID INTEGER DEFAULT 0, $friendRequestStatus INTEGER DEFAULT 0);"
+ private val threadID = "thread_id"
+ @JvmStatic val createMessageFriendRequestTableCommand = "CREATE TABLE $messageFriendRequestTableName ($messageID INTEGER PRIMARY KEY, $serverID INTEGER DEFAULT 0, $friendRequestStatus INTEGER DEFAULT 0);"
+ @JvmStatic val createMessageToThreadMappingTableCommand = "CREATE TABLE $messageThreadMappingTableName ($messageID INTEGER PRIMARY KEY, $threadID INTEGER);"
}
override fun getQuoteServerID(quoteID: Long, quoteeHexEncodedPublicKey: String): Long? {
@@ -26,14 +29,14 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
fun getServerID(messageID: Long): Long? {
val database = databaseHelper.readableDatabase
- return database.get(tableName, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
+ return database.get(messageFriendRequestTableName, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
cursor.getInt(Companion.serverID)
}?.toLong()
}
fun getMessageID(serverID: Long): Long? {
val database = databaseHelper.readableDatabase
- return database.get(tableName, "${Companion.serverID} = ?", arrayOf( serverID.toString() )) { cursor ->
+ return database.get(messageFriendRequestTableName, "${Companion.serverID} = ?", arrayOf( serverID.toString() )) { cursor ->
cursor.getInt(messageID)
}?.toLong()
}
@@ -43,12 +46,27 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
val contentValues = ContentValues(2)
contentValues.put(Companion.messageID, messageID)
contentValues.put(Companion.serverID, serverID)
- database.insertOrUpdate(tableName, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
+ database.insertOrUpdate(messageFriendRequestTableName, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
+ }
+
+ fun getOriginalThreadID(messageID: Long): Long {
+ val database = databaseHelper.readableDatabase
+ return database.get(messageThreadMappingTableName, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
+ cursor.getInt(Companion.threadID)
+ }?.toLong() ?: -1L
+ }
+
+ fun setOriginalThreadID(messageID: Long, threadID: Long) {
+ val database = databaseHelper.writableDatabase
+ val contentValues = ContentValues(2)
+ contentValues.put(Companion.messageID, messageID)
+ contentValues.put(Companion.threadID, threadID)
+ database.insertOrUpdate(messageThreadMappingTableName, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
}
fun getFriendRequestStatus(messageID: Long): LokiMessageFriendRequestStatus {
val database = databaseHelper.readableDatabase
- val result = database.get(tableName, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
+ val result = database.get(messageFriendRequestTableName, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
cursor.getInt(friendRequestStatus)
}
return if (result != null) {
@@ -63,7 +81,7 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
val contentValues = ContentValues(2)
contentValues.put(Companion.messageID, messageID)
contentValues.put(Companion.friendRequestStatus, friendRequestStatus.rawValue)
- database.insertOrUpdate(tableName, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
+ database.insertOrUpdate(messageFriendRequestTableName, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
val threadID = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageID)
notifyConversationListeners(threadID)
}
diff --git a/src/org/thoughtcrime/securesms/loki/LokiPreKeyBundleDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiPreKeyBundleDatabase.kt
index becd442e51..6eee3387b1 100644
--- a/src/org/thoughtcrime/securesms/loki/LokiPreKeyBundleDatabase.kt
+++ b/src/org/thoughtcrime/securesms/loki/LokiPreKeyBundleDatabase.kt
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki
import android.content.ContentValues
import android.content.Context
+import net.sqlcipher.Cursor
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.PreKeyUtil
import org.thoughtcrime.securesms.database.Database
@@ -35,6 +36,11 @@ class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) :
"$signedPreKeySignature TEXT," + "$identityKey TEXT NOT NULL," + "$deviceID INTEGER," + "$registrationID INTEGER" + ");"
}
+ fun resetAllPreKeyBundleInfo() {
+ TextSecurePreferences.removeLocalRegistrationId(context)
+ TextSecurePreferences.setSignedPreKeyRegistered(context, false)
+ }
+
fun generatePreKeyBundle(hexEncodedPublicKey: String): PreKeyBundle? {
var registrationID = TextSecurePreferences.getLocalRegistrationId(context)
if (registrationID == 0) {
@@ -92,7 +98,14 @@ class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) :
fun hasPreKeyBundle(hexEncodedPublicKey: String): Boolean {
val database = databaseHelper.readableDatabase
- val cursor = database.query(tableName, null, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey ), null, null, null)
- return cursor != null && cursor.count > 0
+ var cursor: Cursor? = null
+ return try {
+ cursor = database.query(tableName, null, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey ), null, null, null)
+ cursor != null && cursor.count > 0
+ } catch (e: Exception) {
+ false
+ } finally {
+ cursor?.close()
+ }
}
}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt b/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt
index 3e6685e429..28bca2fea1 100644
--- a/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt
+++ b/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt
@@ -4,27 +4,23 @@ import android.content.Context
import android.os.Handler
import android.util.Log
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
-import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
-import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.jobs.PushDecryptJob
-import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository
-import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil
-import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
-import org.thoughtcrime.securesms.mms.QuoteModel
-import org.thoughtcrime.securesms.recipients.Recipient
-import org.thoughtcrime.securesms.util.GroupUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
-import org.thoughtcrime.securesms.util.Util
import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.messages.SignalServiceGroup
+import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.loki.api.LokiPublicChat
import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI
import org.whispersystems.signalservice.loki.api.LokiPublicChatMessage
+import org.whispersystems.signalservice.loki.api.LokiStorageAPI
+import org.whispersystems.signalservice.loki.utilities.get
+import org.whispersystems.signalservice.loki.utilities.successBackground
+import java.util.*
class LokiPublicChatPoller(private val context: Context, private val group: LokiPublicChat) {
private val handler = Handler()
@@ -94,18 +90,17 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
// endregion
// region Polling
- private fun pollForNewMessages() {
- fun processIncomingMessage(message: LokiPublicChatMessage) {
- val id = group.id.toByteArray()
- val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, null, null, null)
- val quote = if (message.quote != null) {
- SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteeHexEncodedPublicKey), message.quote!!.quotedMessageBody, listOf())
- } else {
- null
- }
- val attachments = message.attachments.mapNotNull { attachment ->
- if (attachment.kind != LokiPublicChatMessage.Attachment.Kind.Attachment) { return@mapNotNull null }
- SignalServiceAttachmentPointer(
+ private fun getDataMessage(message: LokiPublicChatMessage): SignalServiceDataMessage {
+ val id = group.id.toByteArray()
+ val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, null, null, null)
+ val quote = if (message.quote != null) {
+ SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteeHexEncodedPublicKey), message.quote!!.quotedMessageBody, listOf())
+ } else {
+ null
+ }
+ val attachments = message.attachments.mapNotNull { attachment ->
+ if (attachment.kind != LokiPublicChatMessage.Attachment.Kind.Attachment) { return@mapNotNull null }
+ SignalServiceAttachmentPointer(
attachment.serverID,
attachment.contentType,
ByteArray(0),
@@ -117,30 +112,35 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
false,
Optional.fromNullable(attachment.caption),
attachment.url)
- }
- val linkPreview = message.attachments.firstOrNull { it.kind == LokiPublicChatMessage.Attachment.Kind.LinkPreview }
- val signalLinkPreviews = mutableListOf()
- if (linkPreview != null) {
- val attachment = SignalServiceAttachmentPointer(
- linkPreview.serverID,
- linkPreview.contentType,
- ByteArray(0),
- Optional.of(linkPreview.size),
- Optional.absent(),
- linkPreview.width, linkPreview.height,
- Optional.absent(),
- Optional.of(linkPreview.fileName),
- false,
- Optional.fromNullable(linkPreview.caption),
- linkPreview.url)
- signalLinkPreviews.add(SignalServiceDataMessage.Preview(linkPreview.linkPreviewURL!!, linkPreview.linkPreviewTitle!!, Optional.of(attachment)))
- }
- val body = if (message.body == message.timestamp.toString()) "" else message.body // Workaround for the fact that the back-end doesn't accept messages without a body
- val serviceDataMessage = SignalServiceDataMessage(message.timestamp, serviceGroup, attachments, body, false, 0, false, null, false, quote, null, signalLinkPreviews, null)
+ }
+ val linkPreview = message.attachments.firstOrNull { it.kind == LokiPublicChatMessage.Attachment.Kind.LinkPreview }
+ val signalLinkPreviews = mutableListOf()
+ if (linkPreview != null) {
+ val attachment = SignalServiceAttachmentPointer(
+ linkPreview.serverID,
+ linkPreview.contentType,
+ ByteArray(0),
+ Optional.of(linkPreview.size),
+ Optional.absent(),
+ linkPreview.width, linkPreview.height,
+ Optional.absent(),
+ Optional.of(linkPreview.fileName),
+ false,
+ Optional.fromNullable(linkPreview.caption),
+ linkPreview.url)
+ signalLinkPreviews.add(SignalServiceDataMessage.Preview(linkPreview.linkPreviewURL!!, linkPreview.linkPreviewTitle!!, Optional.of(attachment)))
+ }
+ val body = if (message.body == message.timestamp.toString()) "" else message.body // Workaround for the fact that the back-end doesn't accept messages without a body
+ return SignalServiceDataMessage(message.timestamp, serviceGroup, attachments, body, false, 0, false, null, false, quote, null, signalLinkPreviews, null)
+ }
+
+ private fun pollForNewMessages() {
+ fun processIncomingMessage(message: LokiPublicChatMessage) {
+ val serviceDataMessage = getDataMessage(message)
val serviceContent = SignalServiceContent(serviceDataMessage, message.hexEncodedPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.timestamp, false)
val senderDisplayName = "${message.displayName} (...${message.hexEncodedPublicKey.takeLast(8)})"
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.hexEncodedPublicKey, senderDisplayName)
- if (quote != null || attachments.count() > 0 || linkPreview != null) {
+ if (serviceDataMessage.quote.isPresent || (serviceDataMessage.attachments.isPresent && serviceDataMessage.attachments.get().size > 0) || serviceDataMessage.previews.isPresent) {
PushDecryptJob(context).handleMediaMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID))
} else {
PushDecryptJob(context).handleTextMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID))
@@ -148,61 +148,29 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
}
fun processOutgoingMessage(message: LokiPublicChatMessage) {
val messageServerID = message.serverID ?: return
- val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
- val isDuplicate = lokiMessageDatabase.getMessageID(messageServerID) != null
+ val isDuplicate = DatabaseFactory.getLokiMessageDatabase(context).getMessageID(messageServerID) != null
if (isDuplicate) { return }
if (message.body.isEmpty() && message.attachments.isEmpty() && message.quote == null) { return }
- val id = group.id.toByteArray()
- val mmsDatabase = DatabaseFactory.getMmsDatabase(context)
- val recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(id, false)), false)
- val quote: QuoteModel?
- if (message.quote != null) {
- quote = QuoteModel(message.quote!!.quotedMessageTimestamp, Address.fromSerialized(message.quote!!.quoteeHexEncodedPublicKey), message.quote!!.quotedMessageBody, false, listOf())
+ val localNumber = TextSecurePreferences.getLocalNumber(context)
+ val dataMessage = getDataMessage(message)
+ val transcript = SentTranscriptMessage(localNumber, dataMessage.timestamp, dataMessage, dataMessage.expiresInSeconds.toLong(), Collections.singletonMap(localNumber, false))
+ transcript.messageServerID = messageServerID
+ if (dataMessage.quote.isPresent || (dataMessage.attachments.isPresent && dataMessage.attachments.get().size > 0) || dataMessage.previews.isPresent) {
+ PushDecryptJob(context).handleSynchronizeSentMediaMessage(transcript)
} else {
- quote = null
- }
- // TODO: Handle attachments correctly for our previous messages
- val body = if (message.body == message.timestamp.toString()) "" else message.body // Workaround for the fact that the back-end doesn't accept messages without a body
- val signalMessage = OutgoingMediaMessage(recipient, body, listOf(), message.timestamp, 0, 0,
- ThreadDatabase.DistributionTypes.DEFAULT, quote, listOf(), listOf(), listOf(), listOf())
- val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
- fun finalize() {
- val messageID = mmsDatabase.insertMessageOutbox(signalMessage, threadID, false, null)
- mmsDatabase.markAsSent(messageID, true)
- mmsDatabase.markUnidentified(messageID, false)
- lokiMessageDatabase.setServerID(messageID, messageServerID)
- }
- val urls = LinkPreviewUtil.findWhitelistedUrls(message.body)
- val urlCount = urls.size
- if (urlCount != 0) {
- val lpr = LinkPreviewRepository(context)
- var count = 0
- urls.forEach { url ->
- lpr.getLinkPreview(context, url.url) { lp ->
- Util.runOnMain {
- count += 1
- if (lp.isPresent) { signalMessage.linkPreviews.add(lp.get()) }
- if (count == urlCount) {
- try {
- finalize()
- } catch (e: Exception) {
- // TODO: Handle
- }
-
- }
- }
- }
- }
- } else {
- finalize()
+ PushDecryptJob(context).handleSynchronizeSentTextMessage(transcript)
}
}
- api.getMessages(group.channel, group.server).success { messages ->
- messages.forEach { message ->
- if (message.hexEncodedPublicKey != userHexEncodedPublicKey) {
- processIncomingMessage(message)
- } else {
- processOutgoingMessage(message)
+ api.getMessages(group.channel, group.server).successBackground { messages ->
+ if (messages.isNotEmpty()) {
+ val ourDevices = LokiStorageAPI.shared.getAllDevicePublicKeys(userHexEncodedPublicKey).get(setOf())
+ // Process messages in the background
+ messages.forEach { message ->
+ if (ourDevices.contains(message.hexEncodedPublicKey)) {
+ processOutgoingMessage(message)
+ } else {
+ processIncomingMessage(message)
+ }
}
}
}.fail {
diff --git a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt
index 4c177bc108..3b66359ea9 100644
--- a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt
+++ b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt
@@ -41,6 +41,8 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
}
fun getFriendRequestStatus(threadID: Long): LokiThreadFriendRequestStatus {
+ if (threadID < 0) { return LokiThreadFriendRequestStatus.NONE }
+
val database = databaseHelper.readableDatabase
val result = database.get(friendRequestTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor ->
cursor.getInt(friendRequestStatus)
@@ -53,6 +55,8 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
}
override fun setFriendRequestStatus(threadID: Long, friendRequestStatus: LokiThreadFriendRequestStatus) {
+ if (threadID < 0) { return }
+
val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2)
contentValues.put(Companion.threadID, threadID)
diff --git a/src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt b/src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt
index adaee4ef49..257336de05 100644
--- a/src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt
+++ b/src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt
@@ -1,13 +1,19 @@
+@file:JvmName("MultiDeviceUtilities")
package org.thoughtcrime.securesms.loki
import android.content.Context
import nl.komponents.kovenant.Promise
-import nl.komponents.kovenant.deferred
+import nl.komponents.kovenant.all
+import nl.komponents.kovenant.functional.bind
+import nl.komponents.kovenant.functional.map
+import nl.komponents.kovenant.toFailVoid
import org.thoughtcrime.securesms.ApplicationContext
+import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.logging.Log
import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
@@ -16,54 +22,76 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
+import org.whispersystems.signalservice.loki.utilities.recover
+import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
+import java.util.*
+import kotlin.concurrent.schedule
-fun getAllDevicePublicKeys(context: Context, hexEncodedPublicKey: String, storageAPI: LokiStorageAPI, block: (devicePublicKey: String, isFriend: Boolean, friendCount: Int) -> Unit) {
+fun getAllDeviceFriendRequestStatuses(context: Context, hexEncodedPublicKey: String): Promise