mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-24 16:57:50 +00:00
Merge branch 'dev' into disable-permissions
This commit is contained in:
commit
cf8ed65f4e
@ -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" />
|
||||
|
||||
|
@ -30,6 +30,14 @@
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="+14151231234"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tag"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:textColor="#A2A2A2"
|
||||
tools:text="Secondary Device" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
@ -1572,6 +1572,7 @@
|
||||
<!-- Conversation list activity -->
|
||||
<string name="activity_conversation_list_empty_state_message">Looks like you don\'t have any conversations yet. Get started by messaging a friend.</string>
|
||||
<!-- Settings activity -->
|
||||
<string name="activity_settings_secondary_device_tag">Secondary device</string>
|
||||
<string name="activity_settings_public_key_copied_message">Copied to clipboard</string>
|
||||
<string name="activity_settings_share_public_key_button_title">Share Public Key</string>
|
||||
<string name="activity_settings_show_qr_code_button_title">Show QR Code</string>
|
||||
|
@ -42,7 +42,7 @@
|
||||
android:icon="@drawable/icon_qr_code"/>
|
||||
|
||||
<Preference android:key="preference_category_link_device"
|
||||
android:title="Link Device (Coming Soon)"
|
||||
android:title="Link Device"
|
||||
android:icon="@drawable/icon_link"/>
|
||||
|
||||
<Preference android:key="preference_category_seed"
|
||||
|
@ -87,6 +87,7 @@ import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol;
|
||||
import org.whispersystems.signalservice.loki.api.LokiDotNetAPI;
|
||||
import org.whispersystems.signalservice.loki.api.LokiPublicChat;
|
||||
import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI;
|
||||
import org.whispersystems.signalservice.loki.api.LokiLongPoller;
|
||||
@ -109,6 +110,7 @@ import io.fabric.sdk.android.Fabric;
|
||||
import kotlin.Unit;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import network.loki.messenger.BuildConfig;
|
||||
import okhttp3.Cache;
|
||||
|
||||
import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant;
|
||||
import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
|
||||
@ -124,6 +126,7 @@ import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
|
||||
public class ApplicationContext extends MultiDexApplication implements DependencyInjector, DefaultLifecycleObserver, LokiP2PAPIDelegate {
|
||||
|
||||
private static final String TAG = ApplicationContext.class.getSimpleName();
|
||||
private final static int OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
private ExpiringMessageManager expiringMessageManager;
|
||||
private TypingStatusRepository typingStatusRepository;
|
||||
@ -188,6 +191,12 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
};
|
||||
// Loki - Set up public chat manager
|
||||
lokiPublicChatManager = new LokiPublicChatManager(this);
|
||||
// Loki - Set the cache
|
||||
LokiDotNetAPI.setCache(new Cache(this.getCacheDir(), OK_HTTP_CACHE_SIZE));
|
||||
// Loki - Update device mappings
|
||||
if (setUpStorageAPIIfNeeded()) {
|
||||
LokiStorageAPI.Companion.getShared().updateUserDeviceMappings();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -199,7 +208,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
// Loki - Start long polling if needed
|
||||
startLongPollingIfNeeded();
|
||||
lokiPublicChatManager.startPollersIfNeeded();
|
||||
setUpStorageAPIIfNeeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -450,14 +458,16 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
}
|
||||
|
||||
// region Loki
|
||||
public void setUpStorageAPIIfNeeded() {
|
||||
public boolean setUpStorageAPIIfNeeded() {
|
||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userHexEncodedPublicKey != null && IdentityKeyUtil.hasIdentityKey(this)) {
|
||||
boolean isDebugMode = BuildConfig.DEBUG;
|
||||
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
|
||||
LokiAPIDatabaseProtocol database = DatabaseFactory.getLokiAPIDatabase(this);
|
||||
LokiStorageAPI.Companion.configure(isDebugMode, userHexEncodedPublicKey, userPrivateKey, database);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setUpP2PAPI() {
|
||||
|
@ -26,6 +26,7 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Bundle;
|
||||
@ -39,9 +40,12 @@ import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.loki.DeviceLinkingDialog;
|
||||
import org.thoughtcrime.securesms.loki.DeviceLinkingDialogDelegate;
|
||||
import org.thoughtcrime.securesms.loki.DeviceLinkingView;
|
||||
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
|
||||
import org.thoughtcrime.securesms.loki.QRCodeDialog;
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
|
||||
@ -52,6 +56,7 @@ import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation;
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec;
|
||||
import org.whispersystems.signalservice.loki.utilities.Analytics;
|
||||
import org.whispersystems.signalservice.loki.utilities.SerializationKt;
|
||||
@ -160,23 +165,21 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
boolean isMasterDevice = (masterHexEncodedPublicKey == null);
|
||||
|
||||
Preference profilePreference = this.findPreference(PREFERENCE_CATEGORY_PROFILE);
|
||||
// Hide if this is a slave device
|
||||
profilePreference.setVisible(isMasterDevice);
|
||||
profilePreference.setOnPreferenceClickListener(new ProfileClickListener());
|
||||
if (isMasterDevice) { profilePreference.setOnPreferenceClickListener(new ProfileClickListener()); }
|
||||
/*
|
||||
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS));
|
||||
*/
|
||||
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_NOTIFICATIONS));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_NOTIFICATIONS));
|
||||
this.findPreference(PREFERENCE_CATEGORY_APP_PROTECTION)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APP_PROTECTION));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_APP_PROTECTION));
|
||||
/*
|
||||
this.findPreference(PREFERENCE_CATEGORY_APPEARANCE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE));
|
||||
*/
|
||||
this.findPreference(PREFERENCE_CATEGORY_CHATS)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CHATS));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_CHATS));
|
||||
/*
|
||||
this.findPreference(PREFERENCE_CATEGORY_DEVICES)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DEVICES));
|
||||
@ -184,21 +187,19 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||
*/
|
||||
this.findPreference(PREFERENCE_CATEGORY_PUBLIC_KEY)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PUBLIC_KEY));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_PUBLIC_KEY));
|
||||
this.findPreference(PREFERENCE_CATEGORY_QR_CODE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_QR_CODE));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_QR_CODE));
|
||||
|
||||
// TODO: Enable this again later
|
||||
/*
|
||||
Preference linkDevicePreference = this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE);
|
||||
// Hide if this is a slave device
|
||||
linkDevicePreference.setVisible(isMasterDevice);
|
||||
linkDevicePreference.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_LINK_DEVICE));
|
||||
*/
|
||||
linkDevicePreference.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_LINK_DEVICE));
|
||||
|
||||
Preference seedPreference = this.findPreference(PREFERENCE_CATEGORY_SEED);
|
||||
// Hide if this is a slave device
|
||||
seedPreference.setVisible(isMasterDevice);
|
||||
seedPreference.setOnPreferenceClickListener(new CategoryClickListener((PREFERENCE_CATEGORY_SEED)));
|
||||
seedPreference.setOnPreferenceClickListener(new CategoryClickListener(getContext(), (PREFERENCE_CATEGORY_SEED)));
|
||||
|
||||
if (VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
tintIcons(getActivity());
|
||||
@ -291,10 +292,12 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
this.findPreference(PREFERENCE_CATEGORY_SEED).setIcon(seed);
|
||||
}
|
||||
|
||||
private class CategoryClickListener implements Preference.OnPreferenceClickListener {
|
||||
private class CategoryClickListener implements Preference.OnPreferenceClickListener, DeviceLinkingDialogDelegate {
|
||||
private String category;
|
||||
private Context context;
|
||||
|
||||
CategoryClickListener(String category) {
|
||||
CategoryClickListener(Context context,String category) {
|
||||
this.context = context;
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
@ -347,7 +350,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
QRCodeDialog.INSTANCE.show(getContext());
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_LINK_DEVICE:
|
||||
DeviceLinkingDialog.Companion.show(getContext(), DeviceLinkingView.Mode.Master, null);
|
||||
DeviceLinkingDialog.Companion.show(getContext(), DeviceLinkingView.Mode.Master, this);
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_SEED:
|
||||
Analytics.Companion.getShared().track("Seed Modal Shown");
|
||||
@ -390,6 +393,12 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override public void sendPairingAuthorizedMessage(@NotNull PairingAuthorisation pairingAuthorisation) {
|
||||
AsyncTask.execute(() -> MultiDeviceUtilities.signAndSendPairingAuthorisationMessage(context, pairingAuthorisation));
|
||||
}
|
||||
@Override public void handleDeviceLinkAuthorized(@NotNull PairingAuthorisation pairingAuthorisation) {}
|
||||
@Override public void handleDeviceLinkingDialogDismissed() {}
|
||||
}
|
||||
|
||||
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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<Mention> 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
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ public class Address implements Parcelable, Comparable<Address> {
|
||||
|
||||
private Address(@NonNull String address, Boolean isPublicChat) {
|
||||
if (address == null) throw new AssertionError(address);
|
||||
this.address = address;
|
||||
this.address = address.toLowerCase();
|
||||
this.isPublicChat = isPublicChat;
|
||||
}
|
||||
|
||||
|
@ -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<Long> getAllMessageIDs(long threadID) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
Set<Long> 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);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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),
|
||||
|
@ -24,6 +24,7 @@ public class Data {
|
||||
@JsonProperty private final Map<String, double[]> doubleArrays;
|
||||
@JsonProperty private final Map<String, Boolean> booleans;
|
||||
@JsonProperty private final Map<String, boolean[]> booleanArrays;
|
||||
@JsonProperty private final Map<String, byte[]> byteArrays;
|
||||
|
||||
public Data(@JsonProperty("strings") @NonNull Map<String, String> strings,
|
||||
@JsonProperty("stringArrays") @NonNull Map<String, String[]> stringArrays,
|
||||
@ -36,7 +37,8 @@ public class Data {
|
||||
@JsonProperty("doubles") @NonNull Map<String, Double> doubles,
|
||||
@JsonProperty("doubleArrays") @NonNull Map<String, double[]> doubleArrays,
|
||||
@JsonProperty("booleans") @NonNull Map<String, Boolean> booleans,
|
||||
@JsonProperty("booleanArrays") @NonNull Map<String, boolean[]> booleanArrays)
|
||||
@JsonProperty("booleanArrays") @NonNull Map<String, boolean[]> booleanArrays,
|
||||
@JsonProperty("byteArrays") @NonNull Map<String, byte[]> byteArrays)
|
||||
{
|
||||
this.strings = strings;
|
||||
this.stringArrays = stringArrays;
|
||||
@ -50,6 +52,7 @@ public class Data {
|
||||
this.doubleArrays = doubleArrays;
|
||||
this.booleans = booleans;
|
||||
this.booleanArrays = booleanArrays;
|
||||
this.byteArrays = byteArrays;
|
||||
}
|
||||
|
||||
public boolean hasString(@NonNull String key) {
|
||||
@ -201,6 +204,14 @@ public class Data {
|
||||
return booleanArrays.get(key);
|
||||
}
|
||||
|
||||
public boolean hasByteArray(@NonNull String key) {
|
||||
return byteArrays.containsKey(key);
|
||||
}
|
||||
|
||||
public byte[] getByteArray(@NonNull String key) {
|
||||
throwIfAbsent(byteArrays, key);
|
||||
return byteArrays.get(key);
|
||||
}
|
||||
|
||||
private void throwIfAbsent(@NonNull Map map, @NonNull String key) {
|
||||
if (!map.containsKey(key)) {
|
||||
@ -223,6 +234,7 @@ public class Data {
|
||||
private final Map<String, double[]> doubleArrays = new HashMap<>();
|
||||
private final Map<String, Boolean> booleans = new HashMap<>();
|
||||
private final Map<String, boolean[]> booleanArrays = new HashMap<>();
|
||||
private final Map<String, byte[]> byteArrays = new HashMap<>();
|
||||
|
||||
public Builder putString(@NonNull String key, @Nullable String value) {
|
||||
strings.put(key, value);
|
||||
@ -284,6 +296,11 @@ public class Data {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder putByteArray(@NonNull String key, @NonNull byte[] value) {
|
||||
byteArrays.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Data build() {
|
||||
return new Data(strings,
|
||||
stringArrays,
|
||||
@ -296,7 +313,8 @@ public class Data {
|
||||
doubles,
|
||||
doubleArrays,
|
||||
booleans,
|
||||
booleanArrays);
|
||||
booleanArrays,
|
||||
byteArrays);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}};
|
||||
}
|
||||
|
||||
|
@ -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<IdentityDatabase.IdentityRecord> identityRecord = DatabaseFactory.getIdentityDatabase(context).getIdentity(address);
|
||||
Optional<VerifiedMessage> 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<ContactData> contacts = ContactAccessor.getInstance().getContactsWithPush(context);
|
||||
DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactDataFile));
|
||||
List<ContactData> 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<Integer> expireTimer = recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages()) : Optional.absent();
|
||||
|
||||
out.write(new DeviceContact(address.toPhoneString(), name, getAvatar(contactUri), color, verified, profileKey, blocked, expireTimer));
|
||||
// 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<ContactData> getAllContacts() {
|
||||
List<Address> contactAddresses = DatabaseFactory.getRecipientDatabase(context).getRegistered();
|
||||
List<ContactData> 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<SignalServiceAttachmentStream> 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<VerifiedMessage> getVerifiedMessage(Recipient recipient, Optional<IdentityDatabase.IdentityRecord> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<String> 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<DeviceContact> 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<Long> 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<QuoteModel> quote = getValidatedQuote(message.getQuote());
|
||||
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
|
||||
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
|
||||
Optional<Attachment> 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<QuoteModel> quote = getValidatedQuote(message.getMessage().getQuote());
|
||||
Optional<Attachment> sticker = getStickerAttachment(message.getMessage().getSticker());
|
||||
Optional<List<Contact>> 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> 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<InsertResult> 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);
|
||||
|
@ -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<PushMediaSendJob> 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<Attachment> 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<AttachmentUploadJob> attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId(), destination)).toList();
|
||||
List<AttachmentUploadJob> 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<Job>)(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<AttachmentUploadJob> getAttachmentUploadJobs(@NonNull Context context, long messageId, @NonNull Address destination)
|
||||
throws NoSuchMessageException, MmsException
|
||||
{
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
|
||||
List<Attachment> 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<UnidentifiedAccessPair> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -303,7 +303,8 @@ public abstract class PushSendJob extends SendJob {
|
||||
}
|
||||
|
||||
protected SignalServiceSyncMessage buildSelfSendSyncMessage(@NonNull Context context, @NonNull SignalServiceDataMessage message, Optional<UnidentifiedAccessPair> 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,
|
||||
|
@ -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<UnidentifiedAccessPair> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Optional<UnidentifiedAccessPair>> 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);
|
||||
}
|
||||
}
|
||||
|
100
src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt
Normal file
100
src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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<SignalServiceDataMessage.Preview>()
|
||||
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<SignalServiceDataMessage.Preview>()
|
||||
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 {
|
||||
|
@ -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)
|
||||
|
@ -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<Map<String, LokiThreadFriendRequestStatus>, Exception> {
|
||||
val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context)
|
||||
return LokiStorageAPI.shared.getAllDevicePublicKeys(hexEncodedPublicKey).map { keys ->
|
||||
val map = mutableMapOf<String, LokiThreadFriendRequestStatus>()
|
||||
for (devicePublicKey in keys) {
|
||||
val device = Recipient.from(context, Address.fromSerialized(devicePublicKey), false)
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(device)
|
||||
val friendRequestStatus = if (threadID < 0) LokiThreadFriendRequestStatus.NONE else lokiThreadDatabase.getFriendRequestStatus(threadID)
|
||||
map[devicePublicKey] = friendRequestStatus
|
||||
}
|
||||
map
|
||||
}.recover { mutableMapOf() }
|
||||
}
|
||||
|
||||
fun getAllDevicePublicKeysWithFriendStatus(context: Context, hexEncodedPublicKey: String): Promise<Map<String, Boolean>, Unit> {
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
storageAPI.getAllDevicePublicKeys(hexEncodedPublicKey).success { items ->
|
||||
val devices = items.toMutableSet()
|
||||
return LokiStorageAPI.shared.getAllDevicePublicKeys(hexEncodedPublicKey).map { keys ->
|
||||
val devices = keys.toMutableSet()
|
||||
if (hexEncodedPublicKey != userHexEncodedPublicKey) {
|
||||
devices.remove(userHexEncodedPublicKey)
|
||||
}
|
||||
val friends = getFriendPublicKeys(context, devices)
|
||||
val friendMap = mutableMapOf<String, Boolean>()
|
||||
for (device in devices) {
|
||||
block(device, friends.contains(device), friends.count())
|
||||
friendMap[device] = friends.contains(device)
|
||||
}
|
||||
}
|
||||
friendMap
|
||||
}.toFailVoid()
|
||||
}
|
||||
|
||||
fun shouldAutomaticallyBecomeFriendsWithDevice(publicKey: String, context: Context): Promise<Boolean, Unit> {
|
||||
val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context)
|
||||
val storageAPI = LokiStorageAPI.shared
|
||||
val deferred = deferred<Boolean, Unit>()
|
||||
storageAPI.getPrimaryDevicePublicKey(publicKey).success { primaryDevicePublicKey ->
|
||||
if (primaryDevicePublicKey == null) {
|
||||
deferred.resolve(false)
|
||||
return@success
|
||||
}
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
if (primaryDevicePublicKey == userHexEncodedPublicKey) {
|
||||
storageAPI.getSecondaryDevicePublicKeys(userHexEncodedPublicKey).success { secondaryDevices ->
|
||||
deferred.resolve(secondaryDevices.contains(publicKey))
|
||||
}.fail {
|
||||
deferred.resolve(false)
|
||||
}
|
||||
return@success
|
||||
}
|
||||
val primaryDevice = Recipient.from(context, Address.fromSerialized(primaryDevicePublicKey), false)
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(primaryDevice)
|
||||
if (threadID < 0) {
|
||||
deferred.resolve(false)
|
||||
return@success
|
||||
}
|
||||
deferred.resolve(lokiThreadDatabase.getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS)
|
||||
fun getFriendCount(context: Context, devices: Set<String>): Int {
|
||||
return getFriendPublicKeys(context, devices).count()
|
||||
}
|
||||
|
||||
fun shouldAutomaticallyBecomeFriendsWithDevice(publicKey: String, context: Context): Promise<Boolean, Exception> {
|
||||
// Don't become friends if we're a group
|
||||
if (!Address.fromSerialized(publicKey).isPhone) {
|
||||
return Promise.of(false)
|
||||
}
|
||||
|
||||
// If this public key is our primary device then we should become friends
|
||||
if (publicKey == TextSecurePreferences.getMasterHexEncodedPublicKey(context)) {
|
||||
return Promise.of(true)
|
||||
}
|
||||
|
||||
return LokiStorageAPI.shared.getPrimaryDevicePublicKey(publicKey).bind { primaryDevicePublicKey ->
|
||||
// If the public key doesn't have any other devices then go through regular friend request logic
|
||||
if (primaryDevicePublicKey == null) {
|
||||
return@bind Promise.of(false)
|
||||
}
|
||||
|
||||
// If the primary device public key matches our primary device then we should become friends since this is our other device
|
||||
if (primaryDevicePublicKey == TextSecurePreferences.getMasterHexEncodedPublicKey(context)) {
|
||||
return@bind Promise.of(true)
|
||||
}
|
||||
|
||||
// If we are friends with any of the other devices then we should become friends
|
||||
isFriendsWithAnyLinkedDevice(context, Address.fromSerialized(primaryDevicePublicKey))
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
fun sendPairingAuthorisationMessage(context: Context, contactHexEncodedPublicKey: String, authorisation: PairingAuthorisation): Promise<Unit, Exception> {
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(contactHexEncodedPublicKey)
|
||||
val message = SignalServiceDataMessage.newBuilder().withBody("").withPairingAuthorisation(authorisation)
|
||||
val message = SignalServiceDataMessage.newBuilder().withBody(null).withPairingAuthorisation(authorisation)
|
||||
// A REQUEST should always act as a friend request. A GRANT should always be replying back as a normal message.
|
||||
if (authorisation.type == PairingAuthorisation.Type.REQUEST) {
|
||||
val preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.number)
|
||||
@ -84,4 +112,77 @@ fun sendPairingAuthorisationMessage(context: Context, contactHexEncodedPublicKey
|
||||
Log.d("Loki", "Failed to send authorisation message to: $contactHexEncodedPublicKey.")
|
||||
Promise.ofFail(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun signAndSendPairingAuthorisationMessage(context: Context, pairingAuthorisation: PairingAuthorisation) {
|
||||
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
|
||||
val signedPairingAuthorisation = pairingAuthorisation.sign(PairingAuthorisation.Type.GRANT, userPrivateKey)
|
||||
if (signedPairingAuthorisation == null || signedPairingAuthorisation.type != PairingAuthorisation.Type.GRANT) {
|
||||
Log.d("Loki", "Failed to sign pairing authorization.")
|
||||
return
|
||||
}
|
||||
DatabaseFactory.getLokiAPIDatabase(context).insertOrUpdatePairingAuthorisation(signedPairingAuthorisation)
|
||||
TextSecurePreferences.setMultiDevice(context, true)
|
||||
|
||||
val address = Address.fromSerialized(pairingAuthorisation.secondaryDevicePublicKey);
|
||||
|
||||
val sendPromise = retryIfNeeded(8) {
|
||||
sendPairingAuthorisationMessage(context, address.serialize(), signedPairingAuthorisation)
|
||||
}.fail {
|
||||
Log.d("Loki", "Failed to send pairing authorization message to ${address.serialize()}.")
|
||||
}
|
||||
|
||||
val updatePromise = LokiStorageAPI.shared.updateUserDeviceMappings().fail {
|
||||
Log.d("Loki", "Failed to update device mapping")
|
||||
}
|
||||
|
||||
// If both promises complete successfully then we should sync our contacts
|
||||
all(listOf(sendPromise, updatePromise), cancelOthersOnError = false).success {
|
||||
Log.d("Loki", "Successfully pairing with a secondary device! Syncing contacts.")
|
||||
// Send out sync contact after a delay
|
||||
Timer().schedule(3000) {
|
||||
MessageSender.syncAllContacts(context, address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isOneOfOurDevices(context: Context, address: Address): Promise<Boolean, Exception> {
|
||||
if (address.isGroup || address.isEmail || address.isMmsGroup) {
|
||||
return Promise.of(false)
|
||||
}
|
||||
|
||||
val ourPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
return LokiStorageAPI.shared.getAllDevicePublicKeys(ourPublicKey).map { devices ->
|
||||
devices.contains(address.serialize())
|
||||
}
|
||||
}
|
||||
|
||||
fun isFriendsWithAnyLinkedDevice(context: Context, recipient: Recipient): Promise<Boolean, Exception> {
|
||||
return isFriendsWithAnyLinkedDevice(context, recipient.address)
|
||||
}
|
||||
|
||||
fun isFriendsWithAnyLinkedDevice(context: Context, address: Address): Promise<Boolean, Exception> {
|
||||
if (!address.isPhone) { return Promise.of(true) }
|
||||
|
||||
return getAllDeviceFriendRequestStatuses(context, address.serialize()).map { map ->
|
||||
for (status in map.values) {
|
||||
if (status == LokiThreadFriendRequestStatus.FRIENDS) {
|
||||
return@map true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun hasPendingFriendRequestWithAnyLinkedDevice(context: Context, recipient: Recipient): Promise<Boolean, Exception> {
|
||||
if (recipient.isGroupRecipient) { return Promise.of(false) }
|
||||
|
||||
return getAllDeviceFriendRequestStatuses(context, recipient.address.serialize()).map { map ->
|
||||
for (status in map.values) {
|
||||
if (status == LokiThreadFriendRequestStatus.REQUEST_SENDING || status == LokiThreadFriendRequestStatus.REQUEST_SENT || status == LokiThreadFriendRequestStatus.REQUEST_RECEIVED) {
|
||||
return@map true
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
@ -68,8 +68,9 @@ class NewConversationActivity : PassphraseRequiredActionBarActivity(), ScanListe
|
||||
fun startNewConversationIfPossible(hexEncodedPublicKey: String) {
|
||||
if (!PublicKeyValidation.isValid(hexEncodedPublicKey)) { return Toast.makeText(this, R.string.fragment_new_conversation_invalid_public_key_message, Toast.LENGTH_SHORT).show() }
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
if (hexEncodedPublicKey == userHexEncodedPublicKey) { return Toast.makeText(this, R.string.fragment_new_conversation_note_to_self_not_supported_message, Toast.LENGTH_SHORT).show() }
|
||||
val contact = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), true)
|
||||
// If we try to contact our master device then redirect to note to self
|
||||
val contactPublicKey = if (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == hexEncodedPublicKey) userHexEncodedPublicKey else hexEncodedPublicKey
|
||||
val contact = Recipient.from(this, Address.fromSerialized(contactPublicKey), true)
|
||||
val intent = Intent(this, ConversationActivity::class.java)
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, contact.address)
|
||||
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA))
|
||||
|
@ -0,0 +1,94 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.jobmanager.Data
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobs.BaseJob
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class PushBackgroundMessageSendJob private constructor(
|
||||
parameters: Parameters,
|
||||
private val recipient: String,
|
||||
private val messageBody: String?,
|
||||
private val friendRequest: Boolean
|
||||
) : BaseJob(parameters) {
|
||||
companion object {
|
||||
const val KEY = "PushBackgroundMessageSendJob"
|
||||
|
||||
private val TAG = PushBackgroundMessageSendJob::class.java.simpleName
|
||||
|
||||
private val KEY_RECIPIENT = "recipient"
|
||||
private val KEY_MESSAGE_BODY = "message_body"
|
||||
private val KEY_FRIEND_REQUEST = "asFriendRequest"
|
||||
}
|
||||
|
||||
constructor(recipient: String): this(recipient, null, false)
|
||||
constructor(recipient: String, messageBody: String?, friendRequest: Boolean) : this(Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(1)
|
||||
.build(),
|
||||
recipient, messageBody, friendRequest)
|
||||
|
||||
override fun serialize(): Data {
|
||||
return Data.Builder()
|
||||
.putString(KEY_RECIPIENT, recipient)
|
||||
.putString(KEY_MESSAGE_BODY, messageBody)
|
||||
.putBoolean(KEY_FRIEND_REQUEST, friendRequest)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return KEY
|
||||
}
|
||||
|
||||
public override fun onRun() {
|
||||
val message = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(System.currentTimeMillis())
|
||||
.withBody(messageBody)
|
||||
|
||||
if (friendRequest) {
|
||||
val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(recipient)
|
||||
message.withPreKeyBundle(bundle)
|
||||
.asFriendRequest(true)
|
||||
}
|
||||
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(recipient)
|
||||
try {
|
||||
messageSender.sendMessage(-1, address, Optional.absent<UnidentifiedAccessPair>(), message.build()) // The message ID doesn't matter
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send background message to: $recipient.")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onShouldRetry(e: Exception): Boolean {
|
||||
// Loki - Disable since we have our own retrying when sending messages
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onCanceled() {}
|
||||
|
||||
class Factory : Job.Factory<PushBackgroundMessageSendJob> {
|
||||
override fun create(parameters: Parameters, data: Data): PushBackgroundMessageSendJob {
|
||||
try {
|
||||
val recipient = data.getString(KEY_RECIPIENT)
|
||||
val messageBody = if (data.hasString(KEY_MESSAGE_BODY)) data.getString(KEY_MESSAGE_BODY) else null
|
||||
val friendRequest = data.getBooleanOrDefault(KEY_FRIEND_REQUEST, false)
|
||||
return PushBackgroundMessageSendJob(parameters, recipient, messageBody, friendRequest)
|
||||
} catch (e: IOException) {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType
|
||||
import org.thoughtcrime.securesms.jobmanager.Data
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.jobs.BaseJob
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class PushMessageSyncSendJob private constructor(
|
||||
parameters: Parameters,
|
||||
private val messageID: Long,
|
||||
private val recipient: Address,
|
||||
private val timestamp: Long,
|
||||
private val message: ByteArray,
|
||||
private val ttl: Int
|
||||
) : BaseJob(parameters), InjectableType {
|
||||
|
||||
companion object {
|
||||
const val KEY = "PushMessageSyncSendJob"
|
||||
|
||||
private val TAG = PushMessageSyncSendJob::class.java.simpleName
|
||||
|
||||
private val KEY_MESSAGE_ID = "message_id"
|
||||
private val KEY_RECIPIENT = "recipient"
|
||||
private val KEY_TIMESTAMP = "timestamp"
|
||||
private val KEY_MESSAGE = "message"
|
||||
private val KEY_TTL = "ttl"
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var messageSender: SignalServiceMessageSender
|
||||
|
||||
constructor(messageID: Long, recipient: Address, timestamp: Long, message: ByteArray, ttl: Int) : this(Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(1)
|
||||
.build(),
|
||||
messageID, recipient, timestamp, message, ttl)
|
||||
|
||||
override fun serialize(): Data {
|
||||
return Data.Builder()
|
||||
.putLong(KEY_MESSAGE_ID, messageID)
|
||||
.putString(KEY_RECIPIENT, recipient.serialize())
|
||||
.putLong(KEY_TIMESTAMP, timestamp)
|
||||
.putByteArray(KEY_MESSAGE, message)
|
||||
.putInt(KEY_TTL, ttl)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return KEY
|
||||
}
|
||||
|
||||
@Throws(IOException::class, UntrustedIdentityException::class)
|
||||
public override fun onRun() {
|
||||
// Don't send sync messages to a group
|
||||
if (recipient.isGroup || recipient.isEmail) { return }
|
||||
messageSender.lokiSendSyncMessage(messageID, SignalServiceAddress(recipient.toPhoneString()), timestamp, message, ttl)
|
||||
}
|
||||
|
||||
public override fun onShouldRetry(e: Exception): Boolean {
|
||||
// Loki - Disable since we have our own retrying when sending messages
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onCanceled() {}
|
||||
|
||||
class Factory : Job.Factory<PushMessageSyncSendJob> {
|
||||
override fun create(parameters: Parameters, data: Data): PushMessageSyncSendJob {
|
||||
try {
|
||||
return PushMessageSyncSendJob(parameters,
|
||||
data.getLong(KEY_MESSAGE_ID),
|
||||
Address.fromSerialized(data.getString(KEY_RECIPIENT)),
|
||||
data.getLong(KEY_TIMESTAMP),
|
||||
data.getByteArray(KEY_MESSAGE),
|
||||
data.getInt(KEY_TTL))
|
||||
} catch (e: IOException) {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
@ -22,7 +23,6 @@ import org.thoughtcrime.securesms.util.Hex
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
import org.whispersystems.libsignal.util.KeyHelper
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||
import org.whispersystems.signalservice.loki.utilities.Analytics
|
||||
@ -55,8 +55,7 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
|
||||
copyButton.setOnClickListener { copy() }
|
||||
toggleRegisterModeButton.setOnClickListener { mode = Mode.Register }
|
||||
toggleRestoreModeButton.setOnClickListener { mode = Mode.Restore }
|
||||
// TODO: Enable this again later
|
||||
// toggleLinkModeButton.setOnClickListener { mode = Mode.Link }
|
||||
toggleLinkModeButton.setOnClickListener { mode = Mode.Link }
|
||||
mainButton.setOnClickListener { handleMainButtonTapped() }
|
||||
Analytics.shared.track("Seed Screen Viewed")
|
||||
}
|
||||
@ -205,8 +204,10 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
|
||||
application.setUpP2PAPI()
|
||||
application.setUpStorageAPIIfNeeded()
|
||||
DeviceLinkingDialog.show(this, DeviceLinkingView.Mode.Slave, this)
|
||||
retryIfNeeded(8) {
|
||||
sendPairingAuthorisationMessage(this@SeedActivity, authorisation.primaryDevicePublicKey, authorisation).get()
|
||||
AsyncTask.execute {
|
||||
retryIfNeeded(8) {
|
||||
sendPairingAuthorisationMessage(this@SeedActivity, authorisation.primaryDevicePublicKey, authorisation)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startActivity(Intent(this, DisplayNameActivity::class.java))
|
||||
@ -227,25 +228,9 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
|
||||
resetForRegistration()
|
||||
}
|
||||
|
||||
override fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {
|
||||
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).privateKey.serialize()
|
||||
val signedPairingAuthorisation = pairingAuthorisation.sign(PairingAuthorisation.Type.GRANT, userPrivateKey)
|
||||
if (signedPairingAuthorisation == null || signedPairingAuthorisation.type != PairingAuthorisation.Type.GRANT) {
|
||||
Log.d("Loki", "Failed to sign pairing authorization.")
|
||||
return
|
||||
}
|
||||
retryIfNeeded(8) {
|
||||
sendPairingAuthorisationMessage(this, pairingAuthorisation.secondaryDevicePublicKey, signedPairingAuthorisation).get()
|
||||
}.fail {
|
||||
Log.d("Loki", "Failed to send pairing authorization message to ${pairingAuthorisation.secondaryDevicePublicKey}.")
|
||||
}
|
||||
DatabaseFactory.getLokiAPIDatabase(this).insertOrUpdatePairingAuthorisation(signedPairingAuthorisation)
|
||||
LokiStorageAPI.shared.updateUserDeviceMappings()
|
||||
}
|
||||
|
||||
private fun resetForRegistration() {
|
||||
IdentityKeyUtil.delete(this, IdentityKeyUtil.lokiSeedKey)
|
||||
TextSecurePreferences.removeLocalRegistrationId(this)
|
||||
DatabaseFactory.getLokiPreKeyBundleDatabase(this).resetAllPreKeyBundleInfo()
|
||||
TextSecurePreferences.removeLocalNumber(this)
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, false)
|
||||
TextSecurePreferences.setPromptedPushRegistration(this, false)
|
||||
|
@ -20,8 +20,9 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
|
||||
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
|
||||
|
||||
import java.util.LinkedList;
|
||||
@ -29,6 +30,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import kotlin.Unit;
|
||||
import kotlin.contracts.Returns;
|
||||
|
||||
public class MarkReadReceiver extends BroadcastReceiver {
|
||||
|
||||
@ -76,7 +78,9 @@ public class MarkReadReceiver extends BroadcastReceiver {
|
||||
|
||||
for (MarkedMessageInfo messageInfo : markedReadMessages) {
|
||||
scheduleDeletion(context, messageInfo.getExpirationInfo());
|
||||
syncMessageIds.add(messageInfo.getSyncMessageId());
|
||||
if (!messageInfo.getSyncMessageId().getAddress().isGroup()) {
|
||||
syncMessageIds.add(messageInfo.getSyncMessageId());
|
||||
}
|
||||
}
|
||||
|
||||
ApplicationContext.getInstance(context)
|
||||
@ -88,14 +92,15 @@ public class MarkReadReceiver extends BroadcastReceiver {
|
||||
.collect(Collectors.groupingBy(SyncMessageId::getAddress));
|
||||
|
||||
for (Address address : addressMap.keySet()) {
|
||||
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
|
||||
|
||||
List<Long> timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList();
|
||||
|
||||
MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, address.serialize(), storageAPI, (devicePublicKey, isFriend, friendCount) -> {
|
||||
// Loki - This also prevents read receipts from being sent in group chats as they don't maintain a friend request status
|
||||
if (isFriend) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new SendReadReceiptJob(Address.fromSerialized(devicePublicKey), timestamps));
|
||||
MultiDeviceUtilities.getAllDevicePublicKeysWithFriendStatus(context, address.serialize()).success(devices -> {
|
||||
for (Map.Entry<String, Boolean> entry : devices.entrySet()) {
|
||||
String device = entry.getKey();
|
||||
boolean isFriend = entry.getValue();
|
||||
// Loki - This also prevents read receipts from being sent in group chats as they don't maintain a friend request status
|
||||
if (isFriend) {
|
||||
Util.runOnMain(() -> ApplicationContext.getInstance(context).getJobManager().add(new SendReadReceiptJob(Address.fromSerialized(device), timestamps)));
|
||||
}
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
@ -30,6 +30,7 @@ public class ProfilePreference extends Preference {
|
||||
private ImageView avatarView;
|
||||
private TextView profileNameView;
|
||||
private TextView profileNumberView;
|
||||
private TextView profileTagView;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public ProfilePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
@ -64,6 +65,7 @@ public class ProfilePreference extends Preference {
|
||||
avatarView = (ImageView)viewHolder.findViewById(R.id.avatar);
|
||||
profileNameView = (TextView)viewHolder.findViewById(R.id.profile_name);
|
||||
profileNumberView = (TextView)viewHolder.findViewById(R.id.number);
|
||||
profileTagView = (TextView)viewHolder.findViewById(R.id.tag);
|
||||
|
||||
refresh();
|
||||
}
|
||||
@ -72,13 +74,15 @@ public class ProfilePreference extends Preference {
|
||||
if (profileNumberView == null) return;
|
||||
|
||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
|
||||
final Address localAddress = Address.fromSerialized(userHexEncodedPublicKey);
|
||||
String primaryDevicePublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(getContext());
|
||||
String publicKey = primaryDevicePublicKey != null ? primaryDevicePublicKey : userHexEncodedPublicKey;
|
||||
final Address localAddress = Address.fromSerialized(publicKey);
|
||||
final String profileName = TextSecurePreferences.getProfileName(getContext());
|
||||
|
||||
Context context = getContext();
|
||||
containerView.setOnLongClickListener(v -> {
|
||||
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("Public Key", userHexEncodedPublicKey);
|
||||
ClipData clip = ClipData.newPlainText("Public Key", publicKey);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(context, R.string.activity_settings_public_key_copied_message, Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
@ -100,7 +104,7 @@ public class ProfilePreference extends Preference {
|
||||
int height = avatarView.getHeight();
|
||||
if (width == 0 || height == 0) return true;
|
||||
avatarView.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, userHexEncodedPublicKey.toLowerCase());
|
||||
JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, publicKey.toLowerCase());
|
||||
avatarView.setImageDrawable(identicon);
|
||||
return true;
|
||||
}
|
||||
@ -120,7 +124,9 @@ public class ProfilePreference extends Preference {
|
||||
}
|
||||
|
||||
profileNameView.setVisibility(TextUtils.isEmpty(profileName) ? View.GONE : View.VISIBLE);
|
||||
|
||||
profileNumberView.setText(localAddress.toPhoneString());
|
||||
|
||||
profileTagView.setVisibility(primaryDevicePublicKey == null ? View.GONE : View.VISIBLE);
|
||||
profileTagView.setText(R.string.activity_settings_secondary_device_tag);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,44 @@
|
||||
package org.thoughtcrime.securesms.push;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.loki.FriendRequestHandler;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
public class MessageSenderEventListener implements SignalServiceMessageSender.EventListener {
|
||||
|
||||
private static final String TAG = MessageSenderEventListener.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
|
||||
public MessageSenderEventListener(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSecurityEvent(SignalServiceAddress textSecureAddress) {
|
||||
SecurityEvent.broadcastSecurityUpdateEvent(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSyncEvent(long messageID, long timestamp, byte[] message, int ttl) {
|
||||
if (messageID >= 0 && timestamp > 0 && message != null && ttl > 0) {
|
||||
MessageSender.sendSyncMessageToOurDevices(context, messageID, timestamp, message, ttl);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onFriendRequestSending(long messageID, long threadID) {
|
||||
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, threadID);
|
||||
}
|
||||
|
||||
@Override public void onFriendRequestSent(long messageID, long threadID) {
|
||||
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sent, messageID, threadID);
|
||||
}
|
||||
|
||||
@Override public void onFriendRequestSendingFail(long messageID, long threadID) {
|
||||
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Failed, messageID, threadID);
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package org.thoughtcrime.securesms.push;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
public class SecurityEventListener implements SignalServiceMessageSender.EventListener {
|
||||
|
||||
private static final String TAG = SecurityEventListener.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
|
||||
public SecurityEventListener(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSecurityEvent(SignalServiceAddress textSecureAddress) {
|
||||
SecurityEvent.broadcastSecurityUpdateEvent(context);
|
||||
}
|
||||
}
|
@ -33,8 +33,10 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
||||
@ -42,8 +44,11 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.FriendRequestHandler;
|
||||
import org.thoughtcrime.securesms.loki.GeneralUtilitiesKt;
|
||||
import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
|
||||
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
|
||||
import org.thoughtcrime.securesms.loki.PushBackgroundMessageSendJob;
|
||||
import org.thoughtcrime.securesms.loki.PushMessageSyncSendJob;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
@ -55,10 +60,13 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
|
||||
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.utilities.PromiseUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Function;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
@ -66,6 +74,63 @@ public class MessageSender {
|
||||
|
||||
private static final String TAG = MessageSender.class.getSimpleName();
|
||||
|
||||
private enum MessageType { TEXT, MEDIA }
|
||||
|
||||
public static void syncAllContacts(Context context, Address recipient) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceContactUpdateJob(context, recipient, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a contact sync message to all our devices telling them that we want to sync `contact`
|
||||
*/
|
||||
public static void syncContact(Context context, Address contact) {
|
||||
// Don't bother sending a contact sync message if it's one of our devices that we want to sync across
|
||||
MultiDeviceUtilities.isOneOfOurDevices(context, contact).success(isOneOfOurDevice -> {
|
||||
if (!isOneOfOurDevice) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceContactUpdateJob(context, contact));
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
public static void sendBackgroundMessageToAllDevices(Context context, String contactHexEncodedPublicKey) {
|
||||
// Send the background message to the original pubkey
|
||||
sendBackgroundMessage(context, contactHexEncodedPublicKey);
|
||||
|
||||
// Go through the other devices and only send background messages if we're friends or we have received friend request
|
||||
LokiStorageAPI.shared.getAllDevicePublicKeys(contactHexEncodedPublicKey).success(devices -> {
|
||||
Util.runOnMain(() -> {
|
||||
for (String device : devices) {
|
||||
// Don't send message to the device we already have sent to
|
||||
if (device.equals(contactHexEncodedPublicKey)) { continue; }
|
||||
Recipient recipient = Recipient.from(context, Address.fromSerialized(device), false);
|
||||
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient);
|
||||
if (threadID < 0) { continue; }
|
||||
LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID);
|
||||
if (friendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS || friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_RECEIVED) {
|
||||
sendBackgroundMessage(context, device);
|
||||
} else if (friendRequestStatus == LokiThreadFriendRequestStatus.NONE || friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_EXPIRED) {
|
||||
sendBackgroundFriendRequest(context, device, "Please accept to enable messages to be synced across devices");
|
||||
}
|
||||
}
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
// region Background message
|
||||
|
||||
// We don't call the message sender here directly and instead we just opt to create a specific job for the send
|
||||
// This is because calling message sender directly would cause the application to freeze in some cases as it was blocking the thread when waiting for a response from the send
|
||||
public static void sendBackgroundMessage(Context context, String contactHexEncodedPublicKey) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(contactHexEncodedPublicKey));
|
||||
}
|
||||
|
||||
public static void sendBackgroundFriendRequest(Context context, String contactHexEncodedPublicKey, String messageBody) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(contactHexEncodedPublicKey, messageBody, true));
|
||||
}
|
||||
// endregion
|
||||
|
||||
public static long send(final Context context,
|
||||
final OutgoingTextMessage message,
|
||||
final long threadId,
|
||||
@ -88,7 +153,7 @@ public class MessageSender {
|
||||
|
||||
// Loki - Set the message's friend request status as soon as it has hit the database
|
||||
if (message.isFriendRequest) {
|
||||
DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(messageId, LokiMessageFriendRequestStatus.REQUEST_SENDING);
|
||||
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageId, allocatedThreadId);
|
||||
}
|
||||
|
||||
sendTextMessage(context, recipient, forceSms, keyExchange, messageId);
|
||||
@ -124,7 +189,7 @@ public class MessageSender {
|
||||
long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
|
||||
// Loki - Set the message's friend request status as soon as it has hit the database
|
||||
if (message.isFriendRequest) {
|
||||
DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_SENDING);
|
||||
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId);
|
||||
}
|
||||
sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn());
|
||||
} catch (Exception e) {
|
||||
@ -137,7 +202,7 @@ public class MessageSender {
|
||||
long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
|
||||
// Loki - Set the message's friend request status as soon as it has hit the database
|
||||
if (message.isFriendRequest) {
|
||||
DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_SENDING);
|
||||
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId);
|
||||
}
|
||||
sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn());
|
||||
} catch (MmsException e) {
|
||||
@ -149,6 +214,28 @@ public class MessageSender {
|
||||
return allocatedThreadId;
|
||||
}
|
||||
|
||||
public static void sendSyncMessageToOurDevices(final Context context,
|
||||
final long messageID,
|
||||
final long timestamp,
|
||||
final byte[] message,
|
||||
final int ttl) {
|
||||
String ourPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
LokiStorageAPI.shared.getAllDevicePublicKeys(ourPublicKey).success(devices -> {
|
||||
Util.runOnMain(() -> {
|
||||
for (String device : devices) {
|
||||
// Don't send to ourselves
|
||||
if (device.equals(ourPublicKey)) { continue; }
|
||||
|
||||
// Create a send job for our device
|
||||
Address address = Address.fromSerialized(device);
|
||||
jobManager.add(new PushMessageSyncSendJob(messageID, address, timestamp, message, ttl));
|
||||
}
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
public static void resendGroupMessage(Context context, MessageRecord messageRecord, Address filterAddress) {
|
||||
if (!messageRecord.isMms()) throw new AssertionError("Not Group");
|
||||
sendGroupPush(context, messageRecord.getRecipient(), messageRecord.getId(), filterAddress);
|
||||
@ -195,64 +282,73 @@ public class MessageSender {
|
||||
}
|
||||
|
||||
private static void sendTextPush(Context context, Recipient recipient, long messageId) {
|
||||
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
|
||||
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
|
||||
// Just send the message normally if it's a group message
|
||||
String recipientPublicKey = recipient.getAddress().serialize();
|
||||
if (GeneralUtilitiesKt.isPublicChat(context, recipientPublicKey)) {
|
||||
jobManager.add(new PushTextSendJob(messageId, recipient.getAddress()));
|
||||
return;
|
||||
}
|
||||
|
||||
MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, recipientPublicKey, storageAPI, (devicePublicKey, isFriend, friendCount) -> {
|
||||
Address address = Address.fromSerialized(devicePublicKey);
|
||||
long messageIDToUse = recipientPublicKey.equals(devicePublicKey) ? messageId : -1L;
|
||||
|
||||
if (isFriend) {
|
||||
// Send a normal message if the user is friends with the recipient
|
||||
jobManager.add(new PushTextSendJob(messageId, messageIDToUse, address));
|
||||
} else {
|
||||
// Send friend requests to non friends. If the user is friends with any
|
||||
// of the devices then send out a default friend request message.
|
||||
boolean isFriendsWithAny = (friendCount > 0);
|
||||
String defaultFriendRequestMessage = isFriendsWithAny ? "Accept this friend request to enable messages to be synced across devices" : null;
|
||||
jobManager.add(new PushTextSendJob(messageId, messageIDToUse, address, true, defaultFriendRequestMessage));
|
||||
}
|
||||
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
sendMessagePush(context, MessageType.TEXT, recipient, messageId);
|
||||
}
|
||||
|
||||
private static void sendMediaPush(Context context, Recipient recipient, long messageId) {
|
||||
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
|
||||
sendMessagePush(context, MessageType.MEDIA, recipient, messageId);
|
||||
}
|
||||
|
||||
private static void sendMessagePush(Context context, MessageType type, Recipient recipient, long messageId) {
|
||||
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
|
||||
// Just send the message normally if it's a group message
|
||||
// Just send the message normally if it's a group message or we're sending to one of our devices
|
||||
String recipientPublicKey = recipient.getAddress().serialize();
|
||||
if (GeneralUtilitiesKt.isPublicChat(context, recipientPublicKey)) {
|
||||
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress());
|
||||
if (GeneralUtilitiesKt.isPublicChat(context, recipientPublicKey) || PromiseUtil.get(MultiDeviceUtilities.isOneOfOurDevices(context, recipient.getAddress()), false)) {
|
||||
if (type == MessageType.MEDIA) {
|
||||
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress(), false);
|
||||
} else {
|
||||
jobManager.add(new PushTextSendJob(messageId, recipient.getAddress()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, recipientPublicKey, storageAPI, (devicePublicKey, isFriend, friendCount) -> {
|
||||
Address address = Address.fromSerialized(devicePublicKey);
|
||||
long messageIDToUse = recipientPublicKey.equals(devicePublicKey) ? messageId : -1L;
|
||||
// If we get here then we are sending a message to a device that is not ours
|
||||
boolean[] hasSentSyncMessage = { false };
|
||||
MultiDeviceUtilities.getAllDevicePublicKeysWithFriendStatus(context, recipientPublicKey).success(devices -> {
|
||||
int friendCount = MultiDeviceUtilities.getFriendCount(context, devices.keySet());
|
||||
Util.runOnMain(() -> {
|
||||
ArrayList<Job> jobs = new ArrayList<>();
|
||||
for (Map.Entry<String, Boolean> entry : devices.entrySet()) {
|
||||
String devicePublicKey = entry.getKey();
|
||||
boolean isFriend = entry.getValue();
|
||||
|
||||
if (isFriend) {
|
||||
// Send a normal message if the user is friends with the recipient
|
||||
PushMediaSendJob.enqueue(context, jobManager, messageId, messageIDToUse, address);
|
||||
} else {
|
||||
// Send friend requests to non friends. If the user is friends with any
|
||||
// of the devices then send out a default friend request message.
|
||||
boolean isFriendsWithAny = friendCount > 0;
|
||||
String defaultFriendRequestMessage = isFriendsWithAny ? "Accept this friend request to enable messages to be synced across devices" : null;
|
||||
PushMediaSendJob.enqueue(context, jobManager, messageId, messageIDToUse, address, true, defaultFriendRequestMessage);
|
||||
}
|
||||
Address address = Address.fromSerialized(devicePublicKey);
|
||||
long messageIDToUse = recipientPublicKey.equals(devicePublicKey) ? messageId : -1L;
|
||||
|
||||
if (isFriend) {
|
||||
// Send a normal message if the user is friends with the recipient
|
||||
// We should also send a sync message if we haven't already sent one
|
||||
boolean shouldSendSyncMessage = !hasSentSyncMessage[0] && address.isPhone();
|
||||
if (type == MessageType.MEDIA) {
|
||||
jobs.add(new PushMediaSendJob(messageId, messageIDToUse, address, false, null, shouldSendSyncMessage));
|
||||
} else {
|
||||
jobs.add(new PushTextSendJob(messageId, messageIDToUse, address, shouldSendSyncMessage));
|
||||
}
|
||||
if (shouldSendSyncMessage) { hasSentSyncMessage[0] = true; }
|
||||
} else {
|
||||
// Send friend requests to non friends. If the user is friends with any
|
||||
// of the devices then send out a default friend request message.
|
||||
boolean isFriendsWithAny = (friendCount > 0);
|
||||
String defaultFriendRequestMessage = isFriendsWithAny ? "Please accept to enable messages to be synced across devices" : null;
|
||||
if (type == MessageType.MEDIA) {
|
||||
jobs.add(new PushMediaSendJob(messageId, messageIDToUse, address, true, defaultFriendRequestMessage, false));
|
||||
} else {
|
||||
jobs.add(new PushTextSendJob(messageId, messageIDToUse, address, true, defaultFriendRequestMessage, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start the send
|
||||
if (type == MessageType.MEDIA) {
|
||||
PushMediaSendJob.enqueue(context, jobManager, (List<PushMediaSendJob>)(List)jobs);
|
||||
} else {
|
||||
// Schedule text send jobs
|
||||
jobManager.startChain(jobs).enqueue();
|
||||
}
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private static void sendGroupPush(Context context, Recipient recipient, long messageId, Address filterAddress) {
|
||||
|
@ -640,7 +640,7 @@ public class TextSecurePreferences {
|
||||
}
|
||||
|
||||
public static void setLocalNumber(Context context, String localNumber) {
|
||||
setStringPreference(context, LOCAL_NUMBER_PREF, localNumber);
|
||||
setStringPreference(context, LOCAL_NUMBER_PREF, localNumber.toLowerCase());
|
||||
}
|
||||
|
||||
public static void removeLocalNumber(Context context) {
|
||||
@ -1183,7 +1183,7 @@ public class TextSecurePreferences {
|
||||
}
|
||||
|
||||
public static void setMasterHexEncodedPublicKey(Context context, String masterHexEncodedPublicKey) {
|
||||
setStringPreference(context, "master_hex_encoded_publicKey", masterHexEncodedPublicKey);
|
||||
setStringPreference(context, "master_hex_encoded_public_key", masterHexEncodedPublicKey.toLowerCase());
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user