mirror of
https://github.com/oxen-io/session-android.git
synced 2025-01-12 06:23:40 +00:00
Merge pull request #31 from loki-project/multi-device
Multi Device Support
This commit is contained in:
commit
7116f2502a
@ -20,7 +20,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="40dp"
|
||||
android:text="@string/activity_account_details_title"
|
||||
android:text="@string/activity_display_name_title"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<TextView
|
||||
@ -29,7 +29,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/activity_account_details_subtitle"
|
||||
android:text="@string/activity_display_name_subtitle"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.LabeledEditText
|
||||
@ -38,7 +38,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
app:labeledEditText_background="@color/loki_darkest_gray"
|
||||
app:labeledEditText_label="@string/activity_account_details_name_edit_text_label"/>
|
||||
app:labeledEditText_label="@string/activity_display_name_name_edit_text_label"/>
|
||||
|
||||
<com.dd.CircularProgressButton
|
||||
android:id="@+id/nextButton"
|
||||
@ -53,7 +53,7 @@
|
||||
app:cpb_colorProgress="@color/textsecure_primary"
|
||||
app:cpb_cornerRadius="4dp"
|
||||
app:cpb_selectorIdle="@drawable/progress_button_state"
|
||||
app:cpb_textIdle="@string/activity_account_details_button_title" />
|
||||
app:cpb_textIdle="@string/activity_display_name_button_title" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -76,8 +76,28 @@
|
||||
app:labeledEditText_background="@color/loki_darkest_gray"
|
||||
app:labeledEditText_label="@string/activity_key_pair_mnemonic_edit_text_label"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/linkExplanationTextView"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:visibility="gone"
|
||||
android:text="@string/activity_key_pair_seed_explanation_3"
|
||||
android:textAlignment="center" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.LabeledEditText
|
||||
android:id="@+id/publicKeyEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:visibility="gone"
|
||||
app:labeledEditText_background="@color/loki_darkest_gray"
|
||||
app:labeledEditText_label="@string/activity_key_pair_public_key_edit_text_label"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/toggleModeButton"
|
||||
android:id="@+id/toggleRestoreModeButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:background="@color/transparent"
|
||||
@ -86,8 +106,29 @@
|
||||
android:elevation="0dp"
|
||||
android:stateListAnimator="@null" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/toggleRegisterModeButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:background="@color/transparent"
|
||||
android:textColor="@color/signal_primary"
|
||||
android:text="@string/activity_key_pair_toggle_mode_button_title_2"
|
||||
android:visibility="gone"
|
||||
android:elevation="0dp"
|
||||
android:stateListAnimator="@null" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/toggleLinkModeButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:background="@color/transparent"
|
||||
android:textColor="@color/signal_primary"
|
||||
android:text="@string/activity_key_pair_toggle_mode_button_title_3"
|
||||
android:elevation="0dp"
|
||||
android:stateListAnimator="@null" />
|
||||
|
||||
<com.dd.CircularProgressButton
|
||||
android:id="@+id/registerOrRestoreButton"
|
||||
android:id="@+id/mainButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="20dp"
|
||||
@ -99,7 +140,7 @@
|
||||
app:cpb_colorProgress="@color/textsecure_primary"
|
||||
app:cpb_cornerRadius="4dp"
|
||||
app:cpb_selectorIdle="@drawable/progress_button_state"
|
||||
app:cpb_textIdle="@string/activity_key_pair_register_or_restore_button_title_1" />
|
||||
app:cpb_textIdle="@string/activity_key_pair_main_button_title_1" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -1549,22 +1549,26 @@
|
||||
<string name="activity_landing_permission_dialog_message">Some features of Loki Messenger (such as automatic message backup) require storage access to work.</string>
|
||||
<string name="activity_landing_beta_terms">"Loki Messenger is currently in beta. For development purposes the beta version collects basic usage statistics and crash logs. In addition, the beta version doesn't provide full privacy and shouldn\'t be used to transmit sensitive information."</string>
|
||||
<string name="activity_landing_privacy_policy_button_title">Privacy Policy</string>
|
||||
<!-- Account details activity -->
|
||||
<string name="activity_account_details_title">Create Your Loki Messenger Account</string>
|
||||
<string name="activity_account_details_subtitle">Enter a name to be shown to your contacts</string>
|
||||
<string name="activity_account_details_name_edit_text_label">Display Name (Optional)</string>
|
||||
<string name="activity_account_details_button_title">Next</string>
|
||||
<!-- Display name activity -->
|
||||
<string name="activity_display_name_title">Create Your Loki Messenger Account</string>
|
||||
<string name="activity_display_name_subtitle">Enter a name to be shown to your contacts</string>
|
||||
<string name="activity_display_name_name_edit_text_label">Display Name (Optional)</string>
|
||||
<string name="activity_display_name_button_title">Next</string>
|
||||
<!-- Key pair activity -->
|
||||
<string name="activity_key_pair_title">Create Your Loki Messenger Account</string>
|
||||
<string name="activity_key_pair_seed_explanation_1">Please save the seed below in a safe location. It can be used to restore your account if you lose access, or to migrate to a new device.</string>
|
||||
<string name="activity_key_pair_seed_explanation_2">Restore your account by entering your seed below</string>
|
||||
<string name="activity_key_pair_seed_explanation_3">Link to an existing device by going into its in-app settings and clicking "Link Device".</string>
|
||||
<string name="activity_key_pair_copy_button_title">Copy</string>
|
||||
<string name="activity_key_pair_mnemonic_edit_text_label">Your Seed</string>
|
||||
<string name="activity_key_pair_toggle_mode_button_title_1">Restore Using Seed</string>
|
||||
<string name="activity_key_pair_toggle_mode_button_title_2">Register a New Account</string>
|
||||
<string name="activity_key_pair_toggle_mode_button_title_3">Link Device</string>
|
||||
<string name="activity_key_pair_mnemonic_copied_message">Copied to clipboard</string>
|
||||
<string name="activity_key_pair_register_or_restore_button_title_1">Register</string>
|
||||
<string name="activity_key_pair_register_or_restore_button_title_2">Restore</string>
|
||||
<string name="activity_key_pair_main_button_title_1">Register</string>
|
||||
<string name="activity_key_pair_main_button_title_2">Restore</string>
|
||||
<string name="activity_key_pair_main_button_title_3">Link</string>
|
||||
<string name="activity_key_pair_public_key_edit_text_label">Your Public Key</string>
|
||||
<!-- Conversation list activity -->
|
||||
<string name="activity_conversation_list_empty_state_message">Looks like you don\'t have any conversations yet. Get started by messaging a friend.</string>
|
||||
<!-- Settings activity -->
|
||||
|
@ -36,6 +36,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule;
|
||||
@ -83,12 +84,14 @@ import org.webrtc.voiceengine.WebRtcAudioUtils;
|
||||
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol;
|
||||
import org.whispersystems.signalservice.loki.api.LokiGroupChat;
|
||||
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI;
|
||||
import org.whispersystems.signalservice.loki.api.LokiLongPoller;
|
||||
import org.whispersystems.signalservice.loki.api.LokiP2PAPI;
|
||||
import org.whispersystems.signalservice.loki.api.LokiP2PAPIDelegate;
|
||||
import org.whispersystems.signalservice.loki.api.LokiRSSFeed;
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
|
||||
import org.whispersystems.signalservice.loki.utilities.Analytics;
|
||||
|
||||
import java.security.Security;
|
||||
@ -190,6 +193,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
// Loki - Start long polling if needed
|
||||
startLongPollingIfNeeded();
|
||||
setUpStorageAPIIfNeeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -424,6 +428,16 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
}
|
||||
|
||||
// region Loki
|
||||
public void setUpStorageAPIIfNeeded() {
|
||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (userHexEncodedPublicKey != null && IdentityKeyUtil.hasIdentityKey(this)) {
|
||||
boolean isDebugMode = BuildConfig.DEBUG;
|
||||
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
|
||||
LokiAPIDatabaseProtocol database = DatabaseFactory.getLokiAPIDatabase(this);
|
||||
LokiStorageAPI.Companion.configure(isDebugMode, userHexEncodedPublicKey, userPrivateKey, database);
|
||||
}
|
||||
}
|
||||
|
||||
public void setUpP2PAPI() {
|
||||
String hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
if (hexEncodedPublicKey == null) { return; }
|
||||
|
@ -156,8 +156,13 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
public void onCreate(Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
|
||||
this.findPreference(PREFERENCE_CATEGORY_PROFILE)
|
||||
.setOnPreferenceClickListener(new ProfileClickListener());
|
||||
String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(getContext());
|
||||
boolean isMasterDevice = (masterHexEncodedPublicKey != null);
|
||||
|
||||
Preference profilePreference = this.findPreference(PREFERENCE_CATEGORY_PROFILE);
|
||||
// Hide if this is a slave device
|
||||
profilePreference.setVisible(isMasterDevice);
|
||||
profilePreference.setOnPreferenceClickListener(new ProfileClickListener());
|
||||
/*
|
||||
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS));
|
||||
@ -182,10 +187,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PUBLIC_KEY));
|
||||
this.findPreference(PREFERENCE_CATEGORY_QR_CODE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_QR_CODE));
|
||||
this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_LINK_DEVICE));
|
||||
this.findPreference(PREFERENCE_CATEGORY_SEED)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener((PREFERENCE_CATEGORY_SEED)));
|
||||
Preference linkDevicePreference = this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE);
|
||||
// Hide if this is a slave device
|
||||
linkDevicePreference.setVisible(isMasterDevice);
|
||||
linkDevicePreference.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_LINK_DEVICE));
|
||||
Preference seedPreference = this.findPreference(PREFERENCE_CATEGORY_SEED);
|
||||
// Hide if this is a slave device
|
||||
seedPreference.setVisible(isMasterDevice);
|
||||
seedPreference.setOnPreferenceClickListener(new CategoryClickListener((PREFERENCE_CATEGORY_SEED)));
|
||||
|
||||
if (VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
tintIcons(getActivity());
|
||||
@ -320,7 +329,10 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
*/
|
||||
case PREFERENCE_CATEGORY_PUBLIC_KEY:
|
||||
Analytics.Companion.getShared().track("Public Key Shared");
|
||||
String hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
|
||||
String hexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(getContext());
|
||||
if (hexEncodedPublicKey == null) {
|
||||
hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
|
||||
}
|
||||
Intent shareIntent = new Intent();
|
||||
shareIntent.setAction(Intent.ACTION_SEND);
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey);
|
||||
@ -331,7 +343,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
QRCodeDialog.INSTANCE.show(getContext());
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_LINK_DEVICE:
|
||||
DeviceLinkingDialog.INSTANCE.show(getContext(), DeviceLinkingView.Mode.Master);
|
||||
DeviceLinkingDialog.Companion.show(getContext(), DeviceLinkingView.Mode.Master, null);
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_SEED:
|
||||
Analytics.Companion.getShared().track("Seed Modal Shown");
|
||||
|
@ -5,13 +5,21 @@ import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
||||
import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
@SuppressLint("UseSparseArrays")
|
||||
public class TypingStatusSender {
|
||||
|
||||
@ -74,7 +82,23 @@ public class TypingStatusSender {
|
||||
}
|
||||
|
||||
private void sendTyping(long threadId, boolean typingStarted) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(threadId, typingStarted));
|
||||
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
Recipient recipient = threadDatabase.getRecipientForThreadId(threadId);
|
||||
|
||||
if (recipient == null) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(threadId, typingStarted));
|
||||
return;
|
||||
}
|
||||
|
||||
MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, recipient.getAddress().serialize(), storageAPI, (devicePublicKey, isFriend, friendCount) -> {
|
||||
Recipient device = Recipient.from(context, Address.fromSerialized(devicePublicKey), false);
|
||||
long deviceThreadID = threadDatabase.getThreadIdIfExistsFor(device);
|
||||
if (deviceThreadID > -1) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(deviceThreadID, typingStarted));
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
private class StartRunnable implements Runnable {
|
||||
|
@ -172,8 +172,7 @@ public class IdentityKeyUtil {
|
||||
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
|
||||
}
|
||||
|
||||
private static void delete(Context context, String key) {
|
||||
public static void delete(Context context, String key) {
|
||||
context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0).edit().remove(key).commit();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -31,12 +31,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.loki.LokiAPIDatabase;
|
||||
import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase;
|
||||
import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase;
|
||||
import org.thoughtcrime.securesms.loki.LokiMessageDatabase;
|
||||
import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.LokiUserDatabase;
|
||||
import org.thoughtcrime.securesms.loki.*;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class DatabaseFactory {
|
||||
|
@ -35,12 +35,7 @@ import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.LokiAPIDatabase;
|
||||
import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase;
|
||||
import org.thoughtcrime.securesms.loki.LokiMessageDatabase;
|
||||
import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase;
|
||||
import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.LokiUserDatabase;
|
||||
import org.thoughtcrime.securesms.loki.*;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@ -74,6 +69,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int STICKERS = 21;
|
||||
private static final int lokiV1 = 22;
|
||||
private static final int lokiV2 = 23;
|
||||
private static final int lokiV3 = 24;
|
||||
|
||||
private static final int DATABASE_VERSION = lokiV2; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
@ -129,6 +125,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL(LokiAPIDatabase.getCreateGroupChatAuthTokenTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand());
|
||||
db.execSQL(LokiPreKeyBundleDatabase.getCreateTableCommand());
|
||||
db.execSQL(LokiPreKeyRecordDatabase.getCreateTableCommand());
|
||||
db.execSQL(LokiMessageDatabase.getCreateTableCommand());
|
||||
@ -498,6 +495,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand());
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV3) {
|
||||
db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand());
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
@ -5,8 +5,6 @@ import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
@ -73,6 +71,7 @@ import org.thoughtcrime.securesms.loki.LokiMessageDatabase;
|
||||
import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase;
|
||||
import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase;
|
||||
import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
|
||||
@ -123,6 +122,9 @@ import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOper
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.loki.api.DeviceLinkingSession;
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation;
|
||||
import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher;
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus;
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiServiceMessage;
|
||||
@ -138,6 +140,7 @@ import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import kotlin.Unit;
|
||||
import network.loki.messenger.R;
|
||||
|
||||
public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
@ -276,17 +279,20 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
acceptFriendRequestIfNeeded(envelope, content);
|
||||
|
||||
// Loki - Store pre key bundle if needed
|
||||
if (content.lokiMessage.isPresent()) {
|
||||
LokiServiceMessage lokiMessage = content.lokiMessage.get();
|
||||
if (content.lokiServiceMessage.isPresent()) {
|
||||
LokiServiceMessage lokiMessage = content.lokiServiceMessage.get();
|
||||
if (lokiMessage.getPreKeyBundleMessage() != null) {
|
||||
Log.d("Loki", "Received a pre key bundle from: " + envelope.getSource() + ".");
|
||||
int registrationID = TextSecurePreferences.getLocalRegistrationId(context);
|
||||
if (registrationID > 0) {
|
||||
LokiPreKeyBundleDatabase lokiPreKeyBundleDatabase = DatabaseFactory.getLokiPreKeyBundleDatabase(context);
|
||||
LokiPreKeyBundleDatabase lokiPreKeyBundleDatabase = DatabaseFactory.getLokiPreKeyBundleDatabase(context);
|
||||
|
||||
// Only store the pre key bundle if we don't have one in our database
|
||||
if (registrationID > 0 && !lokiPreKeyBundleDatabase.hasPreKeyBundle(envelope.getSource())) {
|
||||
Log.d("Loki", "Received a pre key bundle from: " + envelope.getSource() + ".");
|
||||
PreKeyBundle preKeyBundle = lokiMessage.getPreKeyBundleMessage().getPreKeyBundle(registrationID);
|
||||
lokiPreKeyBundleDatabase.setPreKeyBundle(envelope.getSource(), preKeyBundle);
|
||||
}
|
||||
}
|
||||
|
||||
if (lokiMessage.getAddressMessage() != null) {
|
||||
// TODO: Loki - Handle address message
|
||||
}
|
||||
@ -295,14 +301,13 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
// Loki - Store the sender display name if needed
|
||||
Optional<String> rawSenderDisplayName = content.senderDisplayName;
|
||||
if (rawSenderDisplayName.isPresent() && rawSenderDisplayName.get().length() > 0) {
|
||||
String senderHexEncodedPublicKey = envelope.getSource();
|
||||
String senderDisplayName = rawSenderDisplayName.get() + " (..." + senderHexEncodedPublicKey.substring(senderHexEncodedPublicKey.length() - 8) + ")";
|
||||
DatabaseFactory.getLokiUserDatabase(context).setDisplayName(senderHexEncodedPublicKey, senderDisplayName);
|
||||
setDisplayName(envelope.getSource(), rawSenderDisplayName.get());
|
||||
}
|
||||
|
||||
// TODO: Deleting the display name
|
||||
|
||||
if (content.getDataMessage().isPresent()) {
|
||||
if (content.getPairingAuthorisation().isPresent()) {
|
||||
handlePairingMessage(content.getPairingAuthorisation().get(), envelope, content);
|
||||
} else if (content.getDataMessage().isPresent()) {
|
||||
SignalServiceDataMessage message = content.getDataMessage().get();
|
||||
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent();
|
||||
|
||||
@ -1003,6 +1008,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
|
||||
private void handleTextMessage(@NonNull SignalServiceDataMessage message, @NonNull IncomingTextMessage textMessage, @NonNull Optional<Long> smsMessageId, @NonNull Optional<Long> messageServerIDOrNull) {
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
|
||||
// Ignore the message if the body is empty
|
||||
if (textMessage.getMessageBody().length() == 0) { return; }
|
||||
|
||||
// Insert the message into the database
|
||||
Optional<InsertResult> insertResult = database.insertMessageInbox(textMessage);
|
||||
|
||||
Long threadId;
|
||||
@ -1020,6 +1030,84 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidPairingMessage(@NonNull PairingAuthorisation authorisation) {
|
||||
boolean isSecondaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context) != null;
|
||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
boolean isRequest = (authorisation.getType() == PairingAuthorisation.Type.REQUEST);
|
||||
if (authorisation.getRequestSignature() == null) {
|
||||
Log.d("Loki", "Ignoring pairing request message without a request signature.");
|
||||
return false;
|
||||
} else if (isRequest && isSecondaryDevice) {
|
||||
Log.d("Loki", "Ignoring unexpected pairing request message (the device is already paired as a secondary device).");
|
||||
return false;
|
||||
} else if (isRequest && !authorisation.getPrimaryDevicePublicKey().equals(userHexEncodedPublicKey)) {
|
||||
Log.d("Loki", "Ignoring pairing request message addressed to another user.");
|
||||
return false;
|
||||
} else if (isRequest && authorisation.getSecondaryDevicePublicKey().equals(userHexEncodedPublicKey)) {
|
||||
Log.d("Loki", "Ignoring pairing request message from self.");
|
||||
return false;
|
||||
}
|
||||
return authorisation.verify();
|
||||
}
|
||||
|
||||
private void handlePairingMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
|
||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
if (authorisation.getType() == PairingAuthorisation.Type.REQUEST) {
|
||||
handlePairingRequestMessage(authorisation, envelope);
|
||||
} else if (authorisation.getSecondaryDevicePublicKey().equals(userHexEncodedPublicKey)) {
|
||||
handlePairingAuthorisationMessage(authorisation, envelope, content);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePairingRequestMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceEnvelope envelope) {
|
||||
boolean isValid = isValidPairingMessage(authorisation);
|
||||
DeviceLinkingSession linkingSession = DeviceLinkingSession.Companion.getShared();
|
||||
if (isValid && linkingSession.isListeningForLinkingRequests()) {
|
||||
linkingSession.processLinkingRequest(authorisation);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePairingAuthorisationMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
|
||||
// Prepare
|
||||
boolean isSecondaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context) != null;
|
||||
if (isSecondaryDevice) {
|
||||
Log.d("Loki", "Ignoring unexpected pairing authorisation message (the device is already paired as a secondary device).");
|
||||
return;
|
||||
}
|
||||
boolean isValid = isValidPairingMessage(authorisation);
|
||||
if (!isValid) {
|
||||
Log.d("Loki", "Ignoring invalid pairing authorisation message.");
|
||||
return;
|
||||
}
|
||||
if (!DeviceLinkingSession.Companion.getShared().isListeningForLinkingRequests()) {
|
||||
Log.d("Loki", "Ignoring pairing authorisation message.");
|
||||
return;
|
||||
}
|
||||
if (authorisation.getType() != PairingAuthorisation.Type.GRANT) { return; }
|
||||
Log.d("Loki", "Received pairing authorisation message from: " + authorisation.getPrimaryDevicePublicKey() + ".");
|
||||
// Process
|
||||
DeviceLinkingSession.Companion.getShared().processLinkingAuthorization(authorisation);
|
||||
// Store the primary device's public key
|
||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
DatabaseFactory.getLokiAPIDatabase(context).removePairingAuthorisations(userHexEncodedPublicKey);
|
||||
DatabaseFactory.getLokiAPIDatabase(context).insertOrUpdatePairingAuthorisation(authorisation);
|
||||
TextSecurePreferences.setMasterHexEncodedPublicKey(context, authorisation.getPrimaryDevicePublicKey());
|
||||
// Send a background message to the primary device
|
||||
sendBackgroundMessage(authorisation.getPrimaryDevicePublicKey());
|
||||
// Propagate the updates to the file server
|
||||
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
|
||||
storageAPI.updateUserDeviceMappings();
|
||||
// Update display names
|
||||
if (content.senderDisplayName.isPresent() && content.senderDisplayName.get().length() > 0) {
|
||||
setDisplayName(envelope.getSource(), content.senderDisplayName.get());
|
||||
}
|
||||
}
|
||||
|
||||
private void setDisplayName(String hexEncodedPublicKey, String profileName) {
|
||||
String displayName = profileName + " (..." + hexEncodedPublicKey.substring(hexEncodedPublicKey.length() - 8) + ")";
|
||||
DatabaseFactory.getLokiUserDatabase(context).setDisplayName(hexEncodedPublicKey, displayName);
|
||||
}
|
||||
|
||||
private void updateGroupChatMessageServerID(Optional<Long> messageServerIDOrNull, Optional<InsertResult> insertResult) {
|
||||
if (insertResult.isPresent() && messageServerIDOrNull.isPresent()) {
|
||||
long messageID = insertResult.get().getMessageId();
|
||||
@ -1031,78 +1119,93 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
private void acceptFriendRequestIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
|
||||
// If we get anything other than a friend request, we can assume that we have a session with the other user
|
||||
if (envelope.isFriendRequest()) { return; }
|
||||
Recipient contactID = Recipient.from(context, Address.fromSerialized(content.getSender()), false);
|
||||
becomeFriendsWithContact(content.getSender());
|
||||
}
|
||||
|
||||
private void becomeFriendsWithContact(String pubKey) {
|
||||
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
|
||||
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(contactID);
|
||||
Recipient contactID = Recipient.from(context, Address.fromSerialized(pubKey), false);
|
||||
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(contactID);
|
||||
LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
|
||||
if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS) { return; }
|
||||
SmsDatabase messageDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
||||
int messageCount = messageDatabase.getMessageCountForThread(threadID);
|
||||
// If the thread's friend request status is not `FRIENDS`, but we're receiving a message,
|
||||
// it must be a friend request accepted message. Declining a friend request doesn't send a message.
|
||||
lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS);
|
||||
long messageID = messageDatabase.getIDForMessageAtIndex(threadID, messageCount - 1);
|
||||
lokiMessageDatabase.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
|
||||
// Update the last message if needed
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
||||
int messageCount = smsDatabase.getMessageCountForThread(threadID);
|
||||
long messageID = smsDatabase.getIDForMessageAtIndex(threadID, messageCount - 1);
|
||||
if (messageID > -1 && lokiMessageDatabase.getFriendRequestStatus(messageID) != LokiMessageFriendRequestStatus.REQUEST_ACCEPTED) {
|
||||
lokiMessageDatabase.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
|
||||
if (!envelope.isFriendRequest()) { return; }
|
||||
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);
|
||||
// This handles the case where another user sends us a regular message without authorisation
|
||||
MultiDeviceUtilitiesKt.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context).success(becomeFriends -> {
|
||||
if (becomeFriends) {
|
||||
// Become friends AND update the message they sent
|
||||
becomeFriendsWithContact(content.getSender());
|
||||
// Send them an accept message back
|
||||
sendBackgroundMessage(content.getSender());
|
||||
} else {
|
||||
// TODO: The code below is ugly due to Java limitations
|
||||
lokiMessageDatabase.setFriendRequestStatus(mmsMessageDatabase.getIDForMessageAtIndex(threadID, 0), LokiMessageFriendRequestStatus.REQUEST_PENDING);
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
|
||||
private void sendBackgroundMessage(String contactHexEncodedPublicKey) {
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
SignalServiceMessageSender messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender();
|
||||
SignalServiceAddress address = new SignalServiceAddress(contactHexEncodedPublicKey);
|
||||
SignalServiceDataMessage message = new SignalServiceDataMessage(System.currentTimeMillis(), "");
|
||||
try {
|
||||
messageSender.sendMessage(0, address, Optional.absent(), message); // The message ID doesn't matter
|
||||
} catch (Exception e) {
|
||||
Log.d("Loki", "Failed to send background message to: " + contactHexEncodedPublicKey + ".");
|
||||
}
|
||||
Util.runOnMain(() -> {
|
||||
SignalServiceMessageSender messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender();
|
||||
SignalServiceAddress address = new SignalServiceAddress(contactHexEncodedPublicKey);
|
||||
SignalServiceDataMessage message = new SignalServiceDataMessage(System.currentTimeMillis(), "");
|
||||
try {
|
||||
messageSender.sendMessage(0, address, Optional.absent(), message); // The message ID doesn't matter
|
||||
} catch (Exception e) {
|
||||
Log.d("Loki", "Failed to send background message to: " + contactHexEncodedPublicKey + ".");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
@ -41,7 +42,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
@ -56,23 +56,49 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
|
||||
private static final String TAG = PushMediaSendJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_TEMPLATE_MESSAGE_ID = "template_message_id";
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_DESTINATION = "destination";
|
||||
private static final String KEY_IS_FRIEND_REQUEST = "is_friend_request";
|
||||
private static final String KEY_CUSTOM_FR_MESSAGE = "custom_friend_request_message";
|
||||
|
||||
@Inject SignalServiceMessageSender messageSender;
|
||||
|
||||
private long messageId;
|
||||
private long messageId; // The message ID
|
||||
private long templateMessageId; // The message ID of the message to template this send job from
|
||||
|
||||
public PushMediaSendJob(long messageId, Address destination) {
|
||||
this(constructParameters(destination), messageId);
|
||||
// Loki - Multi device
|
||||
private Address destination; // Destination to check whether this is another device we're sending to
|
||||
private boolean isFriendRequest; // Whether this is a friend request message
|
||||
private String customFriendRequestMessage; // If this isn't set then we use the message body
|
||||
|
||||
public PushMediaSendJob(long messageId, Address destination) { this(messageId, messageId, destination); }
|
||||
public PushMediaSendJob(long templateMessageId, long messageId, Address destination) { this(templateMessageId, messageId, destination, false, null); }
|
||||
public PushMediaSendJob(long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage) {
|
||||
this(constructParameters(destination), templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage);
|
||||
}
|
||||
|
||||
private PushMediaSendJob(Job.Parameters parameters, long messageId) {
|
||||
private PushMediaSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage) {
|
||||
super(parameters);
|
||||
this.templateMessageId = templateMessageId;
|
||||
this.messageId = messageId;
|
||||
this.destination = destination;
|
||||
this.isFriendRequest = isFriendRequest;
|
||||
this.customFriendRequestMessage = customFriendRequestMessage;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination) {
|
||||
enqueue(context, jobManager, messageId, messageId, destination);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long templateMessageId, long messageId, @NonNull Address destination) {
|
||||
enqueue(context, jobManager, templateMessageId, messageId, destination, false, null);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long templateMessageId, long messageId, @NonNull Address destination, Boolean isFriendRequest, @Nullable String customFriendRequestMessage) {
|
||||
try {
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
|
||||
@ -86,10 +112,10 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
List<AttachmentUploadJob> attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId())).toList();
|
||||
|
||||
if (attachmentJobs.isEmpty()) {
|
||||
jobManager.add(new PushMediaSendJob(messageId, destination));
|
||||
jobManager.add(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage));
|
||||
} else {
|
||||
jobManager.startChain(attachmentJobs)
|
||||
.then(new PushMediaSendJob(messageId, destination))
|
||||
.then(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage))
|
||||
.enqueue();
|
||||
}
|
||||
|
||||
@ -102,7 +128,14 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId).build();
|
||||
Data.Builder builder = new Data.Builder()
|
||||
.putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId)
|
||||
.putLong(KEY_MESSAGE_ID, messageId)
|
||||
.putString(KEY_DESTINATION, destination.serialize())
|
||||
.putBoolean(KEY_IS_FRIEND_REQUEST, isFriendRequest);
|
||||
|
||||
if (customFriendRequestMessage != null) { builder.putString(KEY_CUSTOM_FR_MESSAGE, customFriendRequestMessage); }
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -122,11 +155,9 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
{
|
||||
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
|
||||
OutgoingMediaMessage message = database.getOutgoingMessage(templateMessageId);
|
||||
|
||||
message.isFriendRequest = (DatabaseFactory.getLokiMessageDatabase(context).getFriendRequestStatus(messageId) == LokiMessageFriendRequestStatus.REQUEST_SENDING);
|
||||
|
||||
if (database.isSent(messageId)) {
|
||||
if (messageId >= 0 && database.isSent(messageId)) {
|
||||
warn(TAG, "Message " + messageId + " was already sent. Ignoring.");
|
||||
return;
|
||||
}
|
||||
@ -134,15 +165,17 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
try {
|
||||
log(TAG, "Sending message: " + messageId);
|
||||
|
||||
Recipient recipient = message.getRecipient().resolve();
|
||||
Recipient recipient = Recipient.from(context, destination, false);
|
||||
byte[] profileKey = recipient.getProfileKey();
|
||||
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
|
||||
|
||||
boolean unidentified = deliver(message);
|
||||
|
||||
database.markAsSent(messageId, true);
|
||||
markAttachmentsUploaded(messageId, message.getAttachments());
|
||||
database.markUnidentified(messageId, unidentified);
|
||||
if (messageId >= 0) {
|
||||
database.markAsSent(messageId, true);
|
||||
markAttachmentsUploaded(messageId, message.getAttachments());
|
||||
database.markUnidentified(messageId, unidentified);
|
||||
}
|
||||
|
||||
if (recipient.isLocalNumber()) {
|
||||
SyncMessageId id = new SyncMessageId(recipient.getAddress(), message.getSentTimeMillis());
|
||||
@ -163,7 +196,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
}
|
||||
}
|
||||
|
||||
if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
|
||||
if (messageId > 0 && message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
|
||||
database.markExpireStarted(messageId);
|
||||
expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn());
|
||||
}
|
||||
@ -172,9 +205,11 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
|
||||
} catch (InsecureFallbackApprovalException ifae) {
|
||||
warn(TAG, "Failure", ifae);
|
||||
database.markAsPendingInsecureSmsFallback(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(false));
|
||||
if (messageId >= 0) {
|
||||
database.markAsPendingInsecureSmsFallback(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(false));
|
||||
}
|
||||
} catch (UntrustedIdentityException uie) {
|
||||
warn(TAG, "Failure", uie);
|
||||
database.addMismatchedIdentity(messageId, Address.fromSerialized(uie.getE164Number()), uie.getIdentityKey());
|
||||
@ -191,22 +226,19 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
if (messageId >= 0) {
|
||||
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean deliver(OutgoingMediaMessage message)
|
||||
throws RetryLaterException, InsecureFallbackApprovalException, UntrustedIdentityException,
|
||||
UndeliverableMessageException
|
||||
{
|
||||
if (message.getRecipient() == null) {
|
||||
throw new UndeliverableMessageException("No destination address.");
|
||||
}
|
||||
|
||||
try {
|
||||
// rotateSenderCertificateIfNecessary();
|
||||
|
||||
SignalServiceAddress address = getPushAddress(message.getRecipient().getAddress());
|
||||
Recipient recipient = Recipient.from(context, destination, false);
|
||||
SignalServiceAddress address = getPushAddress(recipient.getAddress());
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
|
||||
List<SignalServiceAttachment> serviceAttachments = getAttachmentPointersFor(attachments);
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
|
||||
@ -216,15 +248,10 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
List<Preview> previews = getPreviewsFor(message);
|
||||
|
||||
// Loki - Include a pre key bundle if the message is a friend request or an end session message
|
||||
PreKeyBundle preKeyBundle;
|
||||
if (message.isFriendRequest) {
|
||||
preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.getNumber());
|
||||
} else {
|
||||
preKeyBundle = null;
|
||||
}
|
||||
|
||||
PreKeyBundle preKeyBundle = isFriendRequest ? DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.getNumber()) : null;
|
||||
String body = (isFriendRequest && customFriendRequestMessage != null) ? customFriendRequestMessage : message.getBody();
|
||||
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withBody(message.getBody())
|
||||
.withBody(body)
|
||||
.withAttachments(serviceAttachments)
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
@ -235,7 +262,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
.withPreviews(previews)
|
||||
.asExpirationUpdate(message.isExpirationUpdate())
|
||||
.withPreKeyBundle(preKeyBundle)
|
||||
.asFriendRequest(message.isFriendRequest)
|
||||
.asFriendRequest(isFriendRequest)
|
||||
.build();
|
||||
|
||||
if (address.getNumber().equals(TextSecurePreferences.getLocalNumber(context))) {
|
||||
@ -245,7 +272,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
messageSender.sendMessage(messageId, syncMessage, syncAccess);
|
||||
return syncAccess.isPresent();
|
||||
} else {
|
||||
return messageSender.sendMessage(messageId, address, UnidentifiedAccessUtil.getAccessFor(context, message.getRecipient()), mediaMessage).getSuccess().isUnidentified();
|
||||
return messageSender.sendMessage(messageId, address, UnidentifiedAccessUtil.getAccessFor(context, recipient), mediaMessage).getSuccess().isUnidentified();
|
||||
}
|
||||
} catch (UnregisteredUserException e) {
|
||||
warn(TAG, e);
|
||||
@ -262,7 +289,12 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
public static final class Factory implements Job.Factory<PushMediaSendJob> {
|
||||
@Override
|
||||
public @NonNull PushMediaSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new PushMediaSendJob(parameters, data.getLong(KEY_MESSAGE_ID));
|
||||
long templateMessageID = data.getLong(KEY_TEMPLATE_MESSAGE_ID);
|
||||
long messageID = data.getLong(KEY_MESSAGE_ID);
|
||||
Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION));
|
||||
boolean isFriendRequest = data.getBoolean(KEY_IS_FRIEND_REQUEST);
|
||||
String frMessage = data.hasString(KEY_CUSTOM_FR_MESSAGE) ? data.getString(KEY_CUSTOM_FR_MESSAGE) : null;
|
||||
return new PushMediaSendJob(parameters, templateMessageID, messageID, destination, isFriendRequest, frMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,24 +40,47 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
|
||||
|
||||
private static final String TAG = PushTextSendJob.class.getSimpleName();
|
||||
|
||||
private static final String KEY_TEMPLATE_MESSAGE_ID = "template_message_id";
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_DESTINATION = "destination";
|
||||
private static final String KEY_IS_FRIEND_REQUEST = "is_friend_request";
|
||||
private static final String KEY_CUSTOM_FR_MESSAGE = "custom_friend_request_message";
|
||||
|
||||
@Inject SignalServiceMessageSender messageSender;
|
||||
|
||||
private long messageId;
|
||||
private long messageId; // The message ID
|
||||
private long templateMessageId; // The message ID of the message to template this send job from
|
||||
|
||||
public PushTextSendJob(long messageId, Address destination) {
|
||||
this(constructParameters(destination), messageId);
|
||||
// Loki - Multi device
|
||||
private Address destination; // Destination to check whether this is another device we're sending to
|
||||
private boolean isFriendRequest; // Whether this is a friend request message
|
||||
private String customFriendRequestMessage; // If this isn't set then we use the message body
|
||||
|
||||
public PushTextSendJob(long messageId, Address destination) { this(messageId, messageId, destination); }
|
||||
public PushTextSendJob(long templateMessageId, long messageId, Address destination) { this(templateMessageId, messageId, destination, false, null); }
|
||||
public PushTextSendJob(long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage) {
|
||||
this(constructParameters(destination), templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage);
|
||||
}
|
||||
|
||||
private PushTextSendJob(@NonNull Job.Parameters parameters, long messageId) {
|
||||
private PushTextSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination, boolean isFriendRequest, String customFriendRequestMessage) {
|
||||
super(parameters);
|
||||
this.templateMessageId = templateMessageId;
|
||||
this.messageId = messageId;
|
||||
this.destination = destination;
|
||||
this.isFriendRequest = isFriendRequest;
|
||||
this.customFriendRequestMessage = customFriendRequestMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId).build();
|
||||
Data.Builder builder = new Data.Builder()
|
||||
.putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId)
|
||||
.putLong(KEY_MESSAGE_ID, messageId)
|
||||
.putString(KEY_DESTINATION, destination.serialize())
|
||||
.putBoolean(KEY_IS_FRIEND_REQUEST, isFriendRequest);
|
||||
|
||||
if (customFriendRequestMessage != null) { builder.putString(KEY_CUSTOM_FR_MESSAGE, customFriendRequestMessage); }
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -67,31 +90,38 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
|
||||
|
||||
@Override
|
||||
public void onAdded() {
|
||||
DatabaseFactory.getSmsDatabase(context).markAsSending(messageId);
|
||||
if (messageId >= 0) {
|
||||
DatabaseFactory.getSmsDatabase(context).markAsSending(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPushSend() throws NoSuchMessageException, RetryLaterException {
|
||||
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
SmsMessageRecord record = database.getMessage(messageId);
|
||||
SmsMessageRecord record = database.getMessage(templateMessageId);
|
||||
|
||||
if (!record.isPending() && !record.isFailed()) {
|
||||
warn(TAG, "Message " + messageId + " was already sent. Ignoring.");
|
||||
Recipient recordRecipient = record.getRecipient().resolve();
|
||||
boolean hasSameDestination = destination.equals(recordRecipient.getAddress());
|
||||
|
||||
if (hasSameDestination && !record.isPending() && !record.isFailed()) {
|
||||
warn(TAG, "Message " + templateMessageId + " was already sent. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log(TAG, "Sending message: " + messageId);
|
||||
log(TAG, "Sending message: " + templateMessageId + (hasSameDestination ? "" : "to another device."));
|
||||
|
||||
Recipient recipient = record.getRecipient().resolve();
|
||||
Recipient recipient = Recipient.from(context, destination, false);
|
||||
byte[] profileKey = recipient.getProfileKey();
|
||||
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
|
||||
|
||||
boolean unidentified = deliver(record);
|
||||
|
||||
database.markAsSent(messageId, true);
|
||||
database.markUnidentified(messageId, unidentified);
|
||||
if (messageId >= 0) {
|
||||
database.markAsSent(messageId, true);
|
||||
database.markUnidentified(messageId, unidentified);
|
||||
}
|
||||
|
||||
if (recipient.isLocalNumber()) {
|
||||
SyncMessageId id = new SyncMessageId(recipient.getAddress(), record.getDateSent());
|
||||
@ -112,12 +142,12 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
|
||||
}
|
||||
}
|
||||
|
||||
if (record.getExpiresIn() > 0) {
|
||||
if (record.getExpiresIn() > 0 && messageId >= 0) {
|
||||
database.markExpireStarted(messageId);
|
||||
expirationManager.scheduleDeletion(record.getId(), record.isMms(), record.getExpiresIn());
|
||||
}
|
||||
|
||||
log(TAG, "Sent message: " + messageId);
|
||||
log(TAG, "Sent message: " + templateMessageId + (hasSameDestination ? "" : "to another device."));
|
||||
|
||||
} catch (InsecureFallbackApprovalException e) {
|
||||
warn(TAG, "Failure", e);
|
||||
@ -142,13 +172,15 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
|
||||
if (messageId >= 0) {
|
||||
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
|
||||
|
||||
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
|
||||
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
|
||||
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
|
||||
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
|
||||
|
||||
if (threadId != -1 && recipient != null) {
|
||||
MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId);
|
||||
if (threadId != -1 && recipient != null) {
|
||||
MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,28 +189,29 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
|
||||
{
|
||||
try {
|
||||
// rotateSenderCertificateIfNecessary();
|
||||
|
||||
SignalServiceAddress address = getPushAddress(message.getIndividualRecipient().getAddress());
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getIndividualRecipient());
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, message.getIndividualRecipient());
|
||||
Recipient recipient = Recipient.from(context, destination, false);
|
||||
SignalServiceAddress address = getPushAddress(recipient.getAddress());
|
||||
Optional<byte[]> profileKey = getProfileKey(recipient);
|
||||
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
|
||||
|
||||
log(TAG, "Have access key to use: " + unidentifiedAccess.isPresent());
|
||||
|
||||
// Loki - Include a pre key bundle if the message is a friend request or an end session message
|
||||
PreKeyBundle preKeyBundle;
|
||||
if (message.isFriendRequest() || message.isEndSession()) {
|
||||
if (isFriendRequest || message.isEndSession()) {
|
||||
preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.getNumber());
|
||||
} else {
|
||||
preKeyBundle = null;
|
||||
}
|
||||
|
||||
String body = (isFriendRequest && customFriendRequestMessage != null) ? customFriendRequestMessage : message.getBody();
|
||||
SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(message.getDateSent())
|
||||
.withBody(message.getBody())
|
||||
.withBody(body)
|
||||
.withExpiration((int)(message.getExpiresIn() / 1000))
|
||||
.withProfileKey(profileKey.orNull())
|
||||
.asEndSessionMessage(message.isEndSession())
|
||||
.asFriendRequest(message.isFriendRequest())
|
||||
.asFriendRequest(isFriendRequest)
|
||||
.withPreKeyBundle(preKeyBundle)
|
||||
.build();
|
||||
|
||||
@ -203,7 +236,12 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
|
||||
public static class Factory implements Job.Factory<PushTextSendJob> {
|
||||
@Override
|
||||
public @NonNull PushTextSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new PushTextSendJob(parameters, data.getLong(KEY_MESSAGE_ID));
|
||||
long templateMessageID = data.getLong(KEY_TEMPLATE_MESSAGE_ID);
|
||||
long messageID = data.getLong(KEY_MESSAGE_ID);
|
||||
Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION));
|
||||
boolean isFriendRequest = data.getBoolean(KEY_IS_FRIEND_REQUEST);
|
||||
String frMessage = data.hasString(KEY_CUSTOM_FR_MESSAGE) ? data.getString(KEY_CUSTOM_FR_MESSAGE) : null;
|
||||
return new PushTextSendJob(parameters, templateMessageID, messageID, destination, isFriendRequest, frMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,23 @@ fun <T> SQLiteDatabase.get(table: String, query: String, arguments: Array<String
|
||||
return null
|
||||
}
|
||||
|
||||
fun <T> SQLiteDatabase.getAll(table: String, query: String, arguments: Array<String>, get: (Cursor) -> T): List<T> {
|
||||
val result = mutableListOf<T>()
|
||||
var cursor: Cursor? = null
|
||||
try {
|
||||
cursor = query(table, null, query, arguments, null, null, null)
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
result.add(get(cursor))
|
||||
}
|
||||
return result
|
||||
} catch (e: Exception) {
|
||||
// Do nothing
|
||||
} finally {
|
||||
cursor?.close()
|
||||
}
|
||||
return listOf()
|
||||
}
|
||||
|
||||
fun SQLiteDatabase.insertOrUpdate(table: String, values: ContentValues, query: String, arguments: Array<String>) {
|
||||
val id = insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE).toInt()
|
||||
if (id == -1) {
|
||||
|
@ -1,165 +1,67 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.os.Handler
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_device_linking.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||
import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.loki.api.DeviceLinkingSession
|
||||
import org.whispersystems.signalservice.loki.api.DeviceLinkingSessionListener
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
|
||||
object DeviceLinkingDialog {
|
||||
class DeviceLinkingDialog private constructor(private val context: Context, private val mode: DeviceLinkingView.Mode, private val delegate: DeviceLinkingDialogDelegate?) : DeviceLinkingViewDelegate, DeviceLinkingSessionListener {
|
||||
private lateinit var view: DeviceLinkingView
|
||||
private lateinit var dialog: AlertDialog
|
||||
|
||||
fun show(context: Context, mode: DeviceLinkingView.Mode) {
|
||||
val view = DeviceLinkingView(context, mode)
|
||||
val dialog = AlertDialog.Builder(context).setView(view).show()
|
||||
view.dismiss = { dialog.dismiss() }
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
|
||||
class DeviceLinkingView private constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, private val mode: Mode) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
private var delegate: DeviceLinkingDialogDelegate? = null
|
||||
private lateinit var languageFileDirectory: File
|
||||
var dismiss: (() -> Unit)? = null
|
||||
|
||||
// region Types
|
||||
enum class Mode { Master, Slave }
|
||||
// endregion
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context, mode: Mode) : this(context, null, 0, mode)
|
||||
private constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0, Mode.Master) // Just pass in a dummy mode
|
||||
private constructor(context: Context) : this(context, null)
|
||||
|
||||
init {
|
||||
if (mode == Mode.Slave) {
|
||||
if (delegate == null) { throw IllegalStateException("Missing delegate for device linking dialog in slave mode.") }
|
||||
}
|
||||
setUpLanguageFileDirectory()
|
||||
setUpViewHierarchy()
|
||||
when (mode) {
|
||||
Mode.Master -> Log.d("Loki", "TODO: DeviceLinkingSession.startListeningForLinkingRequests(this)")
|
||||
Mode.Slave -> Log.d("Loki", "TODO: DeviceLinkingSession.startListeningForAuthorization(this)")
|
||||
fun show(context: Context, mode: DeviceLinkingView.Mode, delegate: DeviceLinkingDialogDelegate?): DeviceLinkingDialog {
|
||||
val dialog = DeviceLinkingDialog(context, mode, delegate)
|
||||
dialog.show()
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpLanguageFileDirectory() {
|
||||
val languages = listOf( "english", "japanese", "portuguese", "spanish" )
|
||||
val directory = File(context.applicationInfo.dataDir)
|
||||
for (language in languages) {
|
||||
val fileName = "$language.txt"
|
||||
if (directory.list().contains(fileName)) { continue }
|
||||
val inputStream = context.assets.open("mnemonic/$fileName")
|
||||
val file = File(directory, fileName)
|
||||
val outputStream = FileOutputStream(file)
|
||||
val buffer = ByteArray(1024)
|
||||
while (true) {
|
||||
val count = inputStream.read(buffer)
|
||||
if (count < 0) { break }
|
||||
outputStream.write(buffer, 0, count)
|
||||
}
|
||||
inputStream.close()
|
||||
outputStream.close()
|
||||
private fun show() {
|
||||
view = DeviceLinkingView(context, mode, this)
|
||||
dialog = AlertDialog.Builder(context).setView(view).show()
|
||||
view.dismiss = { dismiss() }
|
||||
DeviceLinkingSession.shared.startListeningForLinkingRequests()
|
||||
DeviceLinkingSession.shared.addListener(this)
|
||||
}
|
||||
|
||||
private fun dismiss() {
|
||||
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
|
||||
DeviceLinkingSession.shared.removeListener(this)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
override fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
|
||||
delegate?.handleDeviceLinkAuthorized(pairingAuthorisation)
|
||||
}
|
||||
|
||||
override fun handleDeviceLinkingDialogDismissed() {
|
||||
if (mode == DeviceLinkingView.Mode.Master && view.pairingAuthorisation != null) {
|
||||
val authorisation = view.pairingAuthorisation!!
|
||||
DatabaseFactory.getLokiPreKeyBundleDatabase(context).removePreKeyBundle(authorisation.secondaryDevicePublicKey)
|
||||
}
|
||||
languageFileDirectory = directory
|
||||
delegate?.handleDeviceLinkingDialogDismissed()
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
inflate(context, R.layout.view_device_linking, this)
|
||||
spinner.indeterminateDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)
|
||||
val titleID = when (mode) {
|
||||
Mode.Master -> R.string.view_device_linking_title_1
|
||||
Mode.Slave -> R.string.view_device_linking_title_2
|
||||
override fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {
|
||||
delegate?.sendPairingAuthorizedMessage(pairingAuthorisation)
|
||||
}
|
||||
|
||||
override fun requestUserAuthorization(authorisation: PairingAuthorisation) {
|
||||
Util.runOnMain {
|
||||
view.requestUserAuthorization(authorisation)
|
||||
}
|
||||
titleTextView.text = resources.getString(titleID)
|
||||
val explanationID = when (mode) {
|
||||
Mode.Master -> R.string.view_device_linking_explanation_1
|
||||
Mode.Slave -> R.string.view_device_linking_explanation_2
|
||||
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
|
||||
}
|
||||
|
||||
override fun onDeviceLinkRequestAuthorized(authorisation: PairingAuthorisation) {
|
||||
Util.runOnMain {
|
||||
view.onDeviceLinkAuthorized(authorisation)
|
||||
}
|
||||
explanationTextView.text = resources.getString(explanationID)
|
||||
mnemonicTextView.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE
|
||||
if (mode == Mode.Slave) {
|
||||
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded()
|
||||
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
|
||||
}
|
||||
authorizeButton.visibility = View.GONE
|
||||
cancelButton.setOnClickListener { cancel() }
|
||||
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Device Linking
|
||||
private fun requestUserAuthorization() { // TODO: deviceLink parameter
|
||||
// To be called by DeviceLinkingSession when a linking request has been received
|
||||
// TODO: this.deviceLink = deviceLink
|
||||
spinner.visibility = View.GONE
|
||||
val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams
|
||||
titleTextViewLayoutParams.topMargin = toPx(16, resources)
|
||||
titleTextView.layoutParams = titleTextViewLayoutParams
|
||||
titleTextView.text = resources.getString(R.string.view_device_linking_title_3)
|
||||
explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_2)
|
||||
mnemonicTextView.visibility = View.VISIBLE
|
||||
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded() // TODO: deviceLink.slave.hexEncodedPublicKey.removing05PrefixIfNeeded()
|
||||
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
|
||||
authorizeButton.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun authorizeDeviceLink() {
|
||||
// TODO: val deviceLink = this.deviceLink!!
|
||||
// TODO: val linkingAuthorizationMessage = DeviceLinkingUtilities.getLinkingAuthorizationMessage(deviceLink)
|
||||
// TODO: Send the linking authorization message
|
||||
// TODO: val session = DeviceLinkingSession.current!!
|
||||
// TODO: session.stopListeningForLinkingRequests()
|
||||
// TODO: session.markLinkingRequestAsProcessed()
|
||||
dismiss?.invoke()
|
||||
// TODO: val master = DeviceLink.Device(deviceLink.master.hexEncodedPublicKey, linkingAuthorizationMessage.masterSignature)
|
||||
// TODO: val signedDeviceLink = DeviceLink(master, deviceLink.slave)
|
||||
// TODO: LokiStorageAPI.addDeviceLink(signedDeviceLink).fail { error ->
|
||||
// TODO: Log.d("Loki", "Failed to add device link due to error: $error.")
|
||||
// TODO: }
|
||||
}
|
||||
|
||||
private fun handleDeviceLinkAuthorized() { // TODO: deviceLink parameter
|
||||
// To be called by DeviceLinkingSession when a device link has been authorized
|
||||
// TODO: val session = DeviceLinkingSession.current!!
|
||||
// TODO: session.stopListeningForLinkingAuthorization()
|
||||
spinner.visibility = View.GONE
|
||||
val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams
|
||||
titleTextViewLayoutParams.topMargin = toPx(8, resources)
|
||||
titleTextView.layoutParams = titleTextViewLayoutParams
|
||||
titleTextView.text = resources.getString(R.string.view_device_linking_title_4)
|
||||
val explanationTextViewLayoutParams = explanationTextView.layoutParams as LayoutParams
|
||||
explanationTextViewLayoutParams.bottomMargin = toPx(12, resources)
|
||||
explanationTextView.layoutParams = explanationTextViewLayoutParams
|
||||
explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_3)
|
||||
titleTextView.text = resources.getString(R.string.view_device_linking_title_4)
|
||||
mnemonicTextView.visibility = View.GONE
|
||||
buttonContainer.visibility = View.GONE
|
||||
// TODO: LokiStorageAPI.addDeviceLink(signedDeviceLink).fail { error ->
|
||||
// TODO: Log.d("Loki", "Failed to add device link due to error: $error.")
|
||||
// TODO: }
|
||||
Handler().postDelayed({
|
||||
delegate?.handleDeviceLinkAuthorized()
|
||||
dismiss?.invoke()
|
||||
}, 4000)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
private fun cancel() {
|
||||
// TODO: val session = DeviceLinkingSession.current!!
|
||||
// TODO: session.stopListeningForLinkingRequests()
|
||||
// TODO: session.markLinkingRequestAsProcessed() // Only relevant in master mode
|
||||
delegate?.handleDeviceLinkingDialogDismissed() // Only relevant in slave mode
|
||||
dismiss?.invoke()
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
|
||||
interface DeviceLinkingDialogDelegate {
|
||||
|
||||
fun handleDeviceLinkAuthorized() // TODO: Device link
|
||||
fun handleDeviceLinkingDialogDismissed()
|
||||
fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) { }
|
||||
fun handleDeviceLinkingDialogDismissed() { }
|
||||
fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) { }
|
||||
}
|
137
src/org/thoughtcrime/securesms/loki/DeviceLinkingView.kt
Normal file
137
src/org/thoughtcrime/securesms/loki/DeviceLinkingView.kt
Normal file
@ -0,0 +1,137 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.os.Handler
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_device_linking.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||
import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class DeviceLinkingView private constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, private val mode: Mode, private var delegate: DeviceLinkingViewDelegate) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
private lateinit var languageFileDirectory: File
|
||||
var dismiss: (() -> Unit)? = null
|
||||
var pairingAuthorisation: PairingAuthorisation? = null
|
||||
private set
|
||||
|
||||
// region Types
|
||||
enum class Mode { Master, Slave }
|
||||
// endregion
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context, mode: Mode, delegate: DeviceLinkingViewDelegate) : this(context, null, 0, mode, delegate)
|
||||
private constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0, Mode.Master, object : DeviceLinkingViewDelegate { }) // Just pass in a dummy mode
|
||||
private constructor(context: Context) : this(context, null)
|
||||
|
||||
init {
|
||||
setUpLanguageFileDirectory()
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private fun setUpLanguageFileDirectory() {
|
||||
val languages = listOf( "english", "japanese", "portuguese", "spanish" )
|
||||
val directory = File(context.applicationInfo.dataDir)
|
||||
for (language in languages) {
|
||||
val fileName = "$language.txt"
|
||||
if (directory.list().contains(fileName)) { continue }
|
||||
val inputStream = context.assets.open("mnemonic/$fileName")
|
||||
val file = File(directory, fileName)
|
||||
val outputStream = FileOutputStream(file)
|
||||
val buffer = ByteArray(1024)
|
||||
while (true) {
|
||||
val count = inputStream.read(buffer)
|
||||
if (count < 0) { break }
|
||||
outputStream.write(buffer, 0, count)
|
||||
}
|
||||
inputStream.close()
|
||||
outputStream.close()
|
||||
}
|
||||
languageFileDirectory = directory
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
inflate(context, R.layout.view_device_linking, this)
|
||||
spinner.indeterminateDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)
|
||||
val titleID = when (mode) {
|
||||
Mode.Master -> R.string.view_device_linking_title_1
|
||||
Mode.Slave -> R.string.view_device_linking_title_2
|
||||
}
|
||||
titleTextView.text = resources.getString(titleID)
|
||||
val explanationID = when (mode) {
|
||||
Mode.Master -> R.string.view_device_linking_explanation_1
|
||||
Mode.Slave -> R.string.view_device_linking_explanation_2
|
||||
}
|
||||
explanationTextView.text = resources.getString(explanationID)
|
||||
mnemonicTextView.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE
|
||||
if (mode == Mode.Slave) {
|
||||
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded()
|
||||
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
|
||||
}
|
||||
authorizeButton.visibility = View.GONE
|
||||
authorizeButton.setOnClickListener { authorizePairing() }
|
||||
cancelButton.setOnClickListener { cancel() }
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Device Linking
|
||||
fun requestUserAuthorization(pairingAuthorisation: PairingAuthorisation) {
|
||||
if (mode != Mode.Master || pairingAuthorisation.type != PairingAuthorisation.Type.REQUEST || this.pairingAuthorisation != null) { return }
|
||||
this.pairingAuthorisation = pairingAuthorisation
|
||||
spinner.visibility = View.GONE
|
||||
val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams
|
||||
titleTextViewLayoutParams.topMargin = toPx(16, resources)
|
||||
titleTextView.layoutParams = titleTextViewLayoutParams
|
||||
titleTextView.text = resources.getString(R.string.view_device_linking_title_3)
|
||||
explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_2)
|
||||
mnemonicTextView.visibility = View.VISIBLE
|
||||
val hexEncodedPublicKey = pairingAuthorisation.secondaryDevicePublicKey.removing05PrefixIfNeeded()
|
||||
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
|
||||
authorizeButton.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
fun onDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
|
||||
if (mode != Mode.Slave || pairingAuthorisation.type != PairingAuthorisation.Type.GRANT || this.pairingAuthorisation != null) { return }
|
||||
this.pairingAuthorisation = pairingAuthorisation
|
||||
spinner.visibility = View.GONE
|
||||
val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams
|
||||
titleTextViewLayoutParams.topMargin = toPx(8, resources)
|
||||
titleTextView.layoutParams = titleTextViewLayoutParams
|
||||
titleTextView.text = resources.getString(R.string.view_device_linking_title_4)
|
||||
val explanationTextViewLayoutParams = explanationTextView.layoutParams as LayoutParams
|
||||
explanationTextViewLayoutParams.bottomMargin = toPx(12, resources)
|
||||
explanationTextView.layoutParams = explanationTextViewLayoutParams
|
||||
explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_3)
|
||||
titleTextView.text = resources.getString(R.string.view_device_linking_title_4)
|
||||
mnemonicTextView.visibility = View.GONE
|
||||
buttonContainer.visibility = View.GONE
|
||||
cancelButton.visibility = View.GONE
|
||||
Handler().postDelayed({
|
||||
delegate.handleDeviceLinkAuthorized(pairingAuthorisation)
|
||||
dismiss?.invoke()
|
||||
}, 4000)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
private fun authorizePairing() {
|
||||
val pairingAuthorisation = this.pairingAuthorisation
|
||||
if (mode != Mode.Master || pairingAuthorisation == null) { return; }
|
||||
delegate.sendPairingAuthorizedMessage(pairingAuthorisation)
|
||||
delegate.handleDeviceLinkAuthorized(pairingAuthorisation)
|
||||
dismiss?.invoke()
|
||||
}
|
||||
|
||||
private fun cancel() {
|
||||
delegate.handleDeviceLinkingDialogDismissed()
|
||||
dismiss?.invoke()
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
|
||||
interface DeviceLinkingViewDelegate {
|
||||
|
||||
fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) { }
|
||||
fun handleDeviceLinkingDialogDismissed() { }
|
||||
fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) { }
|
||||
}
|
@ -42,6 +42,7 @@ class DisplayNameActivity : BaseActionBarActivity() {
|
||||
val application = ApplicationContext.getInstance(this)
|
||||
application.setUpP2PAPI()
|
||||
application.startLongPollingIfNeeded()
|
||||
application.setUpStorageAPIIfNeeded()
|
||||
startActivity(Intent(this, ConversationListActivity::class.java))
|
||||
finish()
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
|
@ -1,8 +1,14 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.support.annotation.ColorRes
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun Resources.getColorWithID(@ColorRes id: Int, theme: Resources.Theme?): Int {
|
||||
@ -16,4 +22,20 @@ fun Resources.getColorWithID(@ColorRes id: Int, theme: Resources.Theme?): Int {
|
||||
fun toPx(dp: Int, resources: Resources): Int {
|
||||
val scale = resources.displayMetrics.density
|
||||
return (dp * scale).roundToInt()
|
||||
}
|
||||
|
||||
fun isGroupRecipient(recipient: String): Boolean {
|
||||
return (LokiGroupChatAPI.publicChatServer == recipient)
|
||||
}
|
||||
|
||||
fun getFriendPublicKeys(context: Context, devicePublicKeys: Set<String>): Set<String> {
|
||||
val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context)
|
||||
return devicePublicKeys.mapNotNull { device ->
|
||||
val address = Address.fromSerialized(device)
|
||||
val recipient = Recipient.from(context, address, false)
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient)
|
||||
if (threadID < 0) { return@mapNotNull null }
|
||||
val friendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID)
|
||||
if (friendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS) device else null
|
||||
}.toSet()
|
||||
}
|
@ -4,9 +4,11 @@ import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol
|
||||
import org.whispersystems.signalservice.loki.api.LokiAPITarget
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
|
||||
// TODO: Clean this up a bit
|
||||
|
||||
@ -45,6 +47,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
private val lastDeletionServerIDCacheIndex = "loki_api_last_deletion_server_id_cache_index"
|
||||
private val lastDeletionServerID = "last_deletion_server_id"
|
||||
@JvmStatic val createLastDeletionServerIDTableCommand = "CREATE TABLE $lastDeletionServerIDCache ($lastDeletionServerIDCacheIndex STRING PRIMARY KEY, $lastDeletionServerID INTEGER DEFAULT 0);"
|
||||
// Pairing authorisation cache
|
||||
private val pairingAuthorisationCache = "loki_pairing_authorisation_cache"
|
||||
private val primaryDevicePublicKey = "primary_device"
|
||||
private val secondaryDevicePublicKey = "secondary_device"
|
||||
private val requestSignature = "request_signature"
|
||||
private val grantSignature = "grant_signature"
|
||||
@JvmStatic val createPairingAuthorisationTableCommand = "CREATE TABLE $pairingAuthorisationCache ($primaryDevicePublicKey TEXT, $secondaryDevicePublicKey TEXT, " +
|
||||
"$requestSignature TEXT NULLABLE DEFAULT NULL, $grantSignature TEXT NULLABLE DEFAULT NULL, PRIMARY KEY ($primaryDevicePublicKey, $secondaryDevicePublicKey));"
|
||||
}
|
||||
|
||||
override fun getSwarmCache(hexEncodedPublicKey: String): Set<LokiAPITarget>? {
|
||||
@ -95,14 +105,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
database.insertOrUpdate(receivedMessageHashValuesCache, row, "$userID = ?", wrap(userPublicKey))
|
||||
}
|
||||
|
||||
override fun getGroupChatAuthToken(server: String): String? {
|
||||
override fun getAuthToken(server: String): String? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(groupChatAuthTokenTable, "${Companion.server} = ?", wrap(server)) { cursor ->
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(token))
|
||||
}
|
||||
}
|
||||
|
||||
override fun setGroupChatAuthToken(server: String, newValue: String?) {
|
||||
override fun setAuthToken(server: String, newValue: String?) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
if (newValue != null) {
|
||||
val row = wrap(mapOf(Companion.server to server, token to newValue))
|
||||
@ -141,6 +151,32 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
val row = wrap(mapOf( lastDeletionServerIDCacheIndex to index, lastDeletionServerID to newValue.toString() ))
|
||||
database.insertOrUpdate(lastDeletionServerIDCache, row, "$lastDeletionServerIDCacheIndex = ?", wrap(index))
|
||||
}
|
||||
|
||||
override fun getPairingAuthorisations(hexEncodedPublicKey: String): List<PairingAuthorisation> {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.getAll(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey )) { cursor ->
|
||||
val primaryDevicePubKey = cursor.getString(primaryDevicePublicKey)
|
||||
val secondaryDevicePubKey = cursor.getString(secondaryDevicePublicKey)
|
||||
val requestSignature: ByteArray? = if (cursor.isNull(cursor.getColumnIndexOrThrow(requestSignature))) null else cursor.getBase64EncodedData(requestSignature)
|
||||
val grantSignature: ByteArray? = if (cursor.isNull(cursor.getColumnIndexOrThrow(grantSignature))) null else cursor.getBase64EncodedData(grantSignature)
|
||||
PairingAuthorisation(primaryDevicePubKey, secondaryDevicePubKey, requestSignature, grantSignature)
|
||||
}
|
||||
}
|
||||
|
||||
override fun insertOrUpdatePairingAuthorisation(authorisation: PairingAuthorisation) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val values = ContentValues()
|
||||
values.put(primaryDevicePublicKey, authorisation.primaryDevicePublicKey)
|
||||
values.put(secondaryDevicePublicKey, authorisation.secondaryDevicePublicKey)
|
||||
if (authorisation.requestSignature != null) { values.put(requestSignature, Base64.encodeBytes(authorisation.requestSignature)) }
|
||||
if (authorisation.grantSignature != null) { values.put(grantSignature, Base64.encodeBytes(authorisation.grantSignature)) }
|
||||
database.insertOrUpdate(pairingAuthorisationCache, values, "$primaryDevicePublicKey = ? AND $secondaryDevicePublicKey = ?", arrayOf( authorisation.primaryDevicePublicKey, authorisation.secondaryDevicePublicKey ))
|
||||
}
|
||||
|
||||
override fun removePairingAuthorisations(hexEncodedPublicKey: String) {
|
||||
val database = databaseHelper.readableDatabase
|
||||
database.delete(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey ))
|
||||
}
|
||||
}
|
||||
|
||||
// region Convenience
|
||||
|
@ -89,4 +89,10 @@ class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) :
|
||||
val database = databaseHelper.writableDatabase
|
||||
database.delete(tableName, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey ))
|
||||
}
|
||||
|
||||
fun hasPreKeyBundle(hexEncodedPublicKey: String): Boolean {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val cursor = database.query(tableName, null, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey ), null, null, null)
|
||||
return cursor != null && cursor.count > 0
|
||||
}
|
||||
}
|
87
src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt
Normal file
87
src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt
Normal file
@ -0,0 +1,87 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.content.Context
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.deferred
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
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 org.whispersystems.signalservice.loki.api.LokiStorageAPI
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
|
||||
|
||||
fun getAllDevicePublicKeys(context: Context, hexEncodedPublicKey: String, storageAPI: LokiStorageAPI, block: (devicePublicKey: String, isFriend: Boolean, friendCount: Int) -> Unit) {
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
storageAPI.getAllDevicePublicKeys(hexEncodedPublicKey).success { items ->
|
||||
val devices = items.toMutableSet()
|
||||
if (hexEncodedPublicKey != userHexEncodedPublicKey) {
|
||||
devices.remove(userHexEncodedPublicKey)
|
||||
}
|
||||
val friends = getFriendPublicKeys(context, devices)
|
||||
for (device in devices) {
|
||||
block(device, friends.contains(device), friends.count())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun shouldAutomaticallyBecomeFriendsWithDevice(publicKey: String, context: Context): Promise<Boolean, Unit> {
|
||||
val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context)
|
||||
val storageAPI = LokiStorageAPI.shared
|
||||
val deferred = deferred<Boolean, Unit>()
|
||||
storageAPI.getPrimaryDevicePublicKey(publicKey).success { primaryDevicePublicKey ->
|
||||
if (primaryDevicePublicKey == null) {
|
||||
deferred.resolve(false)
|
||||
return@success
|
||||
}
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
if (primaryDevicePublicKey == userHexEncodedPublicKey) {
|
||||
storageAPI.getSecondaryDevicePublicKeys(userHexEncodedPublicKey).success { secondaryDevices ->
|
||||
deferred.resolve(secondaryDevices.contains(publicKey))
|
||||
}.fail {
|
||||
deferred.resolve(false)
|
||||
}
|
||||
return@success
|
||||
}
|
||||
val primaryDevice = Recipient.from(context, Address.fromSerialized(primaryDevicePublicKey), false)
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(primaryDevice)
|
||||
if (threadID < 0) {
|
||||
deferred.resolve(false)
|
||||
return@success
|
||||
}
|
||||
deferred.resolve(lokiThreadDatabase.getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS)
|
||||
}
|
||||
return deferred.promise
|
||||
}
|
||||
|
||||
fun sendPairingAuthorisationMessage(context: Context, contactHexEncodedPublicKey: String, authorisation: PairingAuthorisation): Promise<Unit, Exception> {
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(contactHexEncodedPublicKey)
|
||||
val message = SignalServiceDataMessage.newBuilder().withBody("").withPairingAuthorisation(authorisation)
|
||||
// A REQUEST should always act as a friend request. A GRANT should always be replying back as a normal message.
|
||||
if (authorisation.type == PairingAuthorisation.Type.REQUEST) {
|
||||
val preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.number)
|
||||
message.asFriendRequest(true).withPreKeyBundle(preKeyBundle)
|
||||
}
|
||||
return try {
|
||||
Log.d("Loki", "Sending authorisation message to: $contactHexEncodedPublicKey.")
|
||||
val result = messageSender.sendMessage(0, address, Optional.absent<UnidentifiedAccessPair>(), message.build())
|
||||
if (result.success == null) {
|
||||
val exception = when {
|
||||
result.isNetworkFailure -> "Failed to send authorisation message due to a network error."
|
||||
else -> "Failed to send authorisation message."
|
||||
}
|
||||
throw Exception(exception)
|
||||
}
|
||||
Promise.ofSuccess(Unit)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send authorisation message to: $contactHexEncodedPublicKey.")
|
||||
Promise.ofFail(e)
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ class QRCodeView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : Li
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.view_qr_code, this)
|
||||
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val hexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(getContext()) ?: TextSecurePreferences.getLocalNumber(context)
|
||||
val displayMetrics = DisplayMetrics()
|
||||
ServiceUtil.getWindowManager(context).defaultDisplay.getMetrics(displayMetrics)
|
||||
val size = displayMetrics.widthPixels - 2 * toPx(96, resources)
|
||||
|
@ -9,23 +9,33 @@ import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_seed.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.ConversationListActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
import org.whispersystems.libsignal.util.KeyHelper
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||
import org.whispersystems.signalservice.loki.utilities.Analytics
|
||||
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
|
||||
import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey
|
||||
import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class SeedActivity : BaseActionBarActivity() {
|
||||
class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
|
||||
private lateinit var languageFileDirectory: File
|
||||
private var mode = Mode.Register
|
||||
set(newValue) { field = newValue; updateUI() }
|
||||
@ -35,7 +45,7 @@ class SeedActivity : BaseActionBarActivity() {
|
||||
set(newValue) { field = newValue; updateMnemonicTextView() }
|
||||
|
||||
// region Types
|
||||
enum class Mode { Register, Restore }
|
||||
enum class Mode { Register, Restore, Link }
|
||||
// endregion
|
||||
|
||||
// region Lifecycle
|
||||
@ -45,8 +55,10 @@ class SeedActivity : BaseActionBarActivity() {
|
||||
setUpLanguageFileDirectory()
|
||||
updateSeed()
|
||||
copyButton.setOnClickListener { copy() }
|
||||
toggleModeButton.setOnClickListener { toggleMode() }
|
||||
registerOrRestoreButton.setOnClickListener { registerOrRestore() }
|
||||
toggleRegisterModeButton.setOnClickListener { mode = Mode.Register }
|
||||
toggleRestoreModeButton.setOnClickListener { mode = Mode.Restore }
|
||||
toggleLinkModeButton.setOnClickListener { mode = Mode.Link }
|
||||
mainButton.setOnClickListener { handleMainButtonTapped() }
|
||||
Analytics.shared.track("Seed Screen Viewed")
|
||||
}
|
||||
// endregion
|
||||
@ -86,15 +98,25 @@ class SeedActivity : BaseActionBarActivity() {
|
||||
}
|
||||
|
||||
private fun updateUI() {
|
||||
seedExplanationTextView1.visibility = if (mode == Mode.Register) View.VISIBLE else View.GONE
|
||||
mnemonicTextView.visibility = if (mode == Mode.Register) View.VISIBLE else View.GONE
|
||||
copyButton.visibility = if (mode == Mode.Register) View.VISIBLE else View.GONE
|
||||
seedExplanationTextView2.visibility = if (mode == Mode.Restore) View.VISIBLE else View.GONE
|
||||
mnemonicEditText.visibility = if (mode == Mode.Restore) View.VISIBLE else View.GONE
|
||||
val toggleModeButtonTitleID = if (mode == Mode.Register) R.string.activity_key_pair_toggle_mode_button_title_1 else R.string.activity_key_pair_toggle_mode_button_title_2
|
||||
toggleModeButton.setText(toggleModeButtonTitleID)
|
||||
val registerOrRestoreButtonTitleID = if (mode == Mode.Register) R.string.activity_key_pair_register_or_restore_button_title_1 else R.string.activity_key_pair_register_or_restore_button_title_2
|
||||
registerOrRestoreButton.setText(registerOrRestoreButtonTitleID)
|
||||
val registerModeVisibility = if (mode == Mode.Register) View.VISIBLE else View.GONE
|
||||
val restoreModeVisibility = if (mode == Mode.Restore) View.VISIBLE else View.GONE
|
||||
val linkModeVisibility = if (mode == Mode.Link) View.VISIBLE else View.GONE
|
||||
seedExplanationTextView1.visibility = registerModeVisibility
|
||||
mnemonicTextView.visibility = registerModeVisibility
|
||||
copyButton.visibility = registerModeVisibility
|
||||
seedExplanationTextView2.visibility = restoreModeVisibility
|
||||
mnemonicEditText.visibility = restoreModeVisibility
|
||||
linkExplanationTextView.visibility = linkModeVisibility
|
||||
publicKeyEditText.visibility = linkModeVisibility
|
||||
toggleRegisterModeButton.visibility = if (mode != Mode.Register) View.VISIBLE else View.GONE
|
||||
toggleRestoreModeButton.visibility = if (mode != Mode.Restore) View.VISIBLE else View.GONE
|
||||
toggleLinkModeButton.visibility = if (mode != Mode.Link) View.VISIBLE else View.GONE
|
||||
val mainButtonTitleID = when (mode) {
|
||||
Mode.Register -> R.string.activity_key_pair_main_button_title_1
|
||||
Mode.Restore -> R.string.activity_key_pair_main_button_title_2
|
||||
Mode.Link -> R.string.activity_key_pair_main_button_title_3
|
||||
}
|
||||
mainButton.setText(mainButtonTitleID)
|
||||
if (mode == Mode.Restore) {
|
||||
mnemonicEditText.requestFocus()
|
||||
} else {
|
||||
@ -102,6 +124,13 @@ class SeedActivity : BaseActionBarActivity() {
|
||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(mnemonicEditText.windowToken, 0)
|
||||
}
|
||||
if (mode == Mode.Link) {
|
||||
publicKeyEditText.requestFocus()
|
||||
} else {
|
||||
publicKeyEditText.clearFocus()
|
||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(publicKeyEditText.windowToken, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMnemonic() {
|
||||
@ -122,14 +151,7 @@ class SeedActivity : BaseActionBarActivity() {
|
||||
Toast.makeText(this, R.string.activity_key_pair_mnemonic_copied_message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun toggleMode() {
|
||||
mode = when (mode) {
|
||||
Mode.Register -> Mode.Restore
|
||||
Mode.Restore -> Mode.Register
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerOrRestore() {
|
||||
private fun handleMainButtonTapped() {
|
||||
var seed: ByteArray
|
||||
when (mode) {
|
||||
Mode.Register -> seed = this.seed!!
|
||||
@ -143,10 +165,17 @@ class SeedActivity : BaseActionBarActivity() {
|
||||
return Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
Mode.Link -> {
|
||||
val hexEncodedPublicKey = publicKeyEditText.text.trim().toString()
|
||||
if (!PublicKeyValidation.isValid(hexEncodedPublicKey)) {
|
||||
return Toast.makeText(this, "Invalid public key", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
seed = this.seed!!
|
||||
}
|
||||
}
|
||||
val hexEncodedSeed = Hex.toStringCondensed(seed)
|
||||
IdentityKeyUtil.save(this, IdentityKeyUtil.lokiSeedKey, hexEncodedSeed)
|
||||
if (seed.count() == 16) seed = seed + seed
|
||||
if (seed.count() == 16) seed += seed
|
||||
if (mode == Mode.Restore) {
|
||||
IdentityKeyUtil.generateIdentityKeyPair(this, seed)
|
||||
}
|
||||
@ -161,13 +190,69 @@ class SeedActivity : BaseActionBarActivity() {
|
||||
when (mode) {
|
||||
Mode.Register -> Analytics.shared.track("Seed Created")
|
||||
Mode.Restore -> Analytics.shared.track("Seed Restored")
|
||||
// TODO: Mode.Link -> Analytics.shared.track("Device Linking Attempted")
|
||||
Mode.Link -> Analytics.shared.track("Device Linking Attempted")
|
||||
}
|
||||
startActivity(Intent(this, DisplayNameActivity::class.java))
|
||||
if (mode == Mode.Link) {
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
|
||||
TextSecurePreferences.setPromptedPushRegistration(this, true)
|
||||
val primaryDevicePublicKey = publicKeyEditText.text.trim().toString()
|
||||
val authorisation = PairingAuthorisation(primaryDevicePublicKey, hexEncodedPublicKey).sign(PairingAuthorisation.Type.REQUEST, keyPair.privateKey.serialize())
|
||||
if (authorisation == null) {
|
||||
Log.d("Loki", "Failed to sign outgoing pairing request.")
|
||||
resetForRegistration()
|
||||
return Toast.makeText(application, "Failed to link device.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val application = ApplicationContext.getInstance(this)
|
||||
application.startLongPollingIfNeeded()
|
||||
application.setUpP2PAPI()
|
||||
application.setUpStorageAPIIfNeeded()
|
||||
DeviceLinkingDialog.show(this, DeviceLinkingView.Mode.Slave, this)
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
retryIfNeeded(8) {
|
||||
sendPairingAuthorisationMessage(this@SeedActivity, authorisation.primaryDevicePublicKey, authorisation).get()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startActivity(Intent(this, DisplayNameActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
|
||||
Analytics.shared.track("Device Linked Successfully")
|
||||
if (pairingAuthorisation.secondaryDevicePublicKey == TextSecurePreferences.getLocalNumber(this)) {
|
||||
TextSecurePreferences.setMasterHexEncodedPublicKey(this, pairingAuthorisation.primaryDevicePublicKey)
|
||||
}
|
||||
startActivity(Intent(this, ConversationListActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
|
||||
// TODO: Analytics.shared.track("Device Linked Successfully")
|
||||
override fun handleDeviceLinkingDialogDismissed() {
|
||||
resetForRegistration()
|
||||
}
|
||||
|
||||
override fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {
|
||||
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).privateKey.serialize()
|
||||
val signedPairingAuthorisation = pairingAuthorisation.sign(PairingAuthorisation.Type.GRANT, userPrivateKey)
|
||||
if (signedPairingAuthorisation == null || signedPairingAuthorisation.type != PairingAuthorisation.Type.GRANT) {
|
||||
Log.d("Loki", "Failed to sign pairing authorization.")
|
||||
return
|
||||
}
|
||||
retryIfNeeded(8) {
|
||||
sendPairingAuthorisationMessage(this, pairingAuthorisation.secondaryDevicePublicKey, signedPairingAuthorisation).get()
|
||||
}.fail {
|
||||
Log.d("Loki", "Failed to send pairing authorization message to ${pairingAuthorisation.secondaryDevicePublicKey}.")
|
||||
}
|
||||
DatabaseFactory.getLokiAPIDatabase(this).insertOrUpdatePairingAuthorisation(signedPairingAuthorisation)
|
||||
LokiStorageAPI.shared.updateUserDeviceMappings()
|
||||
}
|
||||
|
||||
private fun resetForRegistration() {
|
||||
IdentityKeyUtil.delete(this, IdentityKeyUtil.lokiSeedKey)
|
||||
TextSecurePreferences.removeLocalRegistrationId(this)
|
||||
TextSecurePreferences.removeLocalNumber(this)
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, false)
|
||||
TextSecurePreferences.setPromptedPushRegistration(this, false)
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -20,14 +20,16 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
public class MarkReadReceiver extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = MarkReadReceiver.class.getSimpleName();
|
||||
@ -86,17 +88,17 @@ public class MarkReadReceiver extends BroadcastReceiver {
|
||||
.collect(Collectors.groupingBy(SyncMessageId::getAddress));
|
||||
|
||||
for (Address address : addressMap.keySet()) {
|
||||
// Loki - This also prevents read receipts from being sent in group chats as they don't maintain a friend request status
|
||||
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
|
||||
long threadID = lokiThreadDatabase.getThreadID(address.serialize());
|
||||
LokiThreadFriendRequestStatus friendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
|
||||
if (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) { return; }
|
||||
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
|
||||
|
||||
List<Long> timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList();
|
||||
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new SendReadReceiptJob(address, timestamps));
|
||||
MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, address.serialize(), storageAPI, (devicePublicKey, isFriend, friendCount) -> {
|
||||
// Loki - This also prevents read receipts from being sent in group chats as they don't maintain a friend request status
|
||||
if (isFriend) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new SendReadReceiptJob(Address.fromSerialized(devicePublicKey), timestamps));
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,8 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.GeneralUtilitiesKt;
|
||||
import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory;
|
||||
@ -52,10 +54,13 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
public class MessageSender {
|
||||
|
||||
private static final String TAG = MessageSender.class.getSimpleName();
|
||||
@ -203,13 +208,64 @@ public class MessageSender {
|
||||
}
|
||||
|
||||
private static void sendTextPush(Context context, Recipient recipient, long messageId) {
|
||||
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
|
||||
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
jobManager.add(new PushTextSendJob(messageId, recipient.getAddress()));
|
||||
|
||||
// Just send the message normally if it's a group message
|
||||
String recipientPublicKey = recipient.getAddress().serialize();
|
||||
if (GeneralUtilitiesKt.isGroupRecipient(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) {
|
||||
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
|
||||
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress());
|
||||
|
||||
// Just send the message normally if it's a group message
|
||||
String recipientPublicKey = recipient.getAddress().serialize();
|
||||
if (GeneralUtilitiesKt.isGroupRecipient(recipientPublicKey)) {
|
||||
PushMediaSendJob.enqueue(context, jobManager, 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
|
||||
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);
|
||||
}
|
||||
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private static void sendGroupPush(Context context, Recipient recipient, long messageId, Address filterAddress) {
|
||||
|
@ -546,6 +546,10 @@ public class TextSecurePreferences {
|
||||
setIntegerPrefrence(context, LOCAL_REGISTRATION_ID_PREF, registrationId);
|
||||
}
|
||||
|
||||
public static void removeLocalRegistrationId(Context context) {
|
||||
removePreference(context, LOCAL_REGISTRATION_ID_PREF);
|
||||
}
|
||||
|
||||
public static boolean isInThreadNotifications(Context context) {
|
||||
return getBooleanPreference(context, IN_THREAD_NOTIFICATION_PREF, true);
|
||||
}
|
||||
@ -639,6 +643,10 @@ public class TextSecurePreferences {
|
||||
setStringPreference(context, LOCAL_NUMBER_PREF, localNumber);
|
||||
}
|
||||
|
||||
public static void removeLocalNumber(Context context) {
|
||||
removePreference(context, LOCAL_NUMBER_PREF);
|
||||
}
|
||||
|
||||
public static String getPushServerPassword(Context context) {
|
||||
return getStringPreference(context, GCM_PASSWORD_PREF, null);
|
||||
}
|
||||
@ -1169,5 +1177,13 @@ public class TextSecurePreferences {
|
||||
public static void markChatSetUp(Context context, String id) {
|
||||
setBooleanPreference(context, "is_chat_set_up" + "?chat=" + id, true);
|
||||
}
|
||||
|
||||
public static String getMasterHexEncodedPublicKey(Context context) {
|
||||
return getStringPreference(context, "master_hex_encoded_public_key", null);
|
||||
}
|
||||
|
||||
public static void setMasterHexEncodedPublicKey(Context context, String masterHexEncodedPublicKey) {
|
||||
setStringPreference(context, "master_hex_encoded_publicKey", masterHexEncodedPublicKey);
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user