Merge pull request #50 from loki-project/profile-avatar-setting

Big PR
This commit is contained in:
gmbnt 2019-12-02 15:03:50 +11:00 committed by GitHub
commit d64d34bd25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 2037 additions and 1354 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M13,8.2l-1,-1 -4,4 -4,-4 -1,1 4,4 -4,4 1,1 4,-4 4,4 1,-1 -4,-4 4,-4zM19,1H9c-1.1,0 -2,0.9 -2,2v3h2V4h10v16H9v-2H7v3c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View File

@ -96,6 +96,17 @@
app:labeledEditText_background="@color/loki_darkest_gray"
app:labeledEditText_label="@string/activity_key_pair_public_key_edit_text_label"/>
<Button
android:id="@+id/scanQRButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/transparent"
android:elevation="0dp"
android:stateListAnimator="@null"
android:text="@string/fragment_scan_qr_code_title"
android:textColor="@color/signal_primary"
android:visibility="gone" />
<Button
android:id="@+id/toggleRestoreModeButton"
android:layout_width="match_parent"
@ -123,7 +134,7 @@
android:layout_height="50dp"
android:background="@color/transparent"
android:textColor="@color/signal_primary"
android:text="Link Device"
android:text="@string/activity_key_pair_toggle_mode_button_title_3"
android:elevation="0dp"
android:stateListAnimator="@null" />

View File

@ -1,58 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:fab="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:fab="http://schemas.android.com/apk/res-auto">
<LinearLayout android:id="@+id/progress_container"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone" >
<ProgressBar
android:id="@+id/activityIndicator"
android:indeterminate="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone" />
<ProgressBar android:id="@+id/progress"
android:indeterminate="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
</ProgressBar>
</LinearLayout>
<TextView
android:id="@+id/emptyStateTextView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="20sp"
android:visibility="gone"
android:text="@string/device_list_fragment__no_devices_linked"
android:paddingStart="16dip"
android:paddingEnd="16dip"
tools:visibility="visible"/>
<TextView android:id="@+id/empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center|center_vertical"
android:gravity="center|center_vertical"
android:textSize="20sp"
android:visibility="gone"
android:text="@string/device_list_fragment__no_devices_linked"
android:paddingStart="16dip"
android:paddingEnd="16dip"
android:layout_weight="1"
tools:visibility="visible"/>
<ListView android:id="@id/android:list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:drawSelectorOnTop="false"
android:paddingStart="16dip"
android:paddingEnd="16dip"
tools:visibility="gone"/>
<ListView
android:id="@id/android:list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:drawSelectorOnTop="false" />
<com.melnykov.fab.FloatingActionButton
android:id="@+id/add_device"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
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" />
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" />
</LinearLayout>
</RelativeLayout>

View File

@ -1,33 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.DeviceListItem xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp">
<org.thoughtcrime.securesms.DeviceListItem
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="64dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:gravity="center_vertical">
<TextView android:id="@+id/name"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/conversation_list_item_contact_color"
android:singleLine="true"
android:ellipsize="marquee"
android:layout_marginTop="8dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:text="Name"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/conversation_list_item_contact_color"
android:textSize="18sp" />
<TextView android:id="@+id/created"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?attr/conversation_list_item_subject_color"
android:fontFamily="sans-serif-light" />
<TextView android:id="@+id/active"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?attr/conversation_list_item_subject_color"
android:fontFamily="sans-serif-light"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/shortId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="marquee"
android:singleLine="true"
android:text="shortId"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="#A2A2A2"
android:textSize="14sp" />
</org.thoughtcrime.securesms.DeviceListItem>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:behavior_hideable="true"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<TextView
android:id="@+id/editDisplayNameText"
style="@style/ActionItem"
android:drawableStart="@drawable/ic_edit_white_24dp"
android:text="@string/fragment_device_list_edit_device_name_title"/>
<TextView
android:id="@+id/unlinkDeviceText"
style="@style/ActionItem"
android:drawableStart="@drawable/ic_phonelink_erase_white_24dp"
android:text="@string/fragment_device_list_unlink_device_title" />
</LinearLayout>

View File

@ -24,13 +24,14 @@
android:layout_height="match_parent"/>
<TextView
android:id="@+id/descriptionTextView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:padding="32dp"
android:gravity="center"
android:background="@color/loki_darkest_gray"
android:text="@string/fragment_scan_qr_code_explanation"
android:text="@string/fragment_scan_qr_code_explanation_new_conversation"
android:textColor="?android:textColorPrimary" />
</LinearLayout>

View File

@ -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" />
<org.thoughtcrime.securesms.components.emoji.EmojiToggle

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
@ -15,6 +16,13 @@
android:indeterminate="true"
android:progressTint="@color/white" />
<ImageView
android:id="@+id/qrCodeImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/white" />
<TextView
android:id="@+id/titleTextView"
style="@style/Signal.Text.Headline"

View File

@ -115,4 +115,10 @@
<dimen name="recording_voice_lock_target">-150dp</dimen>
<dimen name="default_margin">16dp</dimen>
<dimen name="drawable_padding">24dp</dimen>
<dimen name="text_size">16sp</dimen>
<dimen name="normal_padding">16dp</dimen>
<dimen name="action_item_height">56dp</dimen>
</resources>

View File

@ -293,12 +293,14 @@
<!-- DeviceListActivity -->
<string name="DeviceListActivity_unlink_s">Unlink \'%s\'?</string>
<string name="DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive">By unlinking this device, it will no longer be able to send or receive messages.</string>
<string name="DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive">This device will no longer be able to send or receive messages.</string>
<string name="DeviceListActivity_network_connection_failed">Network connection failed</string>
<string name="DeviceListActivity_try_again">Try again</string>
<string name="DeviceListActivity_unlinking_device">Unlinking device...</string>
<string name="DeviceListActivity_unlinking_device_no_ellipsis">Unlinking device</string>
<string name="DeviceListActivity_network_failed">Network failed!</string>
<string name="DeviceListActivity_unlinked_device">Successfully unlinked device</string>
<string name="DeviceListActivity_edit_device_name">Edit device name</string>
<!-- DeviceListItem -->
<string name="DeviceListItem_unnamed_device">Unnamed device</string>
@ -946,7 +948,7 @@
<string name="device_link_fragment__link_device">Link device</string>
<!-- device_list_fragment -->
<string name="device_list_fragment__no_devices_linked">No devices linked</string>
<string name="device_list_fragment__no_devices_linked">You don\'t have any linked devices yet</string>
<string name="device_list_fragment__link_new_device">Link new device</string>
<!-- experience_upgrade_activity -->
@ -1157,7 +1159,7 @@
<string name="AndroidManifest__log_submit">Submit debug log</string>
<string name="AndroidManifest__media_preview">Media preview</string>
<string name="AndroidManifest__message_details">Message details</string>
<string name="AndroidManifest__linked_devices">Linked devices</string>
<string name="AndroidManifest__linked_devices">Linked Devices</string>
<string name="AndroidManifest__invite_friends">Invite friends</string>
<string name="AndroidManifest_archived_conversations">Archived conversations</string>
<string name="AndroidManifest_remove_photo">Remove photo</string>
@ -1572,11 +1574,11 @@
<!-- Conversation list activity -->
<string name="activity_conversation_list_empty_state_message">Looks like you don\'t have any conversations yet. Get started by messaging a friend.</string>
<!-- Settings activity -->
<string name="activity_settings_secondary_device_tag">Secondary device</string>
<string name="activity_settings_linked_device_tag">Linked device (%s)</string>
<string name="activity_settings_public_key_copied_message">Copied to clipboard</string>
<string name="activity_settings_share_public_key_button_title">Share Public Key</string>
<string name="activity_settings_show_qr_code_button_title">Show QR Code</string>
<string name="activity_settings_link_device_button_title">Link Device</string>
<string name="activity_settings_linked_devices_button_title">Linked Devices</string>
<string name="activity_settings_show_seed_button_title">Show Seed</string>
<string name="activity_settings_seed_dialog_title">Your Seed</string>
<string name="activity_settings_seed_dialog_copy_button_title">Copy</string>
@ -1636,11 +1638,18 @@
<string name="view_device_linking_cancel_button_title">Cancel</string>
<!-- Scan QR code fragment -->
<string name="fragment_scan_qr_code_title">Scan QR Code</string>
<string name="fragment_scan_qr_code_explanation">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\".</string>
<string name="fragment_scan_qr_code_explanation_new_conversation">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\".</string>
<string name="fragment_scan_qr_code_explanation_link_device">Link to an existing device by going into its in-app settings and clicking \"Link Device\".</string>
<string name="fragment_scan_qr_code_camera_permission_dialog_message">Loki Messenger needs camera access to scan QR codes.</string>
<!-- Conversation activity -->
<string name="activity_conversation_copy_public_key_button_title">Copy public key</string>
<!-- Conversation list activity -->
<string name="activity_conversation_list_add_public_chat_button_title">Add Public Chat</string>
<!-- Device list bottom sheet fragment -->
<string name="fragment_device_list_edit_device_name_title">Edit device name</string>
<string name="fragment_device_list_unlink_device_title">Unlink device</string>
<!-- Device unlink dialog -->
<string name="dialog_device_unlink_title">Device unlinked</string>
<string name="dialog_device_unlink_message">This device has been successfully unlinked</string>
</resources>

View File

@ -242,4 +242,15 @@
<item name="colorControlActivated">@color/white</item>
</style>
<style name="ActionItem">
<item name="android:textSize">@dimen/text_size</item>
<item name="android:drawablePadding">@dimen/drawable_padding</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">@dimen/action_item_height</item>
<item name="android:padding">@dimen/normal_padding</item>
<item name="android:gravity">center_vertical</item>
<item name="android:selectable">true</item>
<item name="android:foreground">?attr/selectableItemBackground</item>
</style>
</resources>

View File

@ -133,6 +133,7 @@
<item name="android:windowBackground">@color/loki_darkest_gray</item>
<item name="alertDialogTheme">@style/AppCompatAlertDialogStyleLight</item>
<item name="android:alertDialogTheme">@style/AppCompatDialogStyleLight</item>
<item name="bottomSheetDialogTheme">@style/Theme.MaterialComponents.Light.BottomSheetDialog</item>
<!--<item name="android:windowContentOverlay">@drawable/compat_actionbar_shadow_background</item>-->
<item name="attachment_type_selector_background">@color/white</item>
@ -317,6 +318,7 @@
<item name="android:windowBackground">@color/loki_darkest_gray</item>
<item name="alertDialogTheme">@style/AppCompatAlertDialogStyleDark</item>
<item name="android:alertDialogTheme">@style/AppCompatDialogStyleDark</item>
<item name="bottomSheetDialogTheme">@style/Theme.MaterialComponents.BottomSheetDialog</item>
<item name="attachment_type_selector_background">@color/gray95</item>
<item name="attachment_document_icon_small">@drawable/ic_document_small_dark</item>

View File

@ -33,6 +33,10 @@
android:title="@string/preferences__advanced"
android:icon="@drawable/ic_advanced_24dp"/> -->
<Preference android:key="preference_category_linked_devices"
android:title="@string/activity_settings_linked_devices_button_title"
android:icon="@drawable/icon_link"/>
<Preference android:key="preference_category_public_key"
android:title="@string/activity_settings_share_public_key_button_title"
android:icon="@drawable/icon_share"/>
@ -41,10 +45,6 @@
android:title="@string/activity_settings_show_qr_code_button_title"
android:icon="@drawable/icon_qr_code"/>
<Preference android:key="preference_category_link_device"
android:title="Link Device"
android:icon="@drawable/icon_link"/>
<Preference android:key="preference_category_seed"
android:title="@string/activity_settings_show_seed_button_title"
android:icon="@drawable/icon_seedling"/>

View File

@ -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<String> 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);
}
}

View File

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

View File

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

View File

@ -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;
}

View File

@ -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<List<Device>>,
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<String, Void> handleDisconnectDevice;
private Function<Pair<String, String>, 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<String, Void> handler) {
this.handleDisconnectDevice = handler;
}
public void setHandleDeviceNameChange(Function<Pair<String, String>, Void> handler) {
this.handleDeviceNameChange = handler;
}
@Override
public @NonNull Loader<List<Device>> 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<Void, Void, Void>(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);

View File

@ -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;
}
}

View File

@ -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;

View File

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

View File

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

View File

@ -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();
}

View File

@ -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()

View File

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

View File

@ -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;

View File

@ -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<List<Device>> {
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<Device> loadInBackground() {
try {
List<Device> devices = Stream.of(accountManager.getDevices())
.filter(d -> d.getId() != SignalServiceAddress.DEFAULT_DEVICE_ID)
.map(this::mapToDevice)
.toList();
String ourPublicKey = TextSecurePreferences.getLocalNumber(getContext());
List<String> secondaryDevicePublicKeys = LokiStorageAPI.shared.getSecondaryDevicePublicKeys(ourPublicKey).get();
List<Device> 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<Device> {
@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());
}
}
}

View File

@ -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; }
}

View File

@ -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<String> 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> 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<Boolean, Exception> promise = PromiseUtil.timeout(MultiDeviceUtilities.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context), 8000);
boolean shouldBecomeFriends = PromiseUtil.get(promise, false);
if (shouldBecomeFriends) {
// Become friends AND update the message they sent
becomeFriendsWithContact(content.getSender(), true);
@ -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!.");
}

View File

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

View File

@ -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()));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -54,6 +54,7 @@ class DisplayNameActivity : BaseActionBarActivity() {
application.startRSSFeedPollersIfNeeded()
val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
servers.forEach { publicChatAPI.setDisplayName(name, it) }
application.updatePublicChatProfileAvatarIfNeeded()
}
}
}

View File

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

View File

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

View File

@ -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

View File

@ -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? {

View File

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

View File

@ -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<String>()
var uniqueDevices = setOf<String>()

View File

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

View File

@ -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(" ")
}
}

View File

@ -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<Map<String, LokiThreadFriendRequestStatus>, Exception> {
val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context)
return LokiStorageAPI.shared.getAllDevicePublicKeys(hexEncodedPublicKey).map { keys ->
@ -91,12 +116,16 @@ fun shouldAutomaticallyBecomeFriendsWithDevice(publicKey: String, context: Conte
fun sendPairingAuthorisationMessage(context: Context, contactHexEncodedPublicKey: String, authorisation: PairingAuthorisation): Promise<Unit, Exception> {
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(contactHexEncodedPublicKey)
val message = SignalServiceDataMessage.newBuilder().withBody(null).withPairingAuthorisation(authorisation)
val message = SignalServiceDataMessage.newBuilder().withPairingAuthorisation(authorisation)
// A REQUEST should always act as a friend request. A GRANT should always be replying back as a normal message.
if (authorisation.type == PairingAuthorisation.Type.REQUEST) {
val preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.number)
message.asFriendRequest(true).withPreKeyBundle(preKeyBundle)
} else {
// Send over our profile key so that our linked device can get our profile picture
message.withProfileKey(ProfileKeyUtil.getProfileKey(context))
}
return try {
Log.d("Loki", "Sending authorisation message to: $contactHexEncodedPublicKey.")
val result = messageSender.sendMessage(0, address, Optional.absent<UnidentifiedAccessPair>(), message.build())

View File

@ -11,39 +11,58 @@ import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.util.JsonUtil
import java.io.IOException
import java.util.concurrent.TimeUnit
data class BackgroundMessage private constructor(val recipient: String, val body: String?, val friendRequest: Boolean, val unpairingRequest: Boolean) {
companion object {
@JvmStatic
fun create(recipient: String) = BackgroundMessage(recipient, null, false, false)
@JvmStatic
fun createFriendRequest(recipient: String, messageBody: String) = BackgroundMessage(recipient, messageBody, true, false)
@JvmStatic
fun createUnpairingRequest(recipient: String) = BackgroundMessage(recipient, null, false, true)
internal fun parse(serialized: String): BackgroundMessage {
val node = JsonUtil.fromJson(serialized)
val recipient = node.get("recipient").asText()
val body = if (node.hasNonNull("body")) node.get("body").asText() else null
val friendRequest = node.get("friendRequest").asBoolean(false)
val unpairingRequest = node.get("unpairingRequest").asBoolean(false)
return BackgroundMessage(recipient, body, friendRequest, unpairingRequest)
}
}
fun serialize(): String {
val map = mapOf("recipient" to recipient, "body" to body, "friendRequest" to friendRequest, "unpairingRequest" to unpairingRequest)
return JsonUtil.toJson(map)
}
}
class PushBackgroundMessageSendJob private constructor(
parameters: Parameters,
private val recipient: String,
private val messageBody: String?,
private val friendRequest: Boolean
private val message: BackgroundMessage
) : BaseJob(parameters) {
companion object {
const val KEY = "PushBackgroundMessageSendJob"
private val TAG = PushBackgroundMessageSendJob::class.java.simpleName
private val KEY_RECIPIENT = "recipient"
private val KEY_MESSAGE_BODY = "message_body"
private val KEY_FRIEND_REQUEST = "asFriendRequest"
private val KEY_MESSAGE = "message"
}
constructor(recipient: String): this(recipient, null, false)
constructor(recipient: String, messageBody: String?, friendRequest: Boolean) : this(Parameters.Builder()
constructor(message: BackgroundMessage) : this(Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setQueue(KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(1)
.build(),
recipient, messageBody, friendRequest)
message)
override fun serialize(): Data {
return Data.Builder()
.putString(KEY_RECIPIENT, recipient)
.putString(KEY_MESSAGE_BODY, messageBody)
.putBoolean(KEY_FRIEND_REQUEST, friendRequest)
.putString(KEY_MESSAGE, message.serialize())
.build()
}
@ -52,22 +71,24 @@ class PushBackgroundMessageSendJob private constructor(
}
public override fun onRun() {
val message = SignalServiceDataMessage.newBuilder()
val dataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(System.currentTimeMillis())
.withBody(messageBody)
.withBody(message.body)
if (friendRequest) {
val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(recipient)
message.withPreKeyBundle(bundle)
if (message.friendRequest) {
val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(message.recipient)
dataMessage.withPreKeyBundle(bundle)
.asFriendRequest(true)
} else if (message.unpairingRequest) {
dataMessage.asUnpairingRequest(true)
}
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(recipient)
val address = SignalServiceAddress(message.recipient)
try {
messageSender.sendMessage(-1, address, Optional.absent<UnidentifiedAccessPair>(), message.build()) // The message ID doesn't matter
messageSender.sendMessage(-1, address, Optional.absent<UnidentifiedAccessPair>(), dataMessage.build()) // The message ID doesn't matter
} catch (e: Exception) {
Log.d("Loki", "Failed to send background message to: $recipient.")
Log.d("Loki", "Failed to send background message to: ${message.recipient}.")
throw e
}
}
@ -82,10 +103,8 @@ class PushBackgroundMessageSendJob private constructor(
class Factory : Job.Factory<PushBackgroundMessageSendJob> {
override fun create(parameters: Parameters, data: Data): PushBackgroundMessageSendJob {
try {
val recipient = data.getString(KEY_RECIPIENT)
val messageBody = if (data.hasString(KEY_MESSAGE_BODY)) data.getString(KEY_MESSAGE_BODY) else null
val friendRequest = data.getBooleanOrDefault(KEY_FRIEND_REQUEST, false)
return PushBackgroundMessageSendJob(parameters, recipient, messageBody, friendRequest)
val messageJSON = data.getString(KEY_MESSAGE)
return PushBackgroundMessageSendJob(parameters, BackgroundMessage.parse(messageJSON))
} catch (e: IOException) {
throw AssertionError(e)
}

View File

@ -0,0 +1,5 @@
package org.thoughtcrime.securesms.loki
import org.thoughtcrime.securesms.recipients.Recipient
data class RecipientAvatarModifiedEvent(val recipient: Recipient)

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki
import android.content.res.Configuration
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -14,8 +15,15 @@ import org.thoughtcrime.securesms.qr.ScanningThread
class ScanQRCodeFragment : Fragment() {
private val scanningThread = ScanningThread()
private var viewCreated = false
var scanListener: ScanListener? = null
set(value) { field = value; scanningThread.setScanListener(scanListener) }
var mode: Mode = Mode.NewConversation
set(value) { field = value; updateDescription(); }
// region Types
enum class Mode { NewConversation, LinkDevice }
// endregion
override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? {
return layoutInflater.inflate(R.layout.fragment_scan_qr_code, viewGroup, false)
@ -23,10 +31,12 @@ class ScanQRCodeFragment : Fragment() {
override fun onViewCreated(view: View, bundle: Bundle?) {
super.onViewCreated(view, bundle)
viewCreated = true
when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> overlayView.orientation = LinearLayout.HORIZONTAL
else -> overlayView.orientation = LinearLayout.VERTICAL
}
updateDescription()
}
override fun onResume() {
@ -35,8 +45,10 @@ class ScanQRCodeFragment : Fragment() {
this.cameraView.onResume()
this.cameraView.setPreviewCallback(scanningThread)
this.scanningThread.start()
val activity = activity as NewConversationActivity
activity.supportActionBar!!.setTitle(R.string.fragment_scan_qr_code_title)
if (activity is AppCompatActivity) {
val activity = activity as AppCompatActivity
activity.supportActionBar?.setTitle(R.string.fragment_scan_qr_code_title)
}
}
override fun onPause() {
@ -55,4 +67,13 @@ class ScanQRCodeFragment : Fragment() {
cameraView.onResume()
cameraView.setPreviewCallback(scanningThread)
}
fun updateDescription() {
if (!viewCreated) { return }
val text = when (mode) {
Mode.NewConversation -> R.string.fragment_scan_qr_code_explanation_new_conversation
Mode.LinkDevice -> R.string.fragment_scan_qr_code_explanation_link_device
}
descriptionTextView.setText(text)
}
}

View File

@ -1,11 +1,13 @@
package org.thoughtcrime.securesms.loki
import android.Manifest
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.AsyncTask
import android.os.Bundle
import android.support.v4.app.FragmentManager
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
@ -19,6 +21,8 @@ import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.logging.Log
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.qr.ScanListener
import org.thoughtcrime.securesms.util.Hex
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.curve25519.Curve25519
@ -32,7 +36,7 @@ import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
import java.io.File
import java.io.FileOutputStream
class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
class SeedActivity : BaseActionBarActivity(), DeviceLinkingDelegate, ScanListener {
private lateinit var languageFileDirectory: File
private var mode = Mode.Register
set(newValue) { field = newValue; updateUI() }
@ -57,6 +61,23 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
toggleRestoreModeButton.setOnClickListener { mode = Mode.Restore }
toggleLinkModeButton.setOnClickListener { mode = Mode.Link }
mainButton.setOnClickListener { handleMainButtonTapped() }
scanQRButton.setOnClickListener {
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.fragment_scan_qr_code_camera_permission_dialog_message))
.onAllGranted {
val fragment = ScanQRCodeFragment()
fragment.mode = ScanQRCodeFragment.Mode.LinkDevice
fragment.scanListener = this
supportFragmentManager.beginTransaction().replace(android.R.id.content, fragment).addToBackStack("QR").commitAllowingStateLoss()
publicKeyEditText.clearFocus()
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(publicKeyEditText.windowToken, 0)
}
.onAnyDenied { Toast.makeText(this, R.string.fragment_scan_qr_code_camera_permission_dialog_message, Toast.LENGTH_SHORT).show() }
.execute()
}
Analytics.shared.track("Seed Screen Viewed")
}
// endregion
@ -106,6 +127,7 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
mnemonicEditText.visibility = restoreModeVisibility
linkExplanationTextView.visibility = linkModeVisibility
publicKeyEditText.visibility = linkModeVisibility
scanQRButton.visibility = linkModeVisibility
toggleRegisterModeButton.visibility = if (mode != Mode.Register) View.VISIBLE else View.GONE
toggleRestoreModeButton.visibility = if (mode != Mode.Restore) View.VISIBLE else View.GONE
toggleLinkModeButton.visibility = if (mode != Mode.Link) View.VISIBLE else View.GONE
@ -230,10 +252,19 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
private fun resetForRegistration() {
IdentityKeyUtil.delete(this, IdentityKeyUtil.lokiSeedKey)
DatabaseFactory.getLokiPreKeyBundleDatabase(this).resetAllPreKeyBundleInfo()
TextSecurePreferences.removeLocalNumber(this)
TextSecurePreferences.setHasSeenWelcomeScreen(this, false)
TextSecurePreferences.setPromptedPushRegistration(this, false)
}
// endregion
override fun onQrDataFound(data: String?) {
runOnUiThread {
if (data != null && PublicKeyValidation.isValid(data.trim())) {
publicKeyEditText.setText(data.trim())
supportFragmentManager.popBackStackImmediate("QR", FragmentManager.POP_BACK_STACK_INCLUSIVE)
handleMainButtonTapped()
}
}
}
// endregion
}

View File

@ -4,6 +4,13 @@ import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
@ -237,11 +244,34 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
Bitmap recipientPhotoBitmap = BitmapUtil.createFromDrawable(drawable, largeIconTargetSize, largeIconTargetSize);
if (recipientPhotoBitmap != null) {
setLargeIcon(recipientPhotoBitmap);
setLargeIcon(getCircleBitmap(recipientPhotoBitmap));
}
}
}
private Bitmap getCircleBitmap(Bitmap bitmap) {
final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
bitmap.getHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(output);
final int color = Color.RED;
final Paint paint = new Paint();
final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
final RectF rectF = new RectF(rect);
paint.setAntiAlias(true);
canvas.drawARGB(0, 0, 0, 0);
paint.setColor(color);
canvas.drawOval(rectF, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmap, rect, rect, paint);
bitmap.recycle();
return output;
}
private boolean hasBigPictureSlide(@Nullable SlideDeck slideDeck) {
if (slideDeck == null) {
return false;

View File

@ -5,6 +5,7 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Outline;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.v7.preference.Preference;
@ -13,14 +14,20 @@ import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable;
import org.thoughtcrime.securesms.loki.MnemonicUtilities;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec;
import network.loki.messenger.R;
@ -31,6 +38,7 @@ public class ProfilePreference extends Preference {
private TextView profileNameView;
private TextView profileNumberView;
private TextView profileTagView;
private String ourDeviceWords;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public ProfilePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
@ -73,13 +81,14 @@ public class ProfilePreference extends Preference {
public void refresh() {
if (profileNumberView == null) return;
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
String primaryDevicePublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(getContext());
Context context = getContext();
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
String primaryDevicePublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
String publicKey = primaryDevicePublicKey != null ? primaryDevicePublicKey : userHexEncodedPublicKey;
final Address localAddress = Address.fromSerialized(publicKey);
final String profileName = TextSecurePreferences.getProfileName(getContext());
final Recipient recipient = Recipient.from(context, localAddress, false);
final String profileName = TextSecurePreferences.getProfileName(context);
Context context = getContext();
containerView.setOnLongClickListener(v -> {
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("Public Key", publicKey);
@ -96,28 +105,16 @@ public class ProfilePreference extends Preference {
}
});
avatarView.setClipToOutline(true);
avatarView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
int width = avatarView.getWidth();
int height = avatarView.getHeight();
if (width == 0 || height == 0) return true;
avatarView.getViewTreeObserver().removeOnPreDrawListener(this);
JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, publicKey.toLowerCase());
avatarView.setImageDrawable(identicon);
return true;
}
});
/*
Drawable fallback = recipient.getFallbackContactPhotoDrawable(context, false);
GlideApp.with(getContext().getApplicationContext())
.load(new ProfileContactPhoto(localAddress, String.valueOf(TextSecurePreferences.getProfileAvatarId(getContext()))))
.error(new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp).asDrawable(getContext(), getContext().getResources().getColor(R.color.grey_400)))
.load(recipient.getContactPhoto())
.fallback(fallback)
.error(fallback)
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(avatarView);
*/
if (!TextUtils.isEmpty(profileName)) {
profileNameView.setText(profileName);
@ -127,6 +124,12 @@ public class ProfilePreference extends Preference {
profileNumberView.setText(localAddress.toPhoneString());
profileTagView.setVisibility(primaryDevicePublicKey == null ? View.GONE : View.VISIBLE);
profileTagView.setText(R.string.activity_settings_secondary_device_tag);
if (primaryDevicePublicKey != null && ourDeviceWords == null) {
MnemonicCodec codec = new MnemonicCodec(MnemonicUtilities.getLanguageFileDirectory(context));
ourDeviceWords = MnemonicUtilities.getFirst3Words(codec, userHexEncodedPublicKey);
}
String tag = context.getResources().getString(R.string.activity_settings_linked_device_tag);
profileTagView.setText(String.format(tag, ourDeviceWords != null ? ourDeviceWords : "-"));
}
}

View File

@ -5,12 +5,15 @@ import android.graphics.Color;
import android.support.annotation.NonNull;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import org.thoughtcrime.securesms.logging.Log;
import java.util.HashMap;
public class QrCode {
public static final String TAG = QrCode.class.getSimpleName();
@ -18,10 +21,12 @@ public class QrCode {
public static @NonNull Bitmap create(String data) {
return create(data, 1024);
}
public static @NonNull Bitmap create(String data, int size) {
public static @NonNull Bitmap create(String data, int size) { return create(data, size, 2); }
public static @NonNull Bitmap create(String data, int size, int margin) {
try {
BitMatrix result = new QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, size, size);
HashMap<EncodeHintType, Integer> hintMap = new HashMap<>();
hintMap.put(EncodeHintType.MARGIN, margin);
BitMatrix result = new QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, size, size, hintMap);
Bitmap bitmap = Bitmap.createBitmap(result.getWidth(), result.getHeight(), Bitmap.Config.ARGB_8888);
for (int y = 0; y < result.getHeight(); y++) {

View File

@ -26,6 +26,7 @@ import android.text.TextUtils;
import com.annimon.stream.function.Consumer;
import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
@ -44,10 +45,13 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.JazzIdenticonContactPhoto;
import org.thoughtcrime.securesms.loki.RecipientAvatarModifiedEvent;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.recipients.RecipientProvider.RecipientDetails;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
@ -275,6 +279,11 @@ public class Recipient implements RecipientModifiedListener {
return isLocalNumber;
}
public boolean isOurMasterDevice() {
String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
return ourMasterDevice != null && ourMasterDevice.equals(getAddress().serialize());
}
public synchronized @Nullable Uri getContactUri() {
return this.contactUri;
}
@ -392,6 +401,7 @@ public class Recipient implements RecipientModifiedListener {
}
notifyListeners();
EventBus.getDefault().post(new RecipientAvatarModifiedEvent(this));
}
public synchronized boolean isProfileSharing() {
@ -455,15 +465,19 @@ public class Recipient implements RecipientModifiedListener {
}
public synchronized @NonNull FallbackContactPhoto getFallbackContactPhoto() {
if (isLocalNumber) return new ResourceContactPhoto(R.drawable.ic_note_to_self);
if (isResolving()) return new TransparentContactPhoto();
else if (isGroupRecipient()) return new ResourceContactPhoto(R.drawable.ic_group_white_24dp, R.drawable.ic_group_large);
else if (!TextUtils.isEmpty(name)) return new GeneratedContactPhoto(name, R.drawable.ic_profile_default);
else return new ResourceContactPhoto(R.drawable.ic_profile_default, R.drawable.ic_person_large);
else if (isGroupRecipient()) return new GeneratedContactPhoto(name, R.drawable.ic_profile_default);
else {
String currentUser = TextSecurePreferences.getLocalNumber(context);
String recipientAddress = address.serialize();
String primaryAddress = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
String profileAddress = (recipientAddress.equalsIgnoreCase(currentUser) && primaryAddress != null) ? primaryAddress : recipientAddress;
return new JazzIdenticonContactPhoto(profileAddress);
}
}
public synchronized @Nullable ContactPhoto getContactPhoto() {
if (isLocalNumber) return null;
if (isLocalNumber) return new ProfileContactPhoto(address, String.valueOf(TextSecurePreferences.getProfileAvatarId(context)));
else if (isGroupRecipient() && groupAvatarId != null) return new GroupRecordContactPhoto(address, groupAvatarId);
else if (systemContactPhoto != null) return new SystemContactPhoto(address, systemContactPhoto, 0);
else if (profileAvatar != null) return new ProfileContactPhoto(address, profileAvatar);

View File

@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.registration;
import android.Manifest;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
@ -8,6 +10,7 @@ import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.BaseActionBarActivity;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.loki.utilities.Analytics;
import network.loki.messenger.R;
@ -23,6 +26,24 @@ public class WelcomeActivity extends BaseActionBarActivity {
Analytics.Companion.getShared().track("Landing Screen Viewed");
}
@Override
protected void onResume() {
super.onResume();
if (TextSecurePreferences.databaseResetFromUnpair(this)) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.dialog_device_unlink_title);
builder.setMessage(R.string.dialog_device_unlink_message);
builder.setPositiveButton(R.string.ok, null);
builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
TextSecurePreferences.setDatabaseResetFromUnpair(getBaseContext(), false);
}
});
builder.show();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);

View File

@ -76,6 +76,10 @@ public class KeyCachingService extends Service {
private static MasterSecret masterSecret;
// Loki - Caching
private static MasterSecret cachedSecret;
private static long cacheTime = 0;
public KeyCachingService() {}
public static synchronized boolean isLocked(Context context) {
@ -85,7 +89,13 @@ public class KeyCachingService extends Service {
public static synchronized @Nullable MasterSecret getMasterSecret(Context context) {
if (masterSecret == null && (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context))) {
try {
return MasterSecretUtil.getMasterSecret(context, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
// Loki - Cache the secret.
// Don't know if this will affect any other signal code :( but it makes it so we're not wasting time re-fetching the same secret from the database
if (cachedSecret == null || cacheTime < System.currentTimeMillis()) {
cachedSecret = MasterSecretUtil.getMasterSecret(context, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
cacheTime = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5);
}
return cachedSecret;
} catch (InvalidPassphraseException e) {
Log.w("KeyCachingService", e);
}

View File

@ -17,6 +17,7 @@
package org.thoughtcrime.securesms.sms;
import android.content.Context;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.ApplicationContext;
@ -44,6 +45,7 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.BackgroundMessage;
import org.thoughtcrime.securesms.loki.FriendRequestHandler;
import org.thoughtcrime.securesms.loki.GeneralUtilitiesKt;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
@ -58,7 +60,11 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
@ -123,11 +129,15 @@ public class MessageSender {
// We don't call the message sender here directly and instead we just opt to create a specific job for the send
// This is because calling message sender directly would cause the application to freeze in some cases as it was blocking the thread when waiting for a response from the send
public static void sendBackgroundMessage(Context context, String contactHexEncodedPublicKey) {
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(contactHexEncodedPublicKey));
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.create(contactHexEncodedPublicKey)));
}
public static void sendBackgroundFriendRequest(Context context, String contactHexEncodedPublicKey, String messageBody) {
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(contactHexEncodedPublicKey, messageBody, true));
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createFriendRequest(contactHexEncodedPublicKey, messageBody)));
}
public static void sendUnpairRequest(Context context, String contactHexEncodedPublicKey) {
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createUnpairingRequest(contactHexEncodedPublicKey)));
}
// endregion

View File

@ -11,7 +11,7 @@ import java.io.File;
public class FileProviderUtil {
private static final String AUTHORITY = "org.thoughtcrime.securesms.fileprovider";
private static final String AUTHORITY = "network.loki.securesms.fileprovider";
public static Uri getUriFor(@NonNull Context context, @NonNull File file) {
if (Build.VERSION.SDK_INT >= 24) return FileProvider.getUriForFile(context, AUTHORITY, file);

View File

@ -121,6 +121,7 @@ public class TextSecurePreferences {
private static final String PROFILE_KEY_PREF = "pref_profile_key";
private static final String PROFILE_NAME_PREF = "pref_profile_name";
private static final String PROFILE_AVATAR_ID_PREF = "pref_profile_avatar_id";
private static final String PROFILE_AVATAR_URL_PREF = "pref_profile_avatar_url";
public static final String READ_RECEIPTS_PREF = "pref_read_receipts";
public static final String INCOGNITO_KEYBORAD_PREF = "pref_incognito_keyboard";
private static final String UNAUTHORIZED_RECEIVED = "pref_unauthorized_received";
@ -401,6 +402,14 @@ public class TextSecurePreferences {
return getIntegerPreference(context, PROFILE_AVATAR_ID_PREF, 0);
}
public static void setProfileAvatarUrl(Context context, String url) {
setStringPreference(context, PROFILE_AVATAR_URL_PREF, url);
}
public static String getProfileAvatarUrl(Context context) {
return getStringPreference(context, PROFILE_AVATAR_URL_PREF, null);
}
public static int getNotificationPriority(Context context) {
return Integer.valueOf(getStringPreference(context, NOTIFICATION_PRIORITY_PREF, String.valueOf(NotificationCompat.PRIORITY_HIGH)));
}
@ -1185,5 +1194,35 @@ public class TextSecurePreferences {
public static void setMasterHexEncodedPublicKey(Context context, String masterHexEncodedPublicKey) {
setStringPreference(context, "master_hex_encoded_public_key", masterHexEncodedPublicKey.toLowerCase());
}
public static void setResetDatabase(Context context, boolean resetDatabase) {
// We do it this way so that it gets persisted in storage straight away
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("database_reset", resetDatabase).commit();
}
public static boolean resetDatabase(Context context) {
return getBooleanPreference(context, "database_reset", false);
}
public static void setDatabaseResetFromUnpair(Context context, boolean value) {
// We do it this way so that it gets persisted in storage straight away
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("database_reset_unpair", value).commit();
}
public static boolean databaseResetFromUnpair(Context context) {
return getBooleanPreference(context, "database_reset_unpair", false);
}
public static void setNeedsRevocationCheck(Context context, boolean needsCheck) {
setBooleanPreference(context, "needs_revocation", needsCheck);
}
public static boolean needsRevocationCheck(Context context) {
return getBooleanPreference(context, "needs_revocation", false);
}
// endregion
public static void clearAll(Context context) {
PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit();
}
}