diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 3b18e8b60b..7dfcd2cb5b 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,35 +1,50 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ package="network.loki.messenger">
-
+
-
+
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
+
+ -->
-
-
-
-
-
-
-
+
-
-
-
-
-
+
+
-
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+
-
+
+
+
-
-
-
-
-
+
+
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/ic_edit_white_24dp.xml b/res/drawable/ic_edit_white_24dp.xml
new file mode 100644
index 0000000000..46462b5726
--- /dev/null
+++ b/res/drawable/ic_edit_white_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/res/drawable/ic_phonelink_erase_white_24dp.xml b/res/drawable/ic_phonelink_erase_white_24dp.xml
new file mode 100644
index 0000000000..dc5c1b0260
--- /dev/null
+++ b/res/drawable/ic_phonelink_erase_white_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/res/layout/activity_seed.xml b/res/layout/activity_seed.xml
index 1ac6b7e57a..cb77523182 100644
--- a/res/layout/activity_seed.xml
+++ b/res/layout/activity_seed.xml
@@ -96,6 +96,17 @@
app:labeledEditText_background="@color/loki_darkest_gray"
app:labeledEditText_label="@string/activity_key_pair_public_key_edit_text_label"/>
+
+
diff --git a/res/layout/device_list_fragment.xml b/res/layout/device_list_fragment.xml
index 163b1b5865..475ff4005e 100644
--- a/res/layout/device_list_fragment.xml
+++ b/res/layout/device_list_fragment.xml
@@ -1,58 +1,49 @@
-
+
-
+
-
-
-
+
-
-
-
+
+ android:id="@+id/addDeviceButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_margin="16dp"
+ android:src="@drawable/ic_add_white_original_24dp"
+ android:focusable="true"
+ android:contentDescription="@string/device_list_fragment__link_new_device"
+ fab:fab_colorNormal="?fab_color"
+ fab:fab_colorPressed="@color/textsecure_primary_dark"
+ fab:fab_colorRipple="@color/textsecure_primary_dark" />
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/res/layout/device_list_item_view.xml b/res/layout/device_list_item_view.xml
index c6919fb7d5..43039ba16a 100644
--- a/res/layout/device_list_item_view.xml
+++ b/res/layout/device_list_item_view.xml
@@ -1,33 +1,34 @@
-
+
-
+
-
-
-
+
\ No newline at end of file
diff --git a/res/layout/fragment_device_list_bottom_sheet.xml b/res/layout/fragment_device_list_bottom_sheet.xml
new file mode 100644
index 0000000000..5c1092fa65
--- /dev/null
+++ b/res/layout/fragment_device_list_bottom_sheet.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/res/layout/fragment_scan_qr_code.xml b/res/layout/fragment_scan_qr_code.xml
index e86ae68ba8..4a3aa5fe79 100644
--- a/res/layout/fragment_scan_qr_code.xml
+++ b/res/layout/fragment_scan_qr_code.xml
@@ -24,13 +24,14 @@
android:layout_height="match_parent"/>
diff --git a/res/layout/profile_create_activity.xml b/res/layout/profile_create_activity.xml
index c9b105c593..02347896d3 100644
--- a/res/layout/profile_create_activity.xml
+++ b/res/layout/profile_create_activity.xml
@@ -43,7 +43,6 @@
android:id="@+id/avatar_background"
android:layout_width="80dp"
android:layout_height="80dp"
- android:visibility="gone"
android:layout_marginStart="32dp"
android:layout_marginTop="4dp"
android:src="@drawable/circle_tintable"
@@ -56,7 +55,6 @@
android:id="@+id/avatar_placeholder"
android:layout_width="0dp"
android:layout_height="0dp"
- android:visibility="gone"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_profile_default"
@@ -71,7 +69,6 @@
android:id="@+id/avatar"
android:layout_width="0dp"
android:layout_height="0dp"
- android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/avatar_background"
app:layout_constraintEnd_toEndOf="@+id/avatar_background"
app:layout_constraintStart_toStartOf="@+id/avatar_background"
@@ -81,7 +78,6 @@
android:id="@+id/camera_icon"
android:layout_width="60dp"
android:layout_height="60dp"
- android:visibility="gone"
android:layout_marginStart="35dp"
android:layout_marginTop="35dp"
android:cropToPadding="false"
@@ -94,7 +90,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
- android:layout_marginStart="49dp"
+ android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="4dp"
android:layout_weight="1"
@@ -105,7 +101,7 @@
app:layout_constraintBottom_toTopOf="@+id/description_text"
app:layout_constraintEnd_toStartOf="@+id/emoji_toggle"
app:layout_constraintHorizontal_bias="0.5"
- app:layout_constraintStart_toStartOf="@+id/avatar_background"
+ app:layout_constraintStart_toEndOf="@+id/avatar_background"
app:layout_constraintTop_toBottomOf="@+id/title" />
+
+
-150dp
+ 16dp
+ 24dp
+ 16sp
+ 16dp
+ 56dp
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 307c000c62..d850790d91 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -293,12 +293,14 @@
Unlink \'%s\'?
- By unlinking this device, it will no longer be able to send or receive messages.
+ This device will no longer be able to send or receive messages.
Network connection failed
Try again
Unlinking device...
Unlinking device
Network failed!
+ Successfully unlinked device
+ Edit device name
Unnamed device
@@ -946,7 +948,7 @@
Link device
- No devices linked
+ You don\'t have any linked devices yet
Link new device
@@ -1157,7 +1159,7 @@
Submit debug log
Media preview
Message details
- Linked devices
+ Linked Devices
Invite friends
Archived conversations
Remove photo
@@ -1572,11 +1574,11 @@
Looks like you don\'t have any conversations yet. Get started by messaging a friend.
- Secondary device
+ Linked device (%s)
Copied to clipboard
Share Public Key
Show QR Code
- Link Device
+ Linked Devices
Show Seed
Your Seed
Copy
@@ -1636,11 +1638,18 @@
Cancel
Scan QR Code
- Scan the QR code of the person you\'d like to securely message. They can find their QR code by going into Loki Messenger\'s in-app settings and clicking \"Show QR Code\".
+ Scan the QR code of the person you\'d like to securely message. They can find their QR code by going into Loki Messenger\'s in-app settings and clicking \"Show QR Code\".
+ Link to an existing device by going into its in-app settings and clicking \"Link Device\".
Loki Messenger needs camera access to scan QR codes.
Copy public key
Add Public Chat
+
+ Edit device name
+ Unlink device
+
+ Device unlinked
+ This device has been successfully unlinked
diff --git a/res/values/styles.xml b/res/values/styles.xml
index a9aaf9d788..ecf5dc23a5 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -242,4 +242,15 @@
- @color/white
+
+
diff --git a/res/values/themes.xml b/res/values/themes.xml
index 178c030731..f285cde977 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -133,6 +133,7 @@
- @color/loki_darkest_gray
- @style/AppCompatAlertDialogStyleLight
- @style/AppCompatDialogStyleLight
+ - @style/Theme.MaterialComponents.Light.BottomSheetDialog
- @color/white
@@ -317,6 +318,7 @@
- @color/loki_darkest_gray
- @style/AppCompatAlertDialogStyleDark
- @style/AppCompatDialogStyleDark
+ - @style/Theme.MaterialComponents.BottomSheetDialog
- @color/gray95
- @drawable/ic_document_small_dark
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
index 270c9254d3..4f8b6e4663 100644
--- a/res/xml/preferences.xml
+++ b/res/xml/preferences.xml
@@ -33,6 +33,10 @@
android:title="@string/preferences__advanced"
android:icon="@drawable/ic_advanced_24dp"/> -->
+
+
@@ -41,10 +45,6 @@
android:title="@string/activity_settings_show_qr_code_button_title"
android:icon="@drawable/icon_qr_code"/>
-
-
diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java
index f1c3c872db..a46c1bb69a 100644
--- a/src/org/thoughtcrime/securesms/ApplicationContext.java
+++ b/src/org/thoughtcrime/securesms/ApplicationContext.java
@@ -20,10 +20,13 @@ import android.annotation.SuppressLint;
import android.arch.lifecycle.DefaultLifecycleObserver;
import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.ProcessLifecycleOwner;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.database.ContentObserver;
import android.os.AsyncTask;
import android.os.Build;
+import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.multidex.MultiDexApplication;
@@ -38,6 +41,9 @@ import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusRepository;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
+import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
+import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
+import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule;
@@ -65,10 +71,12 @@ import org.thoughtcrime.securesms.loki.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.LokiPublicChatManager;
import org.thoughtcrime.securesms.loki.LokiRSSFeedPoller;
import org.thoughtcrime.securesms.loki.LokiUserDatabase;
+import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
+import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
@@ -88,11 +96,11 @@ 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.LokiPublicChat;
-import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI;
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 org.whispersystems.signalservice.loki.utilities.Analytics;
@@ -154,8 +162,9 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
@Override
public void onCreate() {
super.onCreate();
- startKovenant();
Log.i(TAG, "onCreate()");
+ checkNeedsDatabaseReset();
+ startKovenant();
initializeSecurityProvider();
initializeLogging();
initializeCrashHandling();
@@ -196,7 +205,11 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
// Loki - Update device mappings
if (setUpStorageAPIIfNeeded()) {
LokiStorageAPI.Companion.getShared().updateUserDeviceMappings();
+ if (TextSecurePreferences.needsRevocationCheck(this)) {
+ checkNeedsRevocation();
+ }
}
+ updatePublicChatProfileAvatarIfNeeded();
}
@Override
@@ -587,5 +600,54 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
if (lokiNewsFeedPoller != null) lokiNewsFeedPoller.startIfNeeded();
if (lokiMessengerUpdatesFeedPoller != null) lokiMessengerUpdatesFeedPoller.startIfNeeded();
}
+
+ public void updatePublicChatProfileAvatarIfNeeded() {
+ AsyncTask.execute(() -> {
+ LokiPublicChatAPI publicChatAPI = getLokiPublicChatAPI();
+ if (publicChatAPI != null) {
+ byte[] profileKey = ProfileKeyUtil.getProfileKey(this);
+ String url = TextSecurePreferences.getProfileAvatarUrl(this);
+ String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(this);
+ if (ourMasterDevice != null) {
+ Recipient masterDevice = Recipient.from(this, Address.fromSerialized(ourMasterDevice), false).resolve();
+ profileKey = masterDevice.getProfileKey();
+ url = masterDevice.getProfileAvatar();
+ }
+ Set servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers();
+ for (String server : servers) {
+ publicChatAPI.setProfilePicture(server, profileKey, url);
+ }
+ }
+ });
+ }
// endregion
+
+ public void checkNeedsRevocation() {
+ MultiDeviceUtilities.checkForRevocation(this);
+ }
+
+ public void checkNeedsDatabaseReset() {
+ if (TextSecurePreferences.resetDatabase(this)) {
+ boolean wasUnlinked = TextSecurePreferences.databaseResetFromUnpair(this);
+ TextSecurePreferences.clearAll(this);
+ TextSecurePreferences.setDatabaseResetFromUnpair(this, wasUnlinked); // Loki - Re-set the preference so we can use it in the starting screen to determine whether device was unlinked or not
+ MasterSecretUtil.clear(this);
+ if (this.deleteDatabase("signal.db")) {
+ Log.d("Loki", "Deleted database");
+ }
+ }
+ }
+
+ public void clearData() {
+ TextSecurePreferences.setResetDatabase(this, true);
+ new Handler().postDelayed(this::restartApplication, 200);
+ }
+
+ public void restartApplication() {
+ Intent intent = new Intent(this, ConversationListActivity.class);
+ ComponentName componentName = intent.getComponent();
+ Intent mainIntent = Intent.makeRestartActivityTask(componentName);
+ this.startActivity(mainIntent);
+ Runtime.getRuntime().exit(0);
+ }
}
diff --git a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java
index f9309eaf83..0bed4c6adb 100644
--- a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java
+++ b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java
@@ -26,7 +26,6 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
-import android.os.AsyncTask;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Bundle;
@@ -40,13 +39,8 @@ import android.support.v7.app.AlertDialog;
import android.support.v7.preference.Preference;
import android.widget.Toast;
-import org.jetbrains.annotations.NotNull;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
-import org.thoughtcrime.securesms.database.DatabaseFactory;
-import org.thoughtcrime.securesms.loki.DeviceLinkingDialog;
-import org.thoughtcrime.securesms.loki.DeviceLinkingDialogDelegate;
-import org.thoughtcrime.securesms.loki.DeviceLinkingView;
-import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
+import org.thoughtcrime.securesms.loki.LinkedDevicesActivity;
import org.thoughtcrime.securesms.loki.QRCodeDialog;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
@@ -57,7 +51,6 @@ import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.whispersystems.signalservice.loki.api.PairingAuthorisation;
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec;
import org.whispersystems.signalservice.loki.utilities.Analytics;
import org.whispersystems.signalservice.loki.utilities.SerializationKt;
@@ -89,7 +82,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
// private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
private static final String PREFERENCE_CATEGORY_PUBLIC_KEY = "preference_category_public_key";
private static final String PREFERENCE_CATEGORY_QR_CODE = "preference_category_qr_code";
- private static final String PREFERENCE_CATEGORY_LINK_DEVICE = "preference_category_link_device";
+ private static final String PREFERENCE_CATEGORY_LINKED_DEVICES = "preference_category_linked_devices";
private static final String PREFERENCE_CATEGORY_SEED = "preference_category_seed";
private final DynamicTheme dynamicTheme = new DynamicTheme();
@@ -172,15 +165,15 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS));
*/
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
- .setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_NOTIFICATIONS));
+ .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_NOTIFICATIONS));
this.findPreference(PREFERENCE_CATEGORY_APP_PROTECTION)
- .setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_APP_PROTECTION));
+ .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APP_PROTECTION));
/*
this.findPreference(PREFERENCE_CATEGORY_APPEARANCE)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE));
*/
this.findPreference(PREFERENCE_CATEGORY_CHATS)
- .setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_CHATS));
+ .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CHATS));
/*
this.findPreference(PREFERENCE_CATEGORY_DEVICES)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DEVICES));
@@ -188,29 +181,18 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
*/
this.findPreference(PREFERENCE_CATEGORY_PUBLIC_KEY)
- .setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_PUBLIC_KEY));
+ .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PUBLIC_KEY));
this.findPreference(PREFERENCE_CATEGORY_QR_CODE)
- .setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_QR_CODE));
+ .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_QR_CODE));
- Preference linkDevicePreference = this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE);
- linkDevicePreference.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_LINK_DEVICE));
-
- // Disable if we hit the cap of 1 linked device
- if (isMasterDevice) {
- Context context = getContext();
- String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
- boolean isDeviceLinkingEnabled = DatabaseFactory.getLokiAPIDatabase(context).getPairingAuthorisations(userHexEncodedPublicKey).size() <= 1;
- linkDevicePreference.setEnabled(isDeviceLinkingEnabled);
- linkDevicePreference.getIcon().setAlpha(isDeviceLinkingEnabled ? 255 : 124);
- } else {
- // Hide if this is a slave device
- linkDevicePreference.setVisible(false);
- }
+ Preference linkDevicesPreference = this.findPreference(PREFERENCE_CATEGORY_LINKED_DEVICES);
+ linkDevicesPreference.setVisible(isMasterDevice);
+ linkDevicesPreference.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_LINKED_DEVICES));
Preference seedPreference = this.findPreference(PREFERENCE_CATEGORY_SEED);
// Hide if this is a slave device
seedPreference.setVisible(isMasterDevice);
- seedPreference.setOnPreferenceClickListener(new CategoryClickListener(getContext(), (PREFERENCE_CATEGORY_SEED)));
+ seedPreference.setOnPreferenceClickListener(new CategoryClickListener((PREFERENCE_CATEGORY_SEED)));
if (VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
tintIcons(getActivity());
@@ -299,16 +281,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
// this.findPreference(PREFERENCE_CATEGORY_ADVANCED).setIcon(advanced);
this.findPreference(PREFERENCE_CATEGORY_PUBLIC_KEY).setIcon(publicKey);
this.findPreference(PREFERENCE_CATEGORY_QR_CODE).setIcon(qrCode);
- this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE).setIcon(linkDevice);
+ this.findPreference(PREFERENCE_CATEGORY_LINKED_DEVICES).setIcon(linkDevice);
this.findPreference(PREFERENCE_CATEGORY_SEED).setIcon(seed);
}
- private class CategoryClickListener implements Preference.OnPreferenceClickListener, DeviceLinkingDialogDelegate {
+ private class CategoryClickListener implements Preference.OnPreferenceClickListener {
private String category;
- private Context context;
- CategoryClickListener(Context context,String category) {
- this.context = context;
+ CategoryClickListener(String category) {
this.category = category;
}
@@ -360,8 +340,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
case PREFERENCE_CATEGORY_QR_CODE:
QRCodeDialog.INSTANCE.show(getContext());
break;
- case PREFERENCE_CATEGORY_LINK_DEVICE:
- DeviceLinkingDialog.Companion.show(getContext(), DeviceLinkingView.Mode.Master, this);
+ case PREFERENCE_CATEGORY_LINKED_DEVICES:
+ Intent intent = new Intent(getActivity(), LinkedDevicesActivity.class);
+ startActivity(intent);
break;
case PREFERENCE_CATEGORY_SEED:
Analytics.Companion.getShared().track("Seed Modal Shown");
@@ -404,12 +385,6 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
return true;
}
-
- @Override public void sendPairingAuthorizedMessage(@NotNull PairingAuthorisation pairingAuthorisation) {
- AsyncTask.execute(() -> MultiDeviceUtilities.signAndSendPairingAuthorisationMessage(context, pairingAuthorisation));
- }
- @Override public void handleDeviceLinkAuthorized(@NotNull PairingAuthorisation pairingAuthorisation) {}
- @Override public void handleDeviceLinkingDialogDismissed() {}
}
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java
index 57fc652324..96c9e0e84f 100644
--- a/src/org/thoughtcrime/securesms/ConversationListActivity.java
+++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java
@@ -22,6 +22,7 @@ import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.graphics.Outline;
+import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
@@ -38,8 +39,14 @@ import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.Toast;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.components.RatingManager;
import org.thoughtcrime.securesms.components.SearchToolbar;
+import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.Address;
@@ -48,6 +55,8 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
import org.thoughtcrime.securesms.loki.AddPublicChatActivity;
import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable;
+import org.thoughtcrime.securesms.loki.RecipientAvatarModifiedEvent;
+import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.permissions.Permissions;
@@ -129,6 +138,18 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
super.onDestroy();
}
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuInflater inflater = this.getMenuInflater();
@@ -197,45 +218,23 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
outline.setOval(0, 0, view.getWidth(), view.getHeight());
}
});
+ profilePictureImageView.setClipToOutline(true);
// Display the correct identicon if we're a secondary device
- String currentUser = TextSecurePreferences.getLocalNumber(this);
- String recipientAddress = recipient.getAddress().serialize();
String primaryAddress = TextSecurePreferences.getMasterHexEncodedPublicKey(this);
- String profileAddress = (recipientAddress.equalsIgnoreCase(currentUser) && primaryAddress != null) ? primaryAddress : recipientAddress;
+ String profileAddress = (recipient.isLocalNumber() && primaryAddress != null) ? primaryAddress : recipient.getAddress().serialize();
+ Recipient primaryRecipient = Recipient.from(this, Address.fromSerialized(profileAddress), false);
- profilePictureImageView.setClipToOutline(true);
- profilePictureImageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
-
- @Override
- public boolean onPreDraw() {
- int width = profilePictureImageView.getWidth();
- int height = profilePictureImageView.getHeight();
- if (width == 0 || height == 0) return true;
- profilePictureImageView.getViewTreeObserver().removeOnPreDrawListener(this);
- JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, profileAddress.toLowerCase());
- profilePictureImageView.setImageDrawable(identicon);
- return true;
- }
- });
-
- /*
- String name = Optional.fromNullable(recipient.getName()).or(Optional.fromNullable(TextSecurePreferences.getProfileName(this))).or("");
- MaterialColor fallbackColor = recipient.getColor();
-
- if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
- fallbackColor = ContactColors.generateFor(name);
- }
-
- Drawable fallback = new GeneratedContactPhoto(name, R.drawable.ic_profile_default).asDrawable(this, fallbackColor.toAvatarColor(this));
+ Drawable fallback = primaryRecipient.getFallbackContactPhotoDrawable(this, false);
GlideApp.with(this)
- .load(new ProfileContactPhoto(recipient.getAddress(), String.valueOf(TextSecurePreferences.getProfileAvatarId(this))))
+ .load(primaryRecipient.getContactPhoto())
+ .fallback(fallback)
.error(fallback)
- .circleCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL)
- .into(icon);
- */
+ .circleCrop()
+ .into(profilePictureImageView);
+
profilePictureImageView.setOnClickListener(v -> handleDisplaySettings());
}
@@ -336,4 +335,12 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
private void addNewPublicChat() {
startActivity(new Intent(this, AddPublicChatActivity.class));
}
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onAvatarModified(RecipientAvatarModifiedEvent event) {
+ Recipient recipient = event.getRecipient();
+ if (recipient.isLocalNumber() || recipient.isOurMasterDevice()) {
+ initializeProfileIcon(recipient);
+ }
+ }
}
diff --git a/src/org/thoughtcrime/securesms/CreateProfileActivity.java b/src/org/thoughtcrime/securesms/CreateProfileActivity.java
index d30324ae2a..14347fbb8f 100644
--- a/src/org/thoughtcrime/securesms/CreateProfileActivity.java
+++ b/src/org/thoughtcrime/securesms/CreateProfileActivity.java
@@ -39,7 +39,6 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.InjectableType;
-import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
@@ -58,13 +57,16 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
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.utilities.Analytics;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.security.SecureRandom;
+import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
@@ -96,6 +98,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
private View reveal;
private Intent nextIntent;
+ private byte[] originalAvatarBytes;
private byte[] avatarBytes;
private File captureFile;
@@ -301,6 +304,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
@Override
protected void onPostExecute(byte[] result) {
if (result != null) {
+ originalAvatarBytes = result;
avatarBytes = result;
GlideApp.with(CreateProfileActivity.this)
.load(result)
@@ -314,6 +318,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
@Override
public void onSuccess(byte[] result) {
if (result != null) {
+ originalAvatarBytes = result;
avatarBytes = result;
GlideApp.with(CreateProfileActivity.this)
.load(result)
@@ -380,7 +385,6 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
@Override
protected Boolean doInBackground(Void... params) {
Context context = CreateProfileActivity.this;
- byte[] profileKey = ProfileKeyUtil.getProfileKey(CreateProfileActivity.this);
Analytics.Companion.getShared().track("Display Name Updated");
@@ -393,31 +397,44 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
}
}
- // Loki - Original code
- // ========
-// try {
-// accountManager.setProfileName(profileKey, name);
-// TextSecurePreferences.setProfileName(context, name);
-// } catch (IOException e) {
-// Log.w(TAG, e);
-// return false;
-// }
- // ========
+ // Loki - Only update avatar if there was a change
+ if (!Arrays.equals(originalAvatarBytes, avatarBytes)) {
+ try {
+ // Loki - Original profile photo code
+ // ========
+ // accountManager.setProfileAvatar(profileKey, avatar);
+ // ========
- try {
- // Loki - Original code
- // ========
- // accountManager.setProfileAvatar(profileKey, avatar);
- // ========
- AvatarHelper.setAvatar(CreateProfileActivity.this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), avatarBytes);
- TextSecurePreferences.setProfileAvatarId(CreateProfileActivity.this, new SecureRandom().nextInt());
- } catch (IOException e) {
- Log.w(TAG, e);
- return false;
+ // Try upload photo with a new profile key
+ String newProfileKey = ProfileKeyUtil.generateEncodedProfileKey(context);
+ byte[] profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(newProfileKey);
+
+ //Loki - Upload the profile photo here
+ if (avatar != null) {
+ Log.d("Loki", "Start uploading profile photo");
+ LokiStorageAPI storageAPI = LokiStorageAPI.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());
+ } else {
+ TextSecurePreferences.setProfileAvatarUrl(context, null);
+ }
+
+ AvatarHelper.setAvatar(context, Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), avatarBytes);
+ TextSecurePreferences.setProfileAvatarId(context, new SecureRandom().nextInt());
+
+ // Upload was successful with this new profile key, we should set it so the other users know to re-fetch profiles
+ ProfileKeyUtil.setEncodedProfileKey(context, newProfileKey);
+
+ // Update profile key on the public chat server
+ ApplicationContext.getInstance(context).updatePublicChatProfileAvatarIfNeeded();
+ } catch (Exception e) {
+ Log.d("Loki", "Failed to upload profile photo: " + e);
+ return false;
+ }
}
- ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceProfileKeyUpdateJob());
-
+ // ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceProfileKeyUpdateJob());
return true;
}
diff --git a/src/org/thoughtcrime/securesms/DeviceListFragment.java b/src/org/thoughtcrime/securesms/DeviceListFragment.java
index 220b605642..dbabf62754 100644
--- a/src/org/thoughtcrime/securesms/DeviceListFragment.java
+++ b/src/org/thoughtcrime/securesms/DeviceListFragment.java
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
-import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.ListFragment;
@@ -16,29 +15,32 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
import android.widget.ListView;
-import android.widget.Toast;
import com.melnykov.fab.FloatingActionButton;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.devicelist.Device;
-import org.thoughtcrime.securesms.jobs.RefreshUnidentifiedDeliveryAbilityJob;
-import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.loki.DeviceListBottomSheetFragment;
+import org.thoughtcrime.securesms.loki.MnemonicUtilities;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
-import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
-import org.whispersystems.signalservice.api.SignalServiceAccountManager;
+import org.whispersystems.libsignal.util.guava.Function;
-import java.io.IOException;
+import java.io.File;
import java.util.List;
import java.util.Locale;
-import javax.inject.Inject;
-
+import kotlin.Pair;
+import kotlin.Unit;
import network.loki.messenger.R;
+import static org.thoughtcrime.securesms.loki.GeneralUtilitiesKt.toPx;
+
public class DeviceListFragment extends ListFragment
implements LoaderManager.LoaderCallbacks>,
ListView.OnItemClickListener, InjectableType, Button.OnClickListener
@@ -46,14 +48,14 @@ public class DeviceListFragment extends ListFragment
private static final String TAG = DeviceListFragment.class.getSimpleName();
- @Inject
- SignalServiceAccountManager accountManager;
-
+ private File languageFileDirectory;
private Locale locale;
private View empty;
private View progressContainer;
private FloatingActionButton addDeviceButton;
private Button.OnClickListener addDeviceButtonListener;
+ private Function handleDisconnectDevice;
+ private Function, Void> handleDeviceNameChange;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -71,10 +73,11 @@ public class DeviceListFragment extends ListFragment
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
View view = inflater.inflate(R.layout.device_list_fragment, container, false);
- this.empty = view.findViewById(R.id.empty);
- this.progressContainer = view.findViewById(R.id.progress_container);
- this.addDeviceButton = ViewUtil.findById(view, R.id.add_device);
+ this.empty = view.findViewById(R.id.emptyStateTextView);
+ this.progressContainer = view.findViewById(R.id.activityIndicator);
+ this.addDeviceButton = ViewUtil.findById(view, R.id.addDeviceButton);
this.addDeviceButton.setOnClickListener(this);
+ updateAddDeviceButtonVisibility();
return view;
}
@@ -82,6 +85,7 @@ public class DeviceListFragment extends ListFragment
@Override
public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle);
+ this.languageFileDirectory = MnemonicUtilities.getLanguageFileDirectory(getContext());
getLoaderManager().initLoader(0, null, this);
getListView().setOnItemClickListener(this);
}
@@ -90,12 +94,20 @@ public class DeviceListFragment extends ListFragment
this.addDeviceButtonListener = listener;
}
+ public void setHandleDisconnectDevice(Function handler) {
+ this.handleDisconnectDevice = handler;
+ }
+
+ public void setHandleDeviceNameChange(Function, Void> handler) {
+ this.handleDeviceNameChange = handler;
+ }
+
@Override
public @NonNull Loader> onCreateLoader(int id, Bundle args) {
empty.setVisibility(View.GONE);
progressContainer.setVisibility(View.VISIBLE);
- return new DeviceListLoader(getActivity(), accountManager);
+ return new DeviceListLoader(getActivity(), languageFileDirectory);
}
@Override
@@ -124,20 +136,63 @@ public class DeviceListFragment extends ListFragment
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ final boolean hasDeviceName = ((DeviceListItem)view).hasDeviceName(); // Tells us whether the name is set to shortId or the device name
final String deviceName = ((DeviceListItem)view).getDeviceName();
- final long deviceId = ((DeviceListItem)view).getDeviceId();
+ final String deviceId = ((DeviceListItem)view).getDeviceId();
- AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
- builder.setTitle(getActivity().getString(R.string.DeviceListActivity_unlink_s, deviceName));
- builder.setMessage(R.string.DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive);
- builder.setNegativeButton(android.R.string.cancel, null);
- builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- handleDisconnectDevice(deviceId);
- }
+ DeviceListBottomSheetFragment bottomSheet = new DeviceListBottomSheetFragment();
+ bottomSheet.setOnEditTapped(() -> {
+ bottomSheet.dismiss();
+ EditText deviceNameEditText = new EditText(getContext());
+ LinearLayout deviceNameEditTextContainer = new LinearLayout(getContext());
+ LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
+ layoutParams.setMarginStart(toPx(18, getResources()));
+ layoutParams.setMarginEnd(toPx(18, getResources()));
+ deviceNameEditText.setLayoutParams(layoutParams);
+ deviceNameEditTextContainer.addView(deviceNameEditText);
+ deviceNameEditText.setText(hasDeviceName ? deviceName : "");
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.DeviceListActivity_edit_device_name);
+ builder.setView(deviceNameEditTextContainer);
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (handleDeviceNameChange != null) { handleDeviceNameChange.apply(new Pair<>(deviceId, deviceNameEditText.getText().toString().trim())); }
+ }
+ });
+ builder.show();
+ return Unit.INSTANCE;
});
- builder.show();
+ bottomSheet.setOnUnlinkTapped(() -> {
+ bottomSheet.dismiss();
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getActivity().getString(R.string.DeviceListActivity_unlink_s, deviceName));
+ builder.setMessage(R.string.DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive);
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (handleDisconnectDevice != null) { handleDisconnectDevice.apply(deviceId); }
+ }
+ });
+ builder.show();
+ return Unit.INSTANCE;
+ });
+ bottomSheet.show(getFragmentManager(), bottomSheet.getTag());
+ }
+
+ public void refresh() {
+ updateAddDeviceButtonVisibility();
+ getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
+ }
+
+ private void updateAddDeviceButtonVisibility() {
+ if (addDeviceButton != null) {
+ String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
+ boolean isDeviceLinkingEnabled = DatabaseFactory.getLokiAPIDatabase(getContext()).getPairingAuthorisations(userHexEncodedPublicKey).isEmpty();
+ addDeviceButton.setVisibility(isDeviceLinkingEnabled ? View.VISIBLE : View.INVISIBLE);
+ }
}
private void handleLoaderFailed() {
@@ -167,34 +222,6 @@ public class DeviceListFragment extends ListFragment
builder.show();
}
- private void handleDisconnectDevice(final long deviceId) {
- new ProgressDialogAsyncTask(getActivity(),
- R.string.DeviceListActivity_unlinking_device_no_ellipsis,
- R.string.DeviceListActivity_unlinking_device)
- {
- @Override
- protected Void doInBackground(Void... params) {
- try {
- accountManager.removeDevice(deviceId);
-
- ApplicationContext.getInstance(getContext())
- .getJobManager()
- .add(new RefreshUnidentifiedDeliveryAbilityJob());
- } catch (IOException e) {
- Log.w(TAG, e);
- Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show();
- }
- return null;
- }
-
- @Override
- protected void onPostExecute(Void result) {
- super.onPostExecute(result);
- getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-
@Override
public void onClick(View v) {
if (addDeviceButtonListener != null) addDeviceButtonListener.onClick(v);
diff --git a/src/org/thoughtcrime/securesms/DeviceListItem.java b/src/org/thoughtcrime/securesms/DeviceListItem.java
index 47331a549d..bfc96b4d7a 100644
--- a/src/org/thoughtcrime/securesms/DeviceListItem.java
+++ b/src/org/thoughtcrime/securesms/DeviceListItem.java
@@ -15,10 +15,9 @@ import network.loki.messenger.R;
public class DeviceListItem extends LinearLayout {
- private long deviceId;
+ private String deviceId;
private TextView name;
- private TextView created;
- private TextView lastActive;
+ private TextView shortId;
public DeviceListItem(Context context) {
super(context);
@@ -31,29 +30,19 @@ public class DeviceListItem extends LinearLayout {
@Override
public void onFinishInflate() {
super.onFinishInflate();
- this.name = (TextView) findViewById(R.id.name);
- this.created = (TextView) findViewById(R.id.created);
- this.lastActive = (TextView) findViewById(R.id.active);
+ this.name = (TextView) findViewById(R.id.name);
+ this.shortId = (TextView) findViewById(R.id.shortId);
}
public void set(Device deviceInfo, Locale locale) {
- if (TextUtils.isEmpty(deviceInfo.getName())) this.name.setText(R.string.DeviceListItem_unnamed_device);
- else this.name.setText(deviceInfo.getName());
-
- this.created.setText(getContext().getString(R.string.DeviceListItem_linked_s,
- DateUtils.getDayPrecisionTimeSpanString(getContext(),
- locale,
- deviceInfo.getCreated())));
-
- this.lastActive.setText(getContext().getString(R.string.DeviceListItem_last_active_s,
- DateUtils.getDayPrecisionTimeSpanString(getContext(),
- locale,
- deviceInfo.getLastSeen())));
-
this.deviceId = deviceInfo.getId();
+ boolean hasName = !TextUtils.isEmpty(deviceInfo.getName());
+ this.name.setText(hasName ? deviceInfo.getName() : deviceInfo.getShortId());
+ this.shortId.setText(deviceInfo.getShortId());
+ this.shortId.setVisibility(hasName ? VISIBLE : GONE);
}
- public long getDeviceId() {
+ public String getDeviceId() {
return deviceId;
}
@@ -61,4 +50,8 @@ public class DeviceListItem extends LinearLayout {
return name.getText().toString();
}
+ public boolean hasDeviceName() {
+ return shortId.getVisibility() == VISIBLE;
+ }
+
}
diff --git a/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java b/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java
index f36aac543b..5f24485fac 100644
--- a/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java
+++ b/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java
@@ -5,6 +5,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
+import android.preference.PreferenceManager;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
diff --git a/src/org/thoughtcrime/securesms/avatar/AvatarSelection.java b/src/org/thoughtcrime/securesms/avatar/AvatarSelection.java
index c4f0ca3d64..6886b27c8d 100644
--- a/src/org/thoughtcrime/securesms/avatar/AvatarSelection.java
+++ b/src/org/thoughtcrime/securesms/avatar/AvatarSelection.java
@@ -5,6 +5,7 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
+import android.os.Build;
import android.provider.MediaStore;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
@@ -43,7 +44,7 @@ public final class AvatarSelection {
CropImage.activity(inputFile)
.setGuidelines(CropImageView.Guidelines.ON)
.setAspectRatio(1, 1)
- .setCropShape(CropImageView.CropShape.OVAL)
+ .setCropShape(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? CropImageView.CropShape.RECTANGLE : CropImageView.CropShape.OVAL)
.setOutputUri(outputFile)
.setAllowRotation(true)
.setAllowFlipping(true)
diff --git a/src/org/thoughtcrime/securesms/components/AvatarImageView.java b/src/org/thoughtcrime/securesms/components/AvatarImageView.java
index ce11690664..73a44ee5ef 100644
--- a/src/org/thoughtcrime/securesms/components/AvatarImageView.java
+++ b/src/org/thoughtcrime/securesms/components/AvatarImageView.java
@@ -11,22 +11,23 @@ import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.AppCompatImageView;
-import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
-import org.thoughtcrime.securesms.color.MaterialColor;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
-import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
+import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
+import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.database.Address;
-import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable;
+import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientExporter;
-import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
-import org.whispersystems.libsignal.util.guava.Optional;
+
+import java.util.Objects;
import network.loki.messenger.R;
@@ -52,7 +53,9 @@ public class AvatarImageView extends AppCompatImageView {
private boolean inverted;
private Paint outlinePaint;
private OnClickListener listener;
- private Recipient recipient;
+
+ private @Nullable RecipientContactPhoto recipientContactPhoto;
+ private @NonNull Drawable unknownRecipientDrawable;
public AvatarImageView(Context context) {
super(context);
@@ -75,23 +78,27 @@ public class AvatarImageView extends AppCompatImageView {
outlinePaint = ThemeUtil.isDarkTheme(getContext()) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT;
setOutlineProvider(new ViewOutlineProvider() {
-
@Override
public void getOutline(View view, Outline outline) {
outline.setOval(0, 0, view.getWidth(), view.getHeight());
}
});
setClipToOutline(true);
+
+ unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_default).asDrawable(getContext(), ContactColors.UNKNOWN_COLOR.toConversationColor(getContext()), inverted);
}
@Override
- protected void dispatchDraw(Canvas canvas) {
- super.dispatchDraw(canvas);
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
- float cx = canvas.getWidth() / 2;
- float cy = canvas.getHeight() / 2;
- float radius = (canvas.getWidth() / 2) - (outlinePaint.getStrokeWidth() / 2);
-
+ float width = getWidth() - getPaddingRight() - getPaddingLeft();
+ float height = getHeight() - getPaddingBottom() - getPaddingTop();
+ float cx = width / 2f;
+ float cy = height / 2f;
+ float radius = Math.min(cx, cy) - (outlinePaint.getStrokeWidth() / 2f);
+
+ canvas.translate(getPaddingLeft(), getPaddingTop());
canvas.drawCircle(cx, cy, radius, outlinePaint);
}
@@ -101,39 +108,46 @@ public class AvatarImageView extends AppCompatImageView {
super.setOnClickListener(listener);
}
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- updateImage(w, h);
- }
-
public void update(String hexEncodedPublicKey) {
Address address = Address.fromSerialized(hexEncodedPublicKey);
- if (recipient == null || !address.equals(recipient.getAddress())) {
- this.recipient = Recipient.from(getContext(), address, false);
- updateImage();
- }
+ Recipient recipient = Recipient.from(getContext(), address, false);
+ updateAvatar(recipient);
+ }
+
+ private void updateAvatar(Recipient recipient) {
+ setAvatar(GlideApp.with(getContext()), recipient, false);
}
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
- if (this.recipient == null || !this.recipient.equals(recipient)) {
- this.recipient = recipient;
- updateImage();
- }
- /*
if (recipient != null) {
- requestManager.load(recipient.getContactPhoto())
- .fallback(recipient.getFallbackContactPhotoDrawable(getContext(), inverted))
- .error(recipient.getFallbackContactPhotoDrawable(getContext(), inverted))
+ if (recipient.isLocalNumber()) {
+ setImageDrawable(new ResourceContactPhoto(R.drawable.ic_note_to_self).asDrawable(getContext(), recipient.getColor().toAvatarColor(getContext()), inverted));
+ } else {
+ RecipientContactPhoto photo = new RecipientContactPhoto(recipient);
+ if (!photo.equals(recipientContactPhoto)) {
+ requestManager.clear(this);
+ recipientContactPhoto = photo;
+
+ Drawable fallbackContactPhotoDrawable = photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted);
+
+ if (photo.contactPhoto != null) {
+ requestManager.load(photo.contactPhoto)
+ .fallback(fallbackContactPhotoDrawable)
+ .error(fallbackContactPhotoDrawable)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.circleCrop()
.into(this);
- setAvatarClickHandler(recipient, quickContactEnabled);
+ } else {
+ setImageDrawable(fallbackContactPhotoDrawable);
+ }
+ }
+ }
} else {
- setImageDrawable(new ResourceContactPhoto(R.drawable.ic_profile_default).asDrawable(getContext(), ContactColors.UNKNOWN_COLOR.toConversationColor(getContext()), inverted));
+ recipientContactPhoto = null;
+ requestManager.clear(this);
+ setImageDrawable(unknownRecipientDrawable);
super.setOnClickListener(listener);
}
- */
}
public void clear(@NonNull GlideRequests glideRequests) {
@@ -154,32 +168,25 @@ public class AvatarImageView extends AppCompatImageView {
}
}
- private void updateImage() { updateImage(getWidth(), getHeight()); }
+ private static class RecipientContactPhoto {
- private void updateImage(int w, int h) {
- if (w == 0 || h == 0 || recipient == null) { return; }
+ private final @NonNull Recipient recipient;
+ private final @Nullable ContactPhoto contactPhoto;
+ private final boolean ready;
- Drawable image;
- Context context = this.getContext();
-
- if (recipient.isGroupRecipient()) {
- String name = Optional.fromNullable(recipient.getName()).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context))).or("");
- MaterialColor fallbackColor = recipient.getColor();
-
- if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
- fallbackColor = ContactColors.generateFor(name);
- }
-
- image = new GeneratedContactPhoto(name, R.drawable.ic_profile_default).asDrawable(context, fallbackColor.toAvatarColor(context));
- } else {
- // Default to primary device image
- String ourPublicKey = TextSecurePreferences.getLocalNumber(context);
- String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
- String recipientAddress = recipient.getAddress().serialize();
- String profileAddress = (ourPrimaryDevice != null && ourPublicKey.equals(recipientAddress)) ? ourPrimaryDevice : recipientAddress;
- image = new JazzIdenticonDrawable(w, h, profileAddress.toLowerCase());
+ RecipientContactPhoto(@NonNull Recipient recipient) {
+ this.recipient = recipient;
+ this.ready = !recipient.isResolving();
+ this.contactPhoto = recipient.getContactPhoto();
}
- setImageDrawable(image);
- }
+ public boolean equals(@Nullable RecipientContactPhoto other) {
+ if (other == null) return false;
+
+ return other.recipient.equals(recipient) &&
+ other.recipient.getColor().equals(recipient.getColor()) &&
+ other.ready == ready &&
+ Objects.equals(other.contactPhoto, contactPhoto);
+ }
+ }
}
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index a0dc86eaf4..84f3c11da5 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -3052,12 +3052,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
long originalThreadID = lokiMessageDatabase.getOriginalThreadID(friendRequest.id);
long threadId = originalThreadID < 0 ? this.threadId : originalThreadID;
- Address contact = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadId).getAddress();
- String contactPubKey = contact.toString();
+ Recipient contact = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadId);
+ Address address = contact.getAddress();
+ String contactPubKey = 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.syncContact(this, contact);
+ MessageSender.syncContact(this, address);
updateInputPanel();
}
diff --git a/src/org/thoughtcrime/securesms/crypto/MasterSecretUtil.java b/src/org/thoughtcrime/securesms/crypto/MasterSecretUtil.java
index 0542dbd73f..4d964d0291 100644
--- a/src/org/thoughtcrime/securesms/crypto/MasterSecretUtil.java
+++ b/src/org/thoughtcrime/securesms/crypto/MasterSecretUtil.java
@@ -198,6 +198,10 @@ public class MasterSecretUtil {
return preferences.getBoolean("passphrase_initialized", false);
}
+ public static void clear(Context context) {
+ context.getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
+ }
+
private static void save(Context context, String key, int value) {
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
.edit()
diff --git a/src/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java b/src/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java
index 319d2b6a89..ae88a2141a 100644
--- a/src/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java
+++ b/src/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -31,8 +32,24 @@ public class ProfileKeyUtil {
}
}
+ public static synchronized @NonNull byte[] getProfileKeyFromEncodedString(String encodedProfileKey) {
+ try {
+ return Base64.decode(encodedProfileKey);
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+
public static synchronized @NonNull byte[] rotateProfileKey(@NonNull Context context) {
TextSecurePreferences.setProfileKey(context, null);
return getProfileKey(context);
}
+
+ public static synchronized @NonNull String generateEncodedProfileKey(@NonNull Context context) {
+ return Util.getSecret(32);
+ }
+
+ public static synchronized void setEncodedProfileKey(@NonNull Context context, @Nullable String key) {
+ TextSecurePreferences.setProfileKey(context, key);
+ }
}
diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
index 115cd1090d..6715feb5ee 100644
--- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
+++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
@@ -72,7 +72,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV3 = 24;
private static final int lokiV4 = 25;
- private static final int DATABASE_VERSION = lokiV3; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
+ private static final int DATABASE_VERSION = lokiV4; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final String DATABASE_NAME = "signal.db";
private final Context context;
diff --git a/src/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java b/src/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java
index 7d653dff84..6961feb8a1 100644
--- a/src/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java
+++ b/src/org/thoughtcrime/securesms/database/loaders/DeviceListLoader.java
@@ -7,10 +7,15 @@ import android.text.TextUtils;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
+import org.thoughtcrime.securesms.database.Database;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.devicelist.Device;
+import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.loki.MnemonicUtilities;
import org.thoughtcrime.securesms.util.AsyncLoader;
import org.thoughtcrime.securesms.util.Base64;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPrivateKey;
@@ -19,7 +24,10 @@ import org.whispersystems.libsignal.util.ByteUtil;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
+import org.whispersystems.signalservice.loki.crypto.MnemonicCodec;
+import java.io.File;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
@@ -33,93 +41,43 @@ import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import static org.thoughtcrime.securesms.devicelist.DeviceNameProtos.*;
+import static org.whispersystems.signalservice.loki.utilities.TrimmingKt.removing05PrefixIfNeeded;
public class DeviceListLoader extends AsyncLoader> {
private static final String TAG = DeviceListLoader.class.getSimpleName();
+ private MnemonicCodec mnemonicCodec;
- private final SignalServiceAccountManager accountManager;
-
- public DeviceListLoader(Context context, SignalServiceAccountManager accountManager) {
+ public DeviceListLoader(Context context, File languageFileDirectory) {
super(context);
- this.accountManager = accountManager;
+ this.mnemonicCodec = new MnemonicCodec(languageFileDirectory);
}
@Override
public List loadInBackground() {
try {
- List devices = Stream.of(accountManager.getDevices())
- .filter(d -> d.getId() != SignalServiceAddress.DEFAULT_DEVICE_ID)
- .map(this::mapToDevice)
- .toList();
-
+ String ourPublicKey = TextSecurePreferences.getLocalNumber(getContext());
+ List secondaryDevicePublicKeys = LokiStorageAPI.shared.getSecondaryDevicePublicKeys(ourPublicKey).get();
+ List devices = Stream.of(secondaryDevicePublicKeys).map(this::mapToDevice).toList();
Collections.sort(devices, new DeviceComparator());
-
return devices;
- } catch (IOException e) {
+ } catch (Exception e) {
Log.w(TAG, e);
return null;
}
}
- private Device mapToDevice(@NonNull DeviceInfo deviceInfo) {
- try {
- if (TextUtils.isEmpty(deviceInfo.getName()) || deviceInfo.getName().length() < 4) {
- throw new IOException("Invalid DeviceInfo name.");
- }
-
- DeviceName deviceName = DeviceName.parseFrom(Base64.decode(deviceInfo.getName()));
-
- if (!deviceName.hasCiphertext() || !deviceName.hasEphemeralPublic() || !deviceName.hasSyntheticIv()) {
- throw new IOException("Got a DeviceName that wasn't properly populated.");
- }
-
- byte[] syntheticIv = deviceName.getSyntheticIv().toByteArray();
- byte[] cipherText = deviceName.getCiphertext().toByteArray();
- ECPrivateKey identityKey = IdentityKeyUtil.getIdentityKeyPair(getContext()).getPrivateKey();
- ECPublicKey ephemeralPublic = Curve.decodePoint(deviceName.getEphemeralPublic().toByteArray(), 0);
- byte[] masterSecret = Curve.calculateAgreement(ephemeralPublic, identityKey);
-
- Mac mac = Mac.getInstance("HmacSHA256");
- mac.init(new SecretKeySpec(masterSecret, "HmacSHA256"));
- byte[] cipherKeyPart1 = mac.doFinal("cipher".getBytes());
-
- mac.init(new SecretKeySpec(cipherKeyPart1, "HmacSHA256"));
- byte[] cipherKey = mac.doFinal(syntheticIv);
-
- Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
- cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(new byte[16]));
- final byte[] plaintext = cipher.doFinal(cipherText);
-
- mac.init(new SecretKeySpec(masterSecret, "HmacSHA256"));
- byte[] verificationPart1 = mac.doFinal("auth".getBytes());
-
- mac.init(new SecretKeySpec(verificationPart1, "HmacSHA256"));
- byte[] verificationPart2 = mac.doFinal(plaintext);
- byte[] ourSyntheticIv = ByteUtil.trim(verificationPart2, 16);
-
- if (!MessageDigest.isEqual(ourSyntheticIv, syntheticIv)) {
- throw new GeneralSecurityException("The computed syntheticIv didn't match the actual syntheticIv.");
- }
-
- return new Device(deviceInfo.getId(), new String(plaintext), deviceInfo.getCreated(), deviceInfo.getLastSeen());
-
- } catch (IOException e) {
- Log.w(TAG, "Failed while reading the protobuf.", e);
- } catch (GeneralSecurityException | InvalidKeyException e) {
- Log.w(TAG, "Failed during decryption.", e);
- }
-
- return new Device(deviceInfo.getId(), deviceInfo.getName(), deviceInfo.getCreated(), deviceInfo.getLastSeen());
+ private Device mapToDevice(@NonNull String hexEncodedPublicKey) {
+ String shortId = MnemonicUtilities.getFirst3Words(mnemonicCodec, hexEncodedPublicKey);
+ String name = DatabaseFactory.getLokiUserDatabase(getContext()).getDisplayName(hexEncodedPublicKey);
+ return new Device(hexEncodedPublicKey, shortId, name);
}
private static class DeviceComparator implements Comparator {
@Override
public int compare(Device lhs, Device rhs) {
- if (lhs.getCreated() < rhs.getCreated()) return -1;
- else if (lhs.getCreated() != rhs.getCreated()) return 1;
- else return 0;
+ return lhs.getName().compareTo(rhs.getName());
}
}
}
diff --git a/src/org/thoughtcrime/securesms/devicelist/Device.java b/src/org/thoughtcrime/securesms/devicelist/Device.java
index 1cf302596d..e6f4f13967 100644
--- a/src/org/thoughtcrime/securesms/devicelist/Device.java
+++ b/src/org/thoughtcrime/securesms/devicelist/Device.java
@@ -2,31 +2,19 @@ package org.thoughtcrime.securesms.devicelist;
public class Device {
- private final long id;
+ private final String id;
+ private final String shortId;
private final String name;
- private final long created;
- private final long lastSeen;
- public Device(long id, String name, long created, long lastSeen) {
- this.id = id;
- this.name = name;
- this.created = created;
- this.lastSeen = lastSeen;
+ public Device(String id, String shortId, String name) {
+ this.id = id;
+ this.shortId = shortId;
+ this.name = name;
}
- public long getId() {
+ public String getId() {
return id;
}
-
- public String getName() {
- return name;
- }
-
- public long getCreated() {
- return created;
- }
-
- public long getLastSeen() {
- return lastSeen;
- }
+ public String getShortId() { return shortId; }
+ public String getName() { return name; }
}
diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
index 94a0095349..fee75e62b5 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
+import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -14,7 +15,6 @@ import android.util.Pair;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
-import com.google.android.gms.common.util.IOUtils;
import org.signal.libsignal.metadata.InvalidMetadataMessageException;
import org.signal.libsignal.metadata.InvalidMetadataVersionException;
@@ -130,6 +130,7 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.DeviceLinkingSession;
import org.whispersystems.signalservice.loki.api.LokiAPI;
+import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.api.PairingAuthorisation;
import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher;
@@ -139,7 +140,6 @@ import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestSt
import org.whispersystems.signalservice.loki.messaging.LokiThreadSessionResetStatus;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
-import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.SecureRandom;
@@ -147,11 +147,13 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
+import java.util.Set;
import javax.inject.Inject;
import kotlin.Unit;
import network.loki.messenger.R;
+import nl.komponents.kovenant.Promise;
public class PushDecryptJob extends BaseJob implements InjectableType {
@@ -288,21 +290,14 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Handle friend request acceptance if needed
acceptFriendRequestIfNeeded(envelope, content);
- // Loki - Store pre key bundle if needed
+ // Loki - Store pre key bundle
+ // We shouldn't store it if it's a pairing message
+ if (!content.getPairingAuthorisation().isPresent()) {
+ storePreKeyBundleIfNeeded(envelope, content);
+ }
+
if (content.lokiServiceMessage.isPresent()) {
LokiServiceMessage lokiMessage = content.lokiServiceMessage.get();
- if (lokiMessage.getPreKeyBundleMessage() != null) {
- int registrationID = TextSecurePreferences.getLocalRegistrationId(context);
- LokiPreKeyBundleDatabase lokiPreKeyBundleDatabase = DatabaseFactory.getLokiPreKeyBundleDatabase(context);
-
- // Only store the pre key bundle if we don't have one in our database
- if (registrationID > 0 && !lokiPreKeyBundleDatabase.hasPreKeyBundle(envelope.getSource())) {
- Log.d("Loki", "Received a pre key bundle from: " + envelope.getSource() + ".");
- PreKeyBundle preKeyBundle = lokiMessage.getPreKeyBundleMessage().getPreKeyBundle(registrationID);
- lokiPreKeyBundleDatabase.setPreKeyBundle(envelope.getSource(), preKeyBundle);
- }
- }
-
if (lokiMessage.getAddressMessage() != null) {
// TODO: Loki - Handle address message
}
@@ -311,43 +306,58 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Store the sender display name if needed
Optional rawSenderDisplayName = content.senderDisplayName;
if (rawSenderDisplayName.isPresent() && rawSenderDisplayName.get().length() > 0) {
- setDisplayName(envelope.getSource(), rawSenderDisplayName.get());
-
- // If we got a name from our primary device then we also set that
+ // 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 && envelope.getSource().equals(ourPrimaryDevice)) {
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(envelope.getSource(), rawSenderDisplayName.get()); }
+ return Unit.INSTANCE;
+ });
}
- // TODO: Deleting the display name
if (content.getPairingAuthorisation().isPresent()) {
handlePairingMessage(content.getPairingAuthorisation().get(), envelope, content);
} else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent();
- if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
- else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId);
- else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId);
- else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId, Optional.absent());
- else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, Optional.absent());
+ if (!envelope.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())) {
+ TextSecurePreferences.setDatabaseResetFromUnpair(context, true);
+ MultiDeviceUtilities.checkForRevocation(context);
+ }
+ } else {
+ if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
+ else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId);
+ else if (message.isExpirationUpdate())
+ handleExpirationUpdate(content, message, smsMessageId);
+ else if (isMediaMessage)
+ handleMediaMessage(content, message, smsMessageId, Optional.absent());
+ else if (message.getBody().isPresent())
+ handleTextMessage(content, message, smsMessageId, Optional.absent());
- if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))) {
- handleUnknownGroupMessage(content, message.getGroupInfo().get());
+ if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))) {
+ handleUnknownGroupMessage(content, message.getGroupInfo().get());
+ }
+
+ if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
+ handleProfileKey(content, message);
+ }
+
+ // Loki - This doesn't get invoked for group chats
+ if (content.isNeedsReceipt()) {
+ handleNeedsDeliveryReceipt(content, message);
+ }
+
+ // Loki - Handle friend request logic if needed
+ updateFriendRequestStatusIfNeeded(envelope, content, message);
}
-
- if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
- handleProfileKey(content, message);
- }
-
- // Loki - This doesn't get invoked for group chats
- if (content.isNeedsReceipt()) {
- handleNeedsDeliveryReceipt(content, message);
- }
-
- // Loki - Handle friend request logic if needed
- updateFriendRequestStatusIfNeeded(envelope, content, message);
} else if (content.getSyncMessage().isPresent()) {
TextSecurePreferences.setMultiDevice(context, true);
@@ -716,6 +726,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get());
}
+ String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
+ boolean isSenderMasterDevice = ourMasterDevice != null && ourMasterDevice.equals(content.getSender());
if (message.getMessage().getProfileKey().isPresent()) {
Recipient recipient = null;
@@ -726,6 +738,16 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) {
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
+ if (isSenderMasterDevice) {
+ handleProfileKey(content, message.getMessage());
+ }
+ }
+
+ // Loki - Update display name from master device
+ if (isSenderMasterDevice && content.senderDisplayName.isPresent() && content.senderDisplayName.get().length() > 0) {
+ TextSecurePreferences.setProfileName(context, content.senderDisplayName.get());
}
if (threadId != null) {
@@ -859,18 +881,23 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
database.endTransaction();
}
- // Loki - Store message server ID
- updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);
-
- // Loki - Update mapping of message to original thread id
if (insertResult.isPresent()) {
MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
-
- ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
- LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
- long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient);
- lokiMessageDatabase.setOriginalThreadID(insertResult.get().getMessageId(), originalThreadId);
}
+
+ // Loki - Run db 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
+ if (insertResult.isPresent()) {
+ ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
+ LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
+ long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient);
+ lokiMessageDatabase.setOriginalThreadID(insertResult.get().getMessageId(), originalThreadId);
+ }
+ });
}
private long handleSynchronizeSentExpirationUpdate(@NonNull SentTranscriptMessage message) throws MmsException {
@@ -1014,35 +1041,37 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Insert the message into the database
Optional insertResult = database.insertMessageInbox(textMessage);
- Long messageId = null;
if (insertResult.isPresent()) {
threadId = insertResult.get().getThreadId();
- messageId = insertResult.get().getMessageId();
}
if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get());
- // Loki - Cache the user hex encoded public key (for mentions)
- if (threadId != null) {
- LokiAPIUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(threadId, context);
- LokiAPI.Companion.cache(textMessage.getSender().serialize(), threadId);
- }
-
- // Loki - Store message server ID
- updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);
-
- // Loki - Update mapping of message to original thread id
- if (messageId != null) {
- ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
- LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
- long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient);
- lokiMessageDatabase.setOriginalThreadID(messageId, originalThreadId);
- }
-
boolean isGroupMessage = message.getGroupInfo().isPresent();
if (threadId != null && !isGroupMessage) {
MessageNotifier.updateNotification(context, threadId);
}
+
+ // Loki - Run db updates in background, we should look into fixing this in the future
+ AsyncTask.execute(() -> {
+ if (insertResult.isPresent()) {
+ InsertResult result = insertResult.get();
+ // Loki - Cache the user hex encoded public key (for mentions)
+ LokiAPIUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(result.getThreadId(), context);
+ LokiAPI.Companion.cache(textMessage.getSender().serialize(), result.getThreadId());
+
+ // Loki - Store message server ID
+ updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);
+
+ // Loki - Update mapping of message to original thread id
+ if (result.getMessageId() > -1) {
+ ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
+ LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
+ long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient);
+ lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId);
+ }
+ }
+ });
}
}
@@ -1066,19 +1095,26 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
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 SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
if (authorisation.getType() == PairingAuthorisation.Type.REQUEST) {
- handlePairingRequestMessage(authorisation);
+ handlePairingRequestMessage(authorisation, envelope, content);
} else if (authorisation.getSecondaryDevicePublicKey().equals(userHexEncodedPublicKey)) {
handlePairingAuthorisationMessage(authorisation, envelope, content);
}
}
- private void handlePairingRequestMessage(@NonNull PairingAuthorisation authorisation) {
+ private void handlePairingRequestMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
boolean isValid = isValidPairingMessage(authorisation);
DeviceLinkingSession linkingSession = DeviceLinkingSession.Companion.getShared();
if (isValid && linkingSession.isListeningForLinkingRequests()) {
+ // Loki - If we successfully received a request then we should store the PreKeyBundle
+ storePreKeyBundleIfNeeded(envelope, content);
linkingSession.processLinkingRequest(authorisation);
}
}
@@ -1101,6 +1137,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
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
+ storePreKeyBundleIfNeeded(envelope, content);
// Process
DeviceLinkingSession.Companion.getShared().processLinkingAuthorization(authorisation);
// Store the primary device's public key
@@ -1118,7 +1156,10 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (content.senderDisplayName.isPresent() && content.senderDisplayName.get().length() > 0) {
TextSecurePreferences.setProfileName(context, content.senderDisplayName.get());
}
-
+ // Profile avatar updates
+ if (content.getDataMessage().isPresent()) {
+ handleProfileKey(content, content.getDataMessage().get());
+ }
// Contact sync
if (content.getSyncMessage().isPresent() && content.getSyncMessage().get().getContacts().isPresent()) {
handleSynchronizeContactMessage(content.getSyncMessage().get().getContacts().get());
@@ -1138,6 +1179,23 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
}
+ private void storePreKeyBundleIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
+ if (content.lokiServiceMessage.isPresent()) {
+ LokiServiceMessage lokiMessage = content.lokiServiceMessage.get();
+ if (lokiMessage.getPreKeyBundleMessage() != null) {
+ int registrationID = TextSecurePreferences.getLocalRegistrationId(context);
+ LokiPreKeyBundleDatabase lokiPreKeyBundleDatabase = DatabaseFactory.getLokiPreKeyBundleDatabase(context);
+
+ // Store the latest PreKeyBundle
+ if (registrationID > 0) {
+ Log.d("Loki", "Received a pre key bundle from: " + envelope.getSource() + ".");
+ PreKeyBundle preKeyBundle = lokiMessage.getPreKeyBundleMessage().getPreKeyBundle(registrationID);
+ lokiPreKeyBundleDatabase.setPreKeyBundle(envelope.getSource(), preKeyBundle);
+ }
+ }
+ }
+ }
+
private void acceptFriendRequestIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
// If we get anything other than a friend request, we can assume that we have a session with the other user
if (envelope.isFriendRequest() || isGroupChatMessage(content)) { return; }
@@ -1159,6 +1217,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (syncContact) {
MessageSender.syncContact(context, contactID.getAddress());
}
+ // Allow profile sharing with contact
+ DatabaseFactory.getRecipientDatabase(context).setProfileSharing(contactID, true);
// Update the last message if needed
LokiStorageAPI.shared.getPrimaryDevicePublicKey(pubKey).success(primaryDevice -> {
Util.runOnMain(() -> {
@@ -1172,7 +1232,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
if (!envelope.isFriendRequest() || message.isGroupUpdate()) { return; }
// This handles the case where another user sends us a regular message without authorisation
- boolean shouldBecomeFriends = PromiseUtil.get(MultiDeviceUtilities.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context), false);
+ Promise 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);
@@ -1369,14 +1430,25 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void handleProfileKey(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message)
{
+ if (!message.getProfileKey().isPresent()) { return; }
+
+ /*
+ If we get a profile key then we don't need to map it to the primary device.
+ For now a profile key is mapped one-to-one to avoid secondary devices setting the incorrect avatar for a primary device.
+ */
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
- Address sourceAddress = Address.fromSerialized(content.getSender());
- Recipient recipient = Recipient.from(context, sourceAddress, false);
+ Recipient recipient = Recipient.from(context, Address.fromSerialized(content.getSender()), false);
if (recipient.getProfileKey() == null || !MessageDigest.isEqual(recipient.getProfileKey(), message.getProfileKey().get())) {
database.setProfileKey(recipient, message.getProfileKey().get());
database.setUnidentifiedAccessMode(recipient, RecipientDatabase.UnidentifiedAccessMode.UNKNOWN);
- ApplicationContext.getInstance(context).getJobManager().add(new RetrieveProfileJob(recipient));
+ String url = content.senderProfileAvatarUrl.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();
+ }
}
}
@@ -1631,7 +1703,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
*/
private Recipient getPrimaryDeviceRecipient(String pubKey) {
try {
- String primaryDevice = LokiStorageAPI.shared.getPrimaryDevicePublicKey(pubKey).get();
+ 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);
@@ -1696,7 +1768,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} else if (content.getSyncMessage().isPresent()) {
try {
// We should ignore a sync message if the sender is not one of our devices
- boolean isOurDevice = MultiDeviceUtilities.isOneOfOurDevices(context, sender.getAddress()).get();
+ boolean isOurDevice = PromiseUtil.timeout(MultiDeviceUtilities.isOneOfOurDevices(context, sender.getAddress()), 5000).get();
if (!isOurDevice) {
Log.w(TAG, "Got a sync message from a device that is not ours!.");
}
diff --git a/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java b/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java
index 470bfbbe0e..f9dbfeea25 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java
@@ -25,22 +25,26 @@ public abstract class PushReceivedJob extends BaseJob {
public void processEnvelope(@NonNull SignalServiceEnvelope envelope) {
synchronized (RECEIVE_LOCK) {
- if (envelope.hasSource()) {
- Address source = Address.fromExternal(context, envelope.getSource());
- Recipient recipient = Recipient.from(context, source, false);
+ try {
+ if (envelope.hasSource()) {
+ Address source = Address.fromExternal(context, envelope.getSource());
+ Recipient recipient = Recipient.from(context, source, false);
- if (!isActiveNumber(recipient)) {
- DatabaseFactory.getRecipientDatabase(context).setRegistered(recipient, RecipientDatabase.RegisteredState.REGISTERED);
- ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(recipient, false));
+ if (!isActiveNumber(recipient)) {
+ DatabaseFactory.getRecipientDatabase(context).setRegistered(recipient, RecipientDatabase.RegisteredState.REGISTERED);
+ ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(recipient, false));
+ }
}
- }
- if (envelope.isReceipt()) {
- handleReceipt(envelope);
- } else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender() || envelope.isFriendRequest()) {
- handleMessage(envelope);
- } else {
- Log.w(TAG, "Received envelope of unknown type: " + envelope.getType());
+ if (envelope.isReceipt()) {
+ handleReceipt(envelope);
+ } else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender() || envelope.isFriendRequest()) {
+ handleMessage(envelope);
+ } else {
+ Log.w(TAG, "Received envelope of unknown type: " + envelope.getType());
+ }
+ } catch (Exception e) {
+ Log.d("Loki", "Failed to process envelope: " + e);
}
}
}
diff --git a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java
index 45f551d5aa..ecfa3ecdac 100644
--- a/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java
@@ -49,7 +49,7 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
.setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize())
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.HOURS.toMillis(1))
- .setMaxInstances(1)
+ .setMaxAttempts(2)
.build(),
recipient,
profileAvatar);
@@ -99,8 +99,8 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
File downloadDestination = File.createTempFile("avatar", "jpg", context.getCacheDir());
try {
- InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, MAX_PROFILE_SIZE_BYTES);
- File decryptDestination = File.createTempFile("avatar", "jpg", context.getCacheDir());
+ InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, MAX_PROFILE_SIZE_BYTES);
+ File decryptDestination = File.createTempFile("avatar", "jpg", context.getCacheDir());
Util.copy(avatarStream, new FileOutputStream(decryptDestination));
decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.getAddress()));
diff --git a/src/org/thoughtcrime/securesms/loki/DeviceLinkingDelegate.kt b/src/org/thoughtcrime/securesms/loki/DeviceLinkingDelegate.kt
new file mode 100644
index 0000000000..3a86bdf245
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/DeviceLinkingDelegate.kt
@@ -0,0 +1,29 @@
+package org.thoughtcrime.securesms.loki
+
+import org.whispersystems.signalservice.loki.api.PairingAuthorisation
+
+// Loki - TODO: Remove this yucky delegate pattern for device linking dialog once we have the redesign
+interface DeviceLinkingDelegate {
+ companion object {
+ fun combine(vararg delegates: DeviceLinkingDelegate?): DeviceLinkingDelegate {
+ val validDelegates = delegates.filterNotNull()
+ return object : DeviceLinkingDelegate {
+ override fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
+ for (delegate in validDelegates) { delegate.handleDeviceLinkAuthorized(pairingAuthorisation) }
+ }
+
+ override fun handleDeviceLinkingDialogDismissed() {
+ for (delegate in validDelegates) { delegate.handleDeviceLinkingDialogDismissed() }
+ }
+
+ override fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {
+ for (delegate in validDelegates) { delegate.sendPairingAuthorizedMessage(pairingAuthorisation) }
+ }
+ }
+ }
+ }
+
+ fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {}
+ fun handleDeviceLinkingDialogDismissed() {}
+ fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {}
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/DeviceLinkingDialog.kt b/src/org/thoughtcrime/securesms/loki/DeviceLinkingDialog.kt
index d5faaaca16..0b6bede2eb 100644
--- a/src/org/thoughtcrime/securesms/loki/DeviceLinkingDialog.kt
+++ b/src/org/thoughtcrime/securesms/loki/DeviceLinkingDialog.kt
@@ -8,13 +8,12 @@ import org.whispersystems.signalservice.loki.api.DeviceLinkingSession
import org.whispersystems.signalservice.loki.api.DeviceLinkingSessionListener
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
-class DeviceLinkingDialog private constructor(private val context: Context, private val mode: DeviceLinkingView.Mode, private val delegate: DeviceLinkingDialogDelegate?) : DeviceLinkingViewDelegate, DeviceLinkingSessionListener {
+class DeviceLinkingDialog private constructor(private val context: Context, private val mode: DeviceLinkingView.Mode, private val delegate: DeviceLinkingDelegate?) : DeviceLinkingDelegate, DeviceLinkingSessionListener {
private lateinit var view: DeviceLinkingView
private lateinit var dialog: AlertDialog
companion object {
-
- fun show(context: Context, mode: DeviceLinkingView.Mode, delegate: DeviceLinkingDialogDelegate?): DeviceLinkingDialog {
+ fun show(context: Context, mode: DeviceLinkingView.Mode, delegate: DeviceLinkingDelegate?): DeviceLinkingDialog {
val dialog = DeviceLinkingDialog(context, mode, delegate)
dialog.show()
return dialog
@@ -22,8 +21,10 @@ class DeviceLinkingDialog private constructor(private val context: Context, priv
}
private fun show() {
- view = DeviceLinkingView(context, mode, this)
+ val delegate = DeviceLinkingDelegate.combine(this, this.delegate)
+ view = DeviceLinkingView(context, mode, delegate)
dialog = AlertDialog.Builder(context).setView(view).show()
+ dialog.setCanceledOnTouchOutside(false)
view.dismiss = { dismiss() }
DeviceLinkingSession.shared.startListeningForLinkingRequests()
DeviceLinkingSession.shared.addListener(this)
@@ -35,20 +36,11 @@ class DeviceLinkingDialog private constructor(private val context: Context, priv
dialog.dismiss()
}
- override fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
- delegate?.handleDeviceLinkAuthorized(pairingAuthorisation)
- }
-
override fun handleDeviceLinkingDialogDismissed() {
if (mode == DeviceLinkingView.Mode.Master && view.pairingAuthorisation != null) {
val authorisation = view.pairingAuthorisation!!
DatabaseFactory.getLokiPreKeyBundleDatabase(context).removePreKeyBundle(authorisation.secondaryDevicePublicKey)
}
- delegate?.handleDeviceLinkingDialogDismissed()
- }
-
- override fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {
- delegate?.sendPairingAuthorizedMessage(pairingAuthorisation)
}
override fun requestUserAuthorization(authorisation: PairingAuthorisation) {
diff --git a/src/org/thoughtcrime/securesms/loki/DeviceLinkingDialogDelegate.kt b/src/org/thoughtcrime/securesms/loki/DeviceLinkingDialogDelegate.kt
deleted file mode 100644
index 48493e25e2..0000000000
--- a/src/org/thoughtcrime/securesms/loki/DeviceLinkingDialogDelegate.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.thoughtcrime.securesms.loki
-
-import org.whispersystems.signalservice.loki.api.PairingAuthorisation
-
-interface DeviceLinkingDialogDelegate {
-
- fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) { }
- fun handleDeviceLinkingDialogDismissed() { }
- fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) { }
-}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/DeviceLinkingView.kt b/src/org/thoughtcrime/securesms/loki/DeviceLinkingView.kt
index b7983cc92b..bf91bf80ec 100644
--- a/src/org/thoughtcrime/securesms/loki/DeviceLinkingView.kt
+++ b/src/org/thoughtcrime/securesms/loki/DeviceLinkingView.kt
@@ -5,19 +5,23 @@ import android.graphics.Color
import android.graphics.PorterDuff
import android.os.Handler
import android.util.AttributeSet
+import android.util.DisplayMetrics
import android.view.View
import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_device_linking.view.*
+import kotlinx.android.synthetic.main.view_device_linking.view.cancelButton
+import kotlinx.android.synthetic.main.view_device_linking.view.explanationTextView
+import kotlinx.android.synthetic.main.view_device_linking.view.titleTextView
import network.loki.messenger.R
+import org.thoughtcrime.securesms.qr.QrCode
+import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
-import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded
import java.io.File
-import java.io.FileOutputStream
-class DeviceLinkingView private constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, private val mode: Mode, private var delegate: DeviceLinkingViewDelegate) : LinearLayout(context, attrs, defStyleAttr) {
- private lateinit var languageFileDirectory: File
+class DeviceLinkingView private constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, private val mode: Mode, private var delegate: DeviceLinkingDelegate) : LinearLayout(context, attrs, defStyleAttr) {
+ private val languageFileDirectory: File = MnemonicUtilities.getLanguageFileDirectory(context)
var dismiss: (() -> Unit)? = null
var pairingAuthorisation: PairingAuthorisation? = null
private set
@@ -27,36 +31,14 @@ class DeviceLinkingView private constructor(context: Context, attrs: AttributeSe
// endregion
// region Lifecycle
- constructor(context: Context, mode: Mode, delegate: DeviceLinkingViewDelegate) : this(context, null, 0, mode, delegate)
- private constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0, Mode.Master, object : DeviceLinkingViewDelegate { }) // Just pass in a dummy mode
+ constructor(context: Context, mode: Mode, delegate: DeviceLinkingDelegate) : this(context, null, 0, mode, delegate)
+ private constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0, Mode.Master, object : DeviceLinkingDelegate { }) // Just pass in a dummy mode
private constructor(context: Context) : this(context, null)
init {
- setUpLanguageFileDirectory()
setUpViewHierarchy()
}
- private fun setUpLanguageFileDirectory() {
- val languages = listOf( "english", "japanese", "portuguese", "spanish" )
- val directory = File(context.applicationInfo.dataDir)
- for (language in languages) {
- val fileName = "$language.txt"
- if (directory.list().contains(fileName)) { continue }
- val inputStream = context.assets.open("mnemonic/$fileName")
- val file = File(directory, fileName)
- val outputStream = FileOutputStream(file)
- val buffer = ByteArray(1024)
- while (true) {
- val count = inputStream.read(buffer)
- if (count < 0) { break }
- outputStream.write(buffer, 0, count)
- }
- inputStream.close()
- outputStream.close()
- }
- languageFileDirectory = directory
- }
-
private fun setUpViewHierarchy() {
inflate(context, R.layout.view_device_linking, this)
spinner.indeterminateDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)
@@ -72,11 +54,24 @@ class DeviceLinkingView private constructor(context: Context, attrs: AttributeSe
explanationTextView.text = resources.getString(explanationID)
mnemonicTextView.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE
if (mode == Mode.Slave) {
- val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded()
- mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
+ val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
+ mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), hexEncodedPublicKey)
}
authorizeButton.visibility = View.GONE
authorizeButton.setOnClickListener { authorizePairing() }
+
+ // QR Code
+ spinner.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE
+ qrCodeImageView.visibility = if (mode == Mode.Master) View.VISIBLE else View.GONE
+ if (mode == Mode.Master) {
+ val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
+ val displayMetrics = DisplayMetrics()
+ ServiceUtil.getWindowManager(context).defaultDisplay.getMetrics(displayMetrics)
+ val size = displayMetrics.widthPixels - 2 * toPx(96, resources)
+ val qrCode = QrCode.create(hexEncodedPublicKey, size)
+ qrCodeImageView.setImageBitmap(qrCode)
+ }
+
cancelButton.setOnClickListener { cancel() }
}
// endregion
@@ -86,14 +81,14 @@ class DeviceLinkingView private constructor(context: Context, attrs: AttributeSe
if (mode != Mode.Master || pairingAuthorisation.type != PairingAuthorisation.Type.REQUEST || this.pairingAuthorisation != null) { return }
this.pairingAuthorisation = pairingAuthorisation
spinner.visibility = View.GONE
+ qrCodeImageView.visibility = View.GONE
val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams
titleTextViewLayoutParams.topMargin = toPx(16, resources)
titleTextView.layoutParams = titleTextViewLayoutParams
titleTextView.text = resources.getString(R.string.view_device_linking_title_3)
explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_2)
mnemonicTextView.visibility = View.VISIBLE
- val hexEncodedPublicKey = pairingAuthorisation.secondaryDevicePublicKey.removing05PrefixIfNeeded()
- mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
+ mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), pairingAuthorisation.secondaryDevicePublicKey)
authorizeButton.visibility = View.VISIBLE
}
diff --git a/src/org/thoughtcrime/securesms/loki/DeviceLinkingViewDelegate.kt b/src/org/thoughtcrime/securesms/loki/DeviceLinkingViewDelegate.kt
deleted file mode 100644
index 56c77bd9c7..0000000000
--- a/src/org/thoughtcrime/securesms/loki/DeviceLinkingViewDelegate.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.thoughtcrime.securesms.loki
-
-import org.whispersystems.signalservice.loki.api.PairingAuthorisation
-
-interface DeviceLinkingViewDelegate {
-
- fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) { }
- fun handleDeviceLinkingDialogDismissed() { }
- fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) { }
-}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/DeviceListBottomSheetFragment.kt b/src/org/thoughtcrime/securesms/loki/DeviceListBottomSheetFragment.kt
new file mode 100644
index 0000000000..013b48a66d
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/DeviceListBottomSheetFragment.kt
@@ -0,0 +1,24 @@
+package org.thoughtcrime.securesms.loki
+
+import android.os.Bundle
+import android.support.design.widget.BottomSheetDialogFragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import kotlinx.android.synthetic.main.fragment_device_list_bottom_sheet.*
+import network.loki.messenger.R
+
+public class DeviceListBottomSheetFragment : BottomSheetDialogFragment() {
+ var onEditTapped: (() -> Unit)? = null
+ var onUnlinkTapped: (() -> Unit)? = null
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.fragment_device_list_bottom_sheet, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ editDisplayNameText.setOnClickListener { onEditTapped?.invoke() }
+ unlinkDeviceText.setOnClickListener { onUnlinkTapped?.invoke() }
+ }
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt b/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt
index c99f480a75..ab091d25c3 100644
--- a/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt
+++ b/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt
@@ -54,6 +54,7 @@ class DisplayNameActivity : BaseActionBarActivity() {
application.startRSSFeedPollersIfNeeded()
val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
servers.forEach { publicChatAPI.setDisplayName(name, it) }
+ application.updatePublicChatProfileAvatarIfNeeded()
}
}
}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/JazzIdenticonContactPhoto.kt b/src/org/thoughtcrime/securesms/loki/JazzIdenticonContactPhoto.kt
new file mode 100644
index 0000000000..979f7e3e88
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/JazzIdenticonContactPhoto.kt
@@ -0,0 +1,22 @@
+package org.thoughtcrime.securesms.loki
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.support.v7.content.res.AppCompatResources
+import network.loki.messenger.R
+import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto
+
+class JazzIdenticonContactPhoto(val hexEncodedPublicKey: String) : FallbackContactPhoto {
+ override fun asDrawable(context: Context, color: Int): Drawable {
+ return asDrawable(context, color, false)
+ }
+
+ override fun asDrawable(context: Context, color: Int, inverted: Boolean): Drawable {
+ val targetSize = context.resources.getDimensionPixelSize(R.dimen.contact_photo_target_size)
+ return JazzIdenticonDrawable(targetSize, targetSize, hexEncodedPublicKey)
+ }
+
+ override fun asCallCard(context: Context): Drawable? {
+ return AppCompatResources.getDrawable(context, R.drawable.ic_person_large)
+ }
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/LinkedDevicesActivity.kt b/src/org/thoughtcrime/securesms/loki/LinkedDevicesActivity.kt
new file mode 100644
index 0000000000..0788095b2e
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/LinkedDevicesActivity.kt
@@ -0,0 +1,85 @@
+package org.thoughtcrime.securesms.loki
+
+import android.os.AsyncTask
+import android.os.Bundle
+import android.view.MenuItem
+import android.widget.Toast
+import org.thoughtcrime.securesms.*
+import org.thoughtcrime.securesms.util.DynamicTheme
+import org.thoughtcrime.securesms.util.DynamicLanguage
+import network.loki.messenger.R
+import nl.komponents.kovenant.then
+import org.thoughtcrime.securesms.database.DatabaseFactory
+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
+
+class LinkedDevicesActivity : PassphraseRequiredActionBarActivity(), DeviceLinkingDelegate {
+
+ companion object {
+ private val TAG = DeviceActivity::class.java.simpleName
+ }
+
+ private val dynamicTheme = DynamicTheme()
+ private val dynamicLanguage = DynamicLanguage()
+ private lateinit var deviceListFragment: DeviceListFragment
+
+ public override fun onPreCreate() {
+ dynamicTheme.onCreate(this)
+ dynamicLanguage.onCreate(this)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
+ super.onCreate(savedInstanceState, ready)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ supportActionBar?.setTitle(R.string.AndroidManifest__linked_devices)
+ this.deviceListFragment = DeviceListFragment()
+ this.deviceListFragment.setAddDeviceButtonListener {
+ DeviceLinkingDialog.show(this, DeviceLinkingView.Mode.Master, this)
+ }
+ this.deviceListFragment.setHandleDisconnectDevice { devicePublicKey ->
+ // Purge the device pairing from our database
+ val ourPublicKey = TextSecurePreferences.getLocalNumber(this)
+ val database = DatabaseFactory.getLokiAPIDatabase(this)
+ database.removePairingAuthorisation(ourPublicKey, devicePublicKey)
+ // Update mapping on the file server
+ LokiStorageAPI.shared.updateUserDeviceMappings().success {
+ // Send an unpair request to let the device know that it has been revoked
+ MessageSender.sendUnpairRequest(this, devicePublicKey)
+ }
+ // Refresh the list
+ this.deviceListFragment.refresh()
+ Toast.makeText(this, R.string.DeviceListActivity_unlinked_device, Toast.LENGTH_LONG).show()
+ return@setHandleDisconnectDevice null
+ }
+ this.deviceListFragment.setHandleDeviceNameChange { pair ->
+ DatabaseFactory.getLokiUserDatabase(this).setDisplayName(pair.first, pair.second)
+ this.deviceListFragment.refresh()
+ return@setHandleDeviceNameChange null
+ }
+ initFragment(android.R.id.content, deviceListFragment, dynamicLanguage.currentLocale)
+ }
+
+ public override fun onResume() {
+ super.onResume()
+ dynamicTheme.onResume(this)
+ dynamicLanguage.onResume(this)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == android.R.id.home) {
+ finish()
+ return true
+ }
+ return false
+ }
+
+ override fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {
+ AsyncTask.execute {
+ signAndSendPairingAuthorisationMessage(this, pairingAuthorisation)
+ Util.runOnMain { this.deviceListFragment.refresh() }
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt
index 9ac5dc0ca0..85d8f38408 100644
--- a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt
+++ b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt
@@ -189,6 +189,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
val database = databaseHelper.readableDatabase
database.delete(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey ))
}
+
+ fun removePairingAuthorisation(primaryDevicePublicKey: String, secondaryDevicePublicKey: String) {
+ val database = databaseHelper.readableDatabase
+ database.delete(pairingAuthorisationCache, "${Companion.primaryDevicePublicKey} = ? OR ${Companion.secondaryDevicePublicKey} = ?", arrayOf( primaryDevicePublicKey, secondaryDevicePublicKey ))
+ }
}
// region Convenience
diff --git a/src/org/thoughtcrime/securesms/loki/LokiMessageDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiMessageDatabase.kt
index 817b76f5dd..f25f39e955 100644
--- a/src/org/thoughtcrime/securesms/loki/LokiMessageDatabase.kt
+++ b/src/org/thoughtcrime/securesms/loki/LokiMessageDatabase.kt
@@ -19,7 +19,7 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
private val friendRequestStatus = "friend_request_status"
private val threadID = "thread_id"
@JvmStatic val createMessageFriendRequestTableCommand = "CREATE TABLE $messageFriendRequestTableName ($messageID INTEGER PRIMARY KEY, $serverID INTEGER DEFAULT 0, $friendRequestStatus INTEGER DEFAULT 0);"
- @JvmStatic val createMessageToThreadMappingTableCommand = "CREATE TABLE $messageThreadMappingTableName ($messageID INTEGER PRIMARY KEY, $threadID INTEGER);"
+ @JvmStatic val createMessageToThreadMappingTableCommand = "CREATE TABLE IF NOT EXISTS $messageThreadMappingTableName ($messageID INTEGER PRIMARY KEY, $threadID INTEGER);"
}
override fun getQuoteServerID(quoteID: Long, quoteeHexEncodedPublicKey: String): Long? {
diff --git a/src/org/thoughtcrime/securesms/loki/LokiPreKeyBundleDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiPreKeyBundleDatabase.kt
index 6eee3387b1..978abd6e91 100644
--- a/src/org/thoughtcrime/securesms/loki/LokiPreKeyBundleDatabase.kt
+++ b/src/org/thoughtcrime/securesms/loki/LokiPreKeyBundleDatabase.kt
@@ -36,11 +36,6 @@ class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) :
"$signedPreKeySignature TEXT," + "$identityKey TEXT NOT NULL," + "$deviceID INTEGER," + "$registrationID INTEGER" + ");"
}
- fun resetAllPreKeyBundleInfo() {
- TextSecurePreferences.removeLocalRegistrationId(context)
- TextSecurePreferences.setSignedPreKeyRegistered(context, false)
- }
-
fun generatePreKeyBundle(hexEncodedPublicKey: String): PreKeyBundle? {
var registrationID = TextSecurePreferences.getLocalRegistrationId(context)
if (registrationID == 0) {
diff --git a/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt b/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt
index 756a7ebc6b..b1af5cc08c 100644
--- a/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt
+++ b/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt
@@ -6,9 +6,14 @@ import android.util.Log
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.then
+import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
+import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
+import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.jobs.PushDecryptJob
+import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
+import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
@@ -21,7 +26,9 @@ 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.messaging.LokiThreadFriendRequestStatus
import org.whispersystems.signalservice.loki.utilities.successBackground
+import java.security.MessageDigest
import java.util.*
class LokiPublicChatPoller(private val context: Context, private val group: LokiPublicChat) {
@@ -155,6 +162,7 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
val senderDisplayName = "${message.displayName} (...${message.hexEncodedPublicKey.takeLast(8)})"
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.hexEncodedPublicKey, senderDisplayName)
}
+
val senderPublicKey = primaryDevice ?: message.hexEncodedPublicKey
val serviceDataMessage = getDataMessage(message)
val serviceContent = SignalServiceContent(serviceDataMessage, senderPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.timestamp, false)
@@ -163,6 +171,25 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
} 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)) {
+ val database = DatabaseFactory.getRecipientDatabase(context)
+ database.setProfileKey(senderRecipient, profileKey)
+ ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderRecipient, 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)
+ if (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) {
+ ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderRecipient, ""))
+ }
+ }
}
fun processOutgoingMessage(message: LokiPublicChatMessage) {
val messageServerID = message.serverID ?: return
@@ -178,6 +205,19 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
} else {
PushDecryptJob(context).handleSynchronizeSentTextMessage(transcript)
}
+
+ // Loki - If we got a message from our master device then make sure our mappings stay 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.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, profileKey)) {
+ val database = DatabaseFactory.getRecipientDatabase(context)
+ database.setProfileKey(recipient, profileKey)
+ database.setProfileAvatar(recipient, url)
+ ApplicationContext.getInstance(context).updatePublicChatProfileAvatarIfNeeded()
+ }
+ }
}
var userDevices = setOf()
var uniqueDevices = setOf()
diff --git a/src/org/thoughtcrime/securesms/loki/LokiUserDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiUserDatabase.kt
index a46307e48b..e72d898ce3 100644
--- a/src/org/thoughtcrime/securesms/loki/LokiUserDatabase.kt
+++ b/src/org/thoughtcrime/securesms/loki/LokiUserDatabase.kt
@@ -16,6 +16,7 @@ class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database
companion object {
// Shared
private val displayName = "display_name"
+ private val profileAvatarUrl = "profile_avatar_url"
// Display name cache
private val displayNameTable = "loki_user_display_name_database"
private val hexEncodedPublicKey = "hex_encoded_public_key"
@@ -66,4 +67,12 @@ class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database
Log.d("Loki", "Couldn't save server display name due to exception: $e.")
}
}
+
+ override fun getProfileAvatarUrl(hexEncodedPublicKey: String): String? {
+ return if (hexEncodedPublicKey == TextSecurePreferences.getLocalNumber(context)) {
+ TextSecurePreferences.getProfileAvatarUrl(context)
+ } else {
+ Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false).resolve().profileAvatar
+ }
+ }
}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/MnemonicUtilities.kt b/src/org/thoughtcrime/securesms/loki/MnemonicUtilities.kt
new file mode 100644
index 0000000000..b5275725f5
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/loki/MnemonicUtilities.kt
@@ -0,0 +1,37 @@
+package org.thoughtcrime.securesms.loki
+
+import android.content.Context
+import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
+import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded
+import java.io.File
+import java.io.FileOutputStream
+
+object MnemonicUtilities {
+
+ @JvmStatic
+ public fun getLanguageFileDirectory(context: Context): File {
+ val languages = listOf( "english", "japanese", "portuguese", "spanish" )
+ val directory = File(context.applicationInfo.dataDir)
+ for (language in languages) {
+ val fileName = "$language.txt"
+ if (directory.list().contains(fileName)) { continue }
+ val inputStream = context.assets.open("mnemonic/$fileName")
+ val file = File(directory, fileName)
+ val outputStream = FileOutputStream(file)
+ val buffer = ByteArray(1024)
+ while (true) {
+ val count = inputStream.read(buffer)
+ if (count < 0) { break }
+ outputStream.write(buffer, 0, count)
+ }
+ inputStream.close()
+ outputStream.close()
+ }
+ return directory
+ }
+
+ @JvmStatic
+ public fun getFirst3Words(codec: MnemonicCodec, hexEncodedPublicKey: String): String {
+ return codec.encode(hexEncodedPublicKey.removing05PrefixIfNeeded()).split(" ").slice(0 until 3).joinToString(" ")
+ }
+}
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt b/src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt
index 257336de05..d3abb182dd 100644
--- a/src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt
+++ b/src/org/thoughtcrime/securesms/loki/MultiDeviceUtilities.kt
@@ -6,9 +6,13 @@ 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.then
import nl.komponents.kovenant.toFailVoid
+import nl.komponents.kovenant.ui.successUi
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
+import org.thoughtcrime.securesms.crypto.PreKeyUtil
+import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.logging.Log
@@ -22,11 +26,32 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
+import org.whispersystems.signalservice.loki.utilities.Analytics
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()
+ }.successUi {
+ Analytics.shared.track("Secondary Device Unlinked")
+ TextSecurePreferences.setNeedsRevocationCheck(context, false)
+ ApplicationContext.getInstance(context).clearData()
+ }.fail { error ->
+ TextSecurePreferences.setNeedsRevocationCheck(context, true)
+ Log.d("Loki", "Revocation check failed: ${error.message ?: error}")
+ }
+}
+
fun getAllDeviceFriendRequestStatuses(context: Context, hexEncodedPublicKey: String): Promise