Merge pull request #31 from loki-project/multi-device

Multi Device Support
This commit is contained in:
gmbnt 2019-10-08 14:29:51 +11:00 committed by GitHub
commit 7116f2502a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1007 additions and 364 deletions

View File

@ -20,7 +20,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="40dp" android:layout_marginTop="40dp"
android:text="@string/activity_account_details_title" android:text="@string/activity_display_name_title"
android:textAlignment="center" /> android:textAlignment="center" />
<TextView <TextView
@ -29,7 +29,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/activity_account_details_subtitle" android:text="@string/activity_display_name_subtitle"
android:textAlignment="center" /> android:textAlignment="center" />
<org.thoughtcrime.securesms.components.LabeledEditText <org.thoughtcrime.securesms.components.LabeledEditText
@ -38,7 +38,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
app:labeledEditText_background="@color/loki_darkest_gray" 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 <com.dd.CircularProgressButton
android:id="@+id/nextButton" android:id="@+id/nextButton"
@ -53,7 +53,7 @@
app:cpb_colorProgress="@color/textsecure_primary" app:cpb_colorProgress="@color/textsecure_primary"
app:cpb_cornerRadius="4dp" app:cpb_cornerRadius="4dp"
app:cpb_selectorIdle="@drawable/progress_button_state" 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> </LinearLayout>

View File

@ -76,8 +76,28 @@
app:labeledEditText_background="@color/loki_darkest_gray" app:labeledEditText_background="@color/loki_darkest_gray"
app:labeledEditText_label="@string/activity_key_pair_mnemonic_edit_text_label"/> 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 <Button
android:id="@+id/toggleModeButton" android:id="@+id/toggleRestoreModeButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="50dp" android:layout_height="50dp"
android:background="@color/transparent" android:background="@color/transparent"
@ -86,8 +106,29 @@
android:elevation="0dp" android:elevation="0dp"
android:stateListAnimator="@null" /> 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 <com.dd.CircularProgressButton
android:id="@+id/registerOrRestoreButton" android:id="@+id/mainButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="50dp" android:layout_height="50dp"
android:layout_marginTop="20dp" android:layout_marginTop="20dp"
@ -99,7 +140,7 @@
app:cpb_colorProgress="@color/textsecure_primary" app:cpb_colorProgress="@color/textsecure_primary"
app:cpb_cornerRadius="4dp" app:cpb_cornerRadius="4dp"
app:cpb_selectorIdle="@drawable/progress_button_state" 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> </LinearLayout>

View File

@ -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_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_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> <string name="activity_landing_privacy_policy_button_title">Privacy Policy</string>
<!-- Account details activity --> <!-- Display name activity -->
<string name="activity_account_details_title">Create Your Loki Messenger Account</string> <string name="activity_display_name_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_display_name_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_display_name_name_edit_text_label">Display Name (Optional)</string>
<string name="activity_account_details_button_title">Next</string> <string name="activity_display_name_button_title">Next</string>
<!-- Key pair activity --> <!-- Key pair activity -->
<string name="activity_key_pair_title">Create Your Loki Messenger Account</string> <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_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_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_copy_button_title">Copy</string>
<string name="activity_key_pair_mnemonic_edit_text_label">Your Seed</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_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_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_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_main_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_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 --> <!-- Conversation list activity -->
<string name="activity_conversation_list_empty_state_message">Looks like you don\'t have any conversations yet. Get started by messaging a friend.</string> <string name="activity_conversation_list_empty_state_message">Looks like you don\'t have any conversations yet. Get started by messaging a friend.</string>
<!-- Settings activity --> <!-- Settings activity -->

View File

@ -36,6 +36,7 @@ import org.jetbrains.annotations.NotNull;
import org.signal.aesgcmprovider.AesGcmProvider; import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusRepository; import org.thoughtcrime.securesms.components.TypingStatusRepository;
import org.thoughtcrime.securesms.components.TypingStatusSender; import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseContentProviders; import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule; import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule;
@ -83,12 +84,14 @@ import org.webrtc.voiceengine.WebRtcAudioUtils;
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider; import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol;
import org.whispersystems.signalservice.loki.api.LokiGroupChat; import org.whispersystems.signalservice.loki.api.LokiGroupChat;
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI;
import org.whispersystems.signalservice.loki.api.LokiLongPoller; import org.whispersystems.signalservice.loki.api.LokiLongPoller;
import org.whispersystems.signalservice.loki.api.LokiP2PAPI; import org.whispersystems.signalservice.loki.api.LokiP2PAPI;
import org.whispersystems.signalservice.loki.api.LokiP2PAPIDelegate; import org.whispersystems.signalservice.loki.api.LokiP2PAPIDelegate;
import org.whispersystems.signalservice.loki.api.LokiRSSFeed; import org.whispersystems.signalservice.loki.api.LokiRSSFeed;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.utilities.Analytics; import org.whispersystems.signalservice.loki.utilities.Analytics;
import java.security.Security; import java.security.Security;
@ -190,6 +193,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
KeyCachingService.onAppForegrounded(this); KeyCachingService.onAppForegrounded(this);
// Loki - Start long polling if needed // Loki - Start long polling if needed
startLongPollingIfNeeded(); startLongPollingIfNeeded();
setUpStorageAPIIfNeeded();
} }
@Override @Override
@ -424,6 +428,16 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
} }
// region Loki // 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() { public void setUpP2PAPI() {
String hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this); String hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this);
if (hexEncodedPublicKey == null) { return; } if (hexEncodedPublicKey == null) { return; }

View File

@ -156,8 +156,13 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
public void onCreate(Bundle icicle) { public void onCreate(Bundle icicle) {
super.onCreate(icicle); super.onCreate(icicle);
this.findPreference(PREFERENCE_CATEGORY_PROFILE) String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(getContext());
.setOnPreferenceClickListener(new ProfileClickListener()); 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) this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
.setOnPreferenceClickListener(new CategoryClickListener(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)); .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PUBLIC_KEY));
this.findPreference(PREFERENCE_CATEGORY_QR_CODE) this.findPreference(PREFERENCE_CATEGORY_QR_CODE)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_QR_CODE)); .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_QR_CODE));
this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE) Preference linkDevicePreference = this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE);
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_LINK_DEVICE)); // Hide if this is a slave device
this.findPreference(PREFERENCE_CATEGORY_SEED) linkDevicePreference.setVisible(isMasterDevice);
.setOnPreferenceClickListener(new CategoryClickListener((PREFERENCE_CATEGORY_SEED))); 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) { if (VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
tintIcons(getActivity()); tintIcons(getActivity());
@ -320,7 +329,10 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
*/ */
case PREFERENCE_CATEGORY_PUBLIC_KEY: case PREFERENCE_CATEGORY_PUBLIC_KEY:
Analytics.Companion.getShared().track("Public Key Shared"); 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(); Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND); shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey); shareIntent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey);
@ -331,7 +343,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
QRCodeDialog.INSTANCE.show(getContext()); QRCodeDialog.INSTANCE.show(getContext());
break; break;
case PREFERENCE_CATEGORY_LINK_DEVICE: case PREFERENCE_CATEGORY_LINK_DEVICE:
DeviceLinkingDialog.INSTANCE.show(getContext(), DeviceLinkingView.Mode.Master); DeviceLinkingDialog.Companion.show(getContext(), DeviceLinkingView.Mode.Master, null);
break; break;
case PREFERENCE_CATEGORY_SEED: case PREFERENCE_CATEGORY_SEED:
Analytics.Companion.getShared().track("Seed Modal Shown"); Analytics.Companion.getShared().track("Seed Modal Shown");

View File

@ -5,13 +5,21 @@ import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.ApplicationContext; 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.jobs.TypingSendJob;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import kotlin.Unit;
@SuppressLint("UseSparseArrays") @SuppressLint("UseSparseArrays")
public class TypingStatusSender { public class TypingStatusSender {
@ -74,7 +82,23 @@ public class TypingStatusSender {
} }
private void sendTyping(long threadId, boolean typingStarted) { private void sendTyping(long threadId, boolean 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)); 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 { private class StartRunnable implements Runnable {

View File

@ -172,8 +172,7 @@ public class IdentityKeyUtil {
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences"); 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(); context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0).edit().remove(key).commit();
} }
} }

View File

@ -31,12 +31,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper; import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper;
import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.loki.LokiAPIDatabase; import org.thoughtcrime.securesms.loki.*;
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.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class DatabaseFactory { public class DatabaseFactory {

View File

@ -35,12 +35,7 @@ import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.LokiAPIDatabase; import org.thoughtcrime.securesms.loki.*;
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.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.TextSecurePreferences; 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 STICKERS = 21;
private static final int lokiV1 = 22; private static final int lokiV1 = 22;
private static final int lokiV2 = 23; private static final int lokiV2 = 23;
private static final int lokiV3 = 24;
private static final int DATABASE_VERSION = lokiV2; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes 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"; private static final String DATABASE_NAME = "signal.db";
@ -129,6 +125,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiAPIDatabase.getCreateGroupChatAuthTokenTableCommand()); db.execSQL(LokiAPIDatabase.getCreateGroupChatAuthTokenTableCommand());
db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDTableCommand()); db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDTableCommand());
db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDTableCommand()); db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDTableCommand());
db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand());
db.execSQL(LokiPreKeyBundleDatabase.getCreateTableCommand()); db.execSQL(LokiPreKeyBundleDatabase.getCreateTableCommand());
db.execSQL(LokiPreKeyRecordDatabase.getCreateTableCommand()); db.execSQL(LokiPreKeyRecordDatabase.getCreateTableCommand());
db.execSQL(LokiMessageDatabase.getCreateTableCommand()); db.execSQL(LokiMessageDatabase.getCreateTableCommand());
@ -498,6 +495,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand()); db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand());
} }
if (oldVersion < lokiV3) {
db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand());
}
db.setTransactionSuccessful(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();

View File

@ -5,8 +5,6 @@ import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat; 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.LokiPreKeyBundleDatabase;
import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase; import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase;
import org.thoughtcrime.securesms.loki.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage; import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
@ -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.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.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.crypto.LokiServiceCipher;
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus; import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus;
import org.whispersystems.signalservice.loki.messaging.LokiServiceMessage; import org.whispersystems.signalservice.loki.messaging.LokiServiceMessage;
@ -138,6 +140,7 @@ import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import kotlin.Unit;
import network.loki.messenger.R; import network.loki.messenger.R;
public class PushDecryptJob extends BaseJob implements InjectableType { public class PushDecryptJob extends BaseJob implements InjectableType {
@ -276,17 +279,20 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
acceptFriendRequestIfNeeded(envelope, content); acceptFriendRequestIfNeeded(envelope, content);
// Loki - Store pre key bundle if needed // Loki - Store pre key bundle if needed
if (content.lokiMessage.isPresent()) { if (content.lokiServiceMessage.isPresent()) {
LokiServiceMessage lokiMessage = content.lokiMessage.get(); LokiServiceMessage lokiMessage = content.lokiServiceMessage.get();
if (lokiMessage.getPreKeyBundleMessage() != null) { if (lokiMessage.getPreKeyBundleMessage() != null) {
Log.d("Loki", "Received a pre key bundle from: " + envelope.getSource() + ".");
int registrationID = TextSecurePreferences.getLocalRegistrationId(context); 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); PreKeyBundle preKeyBundle = lokiMessage.getPreKeyBundleMessage().getPreKeyBundle(registrationID);
lokiPreKeyBundleDatabase.setPreKeyBundle(envelope.getSource(), preKeyBundle); lokiPreKeyBundleDatabase.setPreKeyBundle(envelope.getSource(), preKeyBundle);
} }
} }
if (lokiMessage.getAddressMessage() != null) { if (lokiMessage.getAddressMessage() != null) {
// TODO: Loki - Handle address message // TODO: Loki - Handle address message
} }
@ -295,14 +301,13 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Store the sender display name if needed // Loki - Store the sender display name if needed
Optional<String> rawSenderDisplayName = content.senderDisplayName; Optional<String> rawSenderDisplayName = content.senderDisplayName;
if (rawSenderDisplayName.isPresent() && rawSenderDisplayName.get().length() > 0) { if (rawSenderDisplayName.isPresent() && rawSenderDisplayName.get().length() > 0) {
String senderHexEncodedPublicKey = envelope.getSource(); setDisplayName(envelope.getSource(), rawSenderDisplayName.get());
String senderDisplayName = rawSenderDisplayName.get() + " (..." + senderHexEncodedPublicKey.substring(senderHexEncodedPublicKey.length() - 8) + ")";
DatabaseFactory.getLokiUserDatabase(context).setDisplayName(senderHexEncodedPublicKey, senderDisplayName);
} }
// TODO: Deleting the display name // TODO: Deleting the display name
if (content.getPairingAuthorisation().isPresent()) {
if (content.getDataMessage().isPresent()) { handlePairingMessage(content.getPairingAuthorisation().get(), envelope, content);
} else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get(); SignalServiceDataMessage message = content.getDataMessage().get();
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent(); 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) { private void handleTextMessage(@NonNull SignalServiceDataMessage message, @NonNull IncomingTextMessage textMessage, @NonNull Optional<Long> smsMessageId, @NonNull Optional<Long> messageServerIDOrNull) {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context); 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); Optional<InsertResult> insertResult = database.insertMessageInbox(textMessage);
Long threadId; 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) { private void updateGroupChatMessageServerID(Optional<Long> messageServerIDOrNull, Optional<InsertResult> insertResult) {
if (insertResult.isPresent() && messageServerIDOrNull.isPresent()) { if (insertResult.isPresent() && messageServerIDOrNull.isPresent()) {
long messageID = insertResult.get().getMessageId(); long messageID = insertResult.get().getMessageId();
@ -1031,23 +1119,39 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void acceptFriendRequestIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) { private void acceptFriendRequestIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
// If we get anything other than a friend request, we can assume that we have a session with the other user // If we get anything other than a friend request, we can assume that we have a session with the other user
if (envelope.isFriendRequest()) { return; } if (envelope.isFriendRequest()) { return; }
Recipient contactID = Recipient.from(context, Address.fromSerialized(content.getSender()), false); becomeFriendsWithContact(content.getSender());
}
private void becomeFriendsWithContact(String pubKey) {
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context); 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); LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS) { return; } 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, // If the thread's friend request status is not `FRIENDS`, but we're receiving a message,
// it must be a friend request accepted message. Declining a friend request doesn't send a message. // it must be a friend request accepted message. Declining a friend request doesn't send a message.
lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS); lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS);
long messageID = messageDatabase.getIDForMessageAtIndex(threadID, messageCount - 1); // 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); lokiMessageDatabase.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
} }
}
private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) { private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
if (!envelope.isFriendRequest()) { return; } if (!envelope.isFriendRequest()) { return; }
// This handles the case where another user sends us a regular message without authorisation
MultiDeviceUtilitiesKt.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context).success(becomeFriends -> {
if (becomeFriends) {
// Become friends AND update the message they sent
becomeFriendsWithContact(content.getSender());
// Send them an accept message back
sendBackgroundMessage(content.getSender());
} else {
// Do regular friend request logic checks
Recipient contactID = getMessageDestination(content, message); Recipient contactID = getMessageDestination(content, message);
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context); LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(contactID); long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(contactID);
@ -1089,12 +1193,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} }
} }
} }
return Unit.INSTANCE;
});
}
private void sendBackgroundMessage(String contactHexEncodedPublicKey) { private void sendBackgroundMessage(String contactHexEncodedPublicKey) {
new Handler(Looper.getMainLooper()).post(new Runnable() { Util.runOnMain(() -> {
@Override
public void run() {
SignalServiceMessageSender messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender(); SignalServiceMessageSender messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender();
SignalServiceAddress address = new SignalServiceAddress(contactHexEncodedPublicKey); SignalServiceAddress address = new SignalServiceAddress(contactHexEncodedPublicKey);
SignalServiceDataMessage message = new SignalServiceDataMessage(System.currentTimeMillis(), ""); SignalServiceDataMessage message = new SignalServiceDataMessage(System.currentTimeMillis(), "");
@ -1103,7 +1207,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} catch (Exception e) { } catch (Exception e) {
Log.d("Loki", "Failed to send background message to: " + contactHexEncodedPublicKey + "."); Log.d("Loki", "Failed to send background message to: " + contactHexEncodedPublicKey + ".");
} }
}
}); });
} }

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.jobs;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread; import android.support.annotation.WorkerThread;
import com.annimon.stream.Stream; 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.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; 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 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_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; @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) { // Loki - Multi device
this(constructParameters(destination), messageId); 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); super(parameters);
this.templateMessageId = templateMessageId;
this.messageId = messageId; this.messageId = messageId;
this.destination = destination;
this.isFriendRequest = isFriendRequest;
this.customFriendRequestMessage = customFriendRequestMessage;
} }
@WorkerThread @WorkerThread
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination) { public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, 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 { try {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
OutgoingMediaMessage message = database.getOutgoingMessage(messageId); 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(); List<AttachmentUploadJob> attachmentJobs = Stream.of(attachments).map(a -> new AttachmentUploadJob(((DatabaseAttachment) a).getAttachmentId())).toList();
if (attachmentJobs.isEmpty()) { if (attachmentJobs.isEmpty()) {
jobManager.add(new PushMediaSendJob(messageId, destination)); jobManager.add(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage));
} else { } else {
jobManager.startChain(attachmentJobs) jobManager.startChain(attachmentJobs)
.then(new PushMediaSendJob(messageId, destination)) .then(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage))
.enqueue(); .enqueue();
} }
@ -102,7 +128,14 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
@Override @Override
public @NonNull Data serialize() { 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 @Override
@ -122,11 +155,9 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
{ {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager(); ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); 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 (messageId >= 0 && database.isSent(messageId)) {
if (database.isSent(messageId)) {
warn(TAG, "Message " + messageId + " was already sent. Ignoring."); warn(TAG, "Message " + messageId + " was already sent. Ignoring.");
return; return;
} }
@ -134,15 +165,17 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
try { try {
log(TAG, "Sending message: " + messageId); log(TAG, "Sending message: " + messageId);
Recipient recipient = message.getRecipient().resolve(); Recipient recipient = Recipient.from(context, destination, false);
byte[] profileKey = recipient.getProfileKey(); byte[] profileKey = recipient.getProfileKey();
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode(); UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
boolean unidentified = deliver(message); boolean unidentified = deliver(message);
if (messageId >= 0) {
database.markAsSent(messageId, true); database.markAsSent(messageId, true);
markAttachmentsUploaded(messageId, message.getAttachments()); markAttachmentsUploaded(messageId, message.getAttachments());
database.markUnidentified(messageId, unidentified); database.markUnidentified(messageId, unidentified);
}
if (recipient.isLocalNumber()) { if (recipient.isLocalNumber()) {
SyncMessageId id = new SyncMessageId(recipient.getAddress(), message.getSentTimeMillis()); 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); database.markExpireStarted(messageId);
expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn()); expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn());
} }
@ -172,9 +205,11 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
} catch (InsecureFallbackApprovalException ifae) { } catch (InsecureFallbackApprovalException ifae) {
warn(TAG, "Failure", ifae); warn(TAG, "Failure", ifae);
if (messageId >= 0) {
database.markAsPendingInsecureSmsFallback(messageId); database.markAsPendingInsecureSmsFallback(messageId);
notifyMediaMessageDeliveryFailed(context, messageId); notifyMediaMessageDeliveryFailed(context, messageId);
ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(false)); ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(false));
}
} catch (UntrustedIdentityException uie) { } catch (UntrustedIdentityException uie) {
warn(TAG, "Failure", uie); warn(TAG, "Failure", uie);
database.addMismatchedIdentity(messageId, Address.fromSerialized(uie.getE164Number()), uie.getIdentityKey()); database.addMismatchedIdentity(messageId, Address.fromSerialized(uie.getE164Number()), uie.getIdentityKey());
@ -191,22 +226,19 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
@Override @Override
public void onCanceled() { public void onCanceled() {
if (messageId >= 0) {
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId); DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
notifyMediaMessageDeliveryFailed(context, messageId); notifyMediaMessageDeliveryFailed(context, messageId);
} }
}
private boolean deliver(OutgoingMediaMessage message) private boolean deliver(OutgoingMediaMessage message)
throws RetryLaterException, InsecureFallbackApprovalException, UntrustedIdentityException, throws RetryLaterException, InsecureFallbackApprovalException, UntrustedIdentityException,
UndeliverableMessageException UndeliverableMessageException
{ {
if (message.getRecipient() == null) {
throw new UndeliverableMessageException("No destination address.");
}
try { try {
// rotateSenderCertificateIfNecessary(); Recipient recipient = Recipient.from(context, destination, false);
SignalServiceAddress address = getPushAddress(recipient.getAddress());
SignalServiceAddress address = getPushAddress(message.getRecipient().getAddress());
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList(); List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
List<SignalServiceAttachment> serviceAttachments = getAttachmentPointersFor(attachments); List<SignalServiceAttachment> serviceAttachments = getAttachmentPointersFor(attachments);
Optional<byte[]> profileKey = getProfileKey(message.getRecipient()); Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
@ -216,15 +248,10 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
List<Preview> previews = getPreviewsFor(message); List<Preview> previews = getPreviewsFor(message);
// Loki - Include a pre key bundle if the message is a friend request or an end session message // Loki - Include a pre key bundle if the message is a friend request or an end session message
PreKeyBundle preKeyBundle; PreKeyBundle preKeyBundle = isFriendRequest ? DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.getNumber()) : null;
if (message.isFriendRequest) { String body = (isFriendRequest && customFriendRequestMessage != null) ? customFriendRequestMessage : message.getBody();
preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.getNumber());
} else {
preKeyBundle = null;
}
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder() SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
.withBody(message.getBody()) .withBody(body)
.withAttachments(serviceAttachments) .withAttachments(serviceAttachments)
.withTimestamp(message.getSentTimeMillis()) .withTimestamp(message.getSentTimeMillis())
.withExpiration((int)(message.getExpiresIn() / 1000)) .withExpiration((int)(message.getExpiresIn() / 1000))
@ -235,7 +262,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
.withPreviews(previews) .withPreviews(previews)
.asExpirationUpdate(message.isExpirationUpdate()) .asExpirationUpdate(message.isExpirationUpdate())
.withPreKeyBundle(preKeyBundle) .withPreKeyBundle(preKeyBundle)
.asFriendRequest(message.isFriendRequest) .asFriendRequest(isFriendRequest)
.build(); .build();
if (address.getNumber().equals(TextSecurePreferences.getLocalNumber(context))) { if (address.getNumber().equals(TextSecurePreferences.getLocalNumber(context))) {
@ -245,7 +272,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
messageSender.sendMessage(messageId, syncMessage, syncAccess); messageSender.sendMessage(messageId, syncMessage, syncAccess);
return syncAccess.isPresent(); return syncAccess.isPresent();
} else { } 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) { } catch (UnregisteredUserException e) {
warn(TAG, e); warn(TAG, e);
@ -262,7 +289,12 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
public static final class Factory implements Job.Factory<PushMediaSendJob> { public static final class Factory implements Job.Factory<PushMediaSendJob> {
@Override @Override
public @NonNull PushMediaSendJob create(@NonNull Parameters parameters, @NonNull Data data) { 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);
} }
} }
} }

View File

@ -40,24 +40,47 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
private static final String TAG = PushTextSendJob.class.getSimpleName(); 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_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; @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) { // Loki - Multi device
this(constructParameters(destination), messageId); 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); super(parameters);
this.templateMessageId = templateMessageId;
this.messageId = messageId; this.messageId = messageId;
this.destination = destination;
this.isFriendRequest = isFriendRequest;
this.customFriendRequestMessage = customFriendRequestMessage;
} }
@Override @Override
public @NonNull Data serialize() { 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 @Override
@ -67,31 +90,38 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
@Override @Override
public void onAdded() { public void onAdded() {
if (messageId >= 0) {
DatabaseFactory.getSmsDatabase(context).markAsSending(messageId); DatabaseFactory.getSmsDatabase(context).markAsSending(messageId);
} }
}
@Override @Override
public void onPushSend() throws NoSuchMessageException, RetryLaterException { public void onPushSend() throws NoSuchMessageException, RetryLaterException {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager(); ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
SmsDatabase database = DatabaseFactory.getSmsDatabase(context); SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
SmsMessageRecord record = database.getMessage(messageId); SmsMessageRecord record = database.getMessage(templateMessageId);
if (!record.isPending() && !record.isFailed()) { Recipient recordRecipient = record.getRecipient().resolve();
warn(TAG, "Message " + messageId + " was already sent. Ignoring."); boolean hasSameDestination = destination.equals(recordRecipient.getAddress());
if (hasSameDestination && !record.isPending() && !record.isFailed()) {
warn(TAG, "Message " + templateMessageId + " was already sent. Ignoring.");
return; return;
} }
try { 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(); byte[] profileKey = recipient.getProfileKey();
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode(); UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
boolean unidentified = deliver(record); boolean unidentified = deliver(record);
if (messageId >= 0) {
database.markAsSent(messageId, true); database.markAsSent(messageId, true);
database.markUnidentified(messageId, unidentified); database.markUnidentified(messageId, unidentified);
}
if (recipient.isLocalNumber()) { if (recipient.isLocalNumber()) {
SyncMessageId id = new SyncMessageId(recipient.getAddress(), record.getDateSent()); 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); database.markExpireStarted(messageId);
expirationManager.scheduleDeletion(record.getId(), record.isMms(), record.getExpiresIn()); 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) { } catch (InsecureFallbackApprovalException e) {
warn(TAG, "Failure", e); warn(TAG, "Failure", e);
@ -142,6 +172,7 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
@Override @Override
public void onCanceled() { public void onCanceled() {
if (messageId >= 0) {
DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId); DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId);
long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId); long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
@ -151,34 +182,36 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId); MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId);
} }
} }
}
private boolean deliver(SmsMessageRecord message) private boolean deliver(SmsMessageRecord message)
throws UntrustedIdentityException, InsecureFallbackApprovalException, RetryLaterException throws UntrustedIdentityException, InsecureFallbackApprovalException, RetryLaterException
{ {
try { try {
// rotateSenderCertificateIfNecessary(); // rotateSenderCertificateIfNecessary();
Recipient recipient = Recipient.from(context, destination, false);
SignalServiceAddress address = getPushAddress(message.getIndividualRecipient().getAddress()); SignalServiceAddress address = getPushAddress(recipient.getAddress());
Optional<byte[]> profileKey = getProfileKey(message.getIndividualRecipient()); Optional<byte[]> profileKey = getProfileKey(recipient);
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, message.getIndividualRecipient()); Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
log(TAG, "Have access key to use: " + unidentifiedAccess.isPresent()); 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 // Loki - Include a pre key bundle if the message is a friend request or an end session message
PreKeyBundle preKeyBundle; PreKeyBundle preKeyBundle;
if (message.isFriendRequest() || message.isEndSession()) { if (isFriendRequest || message.isEndSession()) {
preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.getNumber()); preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.getNumber());
} else { } else {
preKeyBundle = null; preKeyBundle = null;
} }
String body = (isFriendRequest && customFriendRequestMessage != null) ? customFriendRequestMessage : message.getBody();
SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder() SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getDateSent()) .withTimestamp(message.getDateSent())
.withBody(message.getBody()) .withBody(body)
.withExpiration((int)(message.getExpiresIn() / 1000)) .withExpiration((int)(message.getExpiresIn() / 1000))
.withProfileKey(profileKey.orNull()) .withProfileKey(profileKey.orNull())
.asEndSessionMessage(message.isEndSession()) .asEndSessionMessage(message.isEndSession())
.asFriendRequest(message.isFriendRequest()) .asFriendRequest(isFriendRequest)
.withPreKeyBundle(preKeyBundle) .withPreKeyBundle(preKeyBundle)
.build(); .build();
@ -203,7 +236,12 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
public static class Factory implements Job.Factory<PushTextSendJob> { public static class Factory implements Job.Factory<PushTextSendJob> {
@Override @Override
public @NonNull PushTextSendJob create(@NonNull Parameters parameters, @NonNull Data data) { 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);
} }
} }
} }

View File

@ -18,6 +18,23 @@ fun <T> SQLiteDatabase.get(table: String, query: String, arguments: Array<String
return null 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>) { fun SQLiteDatabase.insertOrUpdate(table: String, values: ContentValues, query: String, arguments: Array<String>) {
val id = insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE).toInt() val id = insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_IGNORE).toInt()
if (id == -1) { if (id == -1) {

View File

@ -1,165 +1,67 @@
package org.thoughtcrime.securesms.loki package org.thoughtcrime.securesms.loki
import android.content.Context import android.content.Context
import android.graphics.Color
import android.graphics.PorterDuff
import android.os.Handler
import android.support.v7.app.AlertDialog import android.support.v7.app.AlertDialog
import android.util.AttributeSet import org.thoughtcrime.securesms.database.DatabaseFactory
import android.util.Log import org.thoughtcrime.securesms.util.Util
import android.view.View import org.whispersystems.signalservice.loki.api.DeviceLinkingSession
import android.widget.LinearLayout import org.whispersystems.signalservice.loki.api.DeviceLinkingSessionListener
import kotlinx.android.synthetic.main.view_device_linking.view.* import org.whispersystems.signalservice.loki.api.PairingAuthorisation
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
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) { companion object {
val view = DeviceLinkingView(context, mode)
val dialog = AlertDialog.Builder(context).setView(view).show() fun show(context: Context, mode: DeviceLinkingView.Mode, delegate: DeviceLinkingDialogDelegate?): DeviceLinkingDialog {
view.dismiss = { dialog.dismiss() } val dialog = DeviceLinkingDialog(context, mode, delegate)
dialog.show()
return dialog
} }
} }
class DeviceLinkingView private constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, private val mode: Mode) : LinearLayout(context, attrs, defStyleAttr) { private fun show() {
private var delegate: DeviceLinkingDialogDelegate? = null view = DeviceLinkingView(context, mode, this)
private lateinit var languageFileDirectory: File dialog = AlertDialog.Builder(context).setView(view).show()
var dismiss: (() -> Unit)? = null view.dismiss = { dismiss() }
DeviceLinkingSession.shared.startListeningForLinkingRequests()
// region Types DeviceLinkingSession.shared.addListener(this)
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)")
}
} }
private fun setUpLanguageFileDirectory() { private fun dismiss() {
val languages = listOf( "english", "japanese", "portuguese", "spanish" ) DeviceLinkingSession.shared.stopListeningForLinkingRequests()
val directory = File(context.applicationInfo.dataDir) DeviceLinkingSession.shared.removeListener(this)
for (language in languages) { dialog.dismiss()
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() { override fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
inflate(context, R.layout.view_device_linking, this) delegate?.handleDeviceLinkAuthorized(pairingAuthorisation)
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
cancelButton.setOnClickListener { cancel() }
}
// 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() { override fun handleDeviceLinkingDialogDismissed() {
// TODO: val deviceLink = this.deviceLink!! if (mode == DeviceLinkingView.Mode.Master && view.pairingAuthorisation != null) {
// TODO: val linkingAuthorizationMessage = DeviceLinkingUtilities.getLinkingAuthorizationMessage(deviceLink) val authorisation = view.pairingAuthorisation!!
// TODO: Send the linking authorization message DatabaseFactory.getLokiPreKeyBundleDatabase(context).removePreKeyBundle(authorisation.secondaryDevicePublicKey)
// TODO: val session = DeviceLinkingSession.current!! }
// TODO: session.stopListeningForLinkingRequests() delegate?.handleDeviceLinkingDialogDismissed()
// 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 override fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {
// To be called by DeviceLinkingSession when a device link has been authorized delegate?.sendPairingAuthorizedMessage(pairingAuthorisation)
// 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 override fun requestUserAuthorization(authorisation: PairingAuthorisation) {
private fun cancel() { Util.runOnMain {
// TODO: val session = DeviceLinkingSession.current!! view.requestUserAuthorization(authorisation)
// TODO: session.stopListeningForLinkingRequests() }
// TODO: session.markLinkingRequestAsProcessed() // Only relevant in master mode DeviceLinkingSession.shared.stopListeningForLinkingRequests()
delegate?.handleDeviceLinkingDialogDismissed() // Only relevant in slave mode }
dismiss?.invoke()
override fun onDeviceLinkRequestAuthorized(authorisation: PairingAuthorisation) {
Util.runOnMain {
view.onDeviceLinkAuthorized(authorisation)
}
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
} }
// endregion
} }

View File

@ -1,7 +1,10 @@
package org.thoughtcrime.securesms.loki package org.thoughtcrime.securesms.loki
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
interface DeviceLinkingDialogDelegate { interface DeviceLinkingDialogDelegate {
fun handleDeviceLinkAuthorized() // TODO: Device link fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) { }
fun handleDeviceLinkingDialogDismissed() fun handleDeviceLinkingDialogDismissed() { }
fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) { }
} }

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

View File

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

View File

@ -42,6 +42,7 @@ class DisplayNameActivity : BaseActionBarActivity() {
val application = ApplicationContext.getInstance(this) val application = ApplicationContext.getInstance(this)
application.setUpP2PAPI() application.setUpP2PAPI()
application.startLongPollingIfNeeded() application.startLongPollingIfNeeded()
application.setUpStorageAPIIfNeeded()
startActivity(Intent(this, ConversationListActivity::class.java)) startActivity(Intent(this, ConversationListActivity::class.java))
finish() finish()
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this) val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)

View File

@ -1,8 +1,14 @@
package org.thoughtcrime.securesms.loki package org.thoughtcrime.securesms.loki
import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.os.Build import android.os.Build
import android.support.annotation.ColorRes 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 import kotlin.math.roundToInt
fun Resources.getColorWithID(@ColorRes id: Int, theme: Resources.Theme?): Int { fun Resources.getColorWithID(@ColorRes id: Int, theme: Resources.Theme?): Int {
@ -17,3 +23,19 @@ fun toPx(dp: Int, resources: Resources): Int {
val scale = resources.displayMetrics.density val scale = resources.displayMetrics.density
return (dp * scale).roundToInt() 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()
}

View File

@ -4,9 +4,11 @@ import android.content.ContentValues
import android.content.Context import android.content.Context
import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol
import org.whispersystems.signalservice.loki.api.LokiAPITarget import org.whispersystems.signalservice.loki.api.LokiAPITarget
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
// TODO: Clean this up a bit // 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 lastDeletionServerIDCacheIndex = "loki_api_last_deletion_server_id_cache_index"
private val lastDeletionServerID = "last_deletion_server_id" private val lastDeletionServerID = "last_deletion_server_id"
@JvmStatic val createLastDeletionServerIDTableCommand = "CREATE TABLE $lastDeletionServerIDCache ($lastDeletionServerIDCacheIndex STRING PRIMARY KEY, $lastDeletionServerID INTEGER DEFAULT 0);" @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>? { 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)) database.insertOrUpdate(receivedMessageHashValuesCache, row, "$userID = ?", wrap(userPublicKey))
} }
override fun getGroupChatAuthToken(server: String): String? { override fun getAuthToken(server: String): String? {
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
return database.get(groupChatAuthTokenTable, "${Companion.server} = ?", wrap(server)) { cursor -> return database.get(groupChatAuthTokenTable, "${Companion.server} = ?", wrap(server)) { cursor ->
cursor.getString(cursor.getColumnIndexOrThrow(token)) cursor.getString(cursor.getColumnIndexOrThrow(token))
} }
} }
override fun setGroupChatAuthToken(server: String, newValue: String?) { override fun setAuthToken(server: String, newValue: String?) {
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
if (newValue != null) { if (newValue != null) {
val row = wrap(mapOf(Companion.server to server, token to newValue)) 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() )) val row = wrap(mapOf( lastDeletionServerIDCacheIndex to index, lastDeletionServerID to newValue.toString() ))
database.insertOrUpdate(lastDeletionServerIDCache, row, "$lastDeletionServerIDCacheIndex = ?", wrap(index)) 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 // region Convenience

View File

@ -89,4 +89,10 @@ class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) :
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
database.delete(tableName, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey )) 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
}
} }

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

View File

@ -28,7 +28,7 @@ class QRCodeView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : Li
init { init {
inflate(context, R.layout.view_qr_code, this) inflate(context, R.layout.view_qr_code, this)
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context) val hexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(getContext()) ?: TextSecurePreferences.getLocalNumber(context)
val displayMetrics = DisplayMetrics() val displayMetrics = DisplayMetrics()
ServiceUtil.getWindowManager(context).defaultDisplay.getMetrics(displayMetrics) ServiceUtil.getWindowManager(context).defaultDisplay.getMetrics(displayMetrics)
val size = displayMetrics.widthPixels - 2 * toPx(96, resources) val size = displayMetrics.widthPixels - 2 * toPx(96, resources)

View File

@ -9,23 +9,33 @@ import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import kotlinx.android.synthetic.main.activity_seed.* 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 network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.ConversationListActivity
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.IdentityDatabase import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.logging.Log
import org.thoughtcrime.securesms.util.Hex import org.thoughtcrime.securesms.util.Hex
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.curve25519.Curve25519 import org.whispersystems.curve25519.Curve25519
import org.whispersystems.libsignal.util.KeyHelper import org.whispersystems.libsignal.util.KeyHelper
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
import org.whispersystems.signalservice.loki.utilities.Analytics import org.whispersystems.signalservice.loki.utilities.Analytics
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey
import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
class SeedActivity : BaseActionBarActivity() { class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
private lateinit var languageFileDirectory: File private lateinit var languageFileDirectory: File
private var mode = Mode.Register private var mode = Mode.Register
set(newValue) { field = newValue; updateUI() } set(newValue) { field = newValue; updateUI() }
@ -35,7 +45,7 @@ class SeedActivity : BaseActionBarActivity() {
set(newValue) { field = newValue; updateMnemonicTextView() } set(newValue) { field = newValue; updateMnemonicTextView() }
// region Types // region Types
enum class Mode { Register, Restore } enum class Mode { Register, Restore, Link }
// endregion // endregion
// region Lifecycle // region Lifecycle
@ -45,8 +55,10 @@ class SeedActivity : BaseActionBarActivity() {
setUpLanguageFileDirectory() setUpLanguageFileDirectory()
updateSeed() updateSeed()
copyButton.setOnClickListener { copy() } copyButton.setOnClickListener { copy() }
toggleModeButton.setOnClickListener { toggleMode() } toggleRegisterModeButton.setOnClickListener { mode = Mode.Register }
registerOrRestoreButton.setOnClickListener { registerOrRestore() } toggleRestoreModeButton.setOnClickListener { mode = Mode.Restore }
toggleLinkModeButton.setOnClickListener { mode = Mode.Link }
mainButton.setOnClickListener { handleMainButtonTapped() }
Analytics.shared.track("Seed Screen Viewed") Analytics.shared.track("Seed Screen Viewed")
} }
// endregion // endregion
@ -86,15 +98,25 @@ class SeedActivity : BaseActionBarActivity() {
} }
private fun updateUI() { private fun updateUI() {
seedExplanationTextView1.visibility = if (mode == Mode.Register) View.VISIBLE else View.GONE val registerModeVisibility = if (mode == Mode.Register) View.VISIBLE else View.GONE
mnemonicTextView.visibility = if (mode == Mode.Register) View.VISIBLE else View.GONE val restoreModeVisibility = if (mode == Mode.Restore) View.VISIBLE else View.GONE
copyButton.visibility = if (mode == Mode.Register) View.VISIBLE else View.GONE val linkModeVisibility = if (mode == Mode.Link) View.VISIBLE else View.GONE
seedExplanationTextView2.visibility = if (mode == Mode.Restore) View.VISIBLE else View.GONE seedExplanationTextView1.visibility = registerModeVisibility
mnemonicEditText.visibility = if (mode == Mode.Restore) View.VISIBLE else View.GONE mnemonicTextView.visibility = registerModeVisibility
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 copyButton.visibility = registerModeVisibility
toggleModeButton.setText(toggleModeButtonTitleID) seedExplanationTextView2.visibility = restoreModeVisibility
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 mnemonicEditText.visibility = restoreModeVisibility
registerOrRestoreButton.setText(registerOrRestoreButtonTitleID) 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) { if (mode == Mode.Restore) {
mnemonicEditText.requestFocus() mnemonicEditText.requestFocus()
} else { } else {
@ -102,6 +124,13 @@ class SeedActivity : BaseActionBarActivity() {
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(mnemonicEditText.windowToken, 0) 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() { 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() Toast.makeText(this, R.string.activity_key_pair_mnemonic_copied_message, Toast.LENGTH_SHORT).show()
} }
private fun toggleMode() { private fun handleMainButtonTapped() {
mode = when (mode) {
Mode.Register -> Mode.Restore
Mode.Restore -> Mode.Register
}
}
private fun registerOrRestore() {
var seed: ByteArray var seed: ByteArray
when (mode) { when (mode) {
Mode.Register -> seed = this.seed!! Mode.Register -> seed = this.seed!!
@ -143,10 +165,17 @@ class SeedActivity : BaseActionBarActivity() {
return Toast.makeText(this, message, Toast.LENGTH_SHORT).show() 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) val hexEncodedSeed = Hex.toStringCondensed(seed)
IdentityKeyUtil.save(this, IdentityKeyUtil.lokiSeedKey, hexEncodedSeed) IdentityKeyUtil.save(this, IdentityKeyUtil.lokiSeedKey, hexEncodedSeed)
if (seed.count() == 16) seed = seed + seed if (seed.count() == 16) seed += seed
if (mode == Mode.Restore) { if (mode == Mode.Restore) {
IdentityKeyUtil.generateIdentityKeyPair(this, seed) IdentityKeyUtil.generateIdentityKeyPair(this, seed)
} }
@ -161,13 +190,69 @@ class SeedActivity : BaseActionBarActivity() {
when (mode) { when (mode) {
Mode.Register -> Analytics.shared.track("Seed Created") Mode.Register -> Analytics.shared.track("Seed Created")
Mode.Restore -> Analytics.shared.track("Seed Restored") Mode.Restore -> Analytics.shared.track("Seed Restored")
// TODO: Mode.Link -> Analytics.shared.track("Device Linking Attempted") Mode.Link -> Analytics.shared.track("Device Linking Attempted")
} }
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)) startActivity(Intent(this, DisplayNameActivity::class.java))
finish() finish()
} }
}
// TODO: Analytics.shared.track("Device Linked Successfully") 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()
}
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 // endregion
} }

View File

@ -20,14 +20,16 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob; import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
import org.thoughtcrime.securesms.service.ExpiringMessageManager; 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.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import kotlin.Unit;
public class MarkReadReceiver extends BroadcastReceiver { public class MarkReadReceiver extends BroadcastReceiver {
private static final String TAG = MarkReadReceiver.class.getSimpleName(); private static final String TAG = MarkReadReceiver.class.getSimpleName();
@ -86,17 +88,17 @@ public class MarkReadReceiver extends BroadcastReceiver {
.collect(Collectors.groupingBy(SyncMessageId::getAddress)); .collect(Collectors.groupingBy(SyncMessageId::getAddress));
for (Address address : addressMap.keySet()) { for (Address address : addressMap.keySet()) {
// Loki - This also prevents read receipts from being sent in group chats as they don't maintain a friend request status LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
long threadID = lokiThreadDatabase.getThreadID(address.serialize());
LokiThreadFriendRequestStatus friendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
if (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) { return; }
List<Long> timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList(); List<Long> timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList();
ApplicationContext.getInstance(context) MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, address.serialize(), storageAPI, (devicePublicKey, isFriend, friendCount) -> {
.getJobManager() // Loki - This also prevents read receipts from being sent in group chats as they don't maintain a friend request status
.add(new SendReadReceiptJob(address, timestamps)); if (isFriend) {
ApplicationContext.getInstance(context).getJobManager().add(new SendReadReceiptJob(Address.fromSerialized(devicePublicKey), timestamps));
}
return Unit.INSTANCE;
});
} }
} }

View File

@ -42,6 +42,8 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.GeneralUtilitiesKt;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilitiesKt;
import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.push.AccountManagerFactory; import org.thoughtcrime.securesms.push.AccountManagerFactory;
@ -52,10 +54,13 @@ import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus; import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus;
import java.io.IOException; import java.io.IOException;
import kotlin.Unit;
public class MessageSender { public class MessageSender {
private static final String TAG = MessageSender.class.getSimpleName(); 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) { private static void sendTextPush(Context context, Recipient recipient, long messageId) {
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
// 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())); jobManager.add(new PushTextSendJob(messageId, recipient.getAddress()));
return;
}
MultiDeviceUtilitiesKt.getAllDevicePublicKeys(context, recipientPublicKey, storageAPI, (devicePublicKey, isFriend, friendCount) -> {
Address address = Address.fromSerialized(devicePublicKey);
long messageIDToUse = recipientPublicKey.equals(devicePublicKey) ? messageId : -1L;
if (isFriend) {
// Send a normal message if the user is friends with the recipient
jobManager.add(new PushTextSendJob(messageId, messageIDToUse, address));
} else {
// Send friend requests to non friends. If the user is friends with any
// of the devices then send out a default friend request message.
boolean isFriendsWithAny = (friendCount > 0);
String defaultFriendRequestMessage = isFriendsWithAny ? "Accept this friend request to enable messages to be synced across devices" : null;
jobManager.add(new PushTextSendJob(messageId, messageIDToUse, address, true, defaultFriendRequestMessage));
}
return Unit.INSTANCE;
});
} }
private static void sendMediaPush(Context context, Recipient recipient, long messageId) { private static void sendMediaPush(Context context, Recipient recipient, long messageId) {
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager(); JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
// 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()); 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) { private static void sendGroupPush(Context context, Recipient recipient, long messageId, Address filterAddress) {

View File

@ -546,6 +546,10 @@ public class TextSecurePreferences {
setIntegerPrefrence(context, LOCAL_REGISTRATION_ID_PREF, registrationId); 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) { public static boolean isInThreadNotifications(Context context) {
return getBooleanPreference(context, IN_THREAD_NOTIFICATION_PREF, true); return getBooleanPreference(context, IN_THREAD_NOTIFICATION_PREF, true);
} }
@ -639,6 +643,10 @@ public class TextSecurePreferences {
setStringPreference(context, LOCAL_NUMBER_PREF, localNumber); setStringPreference(context, LOCAL_NUMBER_PREF, localNumber);
} }
public static void removeLocalNumber(Context context) {
removePreference(context, LOCAL_NUMBER_PREF);
}
public static String getPushServerPassword(Context context) { public static String getPushServerPassword(Context context) {
return getStringPreference(context, GCM_PASSWORD_PREF, null); return getStringPreference(context, GCM_PASSWORD_PREF, null);
} }
@ -1169,5 +1177,13 @@ public class TextSecurePreferences {
public static void markChatSetUp(Context context, String id) { public static void markChatSetUp(Context context, String id) {
setBooleanPreference(context, "is_chat_set_up" + "?chat=" + id, true); 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 // endregion
} }