mirror of
https://github.com/oxen-io/session-android.git
synced 2025-04-16 03:41:25 +00:00
Merge pull request #31 from loki-project/multi-device
Multi Device Support
This commit is contained in:
commit
7116f2502a
@ -20,7 +20,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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 -->
|
||||||
|
@ -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; }
|
||||||
|
@ -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");
|
||||||
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
|
@ -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 + ".");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
@ -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) { }
|
||||||
}
|
}
|
137
src/org/thoughtcrime/securesms/loki/DeviceLinkingView.kt
Normal file
137
src/org/thoughtcrime/securesms/loki/DeviceLinkingView.kt
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package org.thoughtcrime.securesms.loki
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.os.Handler
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import kotlinx.android.synthetic.main.view_device_linking.view.*
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
|
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||||
|
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||||
|
import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
class DeviceLinkingView private constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, private val mode: Mode, private var delegate: DeviceLinkingViewDelegate) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
|
private lateinit var languageFileDirectory: File
|
||||||
|
var dismiss: (() -> Unit)? = null
|
||||||
|
var pairingAuthorisation: PairingAuthorisation? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
// region Types
|
||||||
|
enum class Mode { Master, Slave }
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Lifecycle
|
||||||
|
constructor(context: Context, mode: Mode, delegate: DeviceLinkingViewDelegate) : this(context, null, 0, mode, delegate)
|
||||||
|
private constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0, Mode.Master, object : DeviceLinkingViewDelegate { }) // Just pass in a dummy mode
|
||||||
|
private constructor(context: Context) : this(context, null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
setUpLanguageFileDirectory()
|
||||||
|
setUpViewHierarchy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUpLanguageFileDirectory() {
|
||||||
|
val languages = listOf( "english", "japanese", "portuguese", "spanish" )
|
||||||
|
val directory = File(context.applicationInfo.dataDir)
|
||||||
|
for (language in languages) {
|
||||||
|
val fileName = "$language.txt"
|
||||||
|
if (directory.list().contains(fileName)) { continue }
|
||||||
|
val inputStream = context.assets.open("mnemonic/$fileName")
|
||||||
|
val file = File(directory, fileName)
|
||||||
|
val outputStream = FileOutputStream(file)
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
while (true) {
|
||||||
|
val count = inputStream.read(buffer)
|
||||||
|
if (count < 0) { break }
|
||||||
|
outputStream.write(buffer, 0, count)
|
||||||
|
}
|
||||||
|
inputStream.close()
|
||||||
|
outputStream.close()
|
||||||
|
}
|
||||||
|
languageFileDirectory = directory
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUpViewHierarchy() {
|
||||||
|
inflate(context, R.layout.view_device_linking, this)
|
||||||
|
spinner.indeterminateDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)
|
||||||
|
val titleID = when (mode) {
|
||||||
|
Mode.Master -> R.string.view_device_linking_title_1
|
||||||
|
Mode.Slave -> R.string.view_device_linking_title_2
|
||||||
|
}
|
||||||
|
titleTextView.text = resources.getString(titleID)
|
||||||
|
val explanationID = when (mode) {
|
||||||
|
Mode.Master -> R.string.view_device_linking_explanation_1
|
||||||
|
Mode.Slave -> R.string.view_device_linking_explanation_2
|
||||||
|
}
|
||||||
|
explanationTextView.text = resources.getString(explanationID)
|
||||||
|
mnemonicTextView.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE
|
||||||
|
if (mode == Mode.Slave) {
|
||||||
|
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded()
|
||||||
|
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
|
||||||
|
}
|
||||||
|
authorizeButton.visibility = View.GONE
|
||||||
|
authorizeButton.setOnClickListener { authorizePairing() }
|
||||||
|
cancelButton.setOnClickListener { cancel() }
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Device Linking
|
||||||
|
fun requestUserAuthorization(pairingAuthorisation: PairingAuthorisation) {
|
||||||
|
if (mode != Mode.Master || pairingAuthorisation.type != PairingAuthorisation.Type.REQUEST || this.pairingAuthorisation != null) { return }
|
||||||
|
this.pairingAuthorisation = pairingAuthorisation
|
||||||
|
spinner.visibility = View.GONE
|
||||||
|
val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams
|
||||||
|
titleTextViewLayoutParams.topMargin = toPx(16, resources)
|
||||||
|
titleTextView.layoutParams = titleTextViewLayoutParams
|
||||||
|
titleTextView.text = resources.getString(R.string.view_device_linking_title_3)
|
||||||
|
explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_2)
|
||||||
|
mnemonicTextView.visibility = View.VISIBLE
|
||||||
|
val hexEncodedPublicKey = pairingAuthorisation.secondaryDevicePublicKey.removing05PrefixIfNeeded()
|
||||||
|
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
|
||||||
|
authorizeButton.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
|
||||||
|
if (mode != Mode.Slave || pairingAuthorisation.type != PairingAuthorisation.Type.GRANT || this.pairingAuthorisation != null) { return }
|
||||||
|
this.pairingAuthorisation = pairingAuthorisation
|
||||||
|
spinner.visibility = View.GONE
|
||||||
|
val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams
|
||||||
|
titleTextViewLayoutParams.topMargin = toPx(8, resources)
|
||||||
|
titleTextView.layoutParams = titleTextViewLayoutParams
|
||||||
|
titleTextView.text = resources.getString(R.string.view_device_linking_title_4)
|
||||||
|
val explanationTextViewLayoutParams = explanationTextView.layoutParams as LayoutParams
|
||||||
|
explanationTextViewLayoutParams.bottomMargin = toPx(12, resources)
|
||||||
|
explanationTextView.layoutParams = explanationTextViewLayoutParams
|
||||||
|
explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_3)
|
||||||
|
titleTextView.text = resources.getString(R.string.view_device_linking_title_4)
|
||||||
|
mnemonicTextView.visibility = View.GONE
|
||||||
|
buttonContainer.visibility = View.GONE
|
||||||
|
cancelButton.visibility = View.GONE
|
||||||
|
Handler().postDelayed({
|
||||||
|
delegate.handleDeviceLinkAuthorized(pairingAuthorisation)
|
||||||
|
dismiss?.invoke()
|
||||||
|
}, 4000)
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Interaction
|
||||||
|
private fun authorizePairing() {
|
||||||
|
val pairingAuthorisation = this.pairingAuthorisation
|
||||||
|
if (mode != Mode.Master || pairingAuthorisation == null) { return; }
|
||||||
|
delegate.sendPairingAuthorizedMessage(pairingAuthorisation)
|
||||||
|
delegate.handleDeviceLinkAuthorized(pairingAuthorisation)
|
||||||
|
dismiss?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancel() {
|
||||||
|
delegate.handleDeviceLinkingDialogDismissed()
|
||||||
|
dismiss?.invoke()
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package org.thoughtcrime.securesms.loki
|
||||||
|
|
||||||
|
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||||
|
|
||||||
|
interface DeviceLinkingViewDelegate {
|
||||||
|
|
||||||
|
fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) { }
|
||||||
|
fun handleDeviceLinkingDialogDismissed() { }
|
||||||
|
fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) { }
|
||||||
|
}
|
@ -42,6 +42,7 @@ class DisplayNameActivity : BaseActionBarActivity() {
|
|||||||
val application = ApplicationContext.getInstance(this)
|
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)
|
||||||
|
@ -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()
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
87
src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt
Normal file
87
src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package org.thoughtcrime.securesms.loki
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import nl.komponents.kovenant.Promise
|
||||||
|
import nl.komponents.kovenant.deferred
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
|
import org.thoughtcrime.securesms.database.Address
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
|
import org.thoughtcrime.securesms.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional
|
||||||
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
|
||||||
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
|
||||||
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||||
|
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
|
||||||
|
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||||
|
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
|
||||||
|
|
||||||
|
fun getAllDevicePublicKeys(context: Context, hexEncodedPublicKey: String, storageAPI: LokiStorageAPI, block: (devicePublicKey: String, isFriend: Boolean, friendCount: Int) -> Unit) {
|
||||||
|
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||||
|
storageAPI.getAllDevicePublicKeys(hexEncodedPublicKey).success { items ->
|
||||||
|
val devices = items.toMutableSet()
|
||||||
|
if (hexEncodedPublicKey != userHexEncodedPublicKey) {
|
||||||
|
devices.remove(userHexEncodedPublicKey)
|
||||||
|
}
|
||||||
|
val friends = getFriendPublicKeys(context, devices)
|
||||||
|
for (device in devices) {
|
||||||
|
block(device, friends.contains(device), friends.count())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shouldAutomaticallyBecomeFriendsWithDevice(publicKey: String, context: Context): Promise<Boolean, Unit> {
|
||||||
|
val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context)
|
||||||
|
val storageAPI = LokiStorageAPI.shared
|
||||||
|
val deferred = deferred<Boolean, Unit>()
|
||||||
|
storageAPI.getPrimaryDevicePublicKey(publicKey).success { primaryDevicePublicKey ->
|
||||||
|
if (primaryDevicePublicKey == null) {
|
||||||
|
deferred.resolve(false)
|
||||||
|
return@success
|
||||||
|
}
|
||||||
|
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||||
|
if (primaryDevicePublicKey == userHexEncodedPublicKey) {
|
||||||
|
storageAPI.getSecondaryDevicePublicKeys(userHexEncodedPublicKey).success { secondaryDevices ->
|
||||||
|
deferred.resolve(secondaryDevices.contains(publicKey))
|
||||||
|
}.fail {
|
||||||
|
deferred.resolve(false)
|
||||||
|
}
|
||||||
|
return@success
|
||||||
|
}
|
||||||
|
val primaryDevice = Recipient.from(context, Address.fromSerialized(primaryDevicePublicKey), false)
|
||||||
|
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(primaryDevice)
|
||||||
|
if (threadID < 0) {
|
||||||
|
deferred.resolve(false)
|
||||||
|
return@success
|
||||||
|
}
|
||||||
|
deferred.resolve(lokiThreadDatabase.getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS)
|
||||||
|
}
|
||||||
|
return deferred.promise
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendPairingAuthorisationMessage(context: Context, contactHexEncodedPublicKey: String, authorisation: PairingAuthorisation): Promise<Unit, Exception> {
|
||||||
|
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||||
|
val address = SignalServiceAddress(contactHexEncodedPublicKey)
|
||||||
|
val message = SignalServiceDataMessage.newBuilder().withBody("").withPairingAuthorisation(authorisation)
|
||||||
|
// A REQUEST should always act as a friend request. A GRANT should always be replying back as a normal message.
|
||||||
|
if (authorisation.type == PairingAuthorisation.Type.REQUEST) {
|
||||||
|
val preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.number)
|
||||||
|
message.asFriendRequest(true).withPreKeyBundle(preKeyBundle)
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
Log.d("Loki", "Sending authorisation message to: $contactHexEncodedPublicKey.")
|
||||||
|
val result = messageSender.sendMessage(0, address, Optional.absent<UnidentifiedAccessPair>(), message.build())
|
||||||
|
if (result.success == null) {
|
||||||
|
val exception = when {
|
||||||
|
result.isNetworkFailure -> "Failed to send authorisation message due to a network error."
|
||||||
|
else -> "Failed to send authorisation message."
|
||||||
|
}
|
||||||
|
throw Exception(exception)
|
||||||
|
}
|
||||||
|
Promise.ofSuccess(Unit)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d("Loki", "Failed to send authorisation message to: $contactHexEncodedPublicKey.")
|
||||||
|
Promise.ofFail(e)
|
||||||
|
}
|
||||||
|
}
|
@ -28,7 +28,7 @@ class QRCodeView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : Li
|
|||||||
|
|
||||||
init {
|
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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user