mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-24 02:25:19 +00:00
Merge branch 'dev' into disable-permissions
This commit is contained in:
commit
cf8ed65f4e
@ -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" />
|
||||||
|
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
@ -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"
|
||||||
|
@ -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() {
|
||||||
|
@ -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,12 @@ 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.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 +56,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 +165,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 +187,19 @@ 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
|
// Hide if this is a slave device
|
||||||
linkDevicePreference.setVisible(isMasterDevice);
|
linkDevicePreference.setVisible(isMasterDevice);
|
||||||
linkDevicePreference.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_LINK_DEVICE));
|
linkDevicePreference.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_LINK_DEVICE));
|
||||||
*/
|
|
||||||
Preference seedPreference = this.findPreference(PREFERENCE_CATEGORY_SEED);
|
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 +292,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 +350,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 +393,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 {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
@ -195,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() {
|
||||||
|
|
||||||
@ -204,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;
|
||||||
}
|
}
|
||||||
|
@ -107,8 +107,9 @@ public class AvatarImageView extends AppCompatImageView {
|
|||||||
if (w == 0 || h == 0 || recipient == null) { return; }
|
if (w == 0 || h == 0 || recipient == null) { return; }
|
||||||
|
|
||||||
Drawable image;
|
Drawable image;
|
||||||
|
Context context = this.getContext();
|
||||||
if (recipient.isGroupRecipient()) {
|
if (recipient.isGroupRecipient()) {
|
||||||
Context context = this.getContext();
|
|
||||||
|
|
||||||
String name = Optional.fromNullable(recipient.getName()).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context))).or("");
|
String name = Optional.fromNullable(recipient.getName()).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context))).or("");
|
||||||
MaterialColor fallbackColor = recipient.getColor();
|
MaterialColor fallbackColor = recipient.getColor();
|
||||||
@ -119,7 +120,12 @@ public class AvatarImageView extends AppCompatImageView {
|
|||||||
|
|
||||||
image = new GeneratedContactPhoto(name, R.drawable.ic_profile_default).asDrawable(context, fallbackColor.toAvatarColor(context));
|
image = new GeneratedContactPhoto(name, R.drawable.ic_profile_default).asDrawable(context, fallbackColor.toAvatarColor(context));
|
||||||
} else {
|
} else {
|
||||||
image = new JazzIdenticonDrawable(w, h, recipient.getAddress().serialize().toLowerCase());
|
// Default to primary device image
|
||||||
|
String ourPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||||
|
String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
|
||||||
|
String recipientAddress = recipient.getAddress().serialize();
|
||||||
|
String profileAddress = (ourPrimaryDevice != null && ourPublicKey.equals(recipientAddress)) ? ourPrimaryDevice : recipientAddress;
|
||||||
|
image = new JazzIdenticonDrawable(w, h, profileAddress.toLowerCase());
|
||||||
}
|
}
|
||||||
setImageDrawable(image);
|
setImageDrawable(image);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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),
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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());
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,12 +186,6 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
|
|||||||
private void generateFullContactUpdate()
|
private void generateFullContactUpdate()
|
||||||
throws IOException, UntrustedIdentityException, NetworkException
|
throws IOException, UntrustedIdentityException, NetworkException
|
||||||
{
|
{
|
||||||
/* Loki - Disabled
|
|
||||||
if (!Permissions.hasAny(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
|
|
||||||
Log.w(TAG, "No contact permissions, skipping multi-device contact update...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isAppVisible = ApplicationContext.getInstance(context).isAppVisible();
|
boolean isAppVisible = ApplicationContext.getInstance(context).isAppVisible();
|
||||||
long timeSinceLastSync = System.currentTimeMillis() - TextSecurePreferences.getLastFullContactSyncTime(context);
|
long timeSinceLastSync = System.currentTimeMillis() - TextSecurePreferences.getLastFullContactSyncTime(context);
|
||||||
|
|
||||||
@ -181,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));
|
||||||
@ -196,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)) {
|
||||||
@ -215,12 +240,31 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
|
|||||||
} finally {
|
} finally {
|
||||||
if (contactDataFile != null) contactDataFile.delete();
|
if (contactDataFile != null) contactDataFile.delete();
|
||||||
}
|
}
|
||||||
*/
|
}
|
||||||
|
|
||||||
|
private List<ContactData> getAllContacts() {
|
||||||
|
List<Address> contactAddresses = DatabaseFactory.getRecipientDatabase(context).getRegistered();
|
||||||
|
List<ContactData> contacts = new ArrayList<>(contactAddresses.size());
|
||||||
|
for (Address address : contactAddresses) {
|
||||||
|
if (!address.isPhone()) { continue; }
|
||||||
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, address, false));
|
||||||
|
String name = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(address.serialize());
|
||||||
|
ContactData contactData = new ContactData(threadId, name);
|
||||||
|
contactData.numbers.add(new ContactAccessor.NumberData("TextSecure", address.serialize()));
|
||||||
|
contacts.add(contactData);
|
||||||
|
}
|
||||||
|
return contacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LokiThreadFriendRequestStatus getFriendRequestStatus(Recipient recipient) {
|
||||||
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient);
|
||||||
|
return DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,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);
|
||||||
}
|
}
|
||||||
@ -251,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();
|
||||||
}
|
}
|
||||||
@ -304,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 {
|
||||||
@ -344,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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
100
src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt
Normal file
100
src/org/thoughtcrime/securesms/loki/FriendRequestHandler.kt
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package org.thoughtcrime.securesms.loki
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import nl.komponents.kovenant.ui.successUi
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
|
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus
|
||||||
|
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
|
||||||
|
|
||||||
|
object FriendRequestHandler {
|
||||||
|
enum class ActionType { Sending, Sent, Failed }
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun updateFriendRequestState(context: Context, type: ActionType, messageId: Long, threadId: Long) {
|
||||||
|
if (threadId < 0) return
|
||||||
|
val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId) ?: return
|
||||||
|
if (!recipient.address.isPhone) { return }
|
||||||
|
|
||||||
|
val currentFriendStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId)
|
||||||
|
// Update thread status if we haven't sent a friend request before
|
||||||
|
if (currentFriendStatus != LokiThreadFriendRequestStatus.REQUEST_RECEIVED &&
|
||||||
|
currentFriendStatus != LokiThreadFriendRequestStatus.REQUEST_SENT &&
|
||||||
|
currentFriendStatus != LokiThreadFriendRequestStatus.FRIENDS
|
||||||
|
) {
|
||||||
|
val threadFriendStatus = when (type) {
|
||||||
|
ActionType.Sending -> LokiThreadFriendRequestStatus.REQUEST_SENDING
|
||||||
|
ActionType.Failed -> LokiThreadFriendRequestStatus.NONE
|
||||||
|
ActionType.Sent -> LokiThreadFriendRequestStatus.REQUEST_SENT
|
||||||
|
}
|
||||||
|
DatabaseFactory.getLokiThreadDatabase(context).setFriendRequestStatus(threadId, threadFriendStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update message status
|
||||||
|
if (messageId >= 0) {
|
||||||
|
val messageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
|
||||||
|
val friendRequestStatus = messageDatabase.getFriendRequestStatus(messageId)
|
||||||
|
if (type == ActionType.Sending) {
|
||||||
|
// We only want to update message status if we aren't friends with another of their devices
|
||||||
|
// This avoids spam in the ui where it would keep telling the user that they sent a friend request on every single message
|
||||||
|
isFriendsWithAnyLinkedDevice(context, recipient).successUi { isFriends ->
|
||||||
|
if (!isFriends && friendRequestStatus == LokiMessageFriendRequestStatus.NONE) {
|
||||||
|
messageDatabase.setFriendRequestStatus(messageId, LokiMessageFriendRequestStatus.REQUEST_SENDING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (friendRequestStatus != LokiMessageFriendRequestStatus.NONE) {
|
||||||
|
// Update the friend request status of the message if we have it
|
||||||
|
val messageFriendRequestStatus = when (type) {
|
||||||
|
ActionType.Failed -> LokiMessageFriendRequestStatus.REQUEST_FAILED
|
||||||
|
ActionType.Sent -> LokiMessageFriendRequestStatus.REQUEST_PENDING
|
||||||
|
else -> throw IllegalStateException()
|
||||||
|
}
|
||||||
|
messageDatabase.setFriendRequestStatus(messageId, messageFriendRequestStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun updateLastFriendRequestMessage(context: Context, threadId: Long, status: LokiMessageFriendRequestStatus) {
|
||||||
|
if (threadId < 0) { return }
|
||||||
|
|
||||||
|
val messages = DatabaseFactory.getSmsDatabase(context).getAllMessageIDs(threadId)
|
||||||
|
val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
|
||||||
|
val lastMessage = messages.find {
|
||||||
|
val friendRequestStatus = lokiMessageDatabase.getFriendRequestStatus(it)
|
||||||
|
friendRequestStatus == LokiMessageFriendRequestStatus.REQUEST_PENDING
|
||||||
|
} ?: return
|
||||||
|
|
||||||
|
DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(lastMessage, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun receivedIncomingFriendRequestMessage(context: Context, threadId: Long) {
|
||||||
|
val smsMessageDatabase = DatabaseFactory.getSmsDatabase(context)
|
||||||
|
|
||||||
|
// We only want to update the last message status if we're not friends with any of their linked devices
|
||||||
|
// This ensures that we don't spam the UI with accept/decline messages
|
||||||
|
val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId) ?: return
|
||||||
|
if (!recipient.address.isPhone) { return }
|
||||||
|
|
||||||
|
isFriendsWithAnyLinkedDevice(context, recipient).successUi { isFriends ->
|
||||||
|
if (isFriends) { return@successUi }
|
||||||
|
|
||||||
|
// Since messages are forwarded to the primary device thread, we need to update it there
|
||||||
|
val messageCount = smsMessageDatabase.getMessageCountForThread(threadId)
|
||||||
|
val messageID = smsMessageDatabase.getIDForMessageAtIndex(threadId, messageCount - 1) // The message that was just received
|
||||||
|
if (messageID < 0) { return@successUi }
|
||||||
|
|
||||||
|
val messageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
|
||||||
|
|
||||||
|
// We need to go through and set all messages which are REQUEST_PENDING to NONE
|
||||||
|
smsMessageDatabase.getAllMessageIDs(threadId)
|
||||||
|
.filter { messageDatabase.getFriendRequestStatus(it) == LokiMessageFriendRequestStatus.REQUEST_PENDING }
|
||||||
|
.forEach {
|
||||||
|
messageDatabase.setFriendRequestStatus(it, LokiMessageFriendRequestStatus.NONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the last message to pending
|
||||||
|
messageDatabase.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_PENDING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,11 +12,14 @@ import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestS
|
|||||||
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
|
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)
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,27 +4,23 @@ import android.content.Context
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
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.get
|
||||||
|
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()
|
||||||
@ -94,18 +90,17 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
|
|||||||
// 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 +112,35 @@ 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 linkPreview = message.attachments.firstOrNull { it.kind == LokiPublicChatMessage.Attachment.Kind.LinkPreview }
|
||||||
val signalLinkPreviews = mutableListOf<SignalServiceDataMessage.Preview>()
|
val signalLinkPreviews = mutableListOf<SignalServiceDataMessage.Preview>()
|
||||||
if (linkPreview != null) {
|
if (linkPreview != null) {
|
||||||
val attachment = SignalServiceAttachmentPointer(
|
val attachment = SignalServiceAttachmentPointer(
|
||||||
linkPreview.serverID,
|
linkPreview.serverID,
|
||||||
linkPreview.contentType,
|
linkPreview.contentType,
|
||||||
ByteArray(0),
|
ByteArray(0),
|
||||||
Optional.of(linkPreview.size),
|
Optional.of(linkPreview.size),
|
||||||
Optional.absent(),
|
Optional.absent(),
|
||||||
linkPreview.width, linkPreview.height,
|
linkPreview.width, linkPreview.height,
|
||||||
Optional.absent(),
|
Optional.absent(),
|
||||||
Optional.of(linkPreview.fileName),
|
Optional.of(linkPreview.fileName),
|
||||||
false,
|
false,
|
||||||
Optional.fromNullable(linkPreview.caption),
|
Optional.fromNullable(linkPreview.caption),
|
||||||
linkPreview.url)
|
linkPreview.url)
|
||||||
signalLinkPreviews.add(SignalServiceDataMessage.Preview(linkPreview.linkPreviewURL!!, linkPreview.linkPreviewTitle!!, Optional.of(attachment)))
|
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 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)
|
return SignalServiceDataMessage(message.timestamp, serviceGroup, attachments, body, false, 0, false, null, false, quote, null, signalLinkPreviews, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pollForNewMessages() {
|
||||||
|
fun processIncomingMessage(message: LokiPublicChatMessage) {
|
||||||
|
val serviceDataMessage = getDataMessage(message)
|
||||||
val serviceContent = SignalServiceContent(serviceDataMessage, message.hexEncodedPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.timestamp, false)
|
val serviceContent = SignalServiceContent(serviceDataMessage, message.hexEncodedPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.timestamp, false)
|
||||||
val senderDisplayName = "${message.displayName} (...${message.hexEncodedPublicKey.takeLast(8)})"
|
val senderDisplayName = "${message.displayName} (...${message.hexEncodedPublicKey.takeLast(8)})"
|
||||||
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.hexEncodedPublicKey, senderDisplayName)
|
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.hexEncodedPublicKey, senderDisplayName)
|
||||||
if (quote != null || attachments.count() > 0 || linkPreview != null) {
|
if (serviceDataMessage.quote.isPresent || (serviceDataMessage.attachments.isPresent && serviceDataMessage.attachments.get().size > 0) || serviceDataMessage.previews.isPresent) {
|
||||||
PushDecryptJob(context).handleMediaMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID))
|
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 +148,29 @@ 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 ->
|
api.getMessages(group.channel, group.server).successBackground { messages ->
|
||||||
messages.forEach { message ->
|
if (messages.isNotEmpty()) {
|
||||||
if (message.hexEncodedPublicKey != userHexEncodedPublicKey) {
|
val ourDevices = LokiStorageAPI.shared.getAllDevicePublicKeys(userHexEncodedPublicKey).get(setOf())
|
||||||
processIncomingMessage(message)
|
// Process messages in the background
|
||||||
} else {
|
messages.forEach { message ->
|
||||||
processOutgoingMessage(message)
|
if (ourDevices.contains(message.hexEncodedPublicKey)) {
|
||||||
|
processOutgoingMessage(message)
|
||||||
|
} else {
|
||||||
|
processIncomingMessage(message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.fail {
|
}.fail {
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -0,0 +1,94 @@
|
|||||||
|
package org.thoughtcrime.securesms.loki
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Data
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||||
|
import org.thoughtcrime.securesms.jobs.BaseJob
|
||||||
|
import org.thoughtcrime.securesms.logging.Log
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional
|
||||||
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class PushBackgroundMessageSendJob private constructor(
|
||||||
|
parameters: Parameters,
|
||||||
|
private val recipient: String,
|
||||||
|
private val messageBody: String?,
|
||||||
|
private val friendRequest: Boolean
|
||||||
|
) : BaseJob(parameters) {
|
||||||
|
companion object {
|
||||||
|
const val KEY = "PushBackgroundMessageSendJob"
|
||||||
|
|
||||||
|
private val TAG = PushBackgroundMessageSendJob::class.java.simpleName
|
||||||
|
|
||||||
|
private val KEY_RECIPIENT = "recipient"
|
||||||
|
private val KEY_MESSAGE_BODY = "message_body"
|
||||||
|
private val KEY_FRIEND_REQUEST = "asFriendRequest"
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(recipient: String): this(recipient, null, false)
|
||||||
|
constructor(recipient: String, messageBody: String?, friendRequest: Boolean) : this(Parameters.Builder()
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setQueue(KEY)
|
||||||
|
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||||
|
.setMaxAttempts(1)
|
||||||
|
.build(),
|
||||||
|
recipient, messageBody, friendRequest)
|
||||||
|
|
||||||
|
override fun serialize(): Data {
|
||||||
|
return Data.Builder()
|
||||||
|
.putString(KEY_RECIPIENT, recipient)
|
||||||
|
.putString(KEY_MESSAGE_BODY, messageBody)
|
||||||
|
.putBoolean(KEY_FRIEND_REQUEST, friendRequest)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFactoryKey(): String {
|
||||||
|
return KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun onRun() {
|
||||||
|
val message = SignalServiceDataMessage.newBuilder()
|
||||||
|
.withTimestamp(System.currentTimeMillis())
|
||||||
|
.withBody(messageBody)
|
||||||
|
|
||||||
|
if (friendRequest) {
|
||||||
|
val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(recipient)
|
||||||
|
message.withPreKeyBundle(bundle)
|
||||||
|
.asFriendRequest(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||||
|
val address = SignalServiceAddress(recipient)
|
||||||
|
try {
|
||||||
|
messageSender.sendMessage(-1, address, Optional.absent<UnidentifiedAccessPair>(), message.build()) // The message ID doesn't matter
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d("Loki", "Failed to send background message to: $recipient.")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun onShouldRetry(e: Exception): Boolean {
|
||||||
|
// Loki - Disable since we have our own retrying when sending messages
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCanceled() {}
|
||||||
|
|
||||||
|
class Factory : Job.Factory<PushBackgroundMessageSendJob> {
|
||||||
|
override fun create(parameters: Parameters, data: Data): PushBackgroundMessageSendJob {
|
||||||
|
try {
|
||||||
|
val recipient = data.getString(KEY_RECIPIENT)
|
||||||
|
val messageBody = if (data.hasString(KEY_MESSAGE_BODY)) data.getString(KEY_MESSAGE_BODY) else null
|
||||||
|
val friendRequest = data.getBooleanOrDefault(KEY_FRIEND_REQUEST, false)
|
||||||
|
return PushBackgroundMessageSendJob(parameters, recipient, messageBody, friendRequest)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw AssertionError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
package org.thoughtcrime.securesms.loki
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.database.Address
|
||||||
|
import org.thoughtcrime.securesms.dependencies.InjectableType
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Data
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||||
|
import org.thoughtcrime.securesms.jobs.BaseJob
|
||||||
|
import org.whispersystems.signalservice.api.SignalServiceMessageSender
|
||||||
|
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class PushMessageSyncSendJob private constructor(
|
||||||
|
parameters: Parameters,
|
||||||
|
private val messageID: Long,
|
||||||
|
private val recipient: Address,
|
||||||
|
private val timestamp: Long,
|
||||||
|
private val message: ByteArray,
|
||||||
|
private val ttl: Int
|
||||||
|
) : BaseJob(parameters), InjectableType {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KEY = "PushMessageSyncSendJob"
|
||||||
|
|
||||||
|
private val TAG = PushMessageSyncSendJob::class.java.simpleName
|
||||||
|
|
||||||
|
private val KEY_MESSAGE_ID = "message_id"
|
||||||
|
private val KEY_RECIPIENT = "recipient"
|
||||||
|
private val KEY_TIMESTAMP = "timestamp"
|
||||||
|
private val KEY_MESSAGE = "message"
|
||||||
|
private val KEY_TTL = "ttl"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var messageSender: SignalServiceMessageSender
|
||||||
|
|
||||||
|
constructor(messageID: Long, recipient: Address, timestamp: Long, message: ByteArray, ttl: Int) : this(Parameters.Builder()
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setQueue(KEY)
|
||||||
|
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||||
|
.setMaxAttempts(1)
|
||||||
|
.build(),
|
||||||
|
messageID, recipient, timestamp, message, ttl)
|
||||||
|
|
||||||
|
override fun serialize(): Data {
|
||||||
|
return Data.Builder()
|
||||||
|
.putLong(KEY_MESSAGE_ID, messageID)
|
||||||
|
.putString(KEY_RECIPIENT, recipient.serialize())
|
||||||
|
.putLong(KEY_TIMESTAMP, timestamp)
|
||||||
|
.putByteArray(KEY_MESSAGE, message)
|
||||||
|
.putInt(KEY_TTL, ttl)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFactoryKey(): String {
|
||||||
|
return KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class, UntrustedIdentityException::class)
|
||||||
|
public override fun onRun() {
|
||||||
|
// Don't send sync messages to a group
|
||||||
|
if (recipient.isGroup || recipient.isEmail) { return }
|
||||||
|
messageSender.lokiSendSyncMessage(messageID, SignalServiceAddress(recipient.toPhoneString()), timestamp, message, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun onShouldRetry(e: Exception): Boolean {
|
||||||
|
// Loki - Disable since we have our own retrying when sending messages
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCanceled() {}
|
||||||
|
|
||||||
|
class Factory : Job.Factory<PushMessageSyncSendJob> {
|
||||||
|
override fun create(parameters: Parameters, data: Data): PushMessageSyncSendJob {
|
||||||
|
try {
|
||||||
|
return PushMessageSyncSendJob(parameters,
|
||||||
|
data.getLong(KEY_MESSAGE_ID),
|
||||||
|
Address.fromSerialized(data.getString(KEY_RECIPIENT)),
|
||||||
|
data.getLong(KEY_TIMESTAMP),
|
||||||
|
data.getByteArray(KEY_MESSAGE),
|
||||||
|
data.getInt(KEY_TTL))
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw AssertionError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import android.content.ClipData
|
|||||||
import android.content.ClipboardManager
|
import android.content.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)
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
package org.thoughtcrime.securesms.push;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||||
|
import org.thoughtcrime.securesms.loki.FriendRequestHandler;
|
||||||
|
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||||
|
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
|
public class MessageSenderEventListener implements SignalServiceMessageSender.EventListener {
|
||||||
|
|
||||||
|
private static final String TAG = MessageSenderEventListener.class.getSimpleName();
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
public MessageSenderEventListener(Context context) {
|
||||||
|
this.context = context.getApplicationContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSecurityEvent(SignalServiceAddress textSecureAddress) {
|
||||||
|
SecurityEvent.broadcastSecurityUpdateEvent(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSyncEvent(long messageID, long timestamp, byte[] message, int ttl) {
|
||||||
|
if (messageID >= 0 && timestamp > 0 && message != null && ttl > 0) {
|
||||||
|
MessageSender.sendSyncMessageToOurDevices(context, messageID, timestamp, message, ttl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void onFriendRequestSending(long messageID, long threadID) {
|
||||||
|
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, threadID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void onFriendRequestSent(long messageID, long threadID) {
|
||||||
|
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sent, messageID, threadID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void onFriendRequestSendingFail(long messageID, long threadID) {
|
||||||
|
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Failed, messageID, threadID);
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.push;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
|
||||||
import org.thoughtcrime.securesms.database.Address;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
|
||||||
|
|
||||||
public class SecurityEventListener implements SignalServiceMessageSender.EventListener {
|
|
||||||
|
|
||||||
private static final String TAG = SecurityEventListener.class.getSimpleName();
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
public SecurityEventListener(Context context) {
|
|
||||||
this.context = context.getApplicationContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSecurityEvent(SignalServiceAddress textSecureAddress) {
|
|
||||||
SecurityEvent.broadcastSecurityUpdateEvent(context);
|
|
||||||
}
|
|
||||||
}
|
|
@ -33,8 +33,10 @@ import org.thoughtcrime.securesms.database.SmsDatabase;
|
|||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.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) {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user