Merge pull request #1 from loki-project/dev

Dev
This commit is contained in:
RyanZhao 2019-11-18 16:16:45 +11:00 committed by GitHub
commit b238d6508c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1601 additions and 593 deletions

View File

@ -11,9 +11,11 @@
<uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.bluetooth" android:required="false" /> <uses-feature android:name="android.hardware.bluetooth" android:required="false" />
<!-- Loki - Enable again once we have location sharing
<uses-feature android:name="android.hardware.location" android:required="false"/> <uses-feature android:name="android.hardware.location" android:required="false"/>
<uses-feature android:name="android.hardware.location.network" android:required="false"/> <uses-feature android:name="android.hardware.location.network" android:required="false"/>
<uses-feature android:name="android.hardware.location.gps" android:required="false"/> <uses-feature android:name="android.hardware.location.gps" android:required="false"/>
-->
<uses-feature android:name="android.hardware.microphone" android:required="false"/> <uses-feature android:name="android.hardware.microphone" android:required="false"/>
<uses-feature android:name="android.hardware.wifi" android:required="false"/> <uses-feature android:name="android.hardware.wifi" android:required="false"/>
<uses-feature android:name="android.hardware.portrait" android:required="false"/> <uses-feature android:name="android.hardware.portrait" android:required="false"/>
@ -26,10 +28,12 @@
<uses-permission android:name="android.permission.WRITE_PROFILE"/> <uses-permission android:name="android.permission.WRITE_PROFILE"/>
<uses-permission android:name="android.permission.BROADCAST_WAP_PUSH" <uses-permission android:name="android.permission.BROADCAST_WAP_PUSH"
tools:ignore="ProtectedPermissions"/> tools:ignore="ProtectedPermissions"/>
<!-- Loki - We don't need these at all
<uses-permission android:name="android.permission.READ_CONTACTS"/> <uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/> <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<!-- <uses-permission android:name="android.permission.RECEIVE_SMS"/> <uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_MMS"/> <uses-permission android:name="android.permission.RECEIVE_MMS"/>
<uses-permission android:name="android.permission.READ_SMS"/> <uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.SEND_SMS"/> <uses-permission android:name="android.permission.SEND_SMS"/>
@ -40,12 +44,14 @@
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<!-- Loki - Enable again once we have location sharing
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
-->
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.READ_CALL_STATE"/> <!-- <uses-permission android:name="android.permission.READ_CALL_STATE"/> -->
<!-- For sending/receiving events --> <!-- For sending/receiving events -->
<uses-permission android:name="android.permission.WRITE_CALENDAR"/> <uses-permission android:name="android.permission.WRITE_CALENDAR"/>

View File

@ -195,8 +195,8 @@ dependencies {
} }
} }
def canonicalVersionCode = 18 def canonicalVersionCode = 20
def canonicalVersionName = "1.4.1" def canonicalVersionName = "1.5.0"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -123,8 +123,7 @@
android:layout_height="50dp" android:layout_height="50dp"
android:background="@color/transparent" android:background="@color/transparent"
android:textColor="@color/signal_primary" android:textColor="@color/signal_primary"
android:text="Link Device (Coming Soon)" android:text="Link Device"
android:alpha="0.24"
android:elevation="0dp" android:elevation="0dp"
android:stateListAnimator="@null" /> android:stateListAnimator="@null" />

View File

@ -30,6 +30,14 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="+14151231234"/> 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>
</LinearLayout> </LinearLayout>

View File

@ -1572,6 +1572,7 @@
<!-- Conversation list activity --> <!-- 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> <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 --> <!-- 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_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_share_public_key_button_title">Share Public Key</string>
<string name="activity_settings_show_qr_code_button_title">Show QR Code</string> <string name="activity_settings_show_qr_code_button_title">Show QR Code</string>

View File

@ -42,7 +42,7 @@
android:icon="@drawable/icon_qr_code"/> android:icon="@drawable/icon_qr_code"/>
<Preference android:key="preference_category_link_device" <Preference android:key="preference_category_link_device"
android:title="Link Device (Coming Soon)" android:title="Link Device"
android:icon="@drawable/icon_link"/> android:icon="@drawable/icon_link"/>
<Preference android:key="preference_category_seed" <Preference android:key="preference_category_seed"

View File

@ -87,6 +87,7 @@ import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol; 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.LokiPublicChat;
import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI; import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI;
import org.whispersystems.signalservice.loki.api.LokiLongPoller; import org.whispersystems.signalservice.loki.api.LokiLongPoller;
@ -109,6 +110,7 @@ import io.fabric.sdk.android.Fabric;
import kotlin.Unit; import kotlin.Unit;
import kotlin.jvm.functions.Function1; import kotlin.jvm.functions.Function1;
import network.loki.messenger.BuildConfig; import network.loki.messenger.BuildConfig;
import okhttp3.Cache;
import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant; import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant;
import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant; 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 { public class ApplicationContext extends MultiDexApplication implements DependencyInjector, DefaultLifecycleObserver, LokiP2PAPIDelegate {
private static final String TAG = ApplicationContext.class.getSimpleName(); 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 ExpiringMessageManager expiringMessageManager;
private TypingStatusRepository typingStatusRepository; private TypingStatusRepository typingStatusRepository;
@ -188,6 +191,12 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
}; };
// Loki - Set up public chat manager // Loki - Set up public chat manager
lokiPublicChatManager = new LokiPublicChatManager(this); 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 @Override
@ -199,7 +208,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
// Loki - Start long polling if needed // Loki - Start long polling if needed
startLongPollingIfNeeded(); startLongPollingIfNeeded();
lokiPublicChatManager.startPollersIfNeeded(); lokiPublicChatManager.startPollersIfNeeded();
setUpStorageAPIIfNeeded();
} }
@Override @Override
@ -450,14 +458,16 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
} }
// region Loki // region Loki
public void setUpStorageAPIIfNeeded() { public boolean setUpStorageAPIIfNeeded() {
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this); String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userHexEncodedPublicKey != null && IdentityKeyUtil.hasIdentityKey(this)) { if (userHexEncodedPublicKey != null && IdentityKeyUtil.hasIdentityKey(this)) {
boolean isDebugMode = BuildConfig.DEBUG; boolean isDebugMode = BuildConfig.DEBUG;
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize(); byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
LokiAPIDatabaseProtocol database = DatabaseFactory.getLokiAPIDatabase(this); LokiAPIDatabaseProtocol database = DatabaseFactory.getLokiAPIDatabase(this);
LokiStorageAPI.Companion.configure(isDebugMode, userHexEncodedPublicKey, userPrivateKey, database); LokiStorageAPI.Companion.configure(isDebugMode, userHexEncodedPublicKey, userPrivateKey, database);
return true;
} }
return false;
} }
public void setUpP2PAPI() { public void setUpP2PAPI() {

View File

@ -26,6 +26,7 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Build.VERSION; import android.os.Build.VERSION;
import android.os.Bundle; import android.os.Bundle;
@ -39,9 +40,13 @@ import android.support.v7.app.AlertDialog;
import android.support.v7.preference.Preference; import android.support.v7.preference.Preference;
import android.widget.Toast; import android.widget.Toast;
import org.jetbrains.annotations.NotNull;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.loki.DeviceLinkingDialog; import org.thoughtcrime.securesms.loki.DeviceLinkingDialog;
import org.thoughtcrime.securesms.loki.DeviceLinkingDialogDelegate;
import org.thoughtcrime.securesms.loki.DeviceLinkingView; import org.thoughtcrime.securesms.loki.DeviceLinkingView;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.loki.QRCodeDialog; import org.thoughtcrime.securesms.loki.QRCodeDialog;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment; import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
@ -52,6 +57,7 @@ import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences; 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.crypto.MnemonicCodec;
import org.whispersystems.signalservice.loki.utilities.Analytics; import org.whispersystems.signalservice.loki.utilities.Analytics;
import org.whispersystems.signalservice.loki.utilities.SerializationKt; import org.whispersystems.signalservice.loki.utilities.SerializationKt;
@ -160,23 +166,21 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
boolean isMasterDevice = (masterHexEncodedPublicKey == null); boolean isMasterDevice = (masterHexEncodedPublicKey == null);
Preference profilePreference = this.findPreference(PREFERENCE_CATEGORY_PROFILE); Preference profilePreference = this.findPreference(PREFERENCE_CATEGORY_PROFILE);
// Hide if this is a slave device if (isMasterDevice) { profilePreference.setOnPreferenceClickListener(new ProfileClickListener()); }
profilePreference.setVisible(isMasterDevice);
profilePreference.setOnPreferenceClickListener(new ProfileClickListener());
/* /*
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS) this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS)); .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS));
*/ */
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS) this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_NOTIFICATIONS)); .setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_NOTIFICATIONS));
this.findPreference(PREFERENCE_CATEGORY_APP_PROTECTION) 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) this.findPreference(PREFERENCE_CATEGORY_APPEARANCE)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE)); .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE));
*/ */
this.findPreference(PREFERENCE_CATEGORY_CHATS) this.findPreference(PREFERENCE_CATEGORY_CHATS)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CHATS)); .setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_CHATS));
/* /*
this.findPreference(PREFERENCE_CATEGORY_DEVICES) this.findPreference(PREFERENCE_CATEGORY_DEVICES)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DEVICES)); .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DEVICES));
@ -184,21 +188,29 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED)); .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
*/ */
this.findPreference(PREFERENCE_CATEGORY_PUBLIC_KEY) 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) 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); Preference linkDevicePreference = this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE);
// Hide if this is a slave device linkDevicePreference.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_LINK_DEVICE));
linkDevicePreference.setVisible(isMasterDevice);
linkDevicePreference.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_LINK_DEVICE)); // Disable if we hit the cap of 1 linked device
*/ if (isMasterDevice) {
Context context = getContext();
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
boolean isDeviceLinkingEnabled = DatabaseFactory.getLokiAPIDatabase(context).getPairingAuthorisations(userHexEncodedPublicKey).size() <= 1;
linkDevicePreference.setEnabled(isDeviceLinkingEnabled);
linkDevicePreference.getIcon().setAlpha(isDeviceLinkingEnabled ? 255 : 124);
} else {
// Hide if this is a slave device
linkDevicePreference.setVisible(false);
}
Preference seedPreference = this.findPreference(PREFERENCE_CATEGORY_SEED); Preference seedPreference = this.findPreference(PREFERENCE_CATEGORY_SEED);
// Hide if this is a slave device // Hide if this is a slave device
seedPreference.setVisible(isMasterDevice); 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) { if (VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
tintIcons(getActivity()); tintIcons(getActivity());
@ -291,10 +303,12 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
this.findPreference(PREFERENCE_CATEGORY_SEED).setIcon(seed); this.findPreference(PREFERENCE_CATEGORY_SEED).setIcon(seed);
} }
private class CategoryClickListener implements Preference.OnPreferenceClickListener { private class CategoryClickListener implements Preference.OnPreferenceClickListener, DeviceLinkingDialogDelegate {
private String category; private String category;
private Context context;
CategoryClickListener(String category) { CategoryClickListener(Context context,String category) {
this.context = context;
this.category = category; this.category = category;
} }
@ -347,7 +361,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
QRCodeDialog.INSTANCE.show(getContext()); QRCodeDialog.INSTANCE.show(getContext());
break; break;
case PREFERENCE_CATEGORY_LINK_DEVICE: case PREFERENCE_CATEGORY_LINK_DEVICE:
DeviceLinkingDialog.Companion.show(getContext(), DeviceLinkingView.Mode.Master, null); DeviceLinkingDialog.Companion.show(getContext(), DeviceLinkingView.Mode.Master, this);
break; break;
case PREFERENCE_CATEGORY_SEED: case PREFERENCE_CATEGORY_SEED:
Analytics.Companion.getShared().track("Seed Modal Shown"); Analytics.Companion.getShared().track("Seed Modal Shown");
@ -390,6 +404,12 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
return true; 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 { private class ProfileClickListener implements Preference.OnPreferenceClickListener {

View File

@ -178,6 +178,7 @@ public class ContactSelectionListFragment extends Fragment
showContactsDescription.setText(R.string.contact_selection_list_fragment__signal_needs_access_to_your_contacts_in_order_to_display_them); showContactsDescription.setText(R.string.contact_selection_list_fragment__signal_needs_access_to_your_contacts_in_order_to_display_them);
showContactsButton.setVisibility(View.VISIBLE); showContactsButton.setVisibility(View.VISIBLE);
/*
showContactsButton.setOnClickListener(v -> { showContactsButton.setOnClickListener(v -> {
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS) .request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
@ -190,6 +191,7 @@ public class ContactSelectionListFragment extends Fragment
}) })
.execute(); .execute();
}); });
*/
} }
public void setQueryFilter(String filter) { public void setQueryFilter(String filter) {

View File

@ -41,6 +41,7 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.components.RatingManager; import org.thoughtcrime.securesms.components.RatingManager;
import org.thoughtcrime.securesms.components.SearchToolbar; import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.conversation.ConversationActivity; import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; 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.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import java.util.List; import java.util.List;
@ -142,6 +144,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
private void initializeSearchListener() { private void initializeSearchListener() {
searchAction.setOnClickListener(v -> { searchAction.setOnClickListener(v -> {
/* Loki - We don't need contact permissions
Permissions.with(this) Permissions.with(this)
.request(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS) .request(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)
.ifNecessary() .ifNecessary()
@ -149,6 +152,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
searchAction.getY() + (searchAction.getHeight() / 2))) searchAction.getY() + (searchAction.getHeight() / 2)))
.withPermanentDenialDialog(getString(R.string.ConversationListActivity_signal_needs_contacts_permission_in_order_to_search_your_contacts_but_it_has_been_permanently_denied)) .withPermanentDenialDialog(getString(R.string.ConversationListActivity_signal_needs_contacts_permission_in_order_to_search_your_contacts_but_it_has_been_permanently_denied))
.execute(); .execute();
*/
}); });
searchToolbar.setListener(new SearchToolbar.SearchListener() { searchToolbar.setListener(new SearchToolbar.SearchListener() {
@ -193,6 +197,13 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
outline.setOval(0, 0, view.getWidth(), view.getHeight()); 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.setClipToOutline(true);
profilePictureImageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { profilePictureImageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@ -202,7 +213,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
int height = profilePictureImageView.getHeight(); int height = profilePictureImageView.getHeight();
if (width == 0 || height == 0) return true; if (width == 0 || height == 0) return true;
profilePictureImageView.getViewTreeObserver().removeOnPreDrawListener(this); 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); profilePictureImageView.setImageDrawable(identicon);
return true; return true;
} }

View File

@ -104,32 +104,22 @@ public class AvatarImageView extends AppCompatImageView {
@Override @Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) { protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh); super.onSizeChanged(w, h, oldw, oldh);
if (w == 0 || h == 0 || recipient == null) { return; } updateImage(w, h);
Drawable image;
if (recipient.isGroupRecipient()) {
Context context = this.getContext();
String name = Optional.fromNullable(recipient.getName()).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context))).or("");
MaterialColor fallbackColor = recipient.getColor();
if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
fallbackColor = ContactColors.generateFor(name);
}
image = new GeneratedContactPhoto(name, R.drawable.ic_profile_default).asDrawable(context, fallbackColor.toAvatarColor(context));
} else {
image = new JazzIdenticonDrawable(w, h, recipient.getAddress().serialize().toLowerCase());
}
setImageDrawable(image);
} }
public void update(String hexEncodedPublicKey) { public void update(String hexEncodedPublicKey) {
this.recipient = Recipient.from(getContext(), Address.fromSerialized(hexEncodedPublicKey), false); Address address = Address.fromSerialized(hexEncodedPublicKey);
if (!address.equals(recipient.getAddress())) {
this.recipient = Recipient.from(getContext(), address, false);
updateImage();
}
} }
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) { public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
this.recipient = recipient; if (this.recipient == null || !this.recipient.equals(recipient)) {
this.recipient = recipient;
updateImage();
}
/* /*
if (recipient != null) { if (recipient != null) {
requestManager.load(recipient.getContactPhoto()) requestManager.load(recipient.getContactPhoto())
@ -164,4 +154,32 @@ public class AvatarImageView extends AppCompatImageView {
} }
} }
private void updateImage() { updateImage(getWidth(), getHeight()); }
private void updateImage(int w, int h) {
if (w == 0 || h == 0 || recipient == null) { return; }
Drawable image;
Context context = this.getContext();
if (recipient.isGroupRecipient()) {
String name = Optional.fromNullable(recipient.getName()).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context))).or("");
MaterialColor fallbackColor = recipient.getColor();
if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
fallbackColor = ContactColors.generateFor(name);
}
image = new GeneratedContactPhoto(name, R.drawable.ic_profile_default).asDrawable(context, fallbackColor.toAvatarColor(context));
} else {
// 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);
}
} }

View File

@ -9,13 +9,14 @@ import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobs.TypingSendJob; 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.recipients.Recipient;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI; import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import kotlin.Unit; import kotlin.Unit;
@ -90,12 +91,13 @@ public class TypingStatusSender {
ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(threadId, typingStarted)); ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(threadId, typingStarted));
return; return;
} }
LokiStorageAPI.shared.getAllDevicePublicKeys(recipient.getAddress().serialize()).success(devices -> {
MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, recipient.getAddress().serialize(), storageAPI, (devicePublicKey, isFriend, friendCount) -> { for (String device : devices) {
Recipient device = Recipient.from(context, Address.fromSerialized(devicePublicKey), false); Recipient deviceRecipient = Recipient.from(context, Address.fromSerialized(device), false);
long deviceThreadID = threadDatabase.getThreadIdIfExistsFor(device); long deviceThreadID = threadDatabase.getThreadIdIfExistsFor(deviceRecipient);
if (deviceThreadID > -1) { if (deviceThreadID > -1) {
ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(deviceThreadID, typingStarted)); ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(deviceThreadID, typingStarted));
}
} }
return Unit.INSTANCE; return Unit.INSTANCE;
}); });

View File

@ -187,6 +187,8 @@ public class ContactsCursorLoader extends CursorLoader {
ContactsDatabase contactsDatabase = DatabaseFactory.getContactsDatabase(getContext()); ContactsDatabase contactsDatabase = DatabaseFactory.getContactsDatabase(getContext());
List<Cursor> cursorList = new ArrayList<>(2); List<Cursor> cursorList = new ArrayList<>(2);
return cursorList;
/*
if (!Permissions.hasAny(getContext(), Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) { if (!Permissions.hasAny(getContext(), Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
return cursorList; return cursorList;
} }
@ -201,6 +203,7 @@ public class ContactsCursorLoader extends CursorLoader {
cursorList.add(filterNonPushContacts(contactsDatabase.querySystemContacts(filter))); cursorList.add(filterNonPushContacts(contactsDatabase.querySystemContacts(filter)));
} }
return cursorList; return cursorList;
*/
} }
private Cursor getGroupsCursor() { private Cursor getGroupsCursor() {

View File

@ -38,7 +38,6 @@ import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Vibrator; import android.os.Vibrator;
import android.provider.Browser; import android.provider.Browser;
import android.provider.ContactsContract; 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.FriendRequestViewDelegate;
import org.thoughtcrime.securesms.loki.LokiAPIUtilities; import org.thoughtcrime.securesms.loki.LokiAPIUtilities;
import org.thoughtcrime.securesms.loki.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.LokiThreadDatabaseDelegate; import org.thoughtcrime.securesms.loki.LokiThreadDatabaseDelegate;
import org.thoughtcrime.securesms.loki.LokiUserDatabase; import org.thoughtcrime.securesms.loki.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.MentionCandidateSelectionView; import org.thoughtcrime.securesms.loki.MentionCandidateSelectionView;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaSendActivity; import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
import org.thoughtcrime.securesms.mms.AttachmentManager; 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.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional; 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.LokiAPI;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus; import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus;
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus; import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.messaging.Mention; import org.whispersystems.signalservice.loki.messaging.Mention;
@ -249,6 +248,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import kotlin.Unit; import kotlin.Unit;
import network.loki.messenger.R; import network.loki.messenger.R;
import static nl.komponents.kovenant.KovenantApi.task;
import static org.thoughtcrime.securesms.TransportOption.Type; import static org.thoughtcrime.securesms.TransportOption.Type;
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK; import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
@ -353,6 +353,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private ArrayList<Mention> mentions = new ArrayList<>(); private ArrayList<Mention> mentions = new ArrayList<>();
private String oldText = ""; private String oldText = "";
// Multi Device
private boolean isFriendsWithAnyDevice = false;
@Override @Override
protected void onPreCreate() { protected void onPreCreate() {
@ -719,7 +721,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (isSingleConversation() && getRecipient().getContactUri() == null) { if (isSingleConversation() && getRecipient().getContactUri() == null) {
inflater.inflate(R.menu.conversation_add_to_contacts, menu); inflater.inflate(R.menu.conversation_add_to_contacts, menu);
} }
*/
if (recipient != null && recipient.isLocalNumber()) { if (recipient != null && recipient.isLocalNumber()) {
if (isSecureText) menu.findItem(R.id.menu_call_secure).setVisible(false); if (isSecureText) menu.findItem(R.id.menu_call_secure).setVisible(false);
@ -731,6 +733,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
muteItem.setVisible(false); muteItem.setVisible(false);
} }
} }
*/
searchViewItem = menu.findItem(R.id.menu_search); searchViewItem = menu.findItem(R.id.menu_search);
@ -2183,21 +2186,74 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override @Override
public void handleThreadFriendRequestStatusChanged(long threadID) { public void handleThreadFriendRequestStatusChanged(long threadID) {
if (threadID != this.threadId) { return; } if (threadID != this.threadId) {
new Handler(getMainLooper()).post(this::updateInputPanel); 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() { private void updateInputPanel() {
boolean hasPendingFriendRequest = !recipient.isGroupRecipient() && DatabaseFactory.getLokiThreadDatabase(this).hasPendingFriendRequest(threadId); /*
updateToggleButtonState(); isFriendsWithAnyDevice caches whether we are friends with any of the other users device.
inputPanel.setEnabled(!hasPendingFriendRequest);
int hintID = hasPendingFriendRequest ? R.string.activity_conversation_pending_friend_request_hint : R.string.activity_conversation_default_hint; This stops the case where the input panel disables and enables rapidly.
inputPanel.setHint(getResources().getString(hintID)); - 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 (!hasPendingFriendRequest) { */
inputPanel.composeText.requestFocus(); if (recipient.isGroupRecipient() || isNoteToSelf() || isFriendsWithAnyDevice) {
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); setInputPanelEnabled(true);
inputMethodManager.showSoftInput(inputPanel.composeText, 0); 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() { private void sendMessage() {
@ -2401,9 +2457,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
private void updateToggleButtonState() { private void updateToggleButtonState() {
// Don't allow attachments if we're not friends // Don't allow attachments if we're not friends with any device
LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(this).getFriendRequestStatus(threadId); if (!isNoteToSelf() && !recipient.isGroupRecipient() && !isFriendsWithAnyDevice) {
if (!recipient.isGroupRecipient() && friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) {
buttonToggle.display(sendButton); buttonToggle.display(sendButton);
quickAttachmentToggle.hide(); quickAttachmentToggle.hide();
inlineAttachmentToggle.hide(); inlineAttachmentToggle.hide();
@ -2988,27 +3043,34 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
// region Loki // region Loki
@Override @Override
public void acceptFriendRequest(@NotNull MessageRecord friendRequest) { public void acceptFriendRequest(@NotNull MessageRecord friendRequest) {
String contactID = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(this.threadId).getAddress().toString(); // Send the accept to the original friend request thread id
SignalServiceMessageSender messageSender = ApplicationContext.getInstance(this).communicationModule.provideSignalMessageSender(); LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(this);
SignalServiceAddress address = new SignalServiceAddress(contactID); long originalThreadID = lokiMessageDatabase.getOriginalThreadID(friendRequest.id);
SignalServiceDataMessage message = new SignalServiceDataMessage(System.currentTimeMillis(), ""); long threadId = originalThreadID < 0 ? this.threadId : originalThreadID;
Context context = this;
AsyncTask.execute(() -> { Address contact = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadId).getAddress();
try { String contactPubKey = contact.toString();
messageSender.sendMessage(0, address, Optional.absent(), message); // The message ID doesn't matter DatabaseFactory.getLokiThreadDatabase(this).setFriendRequestStatus(threadId, LokiThreadFriendRequestStatus.FRIENDS);
DatabaseFactory.getLokiThreadDatabase(context).setFriendRequestStatus(this.threadId, LokiThreadFriendRequestStatus.FRIENDS); lokiMessageDatabase.setFriendRequestStatus(friendRequest.id, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(friendRequest.id, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED); MessageSender.sendBackgroundMessageToAllDevices(this, contactPubKey);
} catch (Exception e) { MessageSender.syncContact(this, contact);
Log.d("Loki", "Failed to send background message to: " + contactID + "."); updateInputPanel();
}
});
} }
@Override @Override
public void rejectFriendRequest(@NotNull MessageRecord friendRequest) { public void rejectFriendRequest(@NotNull MessageRecord friendRequest) {
DatabaseFactory.getLokiThreadDatabase(this).setFriendRequestStatus(this.threadId, LokiThreadFriendRequestStatus.NONE); LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(this);
String contactID = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(this.threadId).getAddress().toString(); 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); DatabaseFactory.getLokiPreKeyBundleDatabase(this).removePreKeyBundle(contactID);
updateInputPanel();
}
public boolean isNoteToSelf() {
return TextSecurePreferences.getLocalNumber(this).equals(recipient.getAddress().serialize());
} }
// endregion // endregion
} }

View File

@ -101,7 +101,6 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.util.concurrent.SettableFuture;
import org.whispersystems.signalservice.loki.api.LokiPublicChat; import org.whispersystems.signalservice.loki.api.LokiPublicChat;
import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI; import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI;
@ -115,7 +114,6 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import kotlin.Unit;
import network.loki.messenger.R; import network.loki.messenger.R;
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
@ -409,12 +407,15 @@ public class ConversationFragment extends Fragment
LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId); LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
boolean isPublicChat = publicChat != null; boolean isPublicChat = publicChat != null;
int selectedMessageCount = messageRecords.size(); int selectedMessageCount = messageRecords.size();
boolean isSentByUser = ((MessageRecord)messageRecords.toArray()[0]).isOutgoing(); boolean areAllSentByUser = true;
menu.findItem(R.id.menu_context_copy_public_key).setVisible(isPublicChat && selectedMessageCount == 1 && !isSentByUser); for (MessageRecord message : messageRecords) {
if (!message.isOutgoing()) { areAllSentByUser = false; }
}
menu.findItem(R.id.menu_context_copy_public_key).setVisible(isPublicChat && selectedMessageCount == 1 && !areAllSentByUser);
menu.findItem(R.id.menu_context_reply).setVisible(isPublicChat && selectedMessageCount == 1); menu.findItem(R.id.menu_context_reply).setVisible(isPublicChat && selectedMessageCount == 1);
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext()); String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
boolean userCanModerate = isPublicChat && LokiPublicChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer()); boolean userCanModerate = isPublicChat && LokiPublicChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer());
boolean isDeleteOptionVisible = isPublicChat && selectedMessageCount == 1 && (isSentByUser || userCanModerate); boolean isDeleteOptionVisible = isPublicChat && (areAllSentByUser || userCanModerate);
menu.findItem(R.id.menu_context_delete_message).setVisible(isDeleteOptionVisible); menu.findItem(R.id.menu_context_delete_message).setVisible(isDeleteOptionVisible);
} else { } else {
menu.findItem(R.id.menu_context_copy_public_key).setVisible(false); menu.findItem(R.id.menu_context_copy_public_key).setVisible(false);
@ -520,54 +521,43 @@ public class ConversationFragment extends Fragment
{ {
@Override @Override
protected Void doInBackground(MessageRecord... messageRecords) { protected Void doInBackground(MessageRecord... messageRecords) {
ArrayList<Long> serverIDs = new ArrayList<>();
ArrayList<Long> ignoredMessages = new ArrayList<>();
ArrayList<Long> failedMessages = new ArrayList<>();
boolean isSentByUser = true;
LokiPublicChatAPI publicChatAPI = ApplicationContext.getInstance(getContext()).getLokiPublicChatAPI();
for (MessageRecord messageRecord : messageRecords) { for (MessageRecord messageRecord : messageRecords) {
boolean isThreadDeleted; isSentByUser = isSentByUser && messageRecord.isOutgoing();
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id);
if (publicChat != null) { if (serverID != null) {
final SettableFuture<?>[] future = { new SettableFuture<Unit>() }; serverIDs.add(serverID);
LokiPublicChatAPI publicChatAPI = ApplicationContext.getInstance(getContext()).getLokiPublicChatAPI();
boolean isSentByUser = messageRecord.isOutgoing();
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id);
if (publicChatAPI != null && serverID != null) {
publicChatAPI
.deleteMessage(serverID, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
.success(l -> {
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>) future[0];
f.set(Unit.INSTANCE);
return Unit.INSTANCE;
}).fail(e -> {
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>) future[0];
f.setException(e);
return Unit.INSTANCE;
});
} else {
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>) future[0];
f.setException(new Exception("Message server ID is null."));
}
try {
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
f.get();
} catch (Exception exception) {
Log.d("Loki", "Couldn't delete message due to error: " + exception.toString() + ".");
return null;
}
}
if (messageRecord.isMms()) {
isThreadDeleted = DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
} else { } else {
isThreadDeleted = DatabaseFactory.getSmsDatabase(getActivity()).deleteMessage(messageRecord.getId()); ignoredMessages.add(messageRecord.getId());
}
if (isThreadDeleted) {
threadId = -1;
listener.setThreadId(threadId);
} }
} }
if (publicChat != null && publicChatAPI != null) {
publicChatAPI
.deleteMessages(serverIDs, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
.success(l -> {
for (MessageRecord messageRecord : messageRecords) {
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id);
if (l.contains(serverID)) {
if (messageRecord.isMms()) {
DatabaseFactory.getMmsDatabase(getActivity()).delete(messageRecord.getId());
} else {
DatabaseFactory.getSmsDatabase(getActivity()).deleteMessage(messageRecord.getId());
}
} else if (!ignoredMessages.contains(serverID)) {
failedMessages.add(messageRecord.getId());
Log.d("Loki", "Failed to delete message: " + messageRecord.getId() + ".");
}
}
return null;
}). fail(e -> {
Log.d("Loki", "Couldn't delete message due to error: " + e.toString() + ".");
return null;
});
}
return null; return null;
} }
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, messageRecords.toArray(new MessageRecord[messageRecords.size()])); }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, messageRecords.toArray(new MessageRecord[messageRecords.size()]));

View File

@ -61,7 +61,7 @@ public class Address implements Parcelable, Comparable<Address> {
private Address(@NonNull String address, Boolean isPublicChat) { private Address(@NonNull String address, Boolean isPublicChat) {
if (address == null) throw new AssertionError(address); if (address == null) throw new AssertionError(address);
this.address = address; this.address = address.toLowerCase();
this.isPublicChat = isPublicChat; this.isPublicChat = isPublicChat;
} }

View File

@ -47,6 +47,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -193,6 +194,23 @@ public class SmsDatabase extends MessagingDatabase {
return -1; 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) { public void markAsEndSession(long id) {
updateTypeBitmask(id, Types.KEY_EXCHANGE_MASK, Types.END_SESSION_BIT); updateTypeBitmask(id, Types.KEY_EXCHANGE_MASK, Types.END_SESSION_BIT);
} }

View File

@ -1265,7 +1265,7 @@ public class ClassicOpenHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN system_contact_photo TEXT DEFAULT NULL"); db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN system_contact_photo TEXT DEFAULT NULL");
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN system_phone_label TEXT DEFAULT NULL"); db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN system_phone_label TEXT DEFAULT NULL");
db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN system_contact_uri TEXT DEFAULT NULL"); db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN system_contact_uri TEXT DEFAULT NULL");
/*
if (Permissions.hasAny(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) { if (Permissions.hasAny(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
try (Cursor cursor = db.query("recipient_preferences", null, null, null, null, null, null)) { try (Cursor cursor = db.query("recipient_preferences", null, null, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
@ -1295,6 +1295,7 @@ public class ClassicOpenHelper extends SQLiteOpenHelper {
} }
} }
} }
*/
} }
db.setTransactionSuccessful(); db.setTransactionSuccessful();

View File

@ -70,6 +70,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV1 = 22; private static final int lokiV1 = 22;
private static final int lokiV2 = 23; private static final int lokiV2 = 23;
private static final int lokiV3 = 24; 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 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"; private static final String DATABASE_NAME = "signal.db";
@ -128,7 +129,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand()); db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand());
db.execSQL(LokiPreKeyBundleDatabase.getCreateTableCommand()); db.execSQL(LokiPreKeyBundleDatabase.getCreateTableCommand());
db.execSQL(LokiPreKeyRecordDatabase.getCreateTableCommand()); db.execSQL(LokiPreKeyRecordDatabase.getCreateTableCommand());
db.execSQL(LokiMessageDatabase.getCreateTableCommand()); db.execSQL(LokiMessageDatabase.getCreateMessageFriendRequestTableCommand());
db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand());
db.execSQL(LokiThreadDatabase.getCreateFriendRequestTableCommand()); db.execSQL(LokiThreadDatabase.getCreateFriendRequestTableCommand());
db.execSQL(LokiThreadDatabase.getCreateSessionResetTableCommand()); db.execSQL(LokiThreadDatabase.getCreateSessionResetTableCommand());
db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand()); db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand());
@ -504,6 +506,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE part ADD COLUMN url TEXT"); db.execSQL("ALTER TABLE part ADD COLUMN url TEXT");
} }
if (oldVersion < lokiV4) {
db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand());
}
db.setTransactionSuccessful(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();

View File

@ -47,8 +47,9 @@ import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
import org.thoughtcrime.securesms.jobs.TypingSendJob; import org.thoughtcrime.securesms.jobs.TypingSendJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.PushMessageSyncSendJob;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; 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.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.service.IncomingMessageObserver; import org.thoughtcrime.securesms.service.IncomingMessageObserver;
import org.thoughtcrime.securesms.service.WebRtcCallService; import org.thoughtcrime.securesms.service.WebRtcCallService;
@ -112,7 +113,8 @@ import network.loki.messenger.BuildConfig;
StickerPackDownloadJob.class, StickerPackDownloadJob.class,
MultiDeviceStickerPackOperationJob.class, MultiDeviceStickerPackOperationJob.class,
MultiDeviceStickerPackSyncJob.class, MultiDeviceStickerPackSyncJob.class,
LinkPreviewRepository.class}) LinkPreviewRepository.class,
PushMessageSyncSendJob.class})
public class SignalCommunicationModule { public class SignalCommunicationModule {
@ -151,7 +153,7 @@ public class SignalCommunicationModule {
TextSecurePreferences.isMultiDevice(context), TextSecurePreferences.isMultiDevice(context),
Optional.fromNullable(IncomingMessageObserver.getPipe()), Optional.fromNullable(IncomingMessageObserver.getPipe()),
Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()), Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()),
Optional.of(new SecurityEventListener(context)), Optional.of(new MessageSenderEventListener(context)),
TextSecurePreferences.getLocalNumber(context), TextSecurePreferences.getLocalNumber(context),
DatabaseFactory.getLokiAPIDatabase(context), DatabaseFactory.getLokiAPIDatabase(context),
DatabaseFactory.getLokiThreadDatabase(context), DatabaseFactory.getLokiThreadDatabase(context),

View File

@ -24,6 +24,7 @@ public class Data {
@JsonProperty private final Map<String, double[]> doubleArrays; @JsonProperty private final Map<String, double[]> doubleArrays;
@JsonProperty private final Map<String, Boolean> booleans; @JsonProperty private final Map<String, Boolean> booleans;
@JsonProperty private final Map<String, boolean[]> booleanArrays; @JsonProperty private final Map<String, boolean[]> booleanArrays;
@JsonProperty private final Map<String, byte[]> byteArrays;
public Data(@JsonProperty("strings") @NonNull Map<String, String> strings, public Data(@JsonProperty("strings") @NonNull Map<String, String> strings,
@JsonProperty("stringArrays") @NonNull Map<String, String[]> stringArrays, @JsonProperty("stringArrays") @NonNull Map<String, String[]> stringArrays,
@ -36,7 +37,8 @@ public class Data {
@JsonProperty("doubles") @NonNull Map<String, Double> doubles, @JsonProperty("doubles") @NonNull Map<String, Double> doubles,
@JsonProperty("doubleArrays") @NonNull Map<String, double[]> doubleArrays, @JsonProperty("doubleArrays") @NonNull Map<String, double[]> doubleArrays,
@JsonProperty("booleans") @NonNull Map<String, Boolean> booleans, @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.strings = strings;
this.stringArrays = stringArrays; this.stringArrays = stringArrays;
@ -50,6 +52,7 @@ public class Data {
this.doubleArrays = doubleArrays; this.doubleArrays = doubleArrays;
this.booleans = booleans; this.booleans = booleans;
this.booleanArrays = booleanArrays; this.booleanArrays = booleanArrays;
this.byteArrays = byteArrays;
} }
public boolean hasString(@NonNull String key) { public boolean hasString(@NonNull String key) {
@ -201,6 +204,14 @@ public class Data {
return booleanArrays.get(key); 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) { private void throwIfAbsent(@NonNull Map map, @NonNull String key) {
if (!map.containsKey(key)) { if (!map.containsKey(key)) {
@ -223,6 +234,7 @@ public class Data {
private final Map<String, double[]> doubleArrays = new HashMap<>(); private final Map<String, double[]> doubleArrays = new HashMap<>();
private final Map<String, Boolean> booleans = new HashMap<>(); private final Map<String, Boolean> booleans = new HashMap<>();
private final Map<String, boolean[]> booleanArrays = 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) { public Builder putString(@NonNull String key, @Nullable String value) {
strings.put(key, value); strings.put(key, value);
@ -284,6 +296,11 @@ public class Data {
return this; return this;
} }
public Builder putByteArray(@NonNull String key, @NonNull byte[] value) {
byteArrays.put(key, value);
return this;
}
public Data build() { public Data build() {
return new Data(strings, return new Data(strings,
stringArrays, stringArrays,
@ -296,7 +313,8 @@ public class Data {
doubles, doubles,
doubleArrays, doubleArrays,
booleans, booleans,
booleanArrays); booleanArrays,
byteArrays);
} }
} }

View File

@ -27,6 +27,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -84,14 +85,17 @@ public class AttachmentUploadJob extends BaseJob implements InjectableType {
if (databaseAttachment == null) { if (databaseAttachment == null) {
throw new IllegalStateException("Cannot find the specified attachment."); 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(); database.updateAttachmentAfterUpload(databaseAttachment.getAttachmentId(), attachment);
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);
} }
@Override @Override

View File

@ -13,6 +13,8 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver; 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.Arrays;
import java.util.HashMap; import java.util.HashMap;
@ -70,6 +72,8 @@ public final class JobManagerFactories {
put(TrimThreadJob.KEY, new TrimThreadJob.Factory()); put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
put(TypingSendJob.KEY, new TypingSendJob.Factory()); put(TypingSendJob.KEY, new TypingSendJob.Factory());
put(UpdateApkJob.KEY, new UpdateApkJob.Factory()); put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
put(PushMessageSyncSendJob.KEY, new PushMessageSyncSendJob.Factory());
put(PushBackgroundMessageSendJob.KEY, new PushBackgroundMessageSendJob.Factory());
}}; }};
} }

View File

@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.Database;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.dependencies.InjectableType; 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.DeviceContactsOutputStream;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; 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.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.inject.Inject; 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 long FULL_SYNC_TIME = TimeUnit.HOURS.toMillis(6);
private static final String KEY_ADDRESS = "address"; private static final String KEY_ADDRESS = "address";
private static final String KEY_RECIPIENT = "recipient";
private static final String KEY_FORCE_SYNC = "force_sync"; private static final String KEY_FORCE_SYNC = "force_sync";
@Inject SignalServiceMessageSender messageSender; @Inject SignalServiceMessageSender messageSender;
private @Nullable String address; 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; private boolean forceSync;
/**
* Create a full contact sync job which syncs across to all other devices
*/
public MultiDeviceContactUpdateJob(@NonNull Context context) { public MultiDeviceContactUpdateJob(@NonNull Context context) {
this(context, false); 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) { 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() this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY) .addConstraint(NetworkConstraint.KEY)
.setQueue("MultiDeviceContactUpdateJob") .setQueue("MultiDeviceContactUpdateJob")
.setLifespan(TimeUnit.DAYS.toMillis(1)) .setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED) .setMaxAttempts(1)
.build(), .build(),
recipient,
address, address,
forceSync); 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); super(parameters);
this.forceSync = forceSync; this.forceSync = forceSync;
this.recipient = (recipient != null) ? recipient.serialize() : null;
if (address != null) this.address = address.serialize(); if (address != null) this.address = address.serialize();
else this.address = null; else this.address = null;
@ -102,6 +123,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
public @NonNull Data serialize() { public @NonNull Data serialize() {
return new Data.Builder().putString(KEY_ADDRESS, address) return new Data.Builder().putString(KEY_ADDRESS, address)
.putBoolean(KEY_FORCE_SYNC, forceSync) .putBoolean(KEY_FORCE_SYNC, forceSync)
.putString(KEY_RECIPIENT, recipient)
.build(); .build();
} }
@ -120,12 +142,15 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
} }
if (address == null) generateFullContactUpdate(); 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) private void generateSingleContactUpdate(@NonNull Address address)
throws IOException, UntrustedIdentityException, NetworkException throws IOException, UntrustedIdentityException, NetworkException
{ {
// Loki - Only sync regular contacts
if (!address.isPhone()) { return; }
File contactDataFile = createTempFile("multidevice-contact-update"); File contactDataFile = createTempFile("multidevice-contact-update");
try { try {
@ -134,16 +159,19 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
Optional<IdentityDatabase.IdentityRecord> identityRecord = DatabaseFactory.getIdentityDatabase(context).getIdentity(address); Optional<IdentityDatabase.IdentityRecord> identityRecord = DatabaseFactory.getIdentityDatabase(context).getIdentity(address);
Optional<VerifiedMessage> verifiedMessage = getVerifiedMessage(recipient, identityRecord); Optional<VerifiedMessage> verifiedMessage = getVerifiedMessage(recipient, identityRecord);
out.write(new DeviceContact(address.toPhoneString(), // Loki - Only sync contacts we are friends with
Optional.fromNullable(recipient.getName()), if (getFriendRequestStatus(recipient) == LokiThreadFriendRequestStatus.FRIENDS) {
getAvatar(recipient.getContactUri()), out.write(new DeviceContact(address.toPhoneString(),
Optional.fromNullable(recipient.getColor().serialize()), Optional.fromNullable(recipient.getName()),
verifiedMessage, getAvatar(recipient.getContactUri()),
Optional.fromNullable(recipient.getProfileKey()), Optional.fromNullable(recipient.getColor().serialize()),
recipient.isBlocked(), verifiedMessage,
recipient.getExpireMessages() > 0 ? Optional.fromNullable(recipient.getProfileKey()),
Optional.of(recipient.getExpireMessages()) : recipient.isBlocked(),
Optional.absent())); recipient.getExpireMessages() > 0 ?
Optional.of(recipient.getExpireMessages()) :
Optional.absent()));
}
out.close(); out.close();
sendUpdate(messageSender, contactDataFile, false); sendUpdate(messageSender, contactDataFile, false);
@ -158,11 +186,6 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
private void generateFullContactUpdate() private void generateFullContactUpdate()
throws IOException, UntrustedIdentityException, NetworkException throws IOException, UntrustedIdentityException, NetworkException
{ {
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(); boolean isAppVisible = ApplicationContext.getInstance(context).isAppVisible();
long timeSinceLastSync = System.currentTimeMillis() - TextSecurePreferences.getLastFullContactSyncTime(context); long timeSinceLastSync = System.currentTimeMillis() - TextSecurePreferences.getLastFullContactSyncTime(context);
@ -180,8 +203,8 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
File contactDataFile = createTempFile("multidevice-contact-update"); File contactDataFile = createTempFile("multidevice-contact-update");
try { try {
DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactDataFile)); DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactDataFile));
Collection<ContactData> contacts = ContactAccessor.getInstance().getContactsWithPush(context); List<ContactData> contacts = getAllContacts();
for (ContactData contactData : contacts) { for (ContactData contactData : contacts) {
Uri contactUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, String.valueOf(contactData.id)); Uri contactUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, String.valueOf(contactData.id));
@ -195,7 +218,10 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
boolean blocked = recipient.isBlocked(); boolean blocked = recipient.isBlocked();
Optional<Integer> expireTimer = recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages()) : Optional.absent(); 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)) { if (ProfileKeyUtil.hasProfileKey(context)) {
@ -216,9 +242,29 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
} }
} }
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 @Override
public boolean onShouldRetry(@NonNull Exception exception) { 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; return false;
} }
@ -238,10 +284,10 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
.withLength(contactsFile.length()) .withLength(contactsFile.length())
.build(); .build();
SignalServiceAddress messageRecipient = recipient != null ? new SignalServiceAddress(recipient) : null;
try { try {
// TODO: Message ID messageSender.sendMessage(0, SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, complete)), messageRecipient);
messageSender.sendMessage(0, SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, complete)),
UnidentifiedAccessUtil.getAccessForSync(context));
} catch (IOException ioe) { } catch (IOException ioe) {
throw new NetworkException(ioe); throw new NetworkException(ioe);
} }
@ -249,6 +295,9 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
} }
private Optional<SignalServiceAttachmentStream> getAvatar(@Nullable Uri uri) throws IOException { 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) { if (uri == null) {
return Optional.absent(); return Optional.absent();
} }
@ -302,6 +351,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
cursor.close(); cursor.close();
} }
} }
*/
} }
private Optional<VerifiedMessage> getVerifiedMessage(Recipient recipient, Optional<IdentityDatabase.IdentityRecord> identity) throws InvalidNumberException { private Optional<VerifiedMessage> getVerifiedMessage(Recipient recipient, Optional<IdentityDatabase.IdentityRecord> identity) throws InvalidNumberException {
@ -342,7 +392,10 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
String serialized = data.getString(KEY_ADDRESS); String serialized = data.getString(KEY_ADDRESS);
Address address = serialized != null ? Address.fromSerialized(serialized) : null; 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));
} }
} }
} }

View File

@ -14,6 +14,7 @@ import android.util.Pair;
import com.annimon.stream.Collectors; import com.annimon.stream.Collectors;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import com.google.android.gms.common.util.IOUtils;
import org.signal.libsignal.metadata.InvalidMetadataMessageException; import org.signal.libsignal.metadata.InvalidMetadataMessageException;
import org.signal.libsignal.metadata.InvalidMetadataVersionException; 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.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.FriendRequestHandler;
import org.thoughtcrime.securesms.loki.LokiAPIUtilities; import org.thoughtcrime.securesms.loki.LokiAPIUtilities;
import org.thoughtcrime.securesms.loki.LokiMessageDatabase; import org.thoughtcrime.securesms.loki.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase; import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase;
import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase; import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase;
import org.thoughtcrime.securesms.loki.LokiThreadDatabase; 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.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage; 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.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage; import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage; import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage; import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage; 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.IceUpdateMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage; import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; 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.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; 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.LokiServiceMessage;
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus; import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.messaging.LokiThreadSessionResetStatus; 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.MessageDigest;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.ArrayList; import java.util.ArrayList;
@ -303,6 +312,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Optional<String> rawSenderDisplayName = content.senderDisplayName; Optional<String> rawSenderDisplayName = content.senderDisplayName;
if (rawSenderDisplayName.isPresent() && rawSenderDisplayName.get().length() > 0) { if (rawSenderDisplayName.isPresent() && rawSenderDisplayName.get().length() > 0) {
setDisplayName(envelope.getSource(), rawSenderDisplayName.get()); 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 // 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.getRead().isPresent()) handleSynchronizeReadMessage(syncMessage.getRead().get(), content.getTimestamp());
else if (syncMessage.getVerified().isPresent()) handleSynchronizeVerifiedMessage(syncMessage.getVerified().get()); else if (syncMessage.getVerified().isPresent()) handleSynchronizeVerifiedMessage(syncMessage.getVerified().get());
else if (syncMessage.getStickerPackOperations().isPresent()) handleSynchronizeStickerPackOperation(syncMessage.getStickerPackOperations().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 Log.w(TAG, "Contains no known sync types...");
} else if (content.getCallMessage().isPresent()) { } else if (content.getCallMessage().isPresent()) {
Log.i(TAG, "Got call message..."); 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() + "."); Log.d("Loki", "Sending a ping back to " + content.getSender() + ".");
String contactID = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId).getAddress().toString(); String contactID = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId).getAddress().toString();
sendBackgroundMessage(contactID); MessageSender.sendBackgroundMessage(context, contactID);
SecurityEvent.broadcastSecurityUpdateEvent(context); SecurityEvent.broadcastSecurityUpdateEvent(context);
MessageNotifier.updateNotification(context, threadId); 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, private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content,
@NonNull SentTranscriptMessage message) @NonNull SentTranscriptMessage message)
throws StorageFailedException throws StorageFailedException
@ -749,13 +807,19 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
@NonNull Optional<Long> messageServerIDOrNull) @NonNull Optional<Long> messageServerIDOrNull)
throws StorageFailedException 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<QuoteModel> quote = getValidatedQuote(message.getQuote());
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts()); Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or("")); Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
Optional<Attachment> sticker = getStickerAttachment(message.getSticker()); 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(), message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(),
quote, sharedContacts, linkPreviews, sticker); quote, sharedContacts, linkPreviews, sticker);
@ -798,14 +862,20 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Store message server ID // Loki - Store message server ID
updateGroupChatMessageServerID(messageServerIDOrNull, insertResult); updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);
// Loki - Update mapping of message to original thread id
if (insertResult.isPresent()) { if (insertResult.isPresent()) {
MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); 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 { private long handleSynchronizeSentExpirationUpdate(@NonNull SentTranscriptMessage message) throws MmsException {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Recipient recipient = getSyncMessageDestination(message); Recipient recipient = getSyncMessagePrimaryDestination(message);
OutgoingExpirationUpdateMessage expirationUpdateMessage = new OutgoingExpirationUpdateMessage(recipient, OutgoingExpirationUpdateMessage expirationUpdateMessage = new OutgoingExpirationUpdateMessage(recipient,
message.getTimestamp(), message.getTimestamp(),
@ -821,11 +891,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
return threadId; return threadId;
} }
private long handleSynchronizeSentMediaMessage(@NonNull SentTranscriptMessage message) public long handleSynchronizeSentMediaMessage(@NonNull SentTranscriptMessage message)
throws MmsException throws MmsException
{ {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Recipient recipients = getSyncMessageDestination(message); Recipient recipients = getSyncMessagePrimaryDestination(message);
Optional<QuoteModel> quote = getValidatedQuote(message.getMessage().getQuote()); Optional<QuoteModel> quote = getValidatedQuote(message.getMessage().getQuote());
Optional<Attachment> sticker = getStickerAttachment(message.getMessage().getSticker()); Optional<Attachment> sticker = getStickerAttachment(message.getMessage().getSticker());
Optional<List<Contact>> sharedContacts = getContacts(message.getMessage().getSharedContacts()); Optional<List<Contact>> sharedContacts = getContacts(message.getMessage().getSharedContacts());
@ -857,6 +927,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
try { try {
long messageId = database.insertMessageOutbox(mediaMessage, threadId, false, null); long messageId = database.insertMessageOutbox(mediaMessage, threadId, false, null);
if (message.messageServerID >= 0) { DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageId, message.messageServerID); }
if (recipients.getAddress().isGroup()) { if (recipients.getAddress().isGroup()) {
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context); GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
@ -912,20 +983,23 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
{ {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context); SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
String body = message.getBody().isPresent() ? message.getBody().get() : ""; 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()); handleExpirationUpdate(content, message, Optional.absent());
} }
Long threadId; Long threadId = null;
if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) { if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) {
threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second; threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second;
} else { } 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(), content.getSenderDevice(),
message.getTimestamp(), body, message.getTimestamp(), body,
message.getGroupInfo(), message.getGroupInfo(),
@ -940,8 +1014,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Insert the message into the database // Insert the message into the database
Optional<InsertResult> insertResult = database.insertMessageInbox(textMessage); Optional<InsertResult> insertResult = database.insertMessageInbox(textMessage);
if (insertResult.isPresent()) threadId = insertResult.get().getThreadId(); Long messageId = null;
else threadId = null; if (insertResult.isPresent()) {
threadId = insertResult.get().getThreadId();
messageId = insertResult.get().getMessageId();
}
if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get()); if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get());
@ -954,6 +1031,14 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Store message server ID // Loki - Store message server ID
updateGroupChatMessageServerID(messageServerIDOrNull, insertResult); 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(); boolean isGroupMessage = message.getGroupInfo().isPresent();
if (threadId != null && !isGroupMessage) { if (threadId != null && !isGroupMessage) {
MessageNotifier.updateNotification(context, threadId); 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) { private void handlePairingMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context); String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
if (authorisation.getType() == PairingAuthorisation.Type.REQUEST) { if (authorisation.getType() == PairingAuthorisation.Type.REQUEST) {
handlePairingRequestMessage(authorisation, envelope); handlePairingRequestMessage(authorisation);
} else if (authorisation.getSecondaryDevicePublicKey().equals(userHexEncodedPublicKey)) { } else if (authorisation.getSecondaryDevicePublicKey().equals(userHexEncodedPublicKey)) {
handlePairingAuthorisationMessage(authorisation, envelope, content); handlePairingAuthorisationMessage(authorisation, envelope, content);
} }
} }
private void handlePairingRequestMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceEnvelope envelope) { private void handlePairingRequestMessage(@NonNull PairingAuthorisation authorisation) {
boolean isValid = isValidPairingMessage(authorisation); boolean isValid = isValidPairingMessage(authorisation);
DeviceLinkingSession linkingSession = DeviceLinkingSession.Companion.getShared(); DeviceLinkingSession linkingSession = DeviceLinkingSession.Companion.getShared();
if (isValid && linkingSession.isListeningForLinkingRequests()) { if (isValid && linkingSession.isListeningForLinkingRequests()) {
@ -1023,8 +1108,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
DatabaseFactory.getLokiAPIDatabase(context).removePairingAuthorisations(userHexEncodedPublicKey); DatabaseFactory.getLokiAPIDatabase(context).removePairingAuthorisations(userHexEncodedPublicKey);
DatabaseFactory.getLokiAPIDatabase(context).insertOrUpdatePairingAuthorisation(authorisation); DatabaseFactory.getLokiAPIDatabase(context).insertOrUpdatePairingAuthorisation(authorisation);
TextSecurePreferences.setMasterHexEncodedPublicKey(context, authorisation.getPrimaryDevicePublicKey()); TextSecurePreferences.setMasterHexEncodedPublicKey(context, authorisation.getPrimaryDevicePublicKey());
TextSecurePreferences.setMultiDevice(context, true);
// Send a background message to the primary device // Send a background message to the primary device
sendBackgroundMessage(authorisation.getPrimaryDevicePublicKey()); MessageSender.sendBackgroundMessage(context, authorisation.getPrimaryDevicePublicKey());
// Propagate the updates to the file server // Propagate the updates to the file server
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared(); LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
storageAPI.updateUserDeviceMappings(); storageAPI.updateUserDeviceMappings();
@ -1032,6 +1118,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (content.senderDisplayName.isPresent() && content.senderDisplayName.get().length() > 0) { if (content.senderDisplayName.isPresent() && content.senderDisplayName.get().length() > 0) {
setDisplayName(envelope.getSource(), content.senderDisplayName.get()); 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) { 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) { 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 we get anything other than a friend request, we can assume that we have a session with the other user
if (envelope.isFriendRequest()) { return; } if (envelope.isFriendRequest() || isGroupChatMessage(content)) { return; }
becomeFriendsWithContact(content.getSender()); becomeFriendsWithContact(content.getSender(), true);
} }
private void becomeFriendsWithContact(String pubKey) { private void becomeFriendsWithContact(String pubKey, boolean syncContact) {
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context); LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
Recipient contactID = Recipient.from(context, Address.fromSerialized(pubKey), false); Recipient contactID = Recipient.from(context, Address.fromSerialized(pubKey), false);
if (contactID.isGroupRecipient()) return;
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(contactID); long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(contactID);
LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID); LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS) { return; } if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS) { return; }
// If the thread's friend request status is not `FRIENDS`, but we're receiving a message, // 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. // it must be a friend request accepted message. Declining a friend request doesn't send a message.
lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS); lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS);
// Update the last message if needed // Send out a contact sync message
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); if (syncContact) {
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); MessageSender.syncContact(context, contactID.getAddress());
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);
} }
} // Update the last message if needed
LokiStorageAPI.shared.getPrimaryDevicePublicKey(pubKey).success(primaryDevice -> {
private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) { Util.runOnMain(() -> {
if (!envelope.isFriendRequest()) { return; } long primaryDeviceThreadID = primaryDevice == null ? threadID : DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(primaryDevice), false));
// This handles the case where another user sends us a regular message without authorisation FriendRequestHandler.updateLastFriendRequestMessage(context, primaryDeviceThreadID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
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);
}
}
}
return Unit.INSTANCE; return Unit.INSTANCE;
}); });
} }
private void sendBackgroundMessage(String contactHexEncodedPublicKey) { private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
Util.runOnMain(() -> { if (!envelope.isFriendRequest() || message.isGroupUpdate()) { return; }
SignalServiceMessageSender messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender(); // This handles the case where another user sends us a regular message without authorisation
SignalServiceAddress address = new SignalServiceAddress(contactHexEncodedPublicKey); boolean shouldBecomeFriends = PromiseUtil.get(MultiDeviceUtilities.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context), false);
SignalServiceDataMessage message = new SignalServiceDataMessage(System.currentTimeMillis(), ""); if (shouldBecomeFriends) {
try { // Become friends AND update the message they sent
messageSender.sendMessage(0, address, Optional.absent(), message); // The message ID doesn't matter becomeFriendsWithContact(content.getSender(), true);
} catch (Exception e) { // Send them an accept message back
Log.d("Loki", "Failed to send background message to: " + contactHexEncodedPublicKey + "."); 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 throws MmsException
{ {
Recipient recipient = getSyncMessageDestination(message); Recipient recipient = getSyncMessagePrimaryDestination(message);
String body = message.getMessage().getBody().or(""); String body = message.getMessage().getBody().or("");
long expiresInMillis = message.getMessage().getExpiresInSeconds() * 1000L; long expiresInMillis = message.getMessage().getExpiresInSeconds() * 1000L;
@ -1164,6 +1246,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
outgoingMediaMessage = new OutgoingSecureMediaMessage(outgoingMediaMessage); outgoingMediaMessage = new OutgoingSecureMediaMessage(outgoingMediaMessage);
messageId = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMediaMessage, threadId, false, null); messageId = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMediaMessage, threadId, false, null);
if (message.messageServerID >= 0) { DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageId, message.messageServerID); }
database = DatabaseFactory.getMmsDatabase(context); database = DatabaseFactory.getMmsDatabase(context);
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context); GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
@ -1308,10 +1392,17 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void handleDeliveryReceipt(@NonNull SignalServiceContent content, private void handleDeliveryReceipt(@NonNull SignalServiceContent content,
@NonNull SignalServiceReceiptMessage message) @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()) { for (long timestamp : message.getTimestamps()) {
Log.i(TAG, String.format("Received encrypted delivery receipt: (XXXXX, %d)", timestamp)); Log.i(TAG, String.format("Received encrypted delivery receipt: (XXXXX, %d)", timestamp));
DatabaseFactory.getMmsSmsDatabase(context) 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) @NonNull SignalServiceReceiptMessage message)
{ {
if (TextSecurePreferences.isReadReceiptsEnabled(context)) { 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()) { for (long timestamp : message.getTimestamps()) {
Log.i(TAG, String.format("Received encrypted read receipt: (XXXXX, %d)", timestamp)); Log.i(TAG, String.format("Received encrypted read receipt: (XXXXX, %d)", timestamp));
DatabaseFactory.getMmsSmsDatabase(context) 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); threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
} else { } else {
// See if we need to redirect the message
author = getPrimaryDeviceRecipient(content.getSender());
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(author); 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) { private Optional<InsertResult> insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp) {
Recipient primaryDevice = getPrimaryDeviceRecipient(sender);
SmsDatabase database = DatabaseFactory.getSmsDatabase(context); SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
IncomingTextMessage textMessage = new IncomingTextMessage(Address.fromSerialized(sender), IncomingTextMessage textMessage = new IncomingTextMessage(primaryDevice.getAddress(),
senderDevice, timestamp, "", senderDevice, timestamp, "",
Optional.absent(), 0, false); 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) { private Recipient getMessageDestination(SignalServiceContent content, SignalServiceDataMessage message) {
if (message.getGroupInfo().isPresent()) { if (message.getGroupInfo().isPresent()) {
return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false)), false); 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) { private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient conversationRecipient, @NonNull String sender, int device) {
Recipient author = Recipient.from(context, Address.fromSerialized(sender), false); Recipient author = Recipient.from(context, Address.fromSerialized(sender), false);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(conversationRecipient); 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); 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(); SignalServiceDataMessage message = content.getDataMessage().get();
Recipient conversation = getMessageDestination(content, message); Recipient conversation = getMessageDestination(content, message);
@ -1550,11 +1693,26 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} }
} else if (content.getCallMessage().isPresent() || content.getTypingMessage().isPresent()) { } else if (content.getCallMessage().isPresent() || content.getTypingMessage().isPresent()) {
return sender.isBlocked(); 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; return false;
} }
private boolean isGroupChatMessage(SignalServiceContent content) {
return content.getDataMessage().isPresent() && content.getDataMessage().get().isGroupUpdate();
}
private void resetRecipientToPush(@NonNull Recipient recipient) { private void resetRecipientToPush(@NonNull Recipient recipient) {
if (recipient.isForceSmsSelection()) { if (recipient.isForceSmsSelection()) {
DatabaseFactory.getRecipientDatabase(context).setForceSmsSelection(recipient, false); DatabaseFactory.getRecipientDatabase(context).setForceSmsSelection(recipient, false);

View File

@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.recipients.Recipient; 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.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; 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.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; 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_DESTINATION = "destination";
private static final String KEY_IS_FRIEND_REQUEST = "is_friend_request"; 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_CUSTOM_FR_MESSAGE = "custom_friend_request_message";
private static final String KEY_SHOULD_SEND_SYNC_MESSAGE = "should_send_sync_message";
@Inject SignalServiceMessageSender messageSender; @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 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 boolean isFriendRequest; // Whether this is a friend request message
private String customFriendRequestMessage; // If this isn't set then we use the message body 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 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) { this(templateMessageId, messageId, destination, false, null, false); }
public PushMediaSendJob(long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage) { public PushMediaSendJob(long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage, boolean shouldSendSyncMessage) {
this(constructParameters(destination), templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage); 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); super(parameters);
this.templateMessageId = templateMessageId; this.templateMessageId = templateMessageId;
this.messageId = messageId; this.messageId = messageId;
this.destination = destination; this.destination = destination;
this.isFriendRequest = isFriendRequest; this.isFriendRequest = isFriendRequest;
this.customFriendRequestMessage = customFriendRequestMessage; 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 @WorkerThread
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination) { public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, List<PushMediaSendJob> jobs) {
enqueue(context, jobManager, messageId, messageId, destination); if (jobs.size() == 0) { return; }
} PushMediaSendJob first = jobs.get(0);
long messageId = first.templateMessageId;
@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) {
try { try {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); List<AttachmentUploadJob> attachmentJobs = getAttachmentUploadJobs(context, messageId, first.destination);
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();
if (attachmentJobs.isEmpty()) { if (attachmentJobs.isEmpty()) {
jobManager.add(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage)); for (PushMediaSendJob job : jobs) { jobManager.add(job); }
} else { } else {
jobManager.startChain(attachmentJobs) jobManager.startChain(attachmentJobs)
.then(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage)) .then((List<Job>)(List)jobs)
.enqueue(); .enqueue();
} }
} catch (NoSuchMessageException | MmsException e) { } catch (NoSuchMessageException | MmsException e) {
Log.w(TAG, "Failed to enqueue message.", e); Log.w(TAG, "Failed to enqueue message.", e);
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId); 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 @Override
public @NonNull Data serialize() { public @NonNull Data serialize() {
Data.Builder builder = new Data.Builder() Data.Builder builder = new Data.Builder()
.putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId) .putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId)
.putLong(KEY_MESSAGE_ID, messageId) .putLong(KEY_MESSAGE_ID, messageId)
.putString(KEY_DESTINATION, destination.serialize()) .putString(KEY_DESTINATION, destination.serialize())
.putBoolean(KEY_IS_FRIEND_REQUEST, isFriendRequest); .putBoolean(KEY_IS_FRIEND_REQUEST, isFriendRequest)
.putBoolean(KEY_SHOULD_SEND_SYNC_MESSAGE, shouldSendSyncMessage);
if (customFriendRequestMessage != null) { builder.putString(KEY_CUSTOM_FR_MESSAGE, customFriendRequestMessage); } if (customFriendRequestMessage != null) { builder.putString(KEY_CUSTOM_FR_MESSAGE, customFriendRequestMessage); }
return builder.build(); return builder.build();
@ -211,8 +227,10 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
} }
} catch (UntrustedIdentityException uie) { } catch (UntrustedIdentityException uie) {
warn(TAG, "Failure", uie); warn(TAG, "Failure", uie);
database.addMismatchedIdentity(messageId, Address.fromSerialized(uie.getE164Number()), uie.getIdentityKey()); if (messageId >= 0) {
database.markAsSentFailed(messageId); 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); Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, mediaMessage, syncAccess); SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, mediaMessage, syncAccess);
messageSender.sendMessage(messageId, syncMessage, syncAccess); messageSender.sendMessage(templateMessageId, syncMessage, syncAccess);
return syncAccess.isPresent(); return syncAccess.isPresent();
} else { } 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) { } catch (UnregisteredUserException e) {
warn(TAG, e); warn(TAG, e);
@ -292,8 +318,9 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
long messageID = data.getLong(KEY_MESSAGE_ID); long messageID = data.getLong(KEY_MESSAGE_ID);
Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION)); Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION));
boolean isFriendRequest = data.getBoolean(KEY_IS_FRIEND_REQUEST); 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; String frMessage = data.hasString(KEY_CUSTOM_FR_MESSAGE) ? data.getString(KEY_CUSTOM_FR_MESSAGE) : null;
return new PushMediaSendJob(parameters, templateMessageID, messageID, destination, isFriendRequest, frMessage); return new PushMediaSendJob(parameters, templateMessageID, messageID, destination, isFriendRequest, frMessage, shouldSendSyncMessage);
} }
} }
} }

View File

@ -303,7 +303,8 @@ public abstract class PushSendJob extends SendJob {
} }
protected SignalServiceSyncMessage buildSelfSendSyncMessage(@NonNull Context context, @NonNull SignalServiceDataMessage message, Optional<UnidentifiedAccessPair> syncAccess) { 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, SentTranscriptMessage transcript = new SentTranscriptMessage(localNumber,
message.getTimestamp(), message.getTimestamp(),
message, message,

View File

@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.ExpiringMessageManager; 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.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; 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; 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_DESTINATION = "destination";
private static final String KEY_IS_FRIEND_REQUEST = "is_friend_request"; 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_CUSTOM_FR_MESSAGE = "custom_friend_request_message";
private static final String KEY_SHOULD_SEND_SYNC_MESSAGE = "should_send_sync_message";
@Inject SignalServiceMessageSender messageSender; @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 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 boolean isFriendRequest; // Whether this is a friend request message
private String customFriendRequestMessage; // If this isn't set then we use the message body 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 messageId, Address destination) { this(messageId, messageId, destination, false); }
public PushTextSendJob(long templateMessageId, long messageId, Address destination) { this(templateMessageId, messageId, destination, false, null); } 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) { public PushTextSendJob(long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage, boolean shouldSendSyncMessage) {
this(constructParameters(destination), templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage); 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); super(parameters);
this.templateMessageId = templateMessageId; this.templateMessageId = templateMessageId;
this.messageId = messageId; this.messageId = messageId;
this.destination = destination; this.destination = destination;
this.isFriendRequest = isFriendRequest; this.isFriendRequest = isFriendRequest;
this.customFriendRequestMessage = customFriendRequestMessage; this.customFriendRequestMessage = customFriendRequestMessage;
this.shouldSendSyncMessage = shouldSendSyncMessage;
} }
@Override @Override
public @NonNull Data serialize() { public @NonNull Data serialize() {
Data.Builder builder = new Data.Builder() Data.Builder builder = new Data.Builder()
.putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId) .putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId)
.putLong(KEY_MESSAGE_ID, messageId) .putLong(KEY_MESSAGE_ID, messageId)
.putString(KEY_DESTINATION, destination.serialize()) .putString(KEY_DESTINATION, destination.serialize())
.putBoolean(KEY_IS_FRIEND_REQUEST, isFriendRequest); .putBoolean(KEY_IS_FRIEND_REQUEST, isFriendRequest)
.putBoolean(KEY_SHOULD_SEND_SYNC_MESSAGE, shouldSendSyncMessage);
if (customFriendRequestMessage != null) { builder.putString(KEY_CUSTOM_FR_MESSAGE, customFriendRequestMessage); } if (customFriendRequestMessage != null) { builder.putString(KEY_CUSTOM_FR_MESSAGE, customFriendRequestMessage); }
return builder.build(); return builder.build();
@ -151,14 +159,18 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
} catch (InsecureFallbackApprovalException e) { } catch (InsecureFallbackApprovalException e) {
warn(TAG, "Failure", e); warn(TAG, "Failure", e);
database.markAsPendingInsecureSmsFallback(record.getId()); if (messageId >= 0) {
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipient(), record.getThreadId()); database.markAsPendingInsecureSmsFallback(record.getId());
ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(false)); MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipient(), record.getThreadId());
ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(false));
}
} catch (UntrustedIdentityException e) { } catch (UntrustedIdentityException e) {
warn(TAG, "Failure", e); warn(TAG, "Failure", e);
database.addMismatchedIdentity(record.getId(), Address.fromSerialized(e.getE164Number()), e.getIdentityKey()); if (messageId >= 0) {
database.markAsSentFailed(record.getId()); database.addMismatchedIdentity(record.getId(), Address.fromSerialized(e.getE164Number()), e.getIdentityKey());
database.markAsPush(record.getId()); 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); Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, textSecureMessage, syncAccess); SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, textSecureMessage, syncAccess);
messageSender.sendMessage(messageId, syncMessage, syncAccess); messageSender.sendMessage(templateMessageId, syncMessage, syncAccess);
return syncAccess.isPresent(); return syncAccess.isPresent();
} else { } 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) { } catch (UnregisteredUserException e) {
warn(TAG, "Failure", e); warn(TAG, "Failure", e);
@ -241,7 +261,8 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION)); Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION));
boolean isFriendRequest = data.getBoolean(KEY_IS_FRIEND_REQUEST); boolean isFriendRequest = data.getBoolean(KEY_IS_FRIEND_REQUEST);
String frMessage = data.hasString(KEY_CUSTOM_FR_MESSAGE) ? data.getString(KEY_CUSTOM_FR_MESSAGE) : null; String frMessage = data.hasString(KEY_CUSTOM_FR_MESSAGE) ? data.getString(KEY_CUSTOM_FR_MESSAGE) : null;
return new PushTextSendJob(parameters, templateMessageID, messageID, destination, isFriendRequest, frMessage); boolean shouldSendSyncMessage = data.getBoolean(KEY_SHOULD_SEND_SYNC_MESSAGE);
return new PushTextSendJob(parameters, templateMessageID, messageID, destination, isFriendRequest, frMessage, shouldSendSyncMessage);
} }
} }
} }

View File

@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; 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;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage.Action; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage.Action;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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(); 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); SignalServiceTypingMessage typingMessage = new SignalServiceTypingMessage(typing ? Action.STARTED : Action.STOPPED, System.currentTimeMillis(), groupId);
// Loki - Don't send typing indicators in group chats // Loki - Don't send typing indicators in group chats or to ourselves
if (!recipient.isGroupRecipient()) { if (recipient.isGroupRecipient()) { return; }
// TODO: Message ID
boolean isOurDevice = PromiseUtil.get(MultiDeviceUtilities.isOneOfOurDevices(context, recipient.getAddress()), false);
if (!isOurDevice) {
messageSender.sendTyping(0, addresses, unidentifiedAccess, typingMessage); messageSender.sendTyping(0, addresses, unidentifiedAccess, typingMessage);
} }
} }

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

View File

@ -12,11 +12,14 @@ import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestS
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol { class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
companion object { 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 messageID = "message_id"
private val serverID = "server_id" private val serverID = "server_id"
private val friendRequestStatus = "friend_request_status" 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? { override fun getQuoteServerID(quoteID: Long, quoteeHexEncodedPublicKey: String): Long? {
@ -26,14 +29,14 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
fun getServerID(messageID: Long): Long? { fun getServerID(messageID: Long): Long? {
val database = databaseHelper.readableDatabase 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) cursor.getInt(Companion.serverID)
}?.toLong() }?.toLong()
} }
fun getMessageID(serverID: Long): Long? { fun getMessageID(serverID: Long): Long? {
val database = databaseHelper.readableDatabase 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) cursor.getInt(messageID)
}?.toLong() }?.toLong()
} }
@ -43,12 +46,27 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
val contentValues = ContentValues(2) val contentValues = ContentValues(2)
contentValues.put(Companion.messageID, messageID) contentValues.put(Companion.messageID, messageID)
contentValues.put(Companion.serverID, serverID) 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 { fun getFriendRequestStatus(messageID: Long): LokiMessageFriendRequestStatus {
val database = databaseHelper.readableDatabase 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) cursor.getInt(friendRequestStatus)
} }
return if (result != null) { return if (result != null) {
@ -63,7 +81,7 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
val contentValues = ContentValues(2) val contentValues = ContentValues(2)
contentValues.put(Companion.messageID, messageID) contentValues.put(Companion.messageID, messageID)
contentValues.put(Companion.friendRequestStatus, friendRequestStatus.rawValue) 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) val threadID = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageID)
notifyConversationListeners(threadID) notifyConversationListeners(threadID)
} }

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import net.sqlcipher.Cursor
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.PreKeyUtil import org.thoughtcrime.securesms.crypto.PreKeyUtil
import org.thoughtcrime.securesms.database.Database 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" + ");" "$signedPreKeySignature TEXT," + "$identityKey TEXT NOT NULL," + "$deviceID INTEGER," + "$registrationID INTEGER" + ");"
} }
fun resetAllPreKeyBundleInfo() {
TextSecurePreferences.removeLocalRegistrationId(context)
TextSecurePreferences.setSignedPreKeyRegistered(context, false)
}
fun generatePreKeyBundle(hexEncodedPublicKey: String): PreKeyBundle? { fun generatePreKeyBundle(hexEncodedPublicKey: String): PreKeyBundle? {
var registrationID = TextSecurePreferences.getLocalRegistrationId(context) var registrationID = TextSecurePreferences.getLocalRegistrationId(context)
if (registrationID == 0) { if (registrationID == 0) {
@ -92,7 +98,14 @@ class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) :
fun hasPreKeyBundle(hexEncodedPublicKey: String): Boolean { fun hasPreKeyBundle(hexEncodedPublicKey: String): Boolean {
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
val cursor = database.query(tableName, null, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey ), null, null, null) var cursor: Cursor? = null
return cursor != null && cursor.count > 0 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()
}
} }
} }

View File

@ -3,28 +3,26 @@ package org.thoughtcrime.securesms.loki
import android.content.Context import android.content.Context
import android.os.Handler import android.os.Handler
import android.util.Log import android.util.Log
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.then
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.jobs.PushDecryptJob 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.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.libsignal.util.guava.Optional import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceContent import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.messages.SignalServiceGroup 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.api.push.SignalServiceAddress
import org.whispersystems.signalservice.loki.api.LokiPublicChat import org.whispersystems.signalservice.loki.api.LokiPublicChat
import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI
import org.whispersystems.signalservice.loki.api.LokiPublicChatMessage import org.whispersystems.signalservice.loki.api.LokiPublicChatMessage
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
import org.whispersystems.signalservice.loki.utilities.successBackground
import java.util.*
class LokiPublicChatPoller(private val context: Context, private val group: LokiPublicChat) { class LokiPublicChatPoller(private val context: Context, private val group: LokiPublicChat) {
private val handler = Handler() private val handler = Handler()
@ -32,6 +30,7 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
// region Convenience // region Convenience
private val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context) private val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
private var displayNameUpdatees = setOf<String>()
private val api: LokiPublicChatAPI private val api: LokiPublicChatAPI
get() = { get() = {
@ -66,6 +65,13 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
handler.postDelayed(this, pollForModeratorsInterval) handler.postDelayed(this, pollForModeratorsInterval)
} }
} }
private val pollForDisplayNamesTask = object : Runnable {
override fun run() {
pollForDisplayNames()
handler.postDelayed(this, pollForDisplayNamesInterval)
}
}
// endregion // endregion
// region Settings // region Settings
@ -73,6 +79,7 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
private val pollForNewMessagesInterval: Long = 4 * 1000 private val pollForNewMessagesInterval: Long = 4 * 1000
private val pollForDeletedMessagesInterval: Long = 20 * 1000 private val pollForDeletedMessagesInterval: Long = 20 * 1000
private val pollForModeratorsInterval: Long = 10 * 60 * 1000 private val pollForModeratorsInterval: Long = 10 * 60 * 1000
private val pollForDisplayNamesInterval: Long = 60 * 1000
} }
// endregion // endregion
@ -82,6 +89,7 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
pollForNewMessagesTask.run() pollForNewMessagesTask.run()
pollForDeletedMessagesTask.run() pollForDeletedMessagesTask.run()
pollForModeratorsTask.run() pollForModeratorsTask.run()
pollForDisplayNamesTask.run()
hasStarted = true hasStarted = true
} }
@ -89,23 +97,23 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
handler.removeCallbacks(pollForNewMessagesTask) handler.removeCallbacks(pollForNewMessagesTask)
handler.removeCallbacks(pollForDeletedMessagesTask) handler.removeCallbacks(pollForDeletedMessagesTask)
handler.removeCallbacks(pollForModeratorsTask) handler.removeCallbacks(pollForModeratorsTask)
handler.removeCallbacks(pollForDisplayNamesTask)
hasStarted = false hasStarted = false
} }
// endregion // endregion
// region Polling // region Polling
private fun pollForNewMessages() { private fun getDataMessage(message: LokiPublicChatMessage): SignalServiceDataMessage {
fun processIncomingMessage(message: LokiPublicChatMessage) { val id = group.id.toByteArray()
val id = group.id.toByteArray() val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, null, null, null)
val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, null, null, null) val quote = if (message.quote != null) {
val quote = if (message.quote != null) { SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteeHexEncodedPublicKey), message.quote!!.quotedMessageBody, listOf())
SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteeHexEncodedPublicKey), message.quote!!.quotedMessageBody, listOf()) } else {
} else { null
null }
} val attachments = message.attachments.mapNotNull { attachment ->
val attachments = message.attachments.mapNotNull { attachment -> if (attachment.kind != LokiPublicChatMessage.Attachment.Kind.Attachment) { return@mapNotNull null }
if (attachment.kind != LokiPublicChatMessage.Attachment.Kind.Attachment) { return@mapNotNull null } SignalServiceAttachmentPointer(
SignalServiceAttachmentPointer(
attachment.serverID, attachment.serverID,
attachment.contentType, attachment.contentType,
ByteArray(0), ByteArray(0),
@ -117,30 +125,40 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
false, false,
Optional.fromNullable(attachment.caption), Optional.fromNullable(attachment.caption),
attachment.url) 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
return SignalServiceDataMessage(message.timestamp, serviceGroup, attachments, body, false, 0, false, null, false, quote, null, signalLinkPreviews, null)
}
private fun pollForNewMessages() {
fun processIncomingMessage(message: LokiPublicChatMessage) {
// If the sender of the current message is not a secondary device, we need to set the display name in the database
val primaryDevice = LokiStorageAPI.shared.getPrimaryDevicePublicKey(message.hexEncodedPublicKey).get()
if (primaryDevice == null) {
val senderDisplayName = "${message.displayName} (...${message.hexEncodedPublicKey.takeLast(8)})"
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.hexEncodedPublicKey, senderDisplayName)
} }
val linkPreview = message.attachments.firstOrNull { it.kind == LokiPublicChatMessage.Attachment.Kind.LinkPreview } val senderPublicKey = primaryDevice ?: message.hexEncodedPublicKey
val signalLinkPreviews = mutableListOf<SignalServiceDataMessage.Preview>() val serviceDataMessage = getDataMessage(message)
if (linkPreview != null) { val serviceContent = SignalServiceContent(serviceDataMessage, senderPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.timestamp, false)
val attachment = SignalServiceAttachmentPointer( if (serviceDataMessage.quote.isPresent || (serviceDataMessage.attachments.isPresent && serviceDataMessage.attachments.get().size > 0) || serviceDataMessage.previews.isPresent) {
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 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) {
PushDecryptJob(context).handleMediaMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID)) PushDecryptJob(context).handleMediaMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID))
} else { } else {
PushDecryptJob(context).handleTextMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID)) PushDecryptJob(context).handleTextMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID))
@ -148,61 +166,51 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
} }
fun processOutgoingMessage(message: LokiPublicChatMessage) { fun processOutgoingMessage(message: LokiPublicChatMessage) {
val messageServerID = message.serverID ?: return val messageServerID = message.serverID ?: return
val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context) val isDuplicate = DatabaseFactory.getLokiMessageDatabase(context).getMessageID(messageServerID) != null
val isDuplicate = lokiMessageDatabase.getMessageID(messageServerID) != null
if (isDuplicate) { return } if (isDuplicate) { return }
if (message.body.isEmpty() && message.attachments.isEmpty() && message.quote == null) { return } if (message.body.isEmpty() && message.attachments.isEmpty() && message.quote == null) { return }
val id = group.id.toByteArray() val localNumber = TextSecurePreferences.getLocalNumber(context)
val mmsDatabase = DatabaseFactory.getMmsDatabase(context) val dataMessage = getDataMessage(message)
val recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(id, false)), false) val transcript = SentTranscriptMessage(localNumber, dataMessage.timestamp, dataMessage, dataMessage.expiresInSeconds.toLong(), Collections.singletonMap(localNumber, false))
val quote: QuoteModel? transcript.messageServerID = messageServerID
if (message.quote != null) { if (dataMessage.quote.isPresent || (dataMessage.attachments.isPresent && dataMessage.attachments.get().size > 0) || dataMessage.previews.isPresent) {
quote = QuoteModel(message.quote!!.quotedMessageTimestamp, Address.fromSerialized(message.quote!!.quoteeHexEncodedPublicKey), message.quote!!.quotedMessageBody, false, listOf()) PushDecryptJob(context).handleSynchronizeSentMediaMessage(transcript)
} else { } else {
quote = null PushDecryptJob(context).handleSynchronizeSentTextMessage(transcript)
}
// 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()
} }
} }
api.getMessages(group.channel, group.server).success { messages -> var userDevices = setOf<String>()
var uniqueDevices = setOf<String>()
LokiStorageAPI.shared.getAllDevicePublicKeys(userHexEncodedPublicKey).bind { devices ->
userDevices = devices
api.getMessages(group.channel, group.server)
}.bind { messages ->
if (messages.isNotEmpty()) {
// We need to fetch device mappings for all the devices we don't have
uniqueDevices = messages.map { it.hexEncodedPublicKey }.toSet()
val devicesToUpdate = uniqueDevices.filter { !userDevices.contains(it) && LokiStorageAPI.shared.hasCacheExpired(it) }
if (devicesToUpdate.isNotEmpty()) {
return@bind LokiStorageAPI.shared.getDeviceMappings(devicesToUpdate.toSet()).then { messages }
}
}
Promise.of(messages)
}.successBackground {
// Get the set of primary device pubKeys FROM the secondary devices in uniqueDevices
val newDisplayNameUpdatees = uniqueDevices.mapNotNull {
// This will return null if current device is primary
// So if it's non-null then we know the device is a secondary device
val primaryDevice = LokiStorageAPI.shared.getPrimaryDevicePublicKey(it).get()
primaryDevice
}.toSet()
// Fetch the display names of the primary devices
displayNameUpdatees = displayNameUpdatees.union(newDisplayNameUpdatees)
}.success { messages ->
// Process messages in the background
messages.forEach { message -> messages.forEach { message ->
if (message.hexEncodedPublicKey != userHexEncodedPublicKey) { if (userDevices.contains(message.hexEncodedPublicKey)) {
processIncomingMessage(message)
} else {
processOutgoingMessage(message) processOutgoingMessage(message)
} else {
processIncomingMessage(message)
} }
} }
}.fail { }.fail {
@ -210,6 +218,20 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
} }
} }
private fun pollForDisplayNames() {
if (displayNameUpdatees.isEmpty()) { return }
val hexEncodedPublicKeys = displayNameUpdatees
displayNameUpdatees = setOf()
api.getDisplayNames(hexEncodedPublicKeys, group.server).successBackground { mapping ->
for (pair in mapping.entries) {
val senderDisplayName = "${pair.value} (...${pair.key.takeLast(8)})"
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, pair.key, senderDisplayName)
}
}.fail {
displayNameUpdatees = displayNameUpdatees.union(hexEncodedPublicKeys)
}
}
private fun pollForDeletedMessages() { private fun pollForDeletedMessages() {
api.getDeletedMessageServerIDs(group.channel, group.server).success { deletedMessageServerIDs -> api.getDeletedMessageServerIDs(group.channel, group.server).success { deletedMessageServerIDs ->
val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context) val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context)

View File

@ -41,6 +41,8 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
} }
fun getFriendRequestStatus(threadID: Long): LokiThreadFriendRequestStatus { fun getFriendRequestStatus(threadID: Long): LokiThreadFriendRequestStatus {
if (threadID < 0) { return LokiThreadFriendRequestStatus.NONE }
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
val result = database.get(friendRequestTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor -> val result = database.get(friendRequestTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor ->
cursor.getInt(friendRequestStatus) cursor.getInt(friendRequestStatus)
@ -53,6 +55,8 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
} }
override fun setFriendRequestStatus(threadID: Long, friendRequestStatus: LokiThreadFriendRequestStatus) { override fun setFriendRequestStatus(threadID: Long, friendRequestStatus: LokiThreadFriendRequestStatus) {
if (threadID < 0) { return }
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2) val contentValues = ContentValues(2)
contentValues.put(Companion.threadID, threadID) contentValues.put(Companion.threadID, threadID)

View File

@ -1,13 +1,19 @@
@file:JvmName("MultiDeviceUtilities")
package org.thoughtcrime.securesms.loki package org.thoughtcrime.securesms.loki
import android.content.Context import android.content.Context
import nl.komponents.kovenant.Promise 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.ApplicationContext
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.logging.Log import org.thoughtcrime.securesms.logging.Log
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.libsignal.util.guava.Optional import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair 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.LokiStorageAPI
import org.whispersystems.signalservice.loki.api.PairingAuthorisation import org.whispersystems.signalservice.loki.api.PairingAuthorisation
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus 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) val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
storageAPI.getAllDevicePublicKeys(hexEncodedPublicKey).success { items -> return LokiStorageAPI.shared.getAllDevicePublicKeys(hexEncodedPublicKey).map { keys ->
val devices = items.toMutableSet() val devices = keys.toMutableSet()
if (hexEncodedPublicKey != userHexEncodedPublicKey) { if (hexEncodedPublicKey != userHexEncodedPublicKey) {
devices.remove(userHexEncodedPublicKey) devices.remove(userHexEncodedPublicKey)
} }
val friends = getFriendPublicKeys(context, devices) val friends = getFriendPublicKeys(context, devices)
val friendMap = mutableMapOf<String, Boolean>()
for (device in devices) { 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> { fun getFriendCount(context: Context, devices: Set<String>): Int {
val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context) return getFriendPublicKeys(context, devices).count()
val storageAPI = LokiStorageAPI.shared }
val deferred = deferred<Boolean, Unit>()
storageAPI.getPrimaryDevicePublicKey(publicKey).success { primaryDevicePublicKey -> fun shouldAutomaticallyBecomeFriendsWithDevice(publicKey: String, context: Context): Promise<Boolean, Exception> {
if (primaryDevicePublicKey == null) { // Don't become friends if we're a group
deferred.resolve(false) if (!Address.fromSerialized(publicKey).isPhone) {
return@success return Promise.of(false)
} }
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
if (primaryDevicePublicKey == userHexEncodedPublicKey) { // If this public key is our primary device then we should become friends
storageAPI.getSecondaryDevicePublicKeys(userHexEncodedPublicKey).success { secondaryDevices -> if (publicKey == TextSecurePreferences.getMasterHexEncodedPublicKey(context)) {
deferred.resolve(secondaryDevices.contains(publicKey)) return Promise.of(true)
}.fail { }
deferred.resolve(false)
} return LokiStorageAPI.shared.getPrimaryDevicePublicKey(publicKey).bind { primaryDevicePublicKey ->
return@success // If the public key doesn't have any other devices then go through regular friend request logic
} if (primaryDevicePublicKey == null) {
val primaryDevice = Recipient.from(context, Address.fromSerialized(primaryDevicePublicKey), false) return@bind Promise.of(false)
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(primaryDevice) }
if (threadID < 0) {
deferred.resolve(false) // If the primary device public key matches our primary device then we should become friends since this is our other device
return@success if (primaryDevicePublicKey == TextSecurePreferences.getMasterHexEncodedPublicKey(context)) {
} return@bind Promise.of(true)
deferred.resolve(lokiThreadDatabase.getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS) }
// 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> { fun sendPairingAuthorisationMessage(context: Context, contactHexEncodedPublicKey: String, authorisation: PairingAuthorisation): Promise<Unit, Exception> {
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender() val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(contactHexEncodedPublicKey) 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. // 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) { if (authorisation.type == PairingAuthorisation.Type.REQUEST) {
val preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.number) 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.") Log.d("Loki", "Failed to send authorisation message to: $contactHexEncodedPublicKey.")
Promise.ofFail(e) 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
}
}

View File

@ -68,8 +68,9 @@ class NewConversationActivity : PassphraseRequiredActionBarActivity(), ScanListe
fun startNewConversationIfPossible(hexEncodedPublicKey: String) { 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() } 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) 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() } // If we try to contact our master device then redirect to note to self
val contact = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), true) 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) val intent = Intent(this, ConversationActivity::class.java)
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, contact.address) intent.putExtra(ConversationActivity.ADDRESS_EXTRA, contact.address)
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA)) intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA))

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.AsyncTask
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
@ -22,7 +23,6 @@ import org.thoughtcrime.securesms.util.Hex
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.curve25519.Curve25519 import org.whispersystems.curve25519.Curve25519
import org.whispersystems.libsignal.util.KeyHelper 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.api.PairingAuthorisation
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
import org.whispersystems.signalservice.loki.utilities.Analytics import org.whispersystems.signalservice.loki.utilities.Analytics
@ -55,8 +55,7 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
copyButton.setOnClickListener { copy() } copyButton.setOnClickListener { copy() }
toggleRegisterModeButton.setOnClickListener { mode = Mode.Register } toggleRegisterModeButton.setOnClickListener { mode = Mode.Register }
toggleRestoreModeButton.setOnClickListener { mode = Mode.Restore } toggleRestoreModeButton.setOnClickListener { mode = Mode.Restore }
// TODO: Enable this again later toggleLinkModeButton.setOnClickListener { mode = Mode.Link }
// toggleLinkModeButton.setOnClickListener { mode = Mode.Link }
mainButton.setOnClickListener { handleMainButtonTapped() } mainButton.setOnClickListener { handleMainButtonTapped() }
Analytics.shared.track("Seed Screen Viewed") Analytics.shared.track("Seed Screen Viewed")
} }
@ -205,8 +204,10 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
application.setUpP2PAPI() application.setUpP2PAPI()
application.setUpStorageAPIIfNeeded() application.setUpStorageAPIIfNeeded()
DeviceLinkingDialog.show(this, DeviceLinkingView.Mode.Slave, this) DeviceLinkingDialog.show(this, DeviceLinkingView.Mode.Slave, this)
retryIfNeeded(8) { AsyncTask.execute {
sendPairingAuthorisationMessage(this@SeedActivity, authorisation.primaryDevicePublicKey, authorisation).get() retryIfNeeded(8) {
sendPairingAuthorisationMessage(this@SeedActivity, authorisation.primaryDevicePublicKey, authorisation)
}
} }
} else { } else {
startActivity(Intent(this, DisplayNameActivity::class.java)) startActivity(Intent(this, DisplayNameActivity::class.java))
@ -227,25 +228,9 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
resetForRegistration() 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() { private fun resetForRegistration() {
IdentityKeyUtil.delete(this, IdentityKeyUtil.lokiSeedKey) IdentityKeyUtil.delete(this, IdentityKeyUtil.lokiSeedKey)
TextSecurePreferences.removeLocalRegistrationId(this) DatabaseFactory.getLokiPreKeyBundleDatabase(this).resetAllPreKeyBundleInfo()
TextSecurePreferences.removeLocalNumber(this) TextSecurePreferences.removeLocalNumber(this)
TextSecurePreferences.setHasSeenWelcomeScreen(this, false) TextSecurePreferences.setHasSeenWelcomeScreen(this, false)
TextSecurePreferences.setPromptedPushRegistration(this, false) TextSecurePreferences.setPromptedPushRegistration(this, false)

View File

@ -410,6 +410,7 @@ public class AttachmentManager {
} }
public static void selectLocation(Activity activity, int requestCode) { public static void selectLocation(Activity activity, int requestCode) {
/* Loki - Enable again once we have location sharing
Permissions.with(activity) Permissions.with(activity)
.request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) .request(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
.ifNecessary() .ifNecessary()
@ -422,6 +423,7 @@ public class AttachmentManager {
} }
}) })
.execute(); .execute();
*/
} }
public static void selectGif(Activity activity, int requestCode, boolean isForMms) { public static void selectGif(Activity activity, int requestCode, boolean isForMms) {

View File

@ -20,8 +20,9 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob; import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
import org.thoughtcrime.securesms.logging.Log; 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.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI; import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import java.util.LinkedList; import java.util.LinkedList;
@ -29,6 +30,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import kotlin.Unit; import kotlin.Unit;
import kotlin.contracts.Returns;
public class MarkReadReceiver extends BroadcastReceiver { public class MarkReadReceiver extends BroadcastReceiver {
@ -76,7 +78,9 @@ public class MarkReadReceiver extends BroadcastReceiver {
for (MarkedMessageInfo messageInfo : markedReadMessages) { for (MarkedMessageInfo messageInfo : markedReadMessages) {
scheduleDeletion(context, messageInfo.getExpirationInfo()); scheduleDeletion(context, messageInfo.getExpirationInfo());
syncMessageIds.add(messageInfo.getSyncMessageId()); if (!messageInfo.getSyncMessageId().getAddress().isGroup()) {
syncMessageIds.add(messageInfo.getSyncMessageId());
}
} }
ApplicationContext.getInstance(context) ApplicationContext.getInstance(context)
@ -88,14 +92,15 @@ public class MarkReadReceiver extends BroadcastReceiver {
.collect(Collectors.groupingBy(SyncMessageId::getAddress)); .collect(Collectors.groupingBy(SyncMessageId::getAddress));
for (Address address : addressMap.keySet()) { for (Address address : addressMap.keySet()) {
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
List<Long> timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList(); List<Long> timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList();
MultiDeviceUtilities.getAllDevicePublicKeysWithFriendStatus(context, address.serialize()).success(devices -> {
MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, address.serialize(), storageAPI, (devicePublicKey, isFriend, friendCount) -> { for (Map.Entry<String, Boolean> entry : devices.entrySet()) {
// Loki - This also prevents read receipts from being sent in group chats as they don't maintain a friend request status String device = entry.getKey();
if (isFriend) { boolean isFriend = entry.getValue();
ApplicationContext.getInstance(context).getJobManager().add(new SendReadReceiptJob(Address.fromSerialized(devicePublicKey), timestamps)); // 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; return Unit.INSTANCE;
}); });

View File

@ -30,6 +30,7 @@ public class ProfilePreference extends Preference {
private ImageView avatarView; private ImageView avatarView;
private TextView profileNameView; private TextView profileNameView;
private TextView profileNumberView; private TextView profileNumberView;
private TextView profileTagView;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public ProfilePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 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); avatarView = (ImageView)viewHolder.findViewById(R.id.avatar);
profileNameView = (TextView)viewHolder.findViewById(R.id.profile_name); profileNameView = (TextView)viewHolder.findViewById(R.id.profile_name);
profileNumberView = (TextView)viewHolder.findViewById(R.id.number); profileNumberView = (TextView)viewHolder.findViewById(R.id.number);
profileTagView = (TextView)viewHolder.findViewById(R.id.tag);
refresh(); refresh();
} }
@ -72,13 +74,15 @@ public class ProfilePreference extends Preference {
if (profileNumberView == null) return; if (profileNumberView == null) return;
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext()); 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()); final String profileName = TextSecurePreferences.getProfileName(getContext());
Context context = getContext(); Context context = getContext();
containerView.setOnLongClickListener(v -> { containerView.setOnLongClickListener(v -> {
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); 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); clipboard.setPrimaryClip(clip);
Toast.makeText(context, R.string.activity_settings_public_key_copied_message, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.activity_settings_public_key_copied_message, Toast.LENGTH_SHORT).show();
return true; return true;
@ -100,7 +104,7 @@ public class ProfilePreference extends Preference {
int height = avatarView.getHeight(); int height = avatarView.getHeight();
if (width == 0 || height == 0) return true; if (width == 0 || height == 0) return true;
avatarView.getViewTreeObserver().removeOnPreDrawListener(this); avatarView.getViewTreeObserver().removeOnPreDrawListener(this);
JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, userHexEncodedPublicKey.toLowerCase()); JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, publicKey.toLowerCase());
avatarView.setImageDrawable(identicon); avatarView.setImageDrawable(identicon);
return true; return true;
} }
@ -120,7 +124,9 @@ public class ProfilePreference extends Preference {
} }
profileNameView.setVisibility(TextUtils.isEmpty(profileName) ? View.GONE : View.VISIBLE); profileNameView.setVisibility(TextUtils.isEmpty(profileName) ? View.GONE : View.VISIBLE);
profileNumberView.setText(localAddress.toPhoneString()); profileNumberView.setText(localAddress.toPhoneString());
profileTagView.setVisibility(primaryDevicePublicKey == null ? View.GONE : View.VISIBLE);
profileTagView.setText(R.string.activity_settings_secondary_device_tag);
} }
} }

View File

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

View File

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

View File

@ -121,6 +121,8 @@ public class SearchRepository {
} }
private CursorList<Recipient> queryContacts(String query) { private CursorList<Recipient> queryContacts(String query) {
return CursorList.emptyList();
/* Loki - Don't need contact permissions
if (!Permissions.hasAny(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) { if (!Permissions.hasAny(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
return CursorList.emptyList(); return CursorList.emptyList();
} }
@ -130,6 +132,7 @@ public class SearchRepository {
MergeCursor contacts = new MergeCursor(new Cursor[]{ textSecureContacts, systemContacts }); MergeCursor contacts = new MergeCursor(new Cursor[]{ textSecureContacts, systemContacts });
return new CursorList<>(contacts, new RecipientModelBuilder(context)); return new CursorList<>(contacts, new RecipientModelBuilder(context));
*/
} }
private CursorList<ThreadRecord> queryConversations(@NonNull String query) { private CursorList<ThreadRecord> queryConversations(@NonNull String query) {

View File

@ -380,9 +380,11 @@ public class WebRtcCallService extends Service implements InjectableType,
try { try {
boolean isSystemContact = false; boolean isSystemContact = false;
/*
if (Permissions.hasAny(WebRtcCallService.this, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) { if (Permissions.hasAny(WebRtcCallService.this, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
isSystemContact = ContactAccessor.getInstance().isSystemContact(WebRtcCallService.this, recipient.getAddress().serialize()); isSystemContact = ContactAccessor.getInstance().isSystemContact(WebRtcCallService.this, recipient.getAddress().serialize());
} }
*/
boolean isAlwaysTurn = TextSecurePreferences.isTurnOnly(WebRtcCallService.this); boolean isAlwaysTurn = TextSecurePreferences.isTurnOnly(WebRtcCallService.this);

View File

@ -33,8 +33,10 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.MmsSendJob; import org.thoughtcrime.securesms.jobs.MmsSendJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PushGroupSendJob; import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
import org.thoughtcrime.securesms.jobs.PushMediaSendJob; import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
import org.thoughtcrime.securesms.jobs.PushTextSendJob; 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.LinkPreviewRepository;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.FriendRequestHandler;
import org.thoughtcrime.securesms.loki.GeneralUtilitiesKt; 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.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.push.AccountManagerFactory; 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.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI; 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.io.IOException;
import java.util.function.Function; import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import kotlin.Unit; import kotlin.Unit;
@ -66,6 +74,63 @@ public class MessageSender {
private static final String TAG = MessageSender.class.getSimpleName(); 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, public static long send(final Context context,
final OutgoingTextMessage message, final OutgoingTextMessage message,
final long threadId, 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 // Loki - Set the message's friend request status as soon as it has hit the database
if (message.isFriendRequest) { 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); sendTextMessage(context, recipient, forceSms, keyExchange, messageId);
@ -124,7 +189,7 @@ public class MessageSender {
long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener); long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
// Loki - Set the message's friend request status as soon as it has hit the database // Loki - Set the message's friend request status as soon as it has hit the database
if (message.isFriendRequest) { 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()); sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn());
} catch (Exception e) { } catch (Exception e) {
@ -137,7 +202,7 @@ public class MessageSender {
long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener); long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
// Loki - Set the message's friend request status as soon as it has hit the database // Loki - Set the message's friend request status as soon as it has hit the database
if (message.isFriendRequest) { 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()); sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn());
} catch (MmsException e) { } catch (MmsException e) {
@ -149,6 +214,28 @@ public class MessageSender {
return allocatedThreadId; 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) { public static void resendGroupMessage(Context context, MessageRecord messageRecord, Address filterAddress) {
if (!messageRecord.isMms()) throw new AssertionError("Not Group"); if (!messageRecord.isMms()) throw new AssertionError("Not Group");
sendGroupPush(context, messageRecord.getRecipient(), messageRecord.getId(), filterAddress); sendGroupPush(context, messageRecord.getRecipient(), messageRecord.getId(), filterAddress);
@ -195,64 +282,73 @@ public class MessageSender {
} }
private static void sendTextPush(Context context, Recipient recipient, long messageId) { private static void sendTextPush(Context context, Recipient recipient, long messageId) {
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared(); sendMessagePush(context, MessageType.TEXT, recipient, messageId);
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;
});
} }
private static void sendMediaPush(Context context, Recipient recipient, long 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(); 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(); String recipientPublicKey = recipient.getAddress().serialize();
if (GeneralUtilitiesKt.isPublicChat(context, recipientPublicKey)) { if (GeneralUtilitiesKt.isPublicChat(context, recipientPublicKey) || PromiseUtil.get(MultiDeviceUtilities.isOneOfOurDevices(context, recipient.getAddress()), false)) {
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress()); if (type == MessageType.MEDIA) {
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress(), false);
} else {
jobManager.add(new PushTextSendJob(messageId, recipient.getAddress()));
}
return; return;
} }
MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, recipientPublicKey, storageAPI, (devicePublicKey, isFriend, friendCount) -> { // If we get here then we are sending a message to a device that is not ours
Address address = Address.fromSerialized(devicePublicKey); boolean[] hasSentSyncMessage = { false };
long messageIDToUse = recipientPublicKey.equals(devicePublicKey) ? messageId : -1L; 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) { Address address = Address.fromSerialized(devicePublicKey);
// Send a normal message if the user is friends with the recipient long messageIDToUse = recipientPublicKey.equals(devicePublicKey) ? messageId : -1L;
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);
}
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; return Unit.INSTANCE;
}); });
} }
private static void sendGroupPush(Context context, Recipient recipient, long messageId, Address filterAddress) { private static void sendGroupPush(Context context, Recipient recipient, long messageId, Address filterAddress) {

View File

@ -640,7 +640,7 @@ public class TextSecurePreferences {
} }
public static void setLocalNumber(Context context, String localNumber) { 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) { public static void removeLocalNumber(Context context) {
@ -1183,7 +1183,7 @@ public class TextSecurePreferences {
} }
public static void setMasterHexEncodedPublicKey(Context context, String masterHexEncodedPublicKey) { 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 // endregion
} }