Merge branch 'dev'

This commit is contained in:
Niels Andriesse 2020-02-17 16:55:48 +11:00
commit 68785fe44f
60 changed files with 740 additions and 955 deletions

View File

@ -199,8 +199,8 @@ dependencies {
implementation "com.github.ybq:Android-SpinKit:1.4.0"
}
def canonicalVersionCode = 35
def canonicalVersionName = "1.0.1"
def canonicalVersionCode = 36
def canonicalVersionName = "1.0.2"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/compose_view_background" />
</shape>

View File

@ -30,7 +30,7 @@
android:textAlignment="center"
android:text="Users can share their Session ID by going into their account settings and tapping &quot;Share Session ID&quot;, or by sharing their QR code." />
<org.thoughtcrime.securesms.loki.redesign.views.SeparatorView
<org.thoughtcrime.securesms.loki.redesign.views.LabeledSeparatorView
android:id="@+id/separatorView"
android:layout_width="match_parent"
android:layout_height="32dp"

View File

@ -1,58 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_height="match_parent"
android:background="@drawable/default_session_background"
android:orientation="vertical">
<TextView
android:id="@+id/titleTextView"
style="@style/Signal.Text.Headline.Registration"
<View
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:text="@string/activity_display_name_title"
android:textAlignment="center" />
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:id="@+id/subtitleTextView"
style="@style/Signal.Text.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/activity_display_name_subtitle"
android:textAlignment="center" />
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginRight="@dimen/very_large_spacing"
android:textSize="@dimen/large_font_size"
android:textStyle="bold"
android:textColor="@color/text"
android:text="Pick your display name" />
<org.thoughtcrime.securesms.components.LabeledEditText
android:id="@+id/nameEditText"
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
app:labeledEditText_background="@color/loki_darkest_gray"
app:labeledEditText_label="@string/activity_display_name_name_edit_text_label"/>
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginTop="6dp"
android:layout_marginRight="@dimen/very_large_spacing"
android:textSize="@dimen/small_font_size"
android:textColor="@color/text"
android:text="This will be your name when you use Session." />
<com.dd.CircularProgressButton
android:id="@+id/nextButton"
<EditText
style="@style/SmallSessionEditText"
android:id="@+id/displayNameEditText"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="40dp"
android:layout_gravity="center_horizontal"
android:background="@color/signal_primary"
android:textColor="@color/white"
app:cpb_colorIndicator="@color/white"
app:cpb_colorProgress="@color/textsecure_primary"
app:cpb_cornerRadius="4dp"
app:cpb_selectorIdle="@drawable/progress_button_state"
app:cpb_textIdle="@string/activity_display_name_button_title" />
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginTop="12dp"
android:layout_marginRight="@dimen/very_large_spacing"
android:hint="Enter a display name" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</ScrollView>
<Button
style="@style/MediumProminentFilledButton"
android:id="@+id/registerButton"
android:layout_width="match_parent"
android:layout_height="@dimen/medium_button_height"
android:layout_marginLeft="@dimen/massive_spacing"
android:layout_marginRight="@dimen/massive_spacing"
android:layout_marginBottom="@dimen/small_spacing"
android:text="Continue" />
</LinearLayout>

View File

@ -1,59 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/default_session_background"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginRight="@dimen/very_large_spacing"
android:textSize="@dimen/large_font_size"
android:textStyle="bold"
android:textColor="@color/text"
android:text="Pick your display name" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginTop="6dp"
android:layout_marginRight="@dimen/very_large_spacing"
android:textSize="@dimen/small_font_size"
android:textColor="@color/text"
android:text="This will be your name when you use Session." />
<EditText
style="@style/SmallSessionEditText"
android:id="@+id/displayNameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginTop="12dp"
android:layout_marginRight="@dimen/very_large_spacing"
android:hint="Enter a display name" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<Button
style="@style/MediumProminentFilledButton"
android:id="@+id/registerButton"
android:layout_width="match_parent"
android:layout_height="@dimen/medium_button_height"
android:layout_marginLeft="@dimen/massive_spacing"
android:layout_marginRight="@dimen/massive_spacing"
android:layout_marginBottom="@dimen/small_spacing"
android:text="Continue" />
</LinearLayout>

View File

@ -1,158 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".loki.SeedActivity">
android:background="@drawable/default_session_background"
android:gravity="center_horizontal"
android:orientation="vertical">
<LinearLayout
<org.thoughtcrime.securesms.loki.redesign.views.SeedReminderView
android:id="@+id/seedReminderView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:orientation="vertical"
android:animateLayoutChanges="true">
android:layout_height="wrap_content" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:id="@+id/titleTextView"
style="@style/Signal.Text.Headline.Registration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:text="@string/activity_key_pair_title"
android:textAlignment="center" />
<TextView
android:id="@+id/seedExplanationTextView1"
style="@style/Signal.Text.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/activity_key_pair_seed_explanation_1"
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginRight="@dimen/very_large_spacing"
android:textSize="@dimen/large_font_size"
android:textStyle="bold"
android:textAlignment="center" />
android:textColor="@color/text"
android:text="Meet your recovery phrase" />
<TextView
android:id="@+id/mnemonicTextView"
style="@style/Signal.Text.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:alpha="0.8"
android:textStyle="italic"
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginTop="10dp"
android:layout_marginRight="@dimen/very_large_spacing"
android:textSize="@dimen/medium_font_size"
android:textColor="@color/text"
android:text="Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and dont give it to anyone. To restore your Session ID, launch Session and tap Continue your Session." />
<TextView
style="@style/SessionIDTextView"
android:id="@+id/seedTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginTop="@dimen/medium_spacing"
android:layout_marginRight="@dimen/very_large_spacing"
android:gravity="center"
android:textSize="@dimen/medium_font_size"
android:textAlignment="center"
tools:text="quick brown fox jump lazy dog" />
android:text="nautical novelty populate onion awkward bent etiquette plant submarine itches vipers september axis maximum populate" />
<TextView
android:id="@+id/revealButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:textAlignment="center"
android:textSize="@dimen/medium_font_size"
android:textColor="@color/text"
android:alpha="0.6"
android:text="Hold to reveal" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<Button
style="@style/MediumProminentOutlineButton"
android:id="@+id/copyButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="20dp"
android:background="@color/transparent"
android:textColor="@color/signal_primary"
android:text="@string/activity_key_pair_copy_button_title"
android:elevation="0dp"
android:stateListAnimator="@null" />
android:layout_width="196dp"
android:layout_height="@dimen/medium_button_height"
android:layout_marginBottom="@dimen/medium_spacing"
android:text="Copy" />
<TextView
android:id="@+id/seedExplanationTextView2"
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_2"
android:textAlignment="center" />
<org.thoughtcrime.securesms.components.LabeledEditText
android:id="@+id/mnemonicEditText"
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_mnemonic_edit_text_label"/>
<TextView
android:id="@+id/linkExplanationTextView"
style="@style/Signal.Text.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:visibility="gone"
android:text="@string/activity_key_pair_seed_explanation_3"
android:textAlignment="center" />
<org.thoughtcrime.securesms.components.LabeledEditText
android:id="@+id/publicKeyEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="20dp"
android:visibility="gone"
app:labeledEditText_background="@color/loki_darkest_gray"
app:labeledEditText_label="@string/activity_key_pair_public_key_edit_text_label"/>
<Button
android:id="@+id/scanQRButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/transparent"
android:elevation="0dp"
android:stateListAnimator="@null"
android:text="@string/fragment_scan_qr_code_title"
android:textColor="@color/signal_primary"
android:visibility="gone" />
<Button
android:id="@+id/toggleRestoreModeButton"
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_1"
android:elevation="0dp"
android:stateListAnimator="@null" />
<Button
android:id="@+id/toggleRegisterModeButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/transparent"
android:textColor="@color/signal_primary"
android:text="@string/activity_key_pair_toggle_mode_button_title_2"
android:visibility="gone"
android:elevation="0dp"
android:stateListAnimator="@null" />
<Button
android:id="@+id/toggleLinkModeButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/transparent"
android:textColor="@color/signal_primary"
android:text="@string/activity_key_pair_toggle_mode_button_title_3"
android:elevation="0dp"
android:stateListAnimator="@null" />
<com.dd.CircularProgressButton
android:id="@+id/mainButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="40dp"
android:layout_gravity="center_horizontal"
android:background="@color/signal_primary"
android:textColor="@color/white"
app:cpb_colorIndicator="@color/white"
app:cpb_colorProgress="@color/textsecure_primary"
app:cpb_cornerRadius="4dp"
app:cpb_selectorIdle="@drawable/progress_button_state"
app:cpb_textIdle="@string/activity_key_pair_main_button_title_1" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -1,77 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/default_session_background"
android:gravity="center_horizontal"
android:orientation="vertical">
<org.thoughtcrime.securesms.loki.redesign.views.SeedReminderView
android:id="@+id/seedReminderView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginRight="@dimen/very_large_spacing"
android:textSize="@dimen/large_font_size"
android:textStyle="bold"
android:textColor="@color/text"
android:text="Meet your recovery phrase" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginTop="10dp"
android:layout_marginRight="@dimen/very_large_spacing"
android:textSize="@dimen/medium_font_size"
android:textColor="@color/text"
android:text="Your recovery phrase is the master key to your Session ID — you can use it to restore your Session ID if you lose access to your device. Store your recovery phrase in a safe place, and dont give it to anyone. To restore your Session ID, launch Session and tap Continue your Session." />
<TextView
style="@style/SessionIDTextView"
android:id="@+id/seedTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/very_large_spacing"
android:layout_marginTop="@dimen/medium_spacing"
android:layout_marginRight="@dimen/very_large_spacing"
android:gravity="center"
android:textSize="@dimen/medium_font_size"
android:textAlignment="center"
android:text="nautical novelty populate onion awkward bent etiquette plant submarine itches vipers september axis maximum populate" />
<TextView
android:id="@+id/revealButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:textAlignment="center"
android:textSize="@dimen/medium_font_size"
android:textColor="@color/text"
android:alpha="0.6"
android:text="Hold to reveal" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<Button
style="@style/MediumProminentOutlineButton"
android:id="@+id/copyButton"
android:layout_width="196dp"
android:layout_height="@dimen/medium_button_height"
android:layout_marginBottom="@dimen/medium_spacing"
android:text="Copy" />
</LinearLayout>

View File

@ -107,7 +107,7 @@
</RelativeLayout>
<org.thoughtcrime.securesms.loki.redesign.views.SeparatorView
<org.thoughtcrime.securesms.loki.redesign.views.LabeledSeparatorView
android:id="@+id/separatorView"
android:layout_width="match_parent"
android:layout_height="32dp"

View File

@ -38,7 +38,7 @@
android:id="@+id/moderator_icon_image_view"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/icon_crown"
android:src="@drawable/ic_crown"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true" />

View File

@ -30,7 +30,7 @@
android:textAlignment="center"
android:text="Users can share their Session ID by going into their account settings and tapping &quot;Share Session ID&quot;, or by sharing their QR code." />
<org.thoughtcrime.securesms.loki.redesign.views.SeparatorView
<org.thoughtcrime.securesms.loki.redesign.views.LabeledSeparatorView
android:id="@+id/separatorView"
android:layout_width="match_parent"
android:layout_height="32dp"

View File

@ -23,7 +23,7 @@
android:id="@+id/moderatorIconImageView"
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/icon_crown"
android:src="@drawable/ic_crown"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true" />

View File

@ -1618,13 +1618,13 @@
<!-- Friend request view -->
<string name="view_friend_request_accept_button_title">Accept</string>
<string name="view_friend_request_reject_button_title">Decline</string>
<string name="view_friend_request_incoming_pending_message">%1$s sent you a message request</string>
<string name="view_friend_request_incoming_accepted_message">You\'ve accepted %1$s\'s message request</string>
<string name="view_friend_request_incoming_declined_message">You\'ve declined %1$s\'s message request</string>
<string name="view_friend_request_incoming_expired_message">%1$s\'s message request has expired</string>
<string name="view_friend_request_outgoing_pending_message">You\'ve sent %1$s a message request</string>
<string name="view_friend_request_outgoing_accepted_message">%1$s accepted your message request</string>
<string name="view_friend_request_outgoing_expired_message">Your message request to %1$s has expired</string>
<string name="view_friend_request_incoming_pending_message">%1$s sent you a session request</string>
<string name="view_friend_request_incoming_accepted_message">You\'ve accepted %1$s\'s session request</string>
<string name="view_friend_request_incoming_declined_message">You\'ve declined %1$s\'s session request</string>
<string name="view_friend_request_incoming_expired_message">%1$s\'s session request has expired</string>
<string name="view_friend_request_outgoing_pending_message">You\'ve sent %1$s a session request</string>
<string name="view_friend_request_outgoing_accepted_message">%1$s accepted your session request</string>
<string name="view_friend_request_outgoing_expired_message">Your session request to %1$s has expired</string>
<!-- Conversation activity -->
<string name="activity_conversation_pending_friend_request_hint">Pending Friend Request…</string>
<string name="activity_conversation_default_hint">New Message</string>

View File

@ -6,6 +6,7 @@
<domain includeSubdomains="true">storage.seed1.loki.network</domain>
<domain includeSubdomains="true">storage.seed2.loki.network</domain>
<domain includeSubdomains="true">public.loki.foundation:22023</domain>
<domain includeSubdomains="true">file-dev.lokinet.org</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
</domain-config>
</network-security-config>

View File

@ -96,14 +96,13 @@ import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol;
import org.whispersystems.signalservice.loki.api.LokiDotNetAPI;
import org.whispersystems.signalservice.loki.api.LokiFileServerAPI;
import org.whispersystems.signalservice.loki.api.LokiLongPoller;
import org.whispersystems.signalservice.loki.api.LokiP2PAPI;
import org.whispersystems.signalservice.loki.api.LokiP2PAPIDelegate;
import org.whispersystems.signalservice.loki.api.LokiPublicChat;
import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI;
import org.whispersystems.signalservice.loki.api.LokiRSSFeed;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import java.security.Security;
import java.util.ArrayList;
@ -115,7 +114,6 @@ import java.util.concurrent.TimeUnit;
import dagger.ObjectGraph;
import kotlin.Unit;
import network.loki.messenger.BuildConfig;
import okhttp3.Cache;
import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant;
import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
@ -184,18 +182,19 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
// Loki - Set up P2P API if needed
setUpP2PAPI();
// Loki - Set the cache
LokiDotNetAPI.setCache(new Cache(this.getCacheDir(), OK_HTTP_CACHE_SIZE));
// Loki - Update device mappings
if (setUpStorageAPIIfNeeded()) {
LokiStorageAPI.Companion.getShared().updateUserDeviceMappings();
if (TextSecurePreferences.needsRevocationCheck(this)) {
checkNeedsRevocation();
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userHexEncodedPublicKey != null) {
LokiFileServerAPI.Companion.getShared().getDeviceLinks(userHexEncodedPublicKey, true);
if (TextSecurePreferences.getNeedsIsRevokedSlaveDeviceCheck(this)) {
MultiDeviceUtilities.checkIsRevokedSlaveDevice(this);
}
}
}
// Loki - Set up public chat manager
lokiPublicChatManager = new LokiPublicChatManager(this);
updatePublicChatProfileAvatarIfNeeded();
updatePublicChatProfilePictureIfNeeded();
}
@Override
@ -206,6 +205,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
KeyCachingService.onAppForegrounded(this);
// Loki - Start long polling if needed
startLongPollingIfNeeded();
// Loki - Start open group polling if needed
lokiPublicChatManager.startPollersIfNeeded();
}
@ -466,7 +466,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
boolean isDebugMode = BuildConfig.DEBUG;
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
LokiAPIDatabaseProtocol database = DatabaseFactory.getLokiAPIDatabase(this);
LokiStorageAPI.Companion.configure(isDebugMode, userHexEncodedPublicKey, userPrivateKey, database);
LokiFileServerAPI.Companion.configure(isDebugMode, userHexEncodedPublicKey, userPrivateKey, database);
return true;
}
return false;
@ -534,7 +534,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
public void createRSSFeedsIfNeeded() {
ArrayList<LokiRSSFeed> feeds = new ArrayList<>();
feeds.add(lokiNewsFeed());
// feeds.add(lokiNewsFeed());
feeds.add(lokiMessengerUpdatesFeed());
for (LokiRSSFeed feed : feeds) {
boolean isFeedSetUp = TextSecurePreferences.isChatSetUp(this, feed.getId());
@ -590,7 +590,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
if (lokiMessengerUpdatesFeedPoller != null) lokiMessengerUpdatesFeedPoller.startIfNeeded();
}
public void updatePublicChatProfileAvatarIfNeeded() {
public void updatePublicChatProfilePictureIfNeeded() {
AsyncTask.execute(() -> {
LokiPublicChatAPI publicChatAPI = null;
try {
@ -616,11 +616,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
}
});
}
// endregion
public void checkNeedsRevocation() {
MultiDeviceUtilities.checkForRevocation(this);
}
public void checkNeedsDatabaseReset() {
if (TextSecurePreferences.resetDatabase(this)) {
@ -646,4 +641,5 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
this.startActivity(mainIntent);
Runtime.getRuntime().exit(0);
}
// endregion
}

View File

@ -59,7 +59,7 @@ import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.loki.api.LokiDotNetAPI;
import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.api.LokiFileServerAPI;
import java.io.ByteArrayInputStream;
import java.io.File;
@ -407,7 +407,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
//Loki - Upload the profile photo here
if (avatar != null) {
Log.d("Loki", "Start uploading profile photo");
LokiStorageAPI storageAPI = LokiStorageAPI.shared;
LokiFileServerAPI storageAPI = LokiFileServerAPI.shared;
LokiDotNetAPI.UploadResult result = storageAPI.uploadProfilePicture(storageAPI.getServer(), profileKey, avatar);
Log.d("Loki", "Profile photo uploaded, the url is " + result.getUrl());
TextSecurePreferences.setProfileAvatarUrl(context, result.getUrl());
@ -422,7 +422,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
ProfileKeyUtil.setEncodedProfileKey(context, newProfileKey);
// Update profile key on the public chat server
ApplicationContext.getInstance(context).updatePublicChatProfileAvatarIfNeeded();
ApplicationContext.getInstance(context).updatePublicChatProfilePictureIfNeeded();
} catch (Exception e) {
Log.d("Loki", "Failed to upload profile photo: " + e);
return false;

View File

@ -190,7 +190,7 @@ public class DeviceListFragment extends ListFragment
private void updateAddDeviceButtonVisibility() {
if (addDeviceButton != null) {
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
boolean isDeviceLinkingEnabled = DatabaseFactory.getLokiAPIDatabase(getContext()).getPairingAuthorisations(userHexEncodedPublicKey).isEmpty();
boolean isDeviceLinkingEnabled = DatabaseFactory.getLokiAPIDatabase(getContext()).getDeviceLinks(userHexEncodedPublicKey).isEmpty();
addDeviceButton.setVisibility(isDeviceLinkingEnabled ? View.VISIBLE : View.INVISIBLE);
}
}

View File

@ -9,14 +9,13 @@ import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.jobs.TypingSendJob;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.api.LokiDeviceLinkUtilities;
import org.whispersystems.signalservice.loki.api.LokiFileServerAPI;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import kotlin.Unit;
@ -83,7 +82,7 @@ public class TypingStatusSender {
}
private void sendTyping(long threadId, boolean typingStarted) {
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
LokiFileServerAPI storageAPI = LokiFileServerAPI.Companion.getShared();
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
Recipient recipient = threadDatabase.getRecipientForThreadId(threadId);
@ -91,7 +90,7 @@ public class TypingStatusSender {
ApplicationContext.getInstance(context).getJobManager().add(new TypingSendJob(threadId, typingStarted));
return;
}
LokiStorageAPI.shared.getAllDevicePublicKeys(recipient.getAddress().serialize()).success(devices -> {
LokiDeviceLinkUtilities.INSTANCE.getAllLinkedDeviceHexEncodedPublicKeys(recipient.getAddress().serialize()).success(devices -> {
for (String device : devices) {
Recipient deviceRecipient = Recipient.from(context, Address.fromSerialized(device), false);
long deviceThreadID = threadDatabase.getThreadIdIfExistsFor(deviceRecipient);

View File

@ -232,10 +232,10 @@ import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.loki.api.DeviceLink;
import org.whispersystems.signalservice.loki.api.LokiAPI;
import org.whispersystems.signalservice.loki.api.LokiDeviceLinkUtilities;
import org.whispersystems.signalservice.loki.api.LokiPublicChat;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.api.PairingAuthorisation;
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus;
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.messaging.Mention;
@ -331,7 +331,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private MenuItem searchViewItem;
private ProgressBar messageStatusProgressBar;
private ImageView muteIndicatorImageView;
private TextView actionBarSubtitleTextView;
private TextView subtitleTextView;
private AttachmentTypeSelector attachmentTypeSelector;
private AttachmentManager attachmentManager;
@ -362,6 +362,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private final DynamicNoActionBarTheme dynamicTheme = new DynamicNoActionBarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
// Message Status Bar
private ArrayList<BroadcastReceiver> broadcastReceivers = new ArrayList<>();
private String messageStatus = null;
@ -404,6 +405,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
registerMessageStatusObserver("sendingMessage");
registerMessageStatusObserver("messageSent");
registerMessageStatusObserver("messageFailed");
BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(ConversationActivity.this, "Your clock is out of sync with the service node network.", Toast.LENGTH_LONG).show();
}
};
broadcastReceivers.add(broadcastReceiver);
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, new IntentFilter("clockOutOfSync"));
initializeReceivers();
initializeActionBar();
@ -543,7 +553,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
initializeIdentityRecords();
composeText.setTransport(sendButton.getSelectedTransport());
updateTitleTextView(glideRequests, recipient);
updateTitleTextView(recipient);
updateSubtitleTextView();
setActionBarColor(recipient.getColor());
setBlockedUserState(recipient, isSecureText, isDefaultSms);
@ -636,7 +646,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case GROUP_EDIT:
recipient = Recipient.from(this, data.getParcelableExtra(GroupCreateActivity.GROUP_ADDRESS_EXTRA), true);
recipient.addListener(this);
updateTitleTextView(glideRequests, recipient);
updateTitleTextView(recipient);
updateSubtitleTextView();
NotificationChannels.updateContactChannelName(this, recipient);
setBlockedUserState(recipient, isSecureText, isDefaultSms);
@ -740,9 +750,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MenuInflater inflater = this.getMenuInflater();
menu.clear();
boolean isLokiGroupChat = recipient.getAddress().isPublicChat() || recipient.getAddress().isRSSFeed();
boolean isOpenGroupOrRSSFeed = recipient.getAddress().isPublicChat() || recipient.getAddress().isRSSFeed();
if (isSecureText && !isLokiGroupChat) {
if (isSecureText && !isOpenGroupOrRSSFeed) {
if (recipient.getExpireMessages() > 0) {
inflater.inflate(R.menu.conversation_expiring_on, menu);
@ -762,7 +772,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (isSecureText) inflater.inflate(R.menu.conversation_callable_secure, menu);
else inflater.inflate(R.menu.conversation_callable_insecure, menu);
*/
} else if (isGroupConversation() && !isLokiGroupChat) {
} else if (isGroupConversation() && !isOpenGroupOrRSSFeed) {
inflater.inflate(R.menu.conversation_group_options, menu);
if (!isPushGroupConversation()) {
@ -1679,7 +1689,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
sessionRestoreBannerView = ViewUtil.findById(this, R.id.sessionRestoreBannerView);
messageStatusProgressBar = ViewUtil.findById(this, R.id.messageStatusProgressBar);
muteIndicatorImageView = ViewUtil.findById(this, R.id.muteIndicatorImageView);
actionBarSubtitleTextView = ViewUtil.findById(this, R.id.subtitleTextView);
subtitleTextView = ViewUtil.findById(this, R.id.subtitleTextView);
ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button);
@ -1870,7 +1880,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Log.i(TAG, "onModified(" + recipient.getAddress().serialize() + ")");
Util.runOnMain(() -> {
Log.i(TAG, "onModifiedRun(): " + recipient.getRegistered());
updateTitleTextView(glideRequests, recipient);
updateTitleTextView(recipient);
updateSubtitleTextView();
// titleView.setVerified(identityRecords.isVerified());
setBlockedUserState(recipient, isSecureText, isDefaultSms);
@ -2290,7 +2300,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (threadID != this.threadId) {
Recipient threadRecipient = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadID);
if (threadRecipient != null && !threadRecipient.isGroupRecipient()) {
LokiStorageAPI.shared.getAllDevicePublicKeys(threadRecipient.getAddress().serialize()).success(devices -> {
LokiDeviceLinkUtilities.INSTANCE.getAllLinkedDeviceHexEncodedPublicKeys(threadRecipient.getAddress().serialize()).success(devices -> {
// We should update our input if this thread is a part of the other threads device
if (devices.contains(recipient.getAddress().serialize())) {
this.updateInputPanel();
@ -2313,33 +2323,29 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void updateInputPanel() {
/*
isFriendsWithAnyDevice caches whether we are friends with any of the other users device.
isFriendsWithAnyDevice reflects whether we are friends with any of the other user's devices.
This stops the case where the input panel disables and enables rapidly.
- This can occur when we are not friends with the current thread BUT multi-device tells us that we are friends with another one of their devices.
This fixes the case where the input panel disables and enables rapidly, which can occur when we are
not friends with the current thread BUT multi device tells us that we are friends with another one of their devices.
*/
if (recipient.isGroupRecipient() || isNoteToSelf() || isFriendsWithAnyDevice) {
setInputPanelEnabled(true);
return;
}
if (recipient.isGroupRecipient() || isNoteToSelf() || isFriendsWithAnyDevice) { setInputPanelEnabled(true); return; }
// It could take a while before our promise resolves, so we assume the best case
// Disable the input panel if a friend request is pending
LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(this).getFriendRequestStatus(threadId);
boolean isPending = friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_SENDING || friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_SENT || friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_RECEIVED;
setInputPanelEnabled(!isPending);
// We should always have the input panel enabled if we are friends with the current user
isFriendsWithAnyDevice = friendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS;
// Always enable the input panel if we are friends with the current user
isFriendsWithAnyDevice = (friendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS);
// Multi-device input logic
if (!isFriendsWithAnyDevice) {
// We should enable the input if we don't have any pending friend requests OR we are friends with a linked device
MultiDeviceUtilities.hasPendingFriendRequestWithAnyLinkedDevice(this, recipient).success(hasPendingRequests -> {
// Enable the input panel if we don't have any pending friend requests OR we are friends with one of the user's linked devices
MultiDeviceUtilities.hasPendingFriendRequestWithAnyLinkedDevice(this, recipient).success( hasPendingRequests -> {
if (!hasPendingRequests) {
setInputPanelEnabled(true);
} else {
MultiDeviceUtilities.isFriendsWithAnyLinkedDevice(this, recipient).success(isFriends -> {
// If we are friend with any of the other devices then we want to make sure the input panel is always enabled for the duration of this conversation
MultiDeviceUtilities.isFriendsWithAnyLinkedDevice(this, recipient).success( isFriends -> {
// Enable the input panel if we're friends with any of the user's devices
isFriendsWithAnyDevice = isFriends;
setInputPanelEnabled(isFriends);
return Unit.INSTANCE;
@ -2353,7 +2359,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void setInputPanelEnabled(boolean enabled) {
Util.runOnMain(() -> {
updateToggleButtonState();
String hint = enabled ? "Message" : "Pending message request";
String hint = enabled ? "Message" : "Pending session request";
inputPanel.setHint(hint);
inputPanel.setEnabled(enabled);
if (enabled) {
@ -2407,13 +2413,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
sendTextMessage(forceSms, expiresIn, subscriptionId, initiating);
}
} catch (RecipientFormattingException ex) {
Toast.makeText(ConversationActivity.this,
R.string.ConversationActivity_recipient_is_not_a_valid_sms_or_email_address_exclamation,
Toast.LENGTH_LONG).show();
Log.w(TAG, ex);
} catch (InvalidMessageException ex) {
// Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_message_is_empty_exclamation,
// Toast.LENGTH_SHORT).show();
Log.w(TAG, ex);
}
}
@ -2565,7 +2566,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void updateToggleButtonState() {
// Don't allow attachments if we're not friends with any device
// Don't allow attachments if we're not friends with any of the user's devices
if (!isNoteToSelf() && !recipient.isGroupRecipient() && !isFriendsWithAnyDevice) {
buttonToggle.display(sendButton);
quickAttachmentToggle.hide();
@ -3160,13 +3161,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
// region Loki
private void updateTitleTextView(GlideRequests glide, Recipient recipient) {
private void updateTitleTextView(Recipient recipient) {
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this);
List<PairingAuthorisation> deviceLinks = DatabaseFactory.getLokiAPIDatabase(this).getPairingAuthorisations(userHexEncodedPublicKey);
Set<DeviceLink> deviceLinks = DatabaseFactory.getLokiAPIDatabase(this).getDeviceLinks(userHexEncodedPublicKey);
HashSet<String> userLinkedDeviceHexEncodedPublicKeys = new HashSet<>();
for (PairingAuthorisation deviceLink : deviceLinks) {
userLinkedDeviceHexEncodedPublicKeys.add(deviceLink.getPrimaryDevicePublicKey().toLowerCase());
userLinkedDeviceHexEncodedPublicKeys.add(deviceLink.getSecondaryDevicePublicKey().toLowerCase());
for (DeviceLink deviceLink : deviceLinks) {
userLinkedDeviceHexEncodedPublicKeys.add(deviceLink.getMasterHexEncodedPublicKey().toLowerCase());
userLinkedDeviceHexEncodedPublicKeys.add(deviceLink.getSlaveHexEncodedPublicKey().toLowerCase());
}
userLinkedDeviceHexEncodedPublicKeys.add(userHexEncodedPublicKey.toLowerCase());
if (recipient == null) {
@ -3174,45 +3175,46 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} else if (userLinkedDeviceHexEncodedPublicKeys.contains(recipient.getAddress().toString().toLowerCase())) {
titleTextView.setText("Note to Self");
} else {
titleTextView.setText((recipient.getName() == null || recipient.getName().isEmpty()) ? recipient.getAddress().toString() : recipient.getName());
boolean hasName = (recipient.getName() != null && !recipient.getName().isEmpty());
titleTextView.setText(hasName ? recipient.getName() : recipient.getAddress().toString());
}
}
private void updateSubtitleTextView() {
muteIndicatorImageView.setVisibility(View.GONE);
actionBarSubtitleTextView.setVisibility(View.VISIBLE);
subtitleTextView.setVisibility(View.VISIBLE);
if (messageStatus != null) {
switch (messageStatus) {
case "calculatingPoW": actionBarSubtitleTextView.setText("Encrypting message"); break;
case "contactingNetwork": actionBarSubtitleTextView.setText("Tracing a path"); break;
case "sendingMessage": actionBarSubtitleTextView.setText("Sending message"); break;
case "messageSent": actionBarSubtitleTextView.setText("Message sent securely"); break;
case "messageFailed": actionBarSubtitleTextView.setText("Message failed to send"); break;
case "calculatingPoW": subtitleTextView.setText("Encrypting message"); break;
case "contactingNetwork": subtitleTextView.setText("Tracing a path"); break;
case "sendingMessage": subtitleTextView.setText("Sending message"); break;
case "messageSent": subtitleTextView.setText("Message sent securely"); break;
case "messageFailed": subtitleTextView.setText("Message failed to send"); break;
}
} else if (recipient.isMuted()) {
muteIndicatorImageView.setVisibility(View.VISIBLE);
actionBarSubtitleTextView.setText("Muted until " + DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()));
subtitleTextView.setText("Muted until " + DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()));
} else if (recipient.isGroupRecipient() && recipient.getName() != null && !recipient.getName().equals("Session Updates") && !recipient.getName().equals("Loki News")) {
LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
if (publicChat != null) {
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(publicChat.getChannel(), publicChat.getServer());
if (userCount == null) { userCount = 0; }
if (userCount >= 200) {
actionBarSubtitleTextView.setText("200+ members");
subtitleTextView.setText("200+ members");
} else {
actionBarSubtitleTextView.setText(userCount + " members");
subtitleTextView.setText(userCount + " members");
}
} else if (PublicKeyValidation.isValid(recipient.getAddress().toString())) {
actionBarSubtitleTextView.setText(recipient.getAddress().toString());
subtitleTextView.setText(recipient.getAddress().toString());
} else {
actionBarSubtitleTextView.setVisibility(View.GONE);
subtitleTextView.setVisibility(View.GONE);
}
} else if (PublicKeyValidation.isValid(recipient.getAddress().toString())) {
actionBarSubtitleTextView.setText(recipient.getAddress().toString());
subtitleTextView.setText(recipient.getAddress().toString());
} else {
actionBarSubtitleTextView.setVisibility(View.GONE);
subtitleTextView.setVisibility(View.GONE);
}
titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension((actionBarSubtitleTextView.getVisibility() == View.GONE) ? R.dimen.very_large_font_size : R.dimen.large_font_size));
titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension((subtitleTextView.getVisibility() == View.GONE) ? R.dimen.very_large_font_size : R.dimen.large_font_size));
}
private void setMessageStatusProgressAnimatedIfPossible(int progress) {
@ -3284,18 +3286,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void acceptFriendRequest(@NotNull MessageRecord friendRequest) {
// Send the accept to the original friend request thread id
// Send the accept to the original friend request thread ID
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(this);
long originalThreadID = lokiMessageDatabase.getOriginalThreadID(friendRequest.id);
long threadId = originalThreadID < 0 ? this.threadId : originalThreadID;
Recipient contact = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadId);
long threadID = originalThreadID < 0 ? this.threadId : originalThreadID;
Recipient contact = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadID);
Address address = contact.getAddress();
String contactPubKey = address.serialize();
DatabaseFactory.getLokiThreadDatabase(this).setFriendRequestStatus(threadId, LokiThreadFriendRequestStatus.FRIENDS);
String contactHexEncodedPublicKey = address.serialize();
DatabaseFactory.getLokiThreadDatabase(this).setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS);
lokiMessageDatabase.setFriendRequestStatus(friendRequest.id, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
DatabaseFactory.getRecipientDatabase(this).setProfileSharing(contact, true);
MessageSender.sendBackgroundMessageToAllDevices(this, contactPubKey);
MessageSender.sendBackgroundMessageToAllDevices(this, contactHexEncodedPublicKey);
MessageSender.syncContact(this, address);
updateInputPanel();
}
@ -3304,10 +3305,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public void rejectFriendRequest(@NotNull MessageRecord friendRequest) {
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(this);
long originalThreadID = lokiMessageDatabase.getOriginalThreadID(friendRequest.id);
long threadId = originalThreadID < 0 ? this.threadId : originalThreadID;
DatabaseFactory.getLokiThreadDatabase(this).setFriendRequestStatus(threadId, LokiThreadFriendRequestStatus.NONE);
String contactID = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadId).getAddress().toString();
long threadID = originalThreadID < 0 ? this.threadId : originalThreadID;
DatabaseFactory.getLokiThreadDatabase(this).setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.NONE);
String contactID = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadID).getAddress().toString();
DatabaseFactory.getLokiPreKeyBundleDatabase(this).removePreKeyBundle(contactID);
updateInputPanel();
}
@ -3315,20 +3315,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public boolean isNoteToSelf() {
return TextSecurePreferences.getLocalNumber(this).equals(recipient.getAddress().serialize());
}
// endregion
public void restoreSession() {
// Loki - User clicked restore session
if (recipient.isGroupRecipient()) { return; }
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(this);
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(this);
Set<String> devices = lokiThreadDatabase.getSessionRestoreDevices(threadId);
for (String device : devices) { MessageSender.sendRestoreSessionMessage(this, device); }
long messageId = smsDatabase.insertMessageOutbox(threadId, new OutgoingTextMessage(recipient,"", 0, 0), false, System.currentTimeMillis(), null);
if (messageId > -1) {
smsDatabase.markAsLokiSessionRestoreSent(messageId);
long messageID = smsDatabase.insertMessageOutbox(threadId, new OutgoingTextMessage(recipient,"", 0, 0), false, System.currentTimeMillis(), null);
if (messageID > -1) {
smsDatabase.markAsLokiSessionRestoreSent(messageID);
}
lokiThreadDatabase.removeAllSessionRestoreDevices(threadId);
updateSessionRestoreBanner();
}
// endregion
}

View File

@ -10,11 +10,11 @@ import org.thoughtcrime.securesms.logging.Log;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.protocol.CiphertextMessage;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.signalservice.loki.messaging.LokiSessionDatabaseProtocol;
import org.whispersystems.libsignal.state.SessionStore;
import java.util.List;
public class TextSecureSessionStore implements LokiSessionDatabaseProtocol {
public class TextSecureSessionStore implements SessionStore {
private static final String TAG = TextSecureSessionStore.class.getSimpleName();

View File

@ -135,7 +135,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiAPIDatabase.getCreateGroupChatAuthTokenTableCommand());
db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDTableCommand());
db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDTableCommand());
db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand());
db.execSQL(LokiAPIDatabase.getCreateDeviceLinkTableCommand());
db.execSQL(LokiAPIDatabase.getCreateUserCountTableCommand());
db.execSQL(LokiPreKeyBundleDatabase.getCreateTableCommand());
db.execSQL(LokiPreKeyRecordDatabase.getCreateTableCommand());
@ -518,7 +518,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
}
if (oldVersion < lokiV3) {
db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand());
db.execSQL(LokiAPIDatabase.getCreateDeviceLinkTableCommand());
db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand());
db.execSQL("ALTER TABLE groups ADD COLUMN avatar_url TEXT");

View File

@ -11,13 +11,14 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.redesign.utilities.MnemonicUtilities;
import org.thoughtcrime.securesms.util.AsyncLoader;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.api.LokiDeviceLinkUtilities;
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec;
import java.io.File;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
public class DeviceListLoader extends AsyncLoader<List<Device>> {
@ -33,7 +34,7 @@ public class DeviceListLoader extends AsyncLoader<List<Device>> {
public List<Device> loadInBackground() {
try {
String ourPublicKey = TextSecurePreferences.getLocalNumber(getContext());
List<String> secondaryDevicePublicKeys = LokiStorageAPI.shared.getSecondaryDevicePublicKeys(ourPublicKey).get();
Set<String> secondaryDevicePublicKeys = LokiDeviceLinkUtilities.INSTANCE.getSlaveHexEncodedPublicKeys(ourPublicKey).get();
List<Device> devices = Stream.of(secondaryDevicePublicKeys).map(this::mapToDevice).toList();
Collections.sort(devices, new DeviceComparator());
return devices;

View File

@ -7,7 +7,6 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.CreateProfileActivity;
import org.thoughtcrime.securesms.DeviceListFragment;
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
import org.thoughtcrime.securesms.gcm.FcmService;
@ -48,6 +47,7 @@ import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
import org.thoughtcrime.securesms.jobs.TypingSendJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.LokiSessionResetImplementation;
import org.thoughtcrime.securesms.loki.PushMessageSyncSendJob;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
import org.thoughtcrime.securesms.push.MessageSenderEventListener;
@ -160,7 +160,7 @@ public class SignalCommunicationModule {
DatabaseFactory.getLokiThreadDatabase(context),
DatabaseFactory.getLokiMessageDatabase(context),
DatabaseFactory.getLokiPreKeyBundleDatabase(context),
new TextSecureSessionStore(context),
new LokiSessionResetImplementation(context),
DatabaseFactory.getLokiUserDatabase(context),
((ApplicationContext)context.getApplicationContext()).broadcaster);
} else {

View File

@ -36,7 +36,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.api.LokiDeviceLinkUtilities;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
import java.util.Collections;
@ -318,7 +318,7 @@ public class GroupMessageProcessor {
try {
String masterHexEncodedPublicKey = hexEncodedPublicKey.equalsIgnoreCase(ourPublicKey)
? TextSecurePreferences.getMasterHexEncodedPublicKey(context)
: PromiseUtil.timeout(LokiStorageAPI.shared.getPrimaryDevicePublicKey(hexEncodedPublicKey), 5000).get();
: PromiseUtil.timeout(LokiDeviceLinkUtilities.INSTANCE.getMasterHexEncodedPublicKey(hexEncodedPublicKey), 5000).get();
return masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : hexEncodedPublicKey;
} catch (Exception e) {
return hexEncodedPublicKey;
@ -329,7 +329,7 @@ public class GroupMessageProcessor {
String ourNumber = TextSecurePreferences.getLocalNumber(context);
for (String member : members) {
// Make sure we have session with all of the members secondary devices
LokiStorageAPI.shared.getAllDevicePublicKeys(member).success(devices -> {
LokiDeviceLinkUtilities.INSTANCE.getAllLinkedDeviceHexEncodedPublicKeys(member).success(devices -> {
if (devices.contains(ourNumber)) { return Unit.INSTANCE; }
for (String device : devices) {
SignalProtocolAddress protocolAddress = new SignalProtocolAddress(device, SignalServiceAddress.DEFAULT_DEVICE_ID);

View File

@ -27,7 +27,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.api.LokiFileServerAPI;
import java.io.IOException;
import java.io.InputStream;

View File

@ -68,13 +68,13 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.FriendRequestHandler;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiAPIUtilities;
import org.thoughtcrime.securesms.loki.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPreKeyBundleDatabase;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPreKeyRecordDatabase;
import org.thoughtcrime.securesms.loki.LokiSessionResetImplementation;
import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.loki.redesign.activities.HomeActivity;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiAPIUtilities;
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPreKeyBundleDatabase;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
@ -101,6 +101,8 @@ import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.loki.LokiSessionResetProtocol;
import org.whispersystems.libsignal.loki.LokiSessionResetStatus;
import org.whispersystems.libsignal.state.PreKeyBundle;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.util.guava.Optional;
@ -129,15 +131,14 @@ import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOper
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.DeviceLink;
import org.whispersystems.signalservice.loki.api.DeviceLinkingSession;
import org.whispersystems.signalservice.loki.api.LokiAPI;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.api.PairingAuthorisation;
import org.whispersystems.signalservice.loki.api.LokiDeviceLinkUtilities;
import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher;
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus;
import org.whispersystems.signalservice.loki.messaging.LokiServiceMessage;
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.messaging.LokiThreadSessionResetStatus;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
import java.io.InputStream;
@ -270,9 +271,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context);
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
LokiPreKeyRecordDatabase lokiPreKeyRecordDatabase = DatabaseFactory.getLokiPreKeyRecordDatabase(context);
LokiSessionResetProtocol lokiSessionResetProtocol = new LokiSessionResetImplementation(context);
SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context));
LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, lokiThreadDatabase, lokiPreKeyRecordDatabase, UnidentifiedAccessUtil.getCertificateValidator());
LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, lokiSessionResetProtocol, UnidentifiedAccessUtil.getCertificateValidator());
SignalServiceContent content = cipher.decrypt(envelope);
@ -282,21 +283,21 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
return;
}
if (shouldIgnore(content)) {
Log.i(TAG, "Ignoring message.");
return;
}
// Loki - Handle friend request acceptance if needed
acceptFriendRequestIfNeeded(content);
if (!content.isFriendRequest() && !isGroupChatMessage(content)) {
becomeFriendsWithContactIfNeeded(content.getSender(), true, false);
}
// Loki - Session requests
// Loki - Handle session request if needed
handleSessionRequestIfNeeded(content);
// Loki - Store pre key bundle
// We shouldn't store it if it's a pairing message
if (!content.getPairingAuthorisation().isPresent()) {
// Loki - Store pre key bundle if needed
if (!content.getDeviceLink().isPresent()) {
storePreKeyBundleIfNeeded(content);
}
@ -310,35 +311,35 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Store the sender display name if needed
Optional<String> rawSenderDisplayName = content.senderDisplayName;
if (rawSenderDisplayName.isPresent() && rawSenderDisplayName.get().length() > 0) {
// If we got a name from our primary device then we set our profile name to match it
String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
if (ourPrimaryDevice != null && content.getSender().equals(ourPrimaryDevice)) {
// If we got a name from our master device then set our display name to match
String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
if (ourMasterDevice != null && content.getSender().equals(ourMasterDevice)) {
TextSecurePreferences.setProfileName(context, rawSenderDisplayName.get());
}
// If we receive a message from our device then don't set the display name in the database (as we probably have a alias set for them)
MultiDeviceUtilities.isOneOfOurDevices(context, Address.fromSerialized(content.getSender())).success(isOneOfOurDevice -> {
if (!isOneOfOurDevice) { setDisplayName(content.getSender(), rawSenderDisplayName.get()); }
MultiDeviceUtilities.isOneOfOurDevices(context, Address.fromSerialized(content.getSender())).success( isOneOfOurDevices -> {
if (!isOneOfOurDevices) { setDisplayName(content.getSender(), rawSenderDisplayName.get()); }
return Unit.INSTANCE;
});
}
if (content.getPairingAuthorisation().isPresent()) {
handlePairingMessage(content.getPairingAuthorisation().get(), content);
if (content.getDeviceLink().isPresent()) {
handleDeviceLinkMessage(content.getDeviceLink().get(), content);
} else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent();
if (!content.isFriendRequest() && message.isUnpairingRequest()) {
// Make sure we got the request from our primary device
String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
if (ourPrimaryDevice != null && ourPrimaryDevice.equals(content.getSender())) {
if (!content.isFriendRequest() && message.isUnlinkingRequest()) {
// Make sure we got the request from our master device
String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
if (ourMasterDevice != null && ourMasterDevice.equals(content.getSender())) {
TextSecurePreferences.setDatabaseResetFromUnpair(context, true);
MultiDeviceUtilities.checkForRevocation(context);
MultiDeviceUtilities.checkIsRevokedSlaveDevice(context);
}
} else {
// Loki - Don't process session restore message any further
if (message.isSessionRestore() || message.isSessionRequest()) { return; }
if (message.isSessionRestorationRequest() || message.isSessionRequest()) { return; }
if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId);
@ -362,6 +363,18 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
handleNeedsDeliveryReceipt(content, message);
}
// If we received a friend request, but we were already friends with the user, reset the session
if (content.isFriendRequest() && !message.isGroupMessage()) {
Recipient sender = Recipient.from(context, Address.fromSerialized(content.getSender()), false);
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
long threadID = threadDatabase.getThreadIdIfExistsFor(sender);
if (lokiThreadDatabase.getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS) {
resetSession(content.getSender());
// Let our other devices know that we have reset the session
MessageSender.syncContact(context, sender.getAddress());
}
}
// Loki - Handle friend request logic if needed
updateFriendRequestStatusIfNeeded(content, message);
}
@ -375,7 +388,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
else if (syncMessage.getRead().isPresent()) handleSynchronizeReadMessage(syncMessage.getRead().get(), content.getTimestamp());
else if (syncMessage.getVerified().isPresent()) handleSynchronizeVerifiedMessage(syncMessage.getVerified().get());
else if (syncMessage.getStickerPackOperations().isPresent()) handleSynchronizeStickerPackOperation(syncMessage.getStickerPackOperations().get());
else if (syncMessage.getContacts().isPresent()) handleSynchronizeContactMessage(syncMessage.getContacts().get());
else if (syncMessage.getContacts().isPresent()) handleContactSyncMessage(syncMessage.getContacts().get());
else Log.w(TAG, "Contains no known sync types...");
} else if (content.getCallMessage().isPresent()) {
Log.i(TAG, "Got call message...");
@ -402,11 +415,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (envelope.isPreKeySignalMessage()) {
ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob());
}
// Loki - Handle session reset logic
if (!content.isFriendRequest()) {
cipher.handleSessionResetRequestIfNeeded(content, cipher.getSessionStatus(content));
}
} catch (ProtocolInvalidVersionException e) {
Log.w(TAG, e);
handleInvalidVersionMessage(e.getSender(), e.getSenderDevice(), envelope.getTimestamp(), smsMessageId);
@ -539,19 +547,19 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
if (threadId != null) {
resetSession(content.getSender(), threadId);
resetSession(content.getSender());
MessageNotifier.updateNotification(context, threadId);
}
}
private void resetSession(String hexEncodedPublicKey, long threadId) {
private void resetSession(String hexEncodedPublicKey) {
TextSecureSessionStore sessionStore = new TextSecureSessionStore(context);
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
Log.d("Loki", "Received a session reset request from: " + hexEncodedPublicKey + "; archiving the session.");
sessionStore.archiveAllSessions(hexEncodedPublicKey);
lokiThreadDatabase.setSessionResetStatus(threadId, LokiThreadSessionResetStatus.REQUEST_RECEIVED);
lokiThreadDatabase.setSessionResetStatus(hexEncodedPublicKey, LokiSessionResetStatus.REQUEST_RECEIVED);
Log.d("Loki", "Sending a ping back to " + hexEncodedPublicKey + ".");
MessageSender.sendBackgroundMessage(context, hexEncodedPublicKey);
@ -593,7 +601,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
{
GroupMessageProcessor.process(context, content, message, false);
if (message.getExpiresInSeconds() != 0 && message.getExpiresInSeconds() != getMessageDestination(content, message).getExpireMessages()) {
if (message.getExpiresInSeconds() != 0 && message.getExpiresInSeconds() != getRecipientForMessage(content, message).getExpireMessages()) {
handleExpirationUpdate(content, message, Optional.absent());
}
@ -619,7 +627,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
{
try {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Recipient recipient = getMessageDestination(content, message);
Recipient recipient = getRecipientForMessage(content, message);
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(Address.fromSerialized(content.getSender()),
message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000L, true,
@ -670,35 +678,35 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
}
private void handleSynchronizeContactMessage(@NonNull ContactsMessage contactsMessage) {
if (contactsMessage.getContactsStream().isStream()) {
Log.d("Loki", "Received contact sync message");
private void handleContactSyncMessage(@NonNull ContactsMessage contactsMessage) {
if (!contactsMessage.getContactsStream().isStream()) { return; }
Log.d("Loki", "Received contact sync message.");
try {
InputStream in = contactsMessage.getContactsStream().asStream().getInputStream();
DeviceContactsInputStream contactsInputStream = new DeviceContactsInputStream(in);
List<DeviceContact> devices = contactsInputStream.readAll();
for (DeviceContact deviceContact : devices) {
List<DeviceContact> deviceContacts = contactsInputStream.readAll();
for (DeviceContact deviceContact : deviceContacts) {
// Check if we have the contact as a friend and that we're not trying to sync our own device
String pubKey = deviceContact.getNumber();
Address address = Address.fromSerialized(pubKey);
String hexEncodedPublicKey = deviceContact.getNumber();
Address address = Address.fromSerialized(hexEncodedPublicKey);
if (!address.isPhone() || address.toPhoneString().equals(TextSecurePreferences.getLocalNumber(context))) { continue; }
/*
If we're not friends with the contact we received or our friend request expired then we should send them a friend request
otherwise if we have received a friend request with from them then we should automatically accept the friend request
If we're not friends with the contact we received or our friend request expired then we should send them a friend request.
Otherwise, if we have received a friend request from them, automatically accept the friend request.
*/
Recipient recipient = Recipient.from(context, address, false);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
LokiThreadFriendRequestStatus status = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId);
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
LokiThreadFriendRequestStatus status = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID);
if (status == LokiThreadFriendRequestStatus.NONE || status == LokiThreadFriendRequestStatus.REQUEST_EXPIRED) {
MessageSender.sendBackgroundFriendRequest(context, pubKey, "Please accept to enable messages to be synced across devices");
Log.d("Loki", "Sent friend request to " + pubKey);
MessageSender.sendBackgroundFriendRequest(context, hexEncodedPublicKey, "Please accept to enable messages to be synced across devices");
Log.d("Loki", "Sent friend request to " + hexEncodedPublicKey);
} else if (status == LokiThreadFriendRequestStatus.REQUEST_RECEIVED) {
// Accept the incoming friend request
becomeFriendsWithContact(pubKey, false, false);
becomeFriendsWithContactIfNeeded(hexEncodedPublicKey, false, false);
// Send them an accept message back
MessageSender.sendBackgroundMessage(context, pubKey);
MessageSender.sendBackgroundMessage(context, hexEncodedPublicKey);
Log.d("Loki", "Became friends with " + deviceContact.getNumber());
}
@ -707,8 +715,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// TODO: Handle avatar - Download and set avatar?
}
} catch (Exception e) {
Log.d("Loki", "Failed to sync contact: " + e);
}
Log.d("Loki", "Failed to sync contact: " + e + ".");
}
}
@ -751,7 +758,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
}
// Loki - If we received a sync message from our master device then we need to extract the avatar url
// Loki - If we received a sync message from our master device then we need to extract the profile picture url
if (isSenderMasterDevice) {
handleProfileKey(content, message.getMessage());
}
@ -841,21 +848,21 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
@NonNull Optional<Long> messageServerIDOrNull)
throws StorageFailedException
{
Recipient originalRecipient = getMessageDestination(content, message);
Recipient primaryDeviceRecipient = getMessagePrimaryDestination(content, message);
Recipient originalRecipient = getRecipientForMessage(content, message);
Recipient masterRecipient = getMasterRecipientForMessage(content, message);
notifyTypingStoppedFromIncomingMessage(primaryDeviceRecipient, content.getSender(), content.getSenderDevice());
notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice());
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
Optional<Attachment> sticker = getStickerAttachment(message.getSticker());
Address sender = primaryDeviceRecipient.getAddress();
Address sender = masterRecipient.getAddress();
// If message is from group then we need to map it to get the sender of the message
if (message.isGroupMessage()) {
sender = getPrimaryDeviceRecipient(content.getSender()).getAddress();
sender = getMasterRecipient(content.getSender()).getAddress();
}
// Ignore messages from ourselves
@ -905,12 +912,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
}
// Loki - Run db updates in the background, we should look into fixing this in the future
// Loki - Run database updates in the background, we should look into fixing this in the future
AsyncTask.execute(() -> {
// Loki - Store message server ID
updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);
// Loki - Update mapping of message to original thread id
// Loki - Update mapping of message to original thread ID
if (insertResult.isPresent()) {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
@ -1030,8 +1037,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
{
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
String body = message.getBody().isPresent() ? message.getBody().get() : "";
Recipient originalRecipient = getMessageDestination(content, message);
Recipient primaryDeviceRecipient = getMessagePrimaryDestination(content, message);
Recipient originalRecipient = getRecipientForMessage(content, message);
Recipient masterRecipient = getMasterRecipientForMessage(content, message);
if (message.getExpiresInSeconds() != originalRecipient.getExpireMessages()) {
handleExpirationUpdate(content, message, Optional.absent());
@ -1042,26 +1049,26 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) {
threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second;
} else {
notifyTypingStoppedFromIncomingMessage(primaryDeviceRecipient, content.getSender(), content.getSenderDevice());
notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice());
Address sender = primaryDeviceRecipient.getAddress();
Address sender = masterRecipient.getAddress();
// If message is from group then we need to map it to get the sender of the message
if (message.isGroupMessage()) {
sender = getPrimaryDeviceRecipient(content.getSender()).getAddress();
sender = getMasterRecipient(content.getSender()).getAddress();
}
// Ignore messages from ourselves
if (sender.serialize().equalsIgnoreCase(TextSecurePreferences.getLocalNumber(context))) { return; }
IncomingTextMessage _textMessage = new IncomingTextMessage(sender,
IncomingTextMessage tm = new IncomingTextMessage(sender,
content.getSenderDevice(),
message.getTimestamp(), body,
message.getGroupInfo(),
message.getExpiresInSeconds() * 1000L,
content.isNeedsReceipt());
IncomingEncryptedMessage textMessage = new IncomingEncryptedMessage(_textMessage, body);
IncomingEncryptedMessage textMessage = new IncomingEncryptedMessage(tm, body);
// Ignore the message if the body is empty
if (textMessage.getMessageBody().length() == 0) { return; }
@ -1079,7 +1086,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
MessageNotifier.updateNotification(context, threadId);
}
// Loki - Run db updates in background, we should look into fixing this in the future
// Loki - Run database updates in background, we should look into fixing this in the future
AsyncTask.execute(() -> {
if (insertResult.isPresent()) {
InsertResult result = insertResult.get();
@ -1090,7 +1097,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Store message server ID
updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);
// Loki - Update mapping of message to original thread id
// Loki - Update mapping of message to original thread ID
if (result.getMessageId() > -1) {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
@ -1102,95 +1109,86 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
}
private boolean isValidPairingMessage(@NonNull PairingAuthorisation authorisation) {
private boolean isValidDeviceLinkMessage(@NonNull DeviceLink authorisation) {
boolean isSecondaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context) != null;
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
boolean isRequest = (authorisation.getType() == PairingAuthorisation.Type.REQUEST);
boolean isRequest = (authorisation.getType() == DeviceLink.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)) {
} else if (isRequest && !authorisation.getMasterHexEncodedPublicKey().equals(userHexEncodedPublicKey)) {
Log.d("Loki", "Ignoring pairing request message addressed to another user.");
return false;
} else if (isRequest && authorisation.getSecondaryDevicePublicKey().equals(userHexEncodedPublicKey)) {
} else if (isRequest && authorisation.getSlaveHexEncodedPublicKey().equals(userHexEncodedPublicKey)) {
Log.d("Loki", "Ignoring pairing request message from self.");
return false;
}
return authorisation.verify();
}
private void handleProfileAvatar(SignalServiceContent content, String url) {
Recipient primaryDevice = getPrimaryDeviceRecipient(content.getSender());
ApplicationContext.getInstance(context).getJobManager().add(new RetrieveProfileAvatarJob(primaryDevice, url));
}
private void handlePairingMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceContent content) {
private void handleDeviceLinkMessage(@NonNull DeviceLink deviceLink, @NonNull SignalServiceContent content) {
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
if (authorisation.getType() == PairingAuthorisation.Type.REQUEST) {
handlePairingRequestMessage(authorisation, content);
} else if (authorisation.getSecondaryDevicePublicKey().equals(userHexEncodedPublicKey)) {
handlePairingAuthorisationMessage(authorisation, content);
if (deviceLink.getType() == DeviceLink.Type.REQUEST) {
handleDeviceLinkRequestMessage(deviceLink, content);
} else if (deviceLink.getSlaveHexEncodedPublicKey().equals(userHexEncodedPublicKey)) {
handleDeviceLinkAuthorizedMessage(deviceLink, content);
}
}
private void handlePairingRequestMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceContent content) {
boolean isValid = isValidPairingMessage(authorisation);
private void handleDeviceLinkRequestMessage(@NonNull DeviceLink deviceLink, @NonNull SignalServiceContent content) {
boolean isValid = isValidDeviceLinkMessage(deviceLink);
DeviceLinkingSession linkingSession = DeviceLinkingSession.Companion.getShared();
if (isValid && linkingSession.isListeningForLinkingRequests()) {
// Loki - If we successfully received a request then we should store the PreKeyBundle
if (!isValid || !linkingSession.isListeningForLinkingRequests()) { return; }
storePreKeyBundleIfNeeded(content);
linkingSession.processLinkingRequest(authorisation);
}
linkingSession.processLinkingRequest(deviceLink);
}
private void handlePairingAuthorisationMessage(@NonNull PairingAuthorisation authorisation, @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).");
private void handleDeviceLinkAuthorizedMessage(@NonNull DeviceLink deviceLink, @NonNull SignalServiceContent content) {
// Check preconditions
boolean hasExistingDeviceLink = TextSecurePreferences.getMasterHexEncodedPublicKey(context) != null;
if (hasExistingDeviceLink) {
Log.d("Loki", "Ignoring unexpected device link message (the device is already linked as a slave device).");
return;
}
boolean isValid = isValidPairingMessage(authorisation);
boolean isValid = isValidDeviceLinkMessage(deviceLink);
if (!isValid) {
Log.d("Loki", "Ignoring invalid pairing authorisation message.");
Log.d("Loki", "Ignoring invalid device link message.");
return;
}
if (!DeviceLinkingSession.Companion.getShared().isListeningForLinkingRequests()) {
Log.d("Loki", "Ignoring pairing authorisation message.");
Log.d("Loki", "Ignoring device link message.");
return;
}
if (authorisation.getType() != PairingAuthorisation.Type.GRANT) { return; }
Log.d("Loki", "Received pairing authorisation message from: " + authorisation.getPrimaryDevicePublicKey() + ".");
// Save PreKeyBundle if for whatever reason we got one
if (deviceLink.getType() != DeviceLink.Type.AUTHORIZATION) { return; }
Log.d("Loki", "Received device link authorized message from: " + deviceLink.getMasterHexEncodedPublicKey() + ".");
// Save pre key bundle if we somehow got one
storePreKeyBundleIfNeeded(content);
// Process
DeviceLinkingSession.Companion.getShared().processLinkingAuthorization(authorisation);
// Store the primary device's public key
DeviceLinkingSession.Companion.getShared().processLinkingAuthorization(deviceLink);
// Store the master device's ID
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
DatabaseFactory.getLokiAPIDatabase(context).removePairingAuthorisations(userHexEncodedPublicKey);
DatabaseFactory.getLokiAPIDatabase(context).insertOrUpdatePairingAuthorisation(authorisation);
TextSecurePreferences.setMasterHexEncodedPublicKey(context, authorisation.getPrimaryDevicePublicKey());
DatabaseFactory.getLokiAPIDatabase(context).clearDeviceLinks(userHexEncodedPublicKey);
DatabaseFactory.getLokiAPIDatabase(context).addDeviceLink(deviceLink);
TextSecurePreferences.setMasterHexEncodedPublicKey(context, deviceLink.getMasterHexEncodedPublicKey());
TextSecurePreferences.setMultiDevice(context, true);
// Send a background message to the primary device
MessageSender.sendBackgroundMessage(context, authorisation.getPrimaryDevicePublicKey());
// Propagate the updates to the file server
LokiStorageAPI storageAPI = LokiStorageAPI.Companion.getShared();
storageAPI.updateUserDeviceMappings();
// Update display names
// Send a background message to the master device
MessageSender.sendBackgroundMessage(context, deviceLink.getMasterHexEncodedPublicKey());
// Update display name if needed
if (content.senderDisplayName.isPresent() && content.senderDisplayName.get().length() > 0) {
TextSecurePreferences.setProfileName(context, content.senderDisplayName.get());
}
// Profile avatar updates
// Update profile picture if needed
if (content.getDataMessage().isPresent()) {
handleProfileKey(content, content.getDataMessage().get());
}
// Contact sync
// Handle contact sync if needed
if (content.getSyncMessage().isPresent() && content.getSyncMessage().get().getContacts().isPresent()) {
handleSynchronizeContactMessage(content.getSyncMessage().get().getContacts().get());
handleContactSyncMessage(content.getSyncMessage().get().getContacts().get());
}
// The device link is propagated to the file server in LandingActivity.onDeviceLinkAuthorized because we can handle the error there
}
private void setDisplayName(String hexEncodedPublicKey, String profileName) {
@ -1199,95 +1197,69 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
private void updateGroupChatMessageServerID(Optional<Long> messageServerIDOrNull, Optional<InsertResult> insertResult) {
if (insertResult.isPresent() && messageServerIDOrNull.isPresent()) {
if (!insertResult.isPresent() || !messageServerIDOrNull.isPresent()) { return; }
long messageID = insertResult.get().getMessageId();
long messageServerID = messageServerIDOrNull.get();
DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, messageServerID);
}
}
private void storePreKeyBundleIfNeeded(@NonNull SignalServiceContent content) {
Recipient sender = Recipient.from(context, Address.fromSerialized(content.getSender()), false);
if (!sender.isGroupRecipient() && content.lokiServiceMessage.isPresent()) {
if (sender.isGroupRecipient() || !content.lokiServiceMessage.isPresent()) { return; }
LokiServiceMessage lokiMessage = content.lokiServiceMessage.get();
if (lokiMessage.getPreKeyBundleMessage() != null) {
if (lokiMessage.getPreKeyBundleMessage() == null) { return; }
int registrationID = TextSecurePreferences.getLocalRegistrationId(context);
LokiPreKeyBundleDatabase lokiPreKeyBundleDatabase = DatabaseFactory.getLokiPreKeyBundleDatabase(context);
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
// Loki - Store the latest pre key bundle
if (registrationID > 0) {
if (registrationID <= 0) { return; }
Log.d("Loki", "Received a pre key bundle from: " + content.getSender() + ".");
PreKeyBundle preKeyBundle = lokiMessage.getPreKeyBundleMessage().getPreKeyBundle(registrationID);
lokiPreKeyBundleDatabase.setPreKeyBundle(content.getSender(), preKeyBundle);
// Loki - If we received a friend request, but we were already friends with this user, then reset the session
if (content.isFriendRequest()) {
long threadID = threadDatabase.getThreadIdIfExistsFor(sender);
if (lokiThreadDatabase.getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS) {
resetSession(content.getSender(), threadID);
// Let our other devices know that we have reset the session
MessageSender.syncContact(context, sender.getAddress());
}
}
}
}
}
}
private void acceptFriendRequestIfNeeded(@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 (content.isFriendRequest() || isGroupChatMessage(content)) { return; }
becomeFriendsWithContact(content.getSender(), true, false);
}
private void handleSessionRequestIfNeeded(@NonNull SignalServiceContent content) {
if (content.isFriendRequest() && isSessionRequest(content)) {
// Check if the session request from a member in one of our groups or our friend
LokiStorageAPI.shared.getPrimaryDevicePublicKey(content.getSender()).success(primaryDevicePublicKey -> {
String sender = primaryDevicePublicKey != null ? primaryDevicePublicKey : content.getSender();
if (!content.isFriendRequest() || !isSessionRequest(content)) { return; }
// Check if the session request came from a member in one of our groups or one of our friends
LokiDeviceLinkUtilities.INSTANCE.getMasterHexEncodedPublicKey(content.getSender()).success( masterHexEncodedPublicKey -> {
String sender = masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : content.getSender();
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(sender), false));
LokiThreadFriendRequestStatus threadFriendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID);
boolean isOurFriend = threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS;
boolean isInOneOfOurGroups = DatabaseFactory.getGroupDatabase(context).signalGroupsHaveMember(sender);
boolean shouldAcceptSessionRequest = isOurFriend || isInOneOfOurGroups;
if (shouldAcceptSessionRequest) {
// Send a background message to acknowledge session request
MessageSender.sendBackgroundMessage(context, content.getSender());
MessageSender.sendBackgroundMessage(context, content.getSender()); // Send a background message to acknowledge
}
return Unit.INSTANCE;
});
}
}
private void becomeFriendsWithContact(String pubKey, boolean syncContact, boolean force) {
private void becomeFriendsWithContactIfNeeded(String hexEncodedPublicKey, boolean requiresContactSync, boolean canSkip) {
// Ignore friend requests to group recipients
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
Recipient contactID = Recipient.from(context, Address.fromSerialized(pubKey), false);
Recipient contactID = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false);
if (contactID.isGroupRecipient()) return;
// Ignore friend requests to recipients we're already friends with
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(contactID);
LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS) { return; }
// We shouldn't be able to skip from None -> Friends in normal circumstances.
// Multi-device is the exception to this rule because we want to automatically be friends with a secondary device
if (!force && threadFriendRequestStatus == LokiThreadFriendRequestStatus.NONE) { return; }
// If the thread's friend request status is not `FRIENDS`, but we're receiving a message,
// We shouldn't be able to skip from NONE to FRIENDS under normal circumstances.
// Multi-device is the one exception to this rule because we want to automatically become friends with slave devices.
if (!canSkip && threadFriendRequestStatus == LokiThreadFriendRequestStatus.NONE) { return; }
// If the thread's friend request status is not `FRIENDS` or `NONE`, but we're receiving a message,
// it must be a friend request accepted message. Declining a friend request doesn't send a message.
lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS);
// Send out a contact sync message
if (syncContact) {
// Send out a contact sync message if needed
if (requiresContactSync) {
MessageSender.syncContact(context, contactID.getAddress());
}
// Allow profile sharing with contact
// Enable profile sharing with the recipient
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(contactID, true);
// Update the last message if needed
LokiStorageAPI.shared.getPrimaryDevicePublicKey(pubKey).success(primaryDevice -> {
LokiDeviceLinkUtilities.INSTANCE.getMasterHexEncodedPublicKey(hexEncodedPublicKey).success( masterHexEncodedPublicKey -> {
Util.runOnMain(() -> {
long primaryDeviceThreadID = primaryDevice == null ? threadID : DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(primaryDevice), false));
FriendRequestHandler.updateLastFriendRequestMessage(context, primaryDeviceThreadID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
long masterThreadID = (masterHexEncodedPublicKey == null) ? threadID : DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(masterHexEncodedPublicKey), false));
FriendRequestHandler.updateLastFriendRequestMessage(context, masterThreadID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
});
return Unit.INSTANCE;
});
@ -1295,25 +1267,24 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
if (!content.isFriendRequest() || message.isGroupMessage() || message.isSessionRequest()) { return; }
// This handles the case where another user sends us a regular message without authorisation
Promise<Boolean, Exception> promise = PromiseUtil.timeout(MultiDeviceUtilities.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context), 8000);
boolean shouldBecomeFriends = PromiseUtil.get(promise, false);
if (shouldBecomeFriends) {
// Become friends AND update the message they sent
becomeFriendsWithContact(content.getSender(), true, true);
becomeFriendsWithContactIfNeeded(content.getSender(), true, true);
// Send them an accept message back
MessageSender.sendBackgroundMessage(context, content.getSender());
} else {
// Do regular friend request logic checks
Recipient originalRecipient = getMessageDestination(content, message);
Recipient primaryDeviceRecipient = getMessagePrimaryDestination(content, message);
Recipient originalRecipient = getRecipientForMessage(content, message);
Recipient masterRecipient = getMasterRecipientForMessage(content, message);
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
// Loki - Friend requests only work in direct chats
if (!originalRecipient.getAddress().isPhone()) { return; }
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(originalRecipient);
long primaryDeviceThreadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(primaryDeviceRecipient);
long primaryDeviceThreadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(masterRecipient);
LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_SENT) {
@ -1482,7 +1453,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
private void triggerSessionRestorePrompt(@NonNull String sender) {
Recipient primaryRecipient = getPrimaryDeviceRecipient(sender);
Recipient primaryRecipient = getMasterRecipient(sender);
if (!primaryRecipient.isGroupRecipient()) {
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(primaryRecipient);
DatabaseFactory.getLokiThreadDatabase(context).addSessionRestoreDevice(threadID, sender);
@ -1537,12 +1508,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (recipient.getProfileKey() == null || !MessageDigest.isEqual(recipient.getProfileKey(), message.getProfileKey().get())) {
database.setProfileKey(recipient, message.getProfileKey().get());
database.setUnidentifiedAccessMode(recipient, RecipientDatabase.UnidentifiedAccessMode.UNKNOWN);
String url = content.senderProfileAvatarUrl.or("");
String url = content.senderProfilePictureURL.or("");
ApplicationContext.getInstance(context).getJobManager().add(new RetrieveProfileAvatarJob(recipient, url));
// Loki - If the recipient is our master device then we need to go and update our avatar mappings on the public chats
if (recipient.isOurMasterDevice()) {
ApplicationContext.getInstance(context).updatePublicChatProfileAvatarIfNeeded();
ApplicationContext.getInstance(context).updatePublicChatProfilePictureIfNeeded();
}
}
}
@ -1559,11 +1530,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void handleDeliveryReceipt(@NonNull SignalServiceContent content,
@NonNull SignalServiceReceiptMessage message)
{
// Redirect message to primary device conversation
// Redirect message to master device conversation
Address sender = Address.fromSerialized(content.getSender());
if (sender.isPhone()) {
Recipient primaryDevice = getPrimaryDeviceRecipient(content.getSender());
sender = primaryDevice.getAddress();
Recipient masterDevice = getMasterRecipient(content.getSender());
sender = masterDevice.getAddress();
}
for (long timestamp : message.getTimestamps()) {
@ -1579,11 +1550,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
{
if (TextSecurePreferences.isReadReceiptsEnabled(context)) {
// Redirect message to primary device conversation
// Redirect message to master device conversation
Address sender = Address.fromSerialized(content.getSender());
if (sender.isPhone()) {
Recipient primaryDevice = getPrimaryDeviceRecipient(content.getSender());
sender = primaryDevice.getAddress();
Recipient masterDevice = getMasterRecipient(content.getSender());
sender = masterDevice.getAddress();
}
for (long timestamp : message.getTimestamps()) {
@ -1614,7 +1585,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(groupRecipient);
} else {
// See if we need to redirect the message
author = getPrimaryDeviceRecipient(content.getSender());
author = getMasterRecipient(content.getSender());
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(author);
}
@ -1748,9 +1719,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
private Optional<InsertResult> insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp) {
Recipient primaryDevice = getPrimaryDeviceRecipient(sender);
Recipient masterDevice = getMasterRecipient(sender);
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
IncomingTextMessage textMessage = new IncomingTextMessage(primaryDevice.getAddress(),
IncomingTextMessage textMessage = new IncomingTextMessage(masterDevice.getAddress(),
senderDevice, timestamp, "",
Optional.absent(), 0, false);
@ -1770,11 +1741,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (message.getMessage().isGroupMessage()) {
return getSyncMessageDestination(message);
} else {
return getPrimaryDeviceRecipient(message.getDestination().get());
return getMasterRecipient(message.getDestination().get());
}
}
private Recipient getMessageDestination(SignalServiceContent content, SignalServiceDataMessage message) {
private Recipient getRecipientForMessage(SignalServiceContent content, SignalServiceDataMessage message) {
if (message.isGroupMessage()) {
return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get())), false);
} else {
@ -1782,34 +1753,34 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
}
private Recipient getMessagePrimaryDestination(SignalServiceContent content, SignalServiceDataMessage message) {
private Recipient getMasterRecipientForMessage(SignalServiceContent content, SignalServiceDataMessage message) {
if (message.isGroupMessage()) {
return getMessageDestination(content, message);
return getRecipientForMessage(content, message);
} else {
return getPrimaryDeviceRecipient(content.getSender());
return getMasterRecipient(content.getSender());
}
}
/**
* Get the primary device recipient of the passed in device.
* Get the master device recipient of the provided device.
*
* If the device doesn't have a primary device then it will return the same device.
* If the device is our primary device then it will return our current device.
* Otherwise it will return the primary device.
* If the device doesn't have a master device this will return the same device.
* If the device is our master device then it will return our current device.
* Otherwise it will return the master device.
*/
private Recipient getPrimaryDeviceRecipient(String pubKey) {
private Recipient getMasterRecipient(String hexEncodedPublicKey) {
try {
String primaryDevice = PromiseUtil.timeout(LokiStorageAPI.shared.getPrimaryDevicePublicKey(pubKey), 5000).get();
String publicKey = (primaryDevice != null) ? primaryDevice : pubKey;
// If the public key matches our primary device then we need to forward the message to ourselves (Note to self)
String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
if (ourPrimaryDevice != null && ourPrimaryDevice.equals(publicKey)) {
publicKey = TextSecurePreferences.getLocalNumber(context);
String masterHexEncodedPublicKey = PromiseUtil.timeout(LokiDeviceLinkUtilities.INSTANCE.getMasterHexEncodedPublicKey(hexEncodedPublicKey), 5000).get();
String targetHexEncodedPublicKey = (masterHexEncodedPublicKey != null) ? masterHexEncodedPublicKey : hexEncodedPublicKey;
// If the public key matches our master device then we need to forward the message to ourselves (note to self)
String ourMasterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
if (ourMasterHexEncodedPublicKey != null && ourMasterHexEncodedPublicKey.equals(targetHexEncodedPublicKey)) {
targetHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
}
return Recipient.from(context, Address.fromSerialized(publicKey), false);
return Recipient.from(context, Address.fromSerialized(targetHexEncodedPublicKey), false);
} catch (Exception e) {
Log.d("Loki", "Failed to get primary device public key for " + pubKey + ". " + e.getMessage());
return Recipient.from(context, Address.fromSerialized(pubKey), false);
Log.d("Loki", "Failed to get master device for: " + hexEncodedPublicKey + ". " + e.getMessage());
return Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false);
}
}
@ -1831,11 +1802,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Recipient sender = Recipient.from(context, Address.fromSerialized(content.getSender()), false);
if (content.getPairingAuthorisation().isPresent()) {
if (content.getDeviceLink().isPresent()) {
return false;
} else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
Recipient conversation = getMessageDestination(content, message);
Recipient conversation = getRecipientForMessage(content, message);
if (conversation.isGroupRecipient() && conversation.isBlocked()) {
return true;

View File

@ -50,8 +50,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
import org.whispersystems.signalservice.loki.api.LokiDeviceLinkUtilities;
import org.whispersystems.signalservice.loki.api.LokiPublicChat;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
import java.io.IOException;
@ -368,7 +368,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
if (!member.isPhone() || member.serialize().equalsIgnoreCase(localNumber)) { continue; }
try {
List<String> secondaryDevices = PromiseUtil.timeout(LokiStorageAPI.shared.getSecondaryDevicePublicKeys(member.serialize()), 5000).get();
Set<String> secondaryDevices = PromiseUtil.timeout(LokiDeviceLinkUtilities.INSTANCE.getSlaveHexEncodedPublicKeys(member.serialize()), 5000).get();
memberSet.addAll(Stream.of(secondaryDevices).map(string -> {
// Loki - Calling .map(Address::fromSerialized) is causing errors, thus we use the long method :(
return Address.fromSerialized(string);

View File

@ -22,7 +22,6 @@ import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -43,13 +42,12 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.api.LokiDeviceLinkUtilities;
import org.whispersystems.signalservice.loki.messaging.LokiSyncMessage;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@ -291,11 +289,11 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
} else {
LokiSyncMessage syncMessage = null;
if (shouldSendSyncMessage) {
// Set the sync message destination the primary device, this way it will show that we sent a message to the primary device and not a secondary device
String primaryDevice = PromiseUtil.get(LokiStorageAPI.shared.getPrimaryDevicePublicKey(address.getNumber()), null);
SignalServiceAddress primaryAddress = primaryDevice == null ? address : new SignalServiceAddress(primaryDevice);
// We also need to use the original message id and not -1
syncMessage = new LokiSyncMessage(primaryAddress, templateMessageId);
// Set the sync message destination to the master device, this way it will show that we sent a message to the master device and not the slave device
String masterDevice = PromiseUtil.get(LokiDeviceLinkUtilities.INSTANCE.getMasterHexEncodedPublicKey(address.getNumber()), null);
SignalServiceAddress masterAddress = masterDevice == null ? address : new SignalServiceAddress(masterDevice);
// We also need to use the original message ID and not -1
syncMessage = new LokiSyncMessage(masterAddress, templateMessageId);
}
return messageSender.sendMessage(messageId, address, UnidentifiedAccessUtil.getAccessFor(context, recipient), mediaMessage, Optional.fromNullable(syncMessage)).getSuccess().isUnidentified();
}

View File

@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
@ -30,7 +29,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.api.LokiDeviceLinkUtilities;
import org.whispersystems.signalservice.loki.messaging.LokiSyncMessage;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
@ -236,11 +235,11 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
} else {
LokiSyncMessage syncMessage = null;
if (shouldSendSyncMessage) {
// Set the sync message destination to the primary device, this way it will show that we sent a message to the primary device and not a secondary device
String primaryDevice = PromiseUtil.get(LokiStorageAPI.shared.getPrimaryDevicePublicKey(address.getNumber()), null);
SignalServiceAddress primaryAddress = primaryDevice == null ? address : new SignalServiceAddress(primaryDevice);
// We also need to use the original message id and not -1
syncMessage = new LokiSyncMessage(primaryAddress, templateMessageId);
// Set the sync message destination to the master device, this way it will show that we sent a message to the master device and not the slave device
String masterDevice = PromiseUtil.get(LokiDeviceLinkUtilities.INSTANCE.getMasterHexEncodedPublicKey(address.getNumber()), null);
SignalServiceAddress masterAddress = masterDevice == null ? address : new SignalServiceAddress(masterDevice);
// We also need to use the original message ID and not -1
syncMessage = new LokiSyncMessage(masterAddress, templateMessageId);
}
return messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage, Optional.fromNullable(syncMessage)).getSuccess().isUnidentified();
}

View File

@ -0,0 +1,34 @@
package org.thoughtcrime.securesms.loki
import android.content.Context
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.sms.MessageSender
import org.whispersystems.libsignal.loki.LokiSessionResetProtocol
import org.whispersystems.libsignal.loki.LokiSessionResetStatus
import org.whispersystems.libsignal.protocol.PreKeySignalMessage
class LokiSessionResetImplementation(private val context: Context) : LokiSessionResetProtocol {
override fun getSessionResetStatus(hexEncodedPublicKey: String): LokiSessionResetStatus {
return DatabaseFactory.getLokiThreadDatabase(context).getSessionResetStatus(hexEncodedPublicKey)
}
override fun setSessionResetStatus(hexEncodedPublicKey: String, sessionResetStatus: LokiSessionResetStatus) {
return DatabaseFactory.getLokiThreadDatabase(context).setSessionResetStatus(hexEncodedPublicKey, sessionResetStatus)
}
override fun onNewSessionAdopted(hexEncodedPublicKey: String, oldSessionResetStatus: LokiSessionResetStatus) {
if (oldSessionResetStatus == LokiSessionResetStatus.IN_PROGRESS) {
// Send a message back to the contact to finalise session reset
MessageSender.sendBackgroundMessage(context, hexEncodedPublicKey)
}
// TODO: Show session reset succeed message
}
override fun validatePreKeySignalMessage(sender: String, message: PreKeySignalMessage) {
val preKeyRecord = DatabaseFactory.getLokiPreKeyRecordDatabase(context).getPreKeyRecord(sender)
check(preKeyRecord != null) { "Received a background message from a user without an associated pre key record." }
check(preKeyRecord.id == (message.preKeyId ?: -1)) { "Received a background message from an unknown source." }
}
}

View File

@ -10,11 +10,11 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.loki.redesign.utilities.*
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.libsignal.loki.LokiSessionResetStatus
import org.whispersystems.signalservice.internal.util.JsonUtil
import org.whispersystems.signalservice.loki.api.LokiPublicChat
import org.whispersystems.signalservice.loki.messaging.LokiThreadDatabaseProtocol
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
import org.whispersystems.signalservice.loki.messaging.LokiThreadSessionResetStatus
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiThreadDatabaseProtocol {
@ -23,11 +23,11 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
companion object {
private val friendRequestTableName = "loki_thread_friend_request_database"
private val sessionResetTableName = "loki_thread_session_reset_database"
public val publicChatTableName = "loki_public_chat_database"
public val threadID = "thread_id"
val publicChatTableName = "loki_public_chat_database"
val threadID = "thread_id"
private val friendRequestStatus = "friend_request_status"
private val sessionResetStatus = "session_reset_status"
public val publicChat = "public_chat"
val publicChat = "public_chat"
@JvmStatic val createFriendRequestTableCommand = "CREATE TABLE $friendRequestTableName ($threadID INTEGER PRIMARY KEY, $friendRequestStatus INTEGER DEFAULT 0);"
@JvmStatic val createSessionResetTableCommand = "CREATE TABLE $sessionResetTableName ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);"
@JvmStatic val createPublicChatTableCommand = "CREATE TABLE $publicChatTableName ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);"
@ -79,19 +79,21 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|| friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_RECEIVED
}
override fun getSessionResetStatus(threadID: Long): LokiThreadSessionResetStatus {
fun getSessionResetStatus(hexEncodedPublicKey: String): LokiSessionResetStatus {
val threadID = getThreadID(hexEncodedPublicKey)
val database = databaseHelper.readableDatabase
val result = database.get(sessionResetTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor ->
cursor.getInt(sessionResetStatus)
}
return if (result != null) {
LokiThreadSessionResetStatus.values().first { it.rawValue == result }
LokiSessionResetStatus.values().first { it.rawValue == result }
} else {
LokiThreadSessionResetStatus.NONE
LokiSessionResetStatus.NONE
}
}
override fun setSessionResetStatus(threadID: Long, sessionResetStatus: LokiThreadSessionResetStatus) {
fun setSessionResetStatus(hexEncodedPublicKey: String, sessionResetStatus: LokiSessionResetStatus) {
val threadID = getThreadID(hexEncodedPublicKey)
val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2)
contentValues.put(Companion.threadID, threadID)

View File

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.loki
import android.content.Context
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map
import nl.komponents.kovenant.toFailVoid
@ -16,40 +15,36 @@ 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.sms.MessageSender
import org.thoughtcrime.securesms.util.TextSecurePreferences
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.api.DeviceLink
import org.whispersystems.signalservice.loki.api.LokiDeviceLinkUtilities
import org.whispersystems.signalservice.loki.api.LokiFileServerAPI
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
import org.whispersystems.signalservice.loki.utilities.recover
import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
import java.util.*
import kotlin.concurrent.schedule
fun checkForRevocation(context: Context) {
val primaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context) ?: return
val ourDevice = TextSecurePreferences.getLocalNumber(context)
LokiStorageAPI.shared.fetchDeviceMappings(primaryDevice).bind { mappings ->
val ourMapping = mappings.find { it.secondaryDevicePublicKey == ourDevice }
if (ourMapping != null) throw Error("Device has not been revoked")
// remove pairing authorisations for our device
DatabaseFactory.getLokiAPIDatabase(context).removePairingAuthorisations(ourDevice)
LokiStorageAPI.shared.updateUserDeviceMappings()
fun checkIsRevokedSlaveDevice(context: Context) {
val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) ?: return
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
LokiFileServerAPI.shared.getDeviceLinks(masterHexEncodedPublicKey, true).bind { deviceLinks ->
val deviceLink = deviceLinks.find { it.masterHexEncodedPublicKey == masterHexEncodedPublicKey && it.slaveHexEncodedPublicKey == hexEncodedPublicKey }
if (deviceLink != null) throw Error("Device hasn't been revoked.")
DatabaseFactory.getLokiAPIDatabase(context).clearDeviceLinks(hexEncodedPublicKey)
LokiFileServerAPI.shared.setDeviceLinks(setOf())
}.successUi {
TextSecurePreferences.setNeedsRevocationCheck(context, false)
TextSecurePreferences.setNeedsIsRevokedSlaveDeviceCheck(context, false)
ApplicationContext.getInstance(context).clearData()
}.fail { error ->
TextSecurePreferences.setNeedsRevocationCheck(context, true)
Log.d("Loki", "Revocation check failed: ${error.message ?: error}")
TextSecurePreferences.setNeedsIsRevokedSlaveDeviceCheck(context, true)
Log.d("Loki", "Revocation check failed due to error: ${error.message ?: error}.")
}
}
fun getAllDeviceFriendRequestStatuses(context: Context, hexEncodedPublicKey: String): Promise<Map<String, LokiThreadFriendRequestStatus>, Exception> {
val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context)
return LokiStorageAPI.shared.getAllDevicePublicKeys(hexEncodedPublicKey).map { keys ->
return LokiDeviceLinkUtilities.getAllLinkedDeviceHexEncodedPublicKeys(hexEncodedPublicKey).map { keys ->
val map = mutableMapOf<String, LokiThreadFriendRequestStatus>()
for (devicePublicKey in keys) {
val device = Recipient.from(context, Address.fromSerialized(devicePublicKey), false)
@ -63,7 +58,7 @@ fun getAllDeviceFriendRequestStatuses(context: Context, hexEncodedPublicKey: Str
fun getAllDevicePublicKeysWithFriendStatus(context: Context, hexEncodedPublicKey: String): Promise<Map<String, Boolean>, Unit> {
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
return LokiStorageAPI.shared.getAllDevicePublicKeys(hexEncodedPublicKey).map { keys ->
return LokiDeviceLinkUtilities.getAllLinkedDeviceHexEncodedPublicKeys(hexEncodedPublicKey).map { keys ->
val devices = keys.toMutableSet()
if (hexEncodedPublicKey != userHexEncodedPublicKey) {
devices.remove(userHexEncodedPublicKey)
@ -92,7 +87,7 @@ fun shouldAutomaticallyBecomeFriendsWithDevice(publicKey: String, context: Conte
return Promise.of(true)
}
return LokiStorageAPI.shared.getPrimaryDevicePublicKey(publicKey).bind { primaryDevicePublicKey ->
return LokiDeviceLinkUtilities.getMasterHexEncodedPublicKey(publicKey).bind { primaryDevicePublicKey ->
// If the public key doesn't have any other devices then go through regular friend request logic
if (primaryDevicePublicKey == null) {
return@bind Promise.of(false)
@ -108,66 +103,44 @@ fun shouldAutomaticallyBecomeFriendsWithDevice(publicKey: String, context: Conte
}
}
fun sendPairingAuthorisationMessage(context: Context, contactHexEncodedPublicKey: String, authorisation: PairingAuthorisation): Promise<Unit, Exception> {
fun sendDeviceLinkMessage(context: Context, hexEncodedPublicKey: String, deviceLink: DeviceLink): Promise<Unit, Exception> {
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(contactHexEncodedPublicKey)
val message = SignalServiceDataMessage.newBuilder().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 address = SignalServiceAddress(hexEncodedPublicKey)
val message = SignalServiceDataMessage.newBuilder().withDeviceLink(deviceLink)
// A REQUEST should always act as a friend request. An AUTHORIZATION should always be a normal message.
if (deviceLink.type == DeviceLink.Type.REQUEST) {
val preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.number)
message.asFriendRequest(true).withPreKeyBundle(preKeyBundle)
} else {
// Send over our profile key so that our linked device can get our profile picture
message.withProfileKey(ProfileKeyUtil.getProfileKey(context))
}
return try {
Log.d("Loki", "Sending authorisation message to: $contactHexEncodedPublicKey.")
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(contactHexEncodedPublicKey), false))
Log.d("Loki", "Sending device link message to: $hexEncodedPublicKey.")
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false))
val result = messageSender.sendMessage(0, address, udAccess, 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."
result.isNetworkFailure -> "Failed to send device link message due to a network error."
else -> "Failed to send device link message."
}
throw Exception(exception)
}
Promise.ofSuccess(Unit)
} catch (e: Exception) {
Log.d("Loki", "Failed to send authorisation message to: $contactHexEncodedPublicKey.")
Log.d("Loki", "Failed to send device link message to: $hexEncodedPublicKey.")
Promise.ofFail(e)
}
}
fun signAndSendPairingAuthorisationMessage(context: Context, pairingAuthorisation: PairingAuthorisation) {
fun signAndSendDeviceLinkMessage(context: Context, deviceLink: DeviceLink): Promise<Unit, Exception> {
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
val signedPairingAuthorisation = pairingAuthorisation.sign(PairingAuthorisation.Type.GRANT, userPrivateKey)
if (signedPairingAuthorisation == null || signedPairingAuthorisation.type != PairingAuthorisation.Type.GRANT) {
Log.d("Loki", "Failed to sign pairing authorization.")
return
}
DatabaseFactory.getLokiAPIDatabase(context).insertOrUpdatePairingAuthorisation(signedPairingAuthorisation)
TextSecurePreferences.setMultiDevice(context, true)
val address = Address.fromSerialized(pairingAuthorisation.secondaryDevicePublicKey);
val sendPromise = retryIfNeeded(8) {
sendPairingAuthorisationMessage(context, address.serialize(), signedPairingAuthorisation)
}.fail {
Log.d("Loki", "Failed to send pairing authorization message to ${address.serialize()}.")
}
val updatePromise = LokiStorageAPI.shared.updateUserDeviceMappings().fail {
Log.d("Loki", "Failed to update device mapping")
}
// If both promises complete successfully then we should sync our contacts
all(listOf(sendPromise, updatePromise), cancelOthersOnError = false).success {
Log.d("Loki", "Successfully pairing with a secondary device! Syncing contacts.")
// Send out sync contact after a delay
Timer().schedule(3000) {
MessageSender.syncAllContacts(context, address)
val signedDeviceLink = deviceLink.sign(DeviceLink.Type.AUTHORIZATION, userPrivateKey)
if (signedDeviceLink == null || signedDeviceLink.type != DeviceLink.Type.AUTHORIZATION) {
return Promise.ofFail(Exception("Failed to sign device link."))
}
return retryIfNeeded(8) {
sendDeviceLinkMessage(context, deviceLink.slaveHexEncodedPublicKey, signedDeviceLink)
}
}
@ -177,7 +150,7 @@ fun isOneOfOurDevices(context: Context, address: Address): Promise<Boolean, Exce
}
val ourPublicKey = TextSecurePreferences.getLocalNumber(context)
return LokiStorageAPI.shared.getAllDevicePublicKeys(ourPublicKey).map { devices ->
return LokiDeviceLinkUtilities.getAllLinkedDeviceHexEncodedPublicKeys(ourPublicKey).map { devices ->
devices.contains(address.serialize())
}
}

View File

@ -10,8 +10,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.BaseJob
import org.thoughtcrime.securesms.logging.Log
import org.thoughtcrime.securesms.recipients.Recipient
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.internal.util.JsonUtil
@ -99,7 +97,7 @@ class PushBackgroundMessageSendJob private constructor(
}
if (message.get("sessionRestore", false)) {
dataMessage.asSessionRestore(true)
dataMessage.asSessionRestorationRequest(true)
}
if (message.get("sessionRequest", false)) {

View File

@ -104,9 +104,9 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), MemberC
val recipients = selectedMembers.map {
Recipient.from(this, Address.fromSerialized(it), false)
}.toSet()
val ourNumber = TextSecurePreferences.getMasterHexEncodedPublicKey(this) ?: TextSecurePreferences.getLocalNumber(this)
val local = Recipient.from(this, Address.fromSerialized(ourNumber), false)
CreateClosedGroupTask(WeakReference(this), null, name.toString(), recipients, setOf(local)).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(this) ?: TextSecurePreferences.getLocalNumber(this)
val admin = Recipient.from(this, Address.fromSerialized(masterHexEncodedPublicKey), false)
CreateClosedGroupTask(WeakReference(this), null, name.toString(), recipients, setOf( admin )).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
}
private fun handleOpenConversation(threadId: Long, recipient: Recipient) {
@ -122,7 +122,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), MemberC
// region Tasks
internal class CreateClosedGroupTask(
private val activity: WeakReference<CreateClosedGroupActivity>,
private val avatar: Bitmap?,
private val profilePicture: Bitmap?,
private val name: String?,
private val members: Set<Recipient>,
private val admins: Set<Recipient>
@ -130,24 +130,18 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), MemberC
override fun doInBackground(vararg params: Void?): Optional<GroupManager.GroupActionResult> {
val activity = activity.get() ?: return Optional.absent()
return Optional.of(GroupManager.createGroup(activity, members, avatar, name, false, admins))
return Optional.of(GroupManager.createGroup(activity, members, profilePicture, name, false, admins))
}
override fun onPostExecute(result: Optional<GroupManager.GroupActionResult>) {
val activity = activity.get()
if (activity == null) {
super.onPostExecute(result)
return
}
val activity = activity.get() ?: return super.onPostExecute(result)
if (result.isPresent && result.get().threadId > -1) {
if (!activity.isFinishing) {
activity.handleOpenConversation(result.get().threadId, result.get().groupRecipient)
}
} else {
super.onPostExecute(result)
Toast.makeText(activity.applicationContext,
R.string.GroupCreateActivity_contacts_invalid_number, Toast.LENGTH_LONG).show()
Toast.makeText(activity.applicationContext, "One of the members of your group has an invalid Session ID.", Toast.LENGTH_LONG).show()
}
}
}

View File

@ -12,9 +12,9 @@ class CreateClosedGroupLoader(context: Context) : AsyncLoader<List<String>>(cont
val threadDatabase = DatabaseFactory.getThreadDatabase(context)
val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context)
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
val deviceLinks = DatabaseFactory.getLokiAPIDatabase(context).getPairingAuthorisations(userHexEncodedPublicKey)
val deviceLinks = DatabaseFactory.getLokiAPIDatabase(context).getDeviceLinks(userHexEncodedPublicKey)
val userLinkedDeviceHexEncodedPublicKeys = deviceLinks.flatMap {
listOf( it.primaryDevicePublicKey.toLowerCase(), it.secondaryDevicePublicKey.toLowerCase() )
listOf( it.masterHexEncodedPublicKey.toLowerCase(), it.slaveHexEncodedPublicKey.toLowerCase() )
}.toMutableSet()
userLinkedDeviceHexEncodedPublicKeys.add(userHexEncodedPublicKey.toLowerCase())
val cursor = threadDatabase.conversationList

View File

@ -4,7 +4,7 @@ import android.content.Intent
import android.os.Bundle
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_display_name_v2.*
import kotlinx.android.synthetic.main.activity_display_name.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
@ -19,7 +19,7 @@ class DisplayNameActivity : BaseActionBarActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setUpActionBarSessionLogo()
setContentView(R.layout.activity_display_name_v2)
setContentView(R.layout.activity_display_name)
displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard
registerButton.setOnClickListener { register() }
}

View File

@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.redesign.fragments.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.loki.redesign.fragments.ScanQRCodeWrapperFragmentDelegate
import org.thoughtcrime.securesms.util.TextSecurePreferences
@ -72,6 +73,8 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
val displayName = TextSecurePreferences.getProfileName(this)
val lokiPublicChatAPI = application.lokiPublicChatAPI!!
application.lokiPublicChatManager.addChat(url, channel).successUi {
DatabaseFactory.getLokiAPIDatabase(this).removeLastMessageServerID(channel, url)
DatabaseFactory.getLokiAPIDatabase(this).removeLastDeletionServerID(channel, url)
lokiPublicChatAPI.getMessages(channel, url)
lokiPublicChatAPI.setDisplayName(displayName, url)
lokiPublicChatAPI.join(channel, url)
@ -133,7 +136,10 @@ class EnterChatURLFragment : Fragment() {
private fun joinPublicChatIfPossible() {
val inputMethodManager = context!!.getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0)
val chatURL = chatURLEditText.text.trim().toString().toLowerCase().replace("http://", "https://")
var chatURL = chatURLEditText.text.trim().toString().toLowerCase().replace("http://", "https://")
if (!chatURL.toLowerCase().startsWith("https")) {
chatURL = "https://$chatURL"
}
(activity!! as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
}
}

View File

@ -18,7 +18,7 @@ import org.thoughtcrime.securesms.loki.redesign.dialogs.LinkDeviceSlaveModeDialo
import org.thoughtcrime.securesms.loki.redesign.utilities.push
import org.thoughtcrime.securesms.loki.redesign.utilities.setUpActionBarSessionLogo
import org.thoughtcrime.securesms.loki.redesign.utilities.show
import org.thoughtcrime.securesms.loki.sendPairingAuthorisationMessage
import org.thoughtcrime.securesms.loki.sendDeviceLinkMessage
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.Hex
import org.thoughtcrime.securesms.util.TextSecurePreferences
@ -26,7 +26,8 @@ import org.whispersystems.curve25519.Curve25519
import org.whispersystems.libsignal.ecc.Curve
import org.whispersystems.libsignal.ecc.ECKeyPair
import org.whispersystems.libsignal.util.KeyHelper
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
import org.whispersystems.signalservice.loki.api.DeviceLink
import org.whispersystems.signalservice.loki.api.LokiFileServerAPI
import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey
import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
@ -92,11 +93,11 @@ class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelega
TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
TextSecurePreferences.setPromptedPushRegistration(this, true)
val authorisation = PairingAuthorisation(hexEncodedPublicKey, userHexEncodedPublicKey).sign(PairingAuthorisation.Type.REQUEST, keyPair!!.privateKey.serialize())
if (authorisation == null) {
val deviceLink = DeviceLink(hexEncodedPublicKey, userHexEncodedPublicKey).sign(DeviceLink.Type.REQUEST, keyPair!!.privateKey.serialize())
if (deviceLink == null) {
Log.d("Loki", "Failed to sign device link request.")
reset()
return Toast.makeText(application, "Couldn't link device.", Toast.LENGTH_SHORT).show()
return Toast.makeText(application, "Couldn't link device.", Toast.LENGTH_LONG).show()
}
val application = ApplicationContext.getInstance(this)
application.startLongPollingIfNeeded()
@ -107,13 +108,14 @@ class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelega
linkDeviceDialog.show(supportFragmentManager, "Link Device Dialog")
AsyncTask.execute {
retryIfNeeded(8) {
sendPairingAuthorisationMessage(this@LandingActivity, authorisation.primaryDevicePublicKey, authorisation)
sendDeviceLinkMessage(this@LandingActivity, deviceLink.masterHexEncodedPublicKey, deviceLink)
}
}
}
override fun onDeviceLinkRequestAuthorized(authorization: PairingAuthorisation) {
TextSecurePreferences.setMasterHexEncodedPublicKey(this, authorization.primaryDevicePublicKey)
override fun onDeviceLinkRequestAuthorized(deviceLink: DeviceLink) {
LokiFileServerAPI.shared.addDeviceLink(deviceLink)
TextSecurePreferences.setMasterHexEncodedPublicKey(this, deviceLink.masterHexEncodedPublicKey)
val intent = Intent(this, HomeActivity::class.java)
show(intent)
finish()

View File

@ -12,16 +12,21 @@ import android.view.View
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_linked_devices.*
import network.loki.messenger.R
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.devicelist.Device
import org.thoughtcrime.securesms.loki.redesign.dialogs.*
import org.thoughtcrime.securesms.loki.signAndSendPairingAuthorisationMessage
import org.thoughtcrime.securesms.loki.signAndSendDeviceLinkMessage
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
import org.whispersystems.signalservice.loki.api.DeviceLink
import org.whispersystems.signalservice.loki.api.LokiFileServerAPI
import java.util.*
import kotlin.concurrent.schedule
class LinkedDevicesActivity : PassphraseRequiredActionBarActivity, LoaderManager.LoaderCallbacks<List<Device>>, DeviceClickListener, EditDeviceNameDialogDelegate, LinkDeviceMasterModeDialogDelegate {
private var devices = listOf<Device>()
@ -118,20 +123,46 @@ class LinkedDevicesActivity : PassphraseRequiredActionBarActivity, LoaderManager
private fun unlinkDevice(slaveDeviceHexEncodedPublicKey: String) {
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)
val database = DatabaseFactory.getLokiAPIDatabase(this)
database.removePairingAuthorisation(userHexEncodedPublicKey, slaveDeviceHexEncodedPublicKey)
LokiStorageAPI.shared.updateUserDeviceMappings().success {
val deviceLinks = database.getDeviceLinks(userHexEncodedPublicKey)
val deviceLink = deviceLinks.find { it.masterHexEncodedPublicKey == userHexEncodedPublicKey && it.slaveHexEncodedPublicKey == slaveDeviceHexEncodedPublicKey }
if (deviceLink == null) {
return Toast.makeText(this, "Couldn't unlink device.", Toast.LENGTH_LONG).show()
}
LokiFileServerAPI.shared.setDeviceLinks(setOf()).successUi {
AsyncTask.execute {
DatabaseFactory.getLokiAPIDatabase(this).clearDeviceLinks(userHexEncodedPublicKey)
deviceLinks.forEach { deviceLink ->
DatabaseFactory.getLokiPreKeyBundleDatabase(this).removePreKeyBundle(deviceLink.slaveHexEncodedPublicKey)
}
MessageSender.sendUnpairRequest(this, slaveDeviceHexEncodedPublicKey)
}
LoaderManager.getInstance(this).restartLoader(0, null, this)
Toast.makeText(this, "Your device was unlinked successfully", Toast.LENGTH_LONG).show()
}.fail {
Toast.makeText(this, "Couldn't unlink device.", Toast.LENGTH_LONG).show()
}
}
override fun onDeviceLinkRequestAuthorized(authorization: PairingAuthorisation) {
AsyncTask.execute {
signAndSendPairingAuthorisationMessage(this, authorization)
override fun onDeviceLinkRequestAuthorized(deviceLink: DeviceLink) {
LokiFileServerAPI.shared.addDeviceLink(deviceLink).success {
signAndSendDeviceLinkMessage(this, deviceLink).success {
TextSecurePreferences.setMultiDevice(this, true)
Util.runOnMain {
LoaderManager.getInstance(this).restartLoader(0, null, this)
}
Timer().schedule(4000) {
MessageSender.syncAllContacts(this@LinkedDevicesActivity, Address.fromSerialized(deviceLink.slaveHexEncodedPublicKey))
}
}.fail {
LokiFileServerAPI.shared.removeDeviceLink(deviceLink) // If this fails we have a problem
DatabaseFactory.getLokiPreKeyBundleDatabase(this).removePreKeyBundle(deviceLink.slaveHexEncodedPublicKey)
Util.runOnMain {
Toast.makeText(this, "Couldn't link device", Toast.LENGTH_LONG).show()
}
}
}.failUi {
DatabaseFactory.getLokiPreKeyBundleDatabase(this).removePreKeyBundle(deviceLink.slaveHexEncodedPublicKey)
Toast.makeText(this, "Couldn't link device", Toast.LENGTH_LONG).show()
}
}

View File

@ -6,7 +6,7 @@ import org.thoughtcrime.securesms.devicelist.Device
import org.thoughtcrime.securesms.loki.redesign.utilities.MnemonicUtilities
import org.thoughtcrime.securesms.util.AsyncLoader
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
import org.whispersystems.signalservice.loki.api.LokiDeviceLinkUtilities
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
import java.io.File
@ -20,7 +20,7 @@ class LinkedDevicesLoader(context: Context) : AsyncLoader<List<Device>>(context)
override fun loadInBackground(): List<Device>? {
try {
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
val slaveDeviceHexEncodedPublicKeys = LokiStorageAPI.shared.getSecondaryDevicePublicKeys(userHexEncodedPublicKey).get()
val slaveDeviceHexEncodedPublicKeys = LokiDeviceLinkUtilities.getSlaveHexEncodedPublicKeys(userHexEncodedPublicKey).get()
return slaveDeviceHexEncodedPublicKeys.map { hexEncodedPublicKey ->
val shortID = MnemonicUtilities.getFirst3Words(mnemonicCodec, hexEncodedPublicKey)
val name = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(hexEncodedPublicKey)

View File

@ -9,7 +9,7 @@ import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.widget.LinearLayout
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_seed_v2.*
import kotlinx.android.synthetic.main.activity_seed.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
@ -33,7 +33,7 @@ class SeedActivity : BaseActionBarActivity() {
// region Lifecycle
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_seed_v2)
setContentView(R.layout.activity_seed)
supportActionBar!!.title = "Your Recovery Phrase"
val seedReminderViewTitle = SpannableString("You're almost finished! 90%")
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

View File

@ -41,7 +41,7 @@ import org.thoughtcrime.securesms.util.BitmapUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.crypto.ProfileCipher
import org.whispersystems.signalservice.api.util.StreamDetails
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
import org.whispersystems.signalservice.loki.api.LokiFileServerAPI
import java.io.ByteArrayInputStream
import java.io.File
import java.security.SecureRandom
@ -159,7 +159,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey)
if (isUpdatingProfilePicture && profilePicture != null) {
val storageAPI = LokiStorageAPI.shared
val storageAPI = LokiFileServerAPI.shared
val deferred = deferred<Unit, Exception>()
AsyncTask.execute {
val stream = StreamDetails(ByteArrayInputStream(profilePicture), "image/jpeg", profilePicture.size.toLong())
@ -178,7 +178,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)), profilePicture)
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
ApplicationContext.getInstance(this).updatePublicChatProfileAvatarIfNeeded()
ApplicationContext.getInstance(this).updatePublicChatProfilePictureIfNeeded()
profilePictureView.update()
}
profilePictureToBeUploaded = null

View File

@ -17,15 +17,15 @@ import org.thoughtcrime.securesms.loki.redesign.utilities.QRCodeUtilities
import org.thoughtcrime.securesms.loki.toPx
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.loki.api.DeviceLink
import org.whispersystems.signalservice.loki.api.DeviceLinkingSession
import org.whispersystems.signalservice.loki.api.DeviceLinkingSessionListener
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
class LinkDeviceMasterModeDialog : DialogFragment(), DeviceLinkingSessionListener {
private val languageFileDirectory by lazy { MnemonicUtilities.getLanguageFileDirectory(context!!) }
private lateinit var contentView: View
private var authorization: PairingAuthorisation? = null
private var deviceLink: DeviceLink? = null
var delegate: LinkDeviceMasterModeDialogDelegate? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -45,10 +45,10 @@ class LinkDeviceMasterModeDialog : DialogFragment(), DeviceLinkingSessionListene
return result
}
override fun requestUserAuthorization(authorization: PairingAuthorisation) {
if (authorization.type != PairingAuthorisation.Type.REQUEST || authorization.primaryDevicePublicKey != TextSecurePreferences.getLocalNumber(context!!) || this.authorization != null) { return }
override fun requestUserAuthorization(deviceLink: DeviceLink) {
if (deviceLink.type != DeviceLink.Type.REQUEST || deviceLink.masterHexEncodedPublicKey != TextSecurePreferences.getLocalNumber(context!!) || this.deviceLink != null) { return }
Util.runOnMain {
this.authorization = authorization
this.deviceLink = deviceLink
contentView.qrCodeImageView.visibility = View.GONE
val titleTextViewLayoutParams = contentView.titleTextView.layoutParams as LinearLayout.LayoutParams
titleTextViewLayoutParams.topMargin = toPx(8, resources)
@ -56,13 +56,13 @@ class LinkDeviceMasterModeDialog : DialogFragment(), DeviceLinkingSessionListene
contentView.titleTextView.text = "Linking Request Received"
contentView.explanationTextView.text = "Please check that the words below match those shown on your other device"
contentView.mnemonicTextView.visibility = View.VISIBLE
contentView.mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), authorization.secondaryDevicePublicKey)
contentView.mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), deviceLink.slaveHexEncodedPublicKey)
contentView.authorizeButton.visibility = View.VISIBLE
}
}
private fun authorizeDeviceLink() {
val authorization = this.authorization ?: return
val authorization = this.deviceLink ?: return
delegate?.onDeviceLinkRequestAuthorized(authorization)
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
DeviceLinkingSession.shared.removeListener(this)
@ -72,8 +72,8 @@ class LinkDeviceMasterModeDialog : DialogFragment(), DeviceLinkingSessionListene
private fun onDeviceLinkCanceled() {
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
DeviceLinkingSession.shared.removeListener(this)
if (authorization != null) {
DatabaseFactory.getLokiPreKeyBundleDatabase(context).removePreKeyBundle(authorization!!.secondaryDevicePublicKey)
if (deviceLink != null) {
DatabaseFactory.getLokiPreKeyBundleDatabase(context).removePreKeyBundle(deviceLink!!.slaveHexEncodedPublicKey)
}
dismiss()
delegate?.onDeviceLinkCanceled()
@ -82,6 +82,6 @@ class LinkDeviceMasterModeDialog : DialogFragment(), DeviceLinkingSessionListene
interface LinkDeviceMasterModeDialogDelegate {
fun onDeviceLinkRequestAuthorized(authorization: PairingAuthorisation)
fun onDeviceLinkRequestAuthorized(authorization: DeviceLink)
fun onDeviceLinkCanceled()
}

View File

@ -15,15 +15,15 @@ import network.loki.messenger.R
import org.thoughtcrime.securesms.loki.redesign.utilities.MnemonicUtilities
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.loki.api.DeviceLink
import org.whispersystems.signalservice.loki.api.DeviceLinkingSession
import org.whispersystems.signalservice.loki.api.DeviceLinkingSessionListener
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
class LinkDeviceSlaveModeDialog : DialogFragment(), DeviceLinkingSessionListener {
private val languageFileDirectory by lazy { MnemonicUtilities.getLanguageFileDirectory(context!!) }
private lateinit var contentView: View
private var authorization: PairingAuthorisation? = null
private var deviceLink: DeviceLink? = null
var delegate: LinkDeviceSlaveModeDialogDelegate? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -40,10 +40,10 @@ class LinkDeviceSlaveModeDialog : DialogFragment(), DeviceLinkingSessionListener
return result
}
override fun onDeviceLinkRequestAuthorized(authorization: PairingAuthorisation) {
if (authorization.type != PairingAuthorisation.Type.GRANT || authorization.secondaryDevicePublicKey != TextSecurePreferences.getLocalNumber(context!!) || this.authorization != null) { return }
override fun onDeviceLinkRequestAuthorized(deviceLink: DeviceLink) {
if (deviceLink.type != DeviceLink.Type.AUTHORIZATION || deviceLink.slaveHexEncodedPublicKey != TextSecurePreferences.getLocalNumber(context!!) || this.deviceLink != null) { return }
Util.runOnMain {
this.authorization = authorization
this.deviceLink = deviceLink
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
DeviceLinkingSession.shared.removeListener(this)
contentView.spinner.visibility = View.GONE
@ -56,7 +56,7 @@ class LinkDeviceSlaveModeDialog : DialogFragment(), DeviceLinkingSessionListener
contentView.cancelButton.visibility = View.GONE
Handler().postDelayed({
dismiss()
delegate?.onDeviceLinkRequestAuthorized(authorization)
delegate?.onDeviceLinkRequestAuthorized(deviceLink)
}, 4000)
}
}
@ -71,6 +71,6 @@ class LinkDeviceSlaveModeDialog : DialogFragment(), DeviceLinkingSessionListener
interface LinkDeviceSlaveModeDialogDelegate {
fun onDeviceLinkRequestAuthorized(authorization: PairingAuthorisation)
fun onDeviceLinkRequestAuthorized(authorization: DeviceLink)
fun onDeviceLinkCanceled()
}

View File

@ -15,7 +15,7 @@ import java.util.concurrent.TimeUnit
class BackgroundPollWorker : PersistentAlarmManagerListener() {
companion object {
private val pollInterval = TimeUnit.MINUTES.toMillis(2)
private val pollInterval = TimeUnit.MINUTES.toMillis(1)
@JvmStatic
fun schedule(context: Context) {

View File

@ -10,7 +10,7 @@ import java.util.concurrent.TimeUnit
class BackgroundPublicChatPollWorker : PersistentAlarmManagerListener() {
companion object {
private val pollInterval = TimeUnit.MINUTES.toMillis(4)
private val pollInterval = TimeUnit.MINUTES.toMillis(2)
@JvmStatic
fun schedule(context: Context) {

View File

@ -7,9 +7,9 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.loki.redesign.utilities.*
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.api.DeviceLink
import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol
import org.whispersystems.signalservice.loki.api.LokiAPITarget
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
// TODO: Clean this up a bit
@ -48,14 +48,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
private val lastDeletionServerIDCacheIndex = "loki_api_last_deletion_server_id_cache_index"
private val lastDeletionServerID = "last_deletion_server_id"
@JvmStatic val createLastDeletionServerIDTableCommand = "CREATE TABLE $lastDeletionServerIDCache ($lastDeletionServerIDCacheIndex STRING PRIMARY KEY, $lastDeletionServerID INTEGER DEFAULT 0);"
// Pairing authorisation cache
private val pairingAuthorisationCache = "loki_pairing_authorisation_cache"
private val primaryDevicePublicKey = "primary_device"
private val secondaryDevicePublicKey = "secondary_device"
// Device link cache
private val deviceLinkCache = "loki_pairing_authorisation_cache"
private val masterHexEncodedPublicKey = "primary_device"
private val slaveHexEncodedPublicKey = "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));"
private val authorizationSignature = "grant_signature"
@JvmStatic val createDeviceLinkTableCommand = "CREATE TABLE $deviceLinkCache ($masterHexEncodedPublicKey TEXT, $slaveHexEncodedPublicKey TEXT, " +
"$requestSignature TEXT NULLABLE DEFAULT NULL, $authorizationSignature TEXT NULLABLE DEFAULT NULL, PRIMARY KEY ($masterHexEncodedPublicKey, $slaveHexEncodedPublicKey));"
// User count cache
private val userCountCache = "loki_user_count_cache"
private val publicChatID = "public_chat_id"
@ -179,35 +179,35 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.delete(lastDeletionServerIDCache,"$lastDeletionServerIDCacheIndex = ?", wrap(index))
}
override fun getPairingAuthorisations(hexEncodedPublicKey: String): List<PairingAuthorisation> {
override fun getDeviceLinks(hexEncodedPublicKey: String): Set<DeviceLink> {
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)
return database.getAll(deviceLinkCache, "$masterHexEncodedPublicKey = ? OR $slaveHexEncodedPublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey )) { cursor ->
val masterHexEncodedPublicKey = cursor.getString(masterHexEncodedPublicKey)
val slaveHexEncodedPublicKey = cursor.getString(slaveHexEncodedPublicKey)
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)
}
val authorizationSignature: ByteArray? = if (cursor.isNull(cursor.getColumnIndexOrThrow(authorizationSignature))) null else cursor.getBase64EncodedData(authorizationSignature)
DeviceLink(masterHexEncodedPublicKey, slaveHexEncodedPublicKey, requestSignature, authorizationSignature)
}.toSet()
}
override fun insertOrUpdatePairingAuthorisation(authorisation: PairingAuthorisation) {
override fun clearDeviceLinks(hexEncodedPublicKey: String) {
val database = databaseHelper.writableDatabase
database.delete(deviceLinkCache, "$masterHexEncodedPublicKey = ? OR $slaveHexEncodedPublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey ))
}
override fun addDeviceLink(deviceLink: DeviceLink) {
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 ))
values.put(masterHexEncodedPublicKey, deviceLink.masterHexEncodedPublicKey)
values.put(slaveHexEncodedPublicKey, deviceLink.slaveHexEncodedPublicKey)
if (deviceLink.requestSignature != null) { values.put(requestSignature, Base64.encodeBytes(deviceLink.requestSignature)) }
if (deviceLink.authorizationSignature != null) { values.put(authorizationSignature, Base64.encodeBytes(deviceLink.authorizationSignature)) }
database.insertOrUpdate(deviceLinkCache, values, "$masterHexEncodedPublicKey = ? AND $slaveHexEncodedPublicKey = ?", arrayOf( deviceLink.masterHexEncodedPublicKey, deviceLink.slaveHexEncodedPublicKey ))
}
override fun removePairingAuthorisations(hexEncodedPublicKey: String) {
override fun removeDeviceLink(deviceLink: DeviceLink) {
val database = databaseHelper.writableDatabase
database.delete(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey ))
}
fun removePairingAuthorisation(primaryDevicePublicKey: String, secondaryDevicePublicKey: String) {
val database = databaseHelper.writableDatabase
database.delete(pairingAuthorisationCache, "${Companion.primaryDevicePublicKey} = ? OR ${Companion.secondaryDevicePublicKey} = ?", arrayOf( primaryDevicePublicKey, secondaryDevicePublicKey ))
database.delete(deviceLinkCache, "$masterHexEncodedPublicKey = ? OR $slaveHexEncodedPublicKey = ?", arrayOf( deviceLink.masterHexEncodedPublicKey, deviceLink.slaveHexEncodedPublicKey ))
}
fun getUserCount(group: Long, server: String): Int? {

View File

@ -21,10 +21,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.messages.SignalServiceGroup
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.loki.api.LokiPublicChat
import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI
import org.whispersystems.signalservice.loki.api.LokiPublicChatMessage
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
import org.whispersystems.signalservice.loki.api.*
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
import org.whispersystems.signalservice.loki.utilities.successBackground
import java.security.MessageDigest
@ -155,36 +152,36 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
fun pollForNewMessages() {
fun processIncomingMessage(message: LokiPublicChatMessage) {
// If the sender of the current message is not a secondary device, we need to set the display name in the database
val primaryDevice = LokiStorageAPI.shared.getPrimaryDevicePublicKey(message.hexEncodedPublicKey).get()
if (primaryDevice == null) {
// If the sender of the current message is not a slave device, set the display name in the database
val masterHexEncodedPublicKey = LokiDeviceLinkUtilities.getMasterHexEncodedPublicKey(message.hexEncodedPublicKey).get()
if (masterHexEncodedPublicKey == null) {
val senderDisplayName = "${message.displayName} (...${message.hexEncodedPublicKey.takeLast(8)})"
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.hexEncodedPublicKey, senderDisplayName)
}
val senderPublicKey = primaryDevice ?: message.hexEncodedPublicKey
val senderHexEncodedPublicKey = masterHexEncodedPublicKey ?: message.hexEncodedPublicKey
val serviceDataMessage = getDataMessage(message)
val serviceContent = SignalServiceContent(serviceDataMessage, senderPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.timestamp, false, false)
val serviceContent = SignalServiceContent(serviceDataMessage, senderHexEncodedPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.timestamp, false, false)
if (serviceDataMessage.quote.isPresent || (serviceDataMessage.attachments.isPresent && serviceDataMessage.attachments.get().size > 0) || serviceDataMessage.previews.isPresent) {
PushDecryptJob(context).handleMediaMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID))
} else {
PushDecryptJob(context).handleTextMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID))
}
// Update profile avatar if needed
val senderRecipient = Recipient.from(context, Address.fromSerialized(senderPublicKey), false)
if (message.avatar != null && message.avatar!!.url.isNotEmpty()) {
val profileKey = message.avatar!!.profileKey
val url = message.avatar!!.url
if (senderRecipient.profileKey == null || !MessageDigest.isEqual(senderRecipient.profileKey, profileKey)) {
// Update profile picture if needed
val senderAsRecipient = Recipient.from(context, Address.fromSerialized(senderHexEncodedPublicKey), false)
if (message.profilePicture != null && message.profilePicture!!.url.isNotEmpty()) {
val profileKey = message.profilePicture!!.profileKey
val url = message.profilePicture!!.url
if (senderAsRecipient.profileKey == null || !MessageDigest.isEqual(senderAsRecipient.profileKey, profileKey)) {
val database = DatabaseFactory.getRecipientDatabase(context)
database.setProfileKey(senderRecipient, profileKey)
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderRecipient, url))
database.setProfileKey(senderAsRecipient, profileKey)
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderAsRecipient, url))
}
} else if (senderRecipient.profileAvatar.orEmpty().isNotEmpty()) {
// Unset the avatar if we had an avatar before and we're not friends with the person
val threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(senderRecipient)
val friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId)
} else if (senderAsRecipient.profileAvatar.orEmpty().isNotEmpty()) {
// Clear the profile picture if we had a profile picture before and we're not friends with the person
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(senderAsRecipient)
val friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID)
if (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) {
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderRecipient, ""))
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderAsRecipient, ""))
}
}
}
@ -193,25 +190,25 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
val isDuplicate = DatabaseFactory.getLokiMessageDatabase(context).getMessageID(messageServerID) != null
if (isDuplicate) { return }
if (message.body.isEmpty() && message.attachments.isEmpty() && message.quote == null) { return }
val localNumber = TextSecurePreferences.getLocalNumber(context)
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
val dataMessage = getDataMessage(message)
val transcript = SentTranscriptMessage(localNumber, dataMessage.timestamp, dataMessage, dataMessage.expiresInSeconds.toLong(), Collections.singletonMap(localNumber, false))
val transcript = SentTranscriptMessage(userHexEncodedPublicKey, dataMessage.timestamp, dataMessage, dataMessage.expiresInSeconds.toLong(), Collections.singletonMap(userHexEncodedPublicKey, false))
transcript.messageServerID = messageServerID
if (dataMessage.quote.isPresent || (dataMessage.attachments.isPresent && dataMessage.attachments.get().size > 0) || dataMessage.previews.isPresent) {
PushDecryptJob(context).handleSynchronizeSentMediaMessage(transcript)
} else {
PushDecryptJob(context).handleSynchronizeSentTextMessage(transcript)
}
// If we got a message from our master device then make sure our mappings stay in sync
// If we got a message from our master device then make sure our mapping stays in sync
val recipient = Recipient.from(context, Address.fromSerialized(message.hexEncodedPublicKey), false)
if (recipient.isOurMasterDevice && message.avatar != null) {
val profileKey = message.avatar!!.profileKey
val url = message.avatar!!.url
if (recipient.isOurMasterDevice && message.profilePicture != null) {
val profileKey = message.profilePicture!!.profileKey
val url = message.profilePicture!!.url
if (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, profileKey)) {
val database = DatabaseFactory.getRecipientDatabase(context)
database.setProfileKey(recipient, profileKey)
database.setProfileAvatar(recipient, url)
ApplicationContext.getInstance(context).updatePublicChatProfileAvatarIfNeeded()
ApplicationContext.getInstance(context).updatePublicChatProfilePictureIfNeeded()
}
}
}
@ -219,29 +216,26 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
var uniqueDevices = setOf<String>()
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
val database = DatabaseFactory.getLokiAPIDatabase(context)
LokiStorageAPI.configure(false, userHexEncodedPublicKey, userPrivateKey, database)
LokiStorageAPI.shared.getAllDevicePublicKeys(userHexEncodedPublicKey).bind { devices ->
LokiFileServerAPI.configure(false, userHexEncodedPublicKey, userPrivateKey, database)
LokiDeviceLinkUtilities.getAllLinkedDeviceHexEncodedPublicKeys(userHexEncodedPublicKey).bind { devices ->
userDevices = devices
api.getMessages(group.channel, group.server)
}.bind { messages ->
if (messages.isNotEmpty()) {
// We need to fetch device mappings for all the devices we don't have
// We need to fetch the device mapping for any devices we don't have
uniqueDevices = messages.map { it.hexEncodedPublicKey }.toSet()
val devicesToUpdate = uniqueDevices.filter { !userDevices.contains(it) && LokiStorageAPI.shared.hasCacheExpired(it) }
val devicesToUpdate = uniqueDevices.filter { !userDevices.contains(it) && LokiFileServerAPI.shared.hasDeviceLinkCacheExpired(hexEncodedPublicKey = it) }
if (devicesToUpdate.isNotEmpty()) {
return@bind LokiStorageAPI.shared.getDeviceMappings(devicesToUpdate.toSet()).then { messages }
return@bind LokiFileServerAPI.shared.getDeviceLinks(devicesToUpdate.toSet()).then { messages }
}
}
Promise.of(messages)
}.successBackground {
// Get the set of primary device pubKeys FROM the secondary devices in uniqueDevices
val newDisplayNameUpdatees = uniqueDevices.mapNotNull {
// This will return null if current device is primary
// So if it's non-null then we know the device is a secondary device
val primaryDevice = LokiStorageAPI.shared.getPrimaryDevicePublicKey(it).get()
primaryDevice
// This will return null if the current device is a master device
LokiDeviceLinkUtilities.getMasterHexEncodedPublicKey(it).get()
}.toSet()
// Fetch the display names of the primary devices
// Fetch the display names of the master devices
displayNameUpdatees = displayNameUpdatees.union(newDisplayNameUpdatees)
}.successBackground { messages ->
// Process messages in the background

View File

@ -14,7 +14,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.messages.SignalServiceGroup
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.loki.api.LokiRSSFeed
import org.whispersystems.signalservice.loki.api.LokiRSSProxy
import org.whispersystems.signalservice.loki.api.LokiRSSFeedProxy
import org.whispersystems.signalservice.loki.utilities.successBackground
import java.text.SimpleDateFormat
import java.util.regex.Pattern
@ -48,7 +48,7 @@ class LokiRSSFeedPoller(private val context: Context, private val feed: LokiRSSF
}
private fun poll() {
LokiRSSProxy.fetch(feed.url).successBackground { xml ->
LokiRSSFeedProxy.fetch(feed.url).successBackground { xml ->
val items = XMLParser(xml).call()
items.reversed().forEach { item ->
val title = item.title ?: return@forEach

View File

@ -6,6 +6,11 @@ import android.support.v4.content.LocalBroadcastManager
class Broadcaster(private val context: Context) : org.whispersystems.signalservice.loki.utilities.Broadcaster {
override fun broadcast(event: String) {
val intent = Intent(event)
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
}
override fun broadcast(event: String, long: Long) {
val intent = Intent(event)
intent.putExtra("long", long)

View File

@ -53,7 +53,7 @@ object MentionUtilities {
}
}
val result = SpannableString(text)
val userLinkedDeviceHexEncodedPublicKeys = DatabaseFactory.getLokiAPIDatabase(context).getPairingAuthorisations(userHexEncodedPublicKey).flatMap { listOf( it.primaryDevicePublicKey, it.secondaryDevicePublicKey ) }.toMutableSet()
val userLinkedDeviceHexEncodedPublicKeys = DatabaseFactory.getLokiAPIDatabase(context).getDeviceLinks(userHexEncodedPublicKey).flatMap { listOf( it.masterHexEncodedPublicKey, it.slaveHexEncodedPublicKey ) }.toMutableSet()
userLinkedDeviceHexEncodedPublicKeys.add(userHexEncodedPublicKey)
for (mention in mentions) {
if (!userLinkedDeviceHexEncodedPublicKeys.contains(mention.second)) { continue }

View File

@ -12,7 +12,7 @@ import network.loki.messenger.R
import org.thoughtcrime.securesms.loki.getColorWithID
import org.thoughtcrime.securesms.loki.toPx
class SeparatorView : RelativeLayout {
class LabeledSeparatorView : RelativeLayout {
private val path = Path()

View File

@ -3,13 +3,12 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.loki.api.LokiFileServerAPI;
public class PushMediaConstraints extends MediaConstraints {
private static final int MAX_IMAGE_DIMEN_LOWMEM = 768;
private static final int MAX_IMAGE_DIMEN = 4096;
private static final int KB = 1024;
private static final int MB = 1024 * KB;
@Override
public int getImageMaxWidth(Context context) {
@ -23,26 +22,26 @@ public class PushMediaConstraints extends MediaConstraints {
@Override
public int getImageMaxSize(Context context) {
return 6 * MB;
return LokiFileServerAPI.Companion.getMaxFileSize();
}
@Override
public int getGifMaxSize(Context context) {
return 25 * MB;
return LokiFileServerAPI.Companion.getMaxFileSize();
}
@Override
public int getVideoMaxSize(Context context) {
return 100 * MB;
return LokiFileServerAPI.Companion.getMaxFileSize();
}
@Override
public int getAudioMaxSize(Context context) {
return 100 * MB;
return LokiFileServerAPI.Companion.getMaxFileSize();
}
@Override
public int getDocumentMaxSize(Context context) {
return 100 * MB;
return LokiFileServerAPI.Companion.getMaxFileSize();
}
}

View File

@ -23,7 +23,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.api.LokiFileServerAPI;
import java.util.LinkedList;
import java.util.List;

View File

@ -9,14 +9,10 @@ fun getOpenGroupDisplayName(recipient: Recipient, threadRecipient: Recipient, co
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(threadRecipient)
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
val hexEncodedPublicKey = recipient.address.toString()
val displayName: String?
displayName = if (publicChat != null) {
val displayName = if (publicChat != null) {
DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, hexEncodedPublicKey)
} else {
DatabaseFactory.getLokiUserDatabase(context).getDisplayName(hexEncodedPublicKey)
}
if (displayName == null) {
return hexEncodedPublicKey
}
return displayName
return displayName ?: hexEncodedPublicKey
}

View File

@ -38,7 +38,7 @@ public class MessageSenderEventListener implements SignalServiceMessageSender.Ev
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sent, messageID, threadID);
}
@Override public void onFriendRequestSendingFail(long messageID, long threadID) {
@Override public void onFriendRequestSendingFailed(long messageID, long threadID) {
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Failed, messageID, threadID);
}
}

View File

@ -60,7 +60,7 @@ import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.api.LokiDeviceLinkUtilities;
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
@ -99,7 +99,7 @@ public class MessageSender {
sendBackgroundMessage(context, contactHexEncodedPublicKey);
// Go through the other devices and only send background messages if we're friends or we have received friend request
LokiStorageAPI.shared.getAllDevicePublicKeys(contactHexEncodedPublicKey).success(devices -> {
LokiDeviceLinkUtilities.INSTANCE.getAllLinkedDeviceHexEncodedPublicKeys(contactHexEncodedPublicKey).success(devices -> {
Util.runOnMain(() -> {
for (String device : devices) {
// Don't send message to the device we already have sent to
@ -234,7 +234,7 @@ public class MessageSender {
final int ttl) {
String ourPublicKey = TextSecurePreferences.getLocalNumber(context);
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
LokiStorageAPI.shared.getAllDevicePublicKeys(ourPublicKey).success(devices -> {
LokiDeviceLinkUtilities.INSTANCE.getAllLinkedDeviceHexEncodedPublicKeys(ourPublicKey).success(devices -> {
Util.runOnMain(() -> {
for (String device : devices) {
// Don't send to ourselves
@ -306,8 +306,8 @@ public class MessageSender {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
// Just send the message normally if it's a group message or we're sending to one of our devices
String recipientPublicKey = recipient.getAddress().serialize();
if (GeneralUtilitiesKt.isPublicChat(context, recipientPublicKey) || PromiseUtil.get(MultiDeviceUtilities.isOneOfOurDevices(context, recipient.getAddress()), false)) {
String recipientHexEncodedPublicKey = recipient.getAddress().serialize();
if (GeneralUtilitiesKt.isPublicChat(context, recipientHexEncodedPublicKey) || PromiseUtil.get(MultiDeviceUtilities.isOneOfOurDevices(context, recipient.getAddress()), false)) {
if (type == MessageType.MEDIA) {
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress(), false);
} else {
@ -318,16 +318,16 @@ public class MessageSender {
// If we get here then we are sending a message to a device that is not ours
boolean[] hasSentSyncMessage = { false };
MultiDeviceUtilities.getAllDevicePublicKeysWithFriendStatus(context, recipientPublicKey).success(devices -> {
MultiDeviceUtilities.getAllDevicePublicKeysWithFriendStatus(context, recipientHexEncodedPublicKey).success(devices -> {
int friendCount = MultiDeviceUtilities.getFriendCount(context, devices.keySet());
Util.runOnMain(() -> {
ArrayList<Job> jobs = new ArrayList<>();
for (Map.Entry<String, Boolean> entry : devices.entrySet()) {
String devicePublicKey = entry.getKey();
String deviceHexEncodedPublicKey = entry.getKey();
boolean isFriend = entry.getValue();
Address address = Address.fromSerialized(devicePublicKey);
long messageIDToUse = recipientPublicKey.equals(devicePublicKey) ? messageId : -1L;
Address address = Address.fromSerialized(deviceHexEncodedPublicKey);
long messageIDToUse = recipientHexEncodedPublicKey.equals(deviceHexEncodedPublicKey) ? messageId : -1L;
if (isFriend) {
// Send a normal message if the user is friends with the recipient
@ -340,7 +340,7 @@ public class MessageSender {
}
if (shouldSendSyncMessage) { hasSentSyncMessage[0] = true; }
} else {
// Send friend requests to non friends. If the user is friends with any
// Send friend requests to non-friends. If the user is friends with any
// of the devices then send out a default friend request message.
boolean isFriendsWithAny = (friendCount > 0);
String defaultFriendRequestMessage = isFriendsWithAny ? "Please accept to enable messages to be synced across devices" : null;

View File

@ -1235,11 +1235,11 @@ public class TextSecurePreferences {
return getBooleanPreference(context, "database_reset_unpair", false);
}
public static void setNeedsRevocationCheck(Context context, boolean needsCheck) {
setBooleanPreference(context, "needs_revocation", needsCheck);
public static void setNeedsIsRevokedSlaveDeviceCheck(Context context, boolean value) {
setBooleanPreference(context, "needs_revocation", value);
}
public static boolean needsRevocationCheck(Context context) {
public static boolean getNeedsIsRevokedSlaveDeviceCheck(Context context) {
return getBooleanPreference(context, "needs_revocation", false);
}