Merge pull request 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
AndroidManifest.xml
res
src/org/thoughtcrime/securesms

File diff suppressed because it is too large Load Diff

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

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

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

@ -1,58 +1,49 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout
android:layout_width="match_parent" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent" android:layout_width="match_parent"
xmlns:tools="http://schemas.android.com/tools" android:layout_height="match_parent"
xmlns:fab="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"> xmlns:fab="http://schemas.android.com/apk/res-auto">
<LinearLayout android:id="@+id/progress_container" <ProgressBar
android:layout_width="fill_parent" android:id="@+id/activityIndicator"
android:layout_height="fill_parent" android:indeterminate="true"
android:gravity="center" android:layout_width="wrap_content"
android:orientation="vertical" android:layout_height="wrap_content"
android:visibility="gone" > android:layout_centerInParent="true"
android:visibility="gone" />
<ProgressBar android:id="@+id/progress" <TextView
android:indeterminate="true" android:id="@+id/emptyStateTextView"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" > android:layout_height="match_parent"
</ProgressBar> android:gravity="center"
</LinearLayout> 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" <ListView
android:layout_width="match_parent" android:id="@id/android:list"
android:layout_height="match_parent" android:layout_width="match_parent"
android:layout_gravity="center|center_vertical" android:layout_height="match_parent"
android:gravity="center|center_vertical" android:drawSelectorOnTop="false" />
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"/>
<com.melnykov.fab.FloatingActionButton <com.melnykov.fab.FloatingActionButton
android:id="@+id/add_device" android:id="@+id/addDeviceButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|right" android:layout_alignParentBottom="true"
android:layout_margin="16dp" android:layout_alignParentRight="true"
android:src="@drawable/ic_add_white_original_24dp" android:layout_margin="16dp"
android:focusable="true" android:src="@drawable/ic_add_white_original_24dp"
android:contentDescription="@string/device_list_fragment__link_new_device" android:focusable="true"
fab:fab_colorNormal="?fab_color" android:contentDescription="@string/device_list_fragment__link_new_device"
fab:fab_colorPressed="@color/textsecure_primary_dark" fab:fab_colorNormal="?fab_color"
fab:fab_colorRipple="@color/textsecure_primary_dark" /> fab:fab_colorPressed="@color/textsecure_primary_dark"
fab:fab_colorRipple="@color/textsecure_primary_dark" />
</LinearLayout> </RelativeLayout>

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

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

@ -24,13 +24,14 @@
android:layout_height="match_parent"/> android:layout_height="match_parent"/>
<TextView <TextView
android:id="@+id/descriptionTextView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1" android:layout_weight="1"
android:padding="32dp" android:padding="32dp"
android:gravity="center" android:gravity="center"
android:background="@color/loki_darkest_gray" 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" /> android:textColor="?android:textColorPrimary" />
</LinearLayout> </LinearLayout>

@ -43,7 +43,6 @@
android:id="@+id/avatar_background" android:id="@+id/avatar_background"
android:layout_width="80dp" android:layout_width="80dp"
android:layout_height="80dp" android:layout_height="80dp"
android:visibility="gone"
android:layout_marginStart="32dp" android:layout_marginStart="32dp"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:src="@drawable/circle_tintable" android:src="@drawable/circle_tintable"
@ -56,7 +55,6 @@
android:id="@+id/avatar_placeholder" android:id="@+id/avatar_placeholder"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:visibility="gone"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:src="@drawable/ic_profile_default" android:src="@drawable/ic_profile_default"
@ -71,7 +69,6 @@
android:id="@+id/avatar" android:id="@+id/avatar"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/avatar_background" app:layout_constraintBottom_toBottomOf="@+id/avatar_background"
app:layout_constraintEnd_toEndOf="@+id/avatar_background" app:layout_constraintEnd_toEndOf="@+id/avatar_background"
app:layout_constraintStart_toStartOf="@+id/avatar_background" app:layout_constraintStart_toStartOf="@+id/avatar_background"
@ -81,7 +78,6 @@
android:id="@+id/camera_icon" android:id="@+id/camera_icon"
android:layout_width="60dp" android:layout_width="60dp"
android:layout_height="60dp" android:layout_height="60dp"
android:visibility="gone"
android:layout_marginStart="35dp" android:layout_marginStart="35dp"
android:layout_marginTop="35dp" android:layout_marginTop="35dp"
android:cropToPadding="false" android:cropToPadding="false"
@ -94,7 +90,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginStart="49dp" android:layout_marginStart="16dp"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:layout_weight="1" android:layout_weight="1"
@ -105,7 +101,7 @@
app:layout_constraintBottom_toTopOf="@+id/description_text" app:layout_constraintBottom_toTopOf="@+id/description_text"
app:layout_constraintEnd_toStartOf="@+id/emoji_toggle" app:layout_constraintEnd_toStartOf="@+id/emoji_toggle"
app:layout_constraintHorizontal_bias="0.5" 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" /> app:layout_constraintTop_toBottomOf="@+id/title" />
<org.thoughtcrime.securesms.components.emoji.EmojiToggle <org.thoughtcrime.securesms.components.emoji.EmojiToggle

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

@ -115,4 +115,10 @@
<dimen name="recording_voice_lock_target">-150dp</dimen> <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> </resources>

@ -293,12 +293,14 @@
<!-- DeviceListActivity --> <!-- DeviceListActivity -->
<string name="DeviceListActivity_unlink_s">Unlink \'%s\'?</string> <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_network_connection_failed">Network connection failed</string>
<string name="DeviceListActivity_try_again">Try again</string> <string name="DeviceListActivity_try_again">Try again</string>
<string name="DeviceListActivity_unlinking_device">Unlinking device...</string> <string name="DeviceListActivity_unlinking_device">Unlinking device...</string>
<string name="DeviceListActivity_unlinking_device_no_ellipsis">Unlinking device</string> <string name="DeviceListActivity_unlinking_device_no_ellipsis">Unlinking device</string>
<string name="DeviceListActivity_network_failed">Network failed!</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 --> <!-- DeviceListItem -->
<string name="DeviceListItem_unnamed_device">Unnamed device</string> <string name="DeviceListItem_unnamed_device">Unnamed device</string>
@ -946,7 +948,7 @@
<string name="device_link_fragment__link_device">Link device</string> <string name="device_link_fragment__link_device">Link device</string>
<!-- device_list_fragment --> <!-- 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> <string name="device_list_fragment__link_new_device">Link new device</string>
<!-- experience_upgrade_activity --> <!-- experience_upgrade_activity -->
@ -1157,7 +1159,7 @@
<string name="AndroidManifest__log_submit">Submit debug log</string> <string name="AndroidManifest__log_submit">Submit debug log</string>
<string name="AndroidManifest__media_preview">Media preview</string> <string name="AndroidManifest__media_preview">Media preview</string>
<string name="AndroidManifest__message_details">Message details</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__invite_friends">Invite friends</string>
<string name="AndroidManifest_archived_conversations">Archived conversations</string> <string name="AndroidManifest_archived_conversations">Archived conversations</string>
<string name="AndroidManifest_remove_photo">Remove photo</string> <string name="AndroidManifest_remove_photo">Remove photo</string>
@ -1572,11 +1574,11 @@
<!-- Conversation list activity --> <!-- Conversation list activity -->
<string name="activity_conversation_list_empty_state_message">Looks like you don\'t have any conversations yet. Get started by messaging a friend.</string> <string name="activity_conversation_list_empty_state_message">Looks like you don\'t have any conversations yet. Get started by messaging a friend.</string>
<!-- Settings activity --> <!-- Settings activity -->
<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_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_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_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_show_seed_button_title">Show Seed</string>
<string name="activity_settings_seed_dialog_title">Your Seed</string> <string name="activity_settings_seed_dialog_title">Your Seed</string>
<string name="activity_settings_seed_dialog_copy_button_title">Copy</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> <string name="view_device_linking_cancel_button_title">Cancel</string>
<!-- Scan QR code fragment --> <!-- Scan QR code fragment -->
<string name="fragment_scan_qr_code_title">Scan QR Code</string> <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> <string name="fragment_scan_qr_code_camera_permission_dialog_message">Loki Messenger needs camera access to scan QR codes.</string>
<!-- Conversation activity --> <!-- Conversation activity -->
<string name="activity_conversation_copy_public_key_button_title">Copy public key</string> <string name="activity_conversation_copy_public_key_button_title">Copy public key</string>
<!-- Conversation list activity --> <!-- Conversation list activity -->
<string name="activity_conversation_list_add_public_chat_button_title">Add Public Chat</string> <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> </resources>

@ -242,4 +242,15 @@
<item name="colorControlActivated">@color/white</item> <item name="colorControlActivated">@color/white</item>
</style> </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> </resources>

@ -133,6 +133,7 @@
<item name="android:windowBackground">@color/loki_darkest_gray</item> <item name="android:windowBackground">@color/loki_darkest_gray</item>
<item name="alertDialogTheme">@style/AppCompatAlertDialogStyleLight</item> <item name="alertDialogTheme">@style/AppCompatAlertDialogStyleLight</item>
<item name="android:alertDialogTheme">@style/AppCompatDialogStyleLight</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="android:windowContentOverlay">@drawable/compat_actionbar_shadow_background</item>-->
<item name="attachment_type_selector_background">@color/white</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="android:windowBackground">@color/loki_darkest_gray</item>
<item name="alertDialogTheme">@style/AppCompatAlertDialogStyleDark</item> <item name="alertDialogTheme">@style/AppCompatAlertDialogStyleDark</item>
<item name="android:alertDialogTheme">@style/AppCompatDialogStyleDark</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_type_selector_background">@color/gray95</item>
<item name="attachment_document_icon_small">@drawable/ic_document_small_dark</item> <item name="attachment_document_icon_small">@drawable/ic_document_small_dark</item>

@ -33,6 +33,10 @@
android:title="@string/preferences__advanced" android:title="@string/preferences__advanced"
android:icon="@drawable/ic_advanced_24dp"/> --> 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" <Preference android:key="preference_category_public_key"
android:title="@string/activity_settings_share_public_key_button_title" android:title="@string/activity_settings_share_public_key_button_title"
android:icon="@drawable/icon_share"/> android:icon="@drawable/icon_share"/>
@ -41,10 +45,6 @@
android:title="@string/activity_settings_show_qr_code_button_title" android:title="@string/activity_settings_show_qr_code_button_title"
android:icon="@drawable/icon_qr_code"/> 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" <Preference android:key="preference_category_seed"
android:title="@string/activity_settings_show_seed_button_title" android:title="@string/activity_settings_show_seed_button_title"
android:icon="@drawable/icon_seedling"/> android:icon="@drawable/icon_seedling"/>

@ -20,10 +20,13 @@ import android.annotation.SuppressLint;
import android.arch.lifecycle.DefaultLifecycleObserver; import android.arch.lifecycle.DefaultLifecycleObserver;
import android.arch.lifecycle.LifecycleOwner; import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.ProcessLifecycleOwner; import android.arch.lifecycle.ProcessLifecycleOwner;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver; import android.database.ContentObserver;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Handler;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.multidex.MultiDexApplication; 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.TypingStatusRepository;
import org.thoughtcrime.securesms.components.TypingStatusSender; import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; 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.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule; import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule;
@ -65,10 +71,12 @@ import org.thoughtcrime.securesms.loki.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.LokiPublicChatManager; import org.thoughtcrime.securesms.loki.LokiPublicChatManager;
import org.thoughtcrime.securesms.loki.LokiRSSFeedPoller; import org.thoughtcrime.securesms.loki.LokiRSSFeedPoller;
import org.thoughtcrime.securesms.loki.LokiUserDatabase; import org.thoughtcrime.securesms.loki.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener; import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.IncomingMessageObserver; 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.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol; import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol;
import org.whispersystems.signalservice.loki.api.LokiDotNetAPI; 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.LokiLongPoller;
import org.whispersystems.signalservice.loki.api.LokiP2PAPI; import org.whispersystems.signalservice.loki.api.LokiP2PAPI;
import org.whispersystems.signalservice.loki.api.LokiP2PAPIDelegate; import org.whispersystems.signalservice.loki.api.LokiP2PAPIDelegate;
import org.whispersystems.signalservice.loki.api.LokiPublicChat;
import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI;
import org.whispersystems.signalservice.loki.api.LokiRSSFeed; import org.whispersystems.signalservice.loki.api.LokiRSSFeed;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI; import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.utilities.Analytics; import org.whispersystems.signalservice.loki.utilities.Analytics;
@ -154,8 +162,9 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
startKovenant();
Log.i(TAG, "onCreate()"); Log.i(TAG, "onCreate()");
checkNeedsDatabaseReset();
startKovenant();
initializeSecurityProvider(); initializeSecurityProvider();
initializeLogging(); initializeLogging();
initializeCrashHandling(); initializeCrashHandling();
@ -196,7 +205,11 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
// Loki - Update device mappings // Loki - Update device mappings
if (setUpStorageAPIIfNeeded()) { if (setUpStorageAPIIfNeeded()) {
LokiStorageAPI.Companion.getShared().updateUserDeviceMappings(); LokiStorageAPI.Companion.getShared().updateUserDeviceMappings();
if (TextSecurePreferences.needsRevocationCheck(this)) {
checkNeedsRevocation();
}
} }
updatePublicChatProfileAvatarIfNeeded();
} }
@Override @Override
@ -587,5 +600,54 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
if (lokiNewsFeedPoller != null) lokiNewsFeedPoller.startIfNeeded(); if (lokiNewsFeedPoller != null) lokiNewsFeedPoller.startIfNeeded();
if (lokiMessengerUpdatesFeedPoller != null) lokiMessengerUpdatesFeedPoller.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 // 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);
}
} }

@ -26,7 +26,6 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Build.VERSION; import android.os.Build.VERSION;
import android.os.Bundle; import android.os.Bundle;
@ -40,13 +39,8 @@ import android.support.v7.app.AlertDialog;
import android.support.v7.preference.Preference; import android.support.v7.preference.Preference;
import android.widget.Toast; import android.widget.Toast;
import org.jetbrains.annotations.NotNull;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.loki.LinkedDevicesActivity;
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.QRCodeDialog; import org.thoughtcrime.securesms.loki.QRCodeDialog;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment; 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.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences; 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.crypto.MnemonicCodec;
import org.whispersystems.signalservice.loki.utilities.Analytics; import org.whispersystems.signalservice.loki.utilities.Analytics;
import org.whispersystems.signalservice.loki.utilities.SerializationKt; 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_ADVANCED = "preference_category_advanced";
private static final String PREFERENCE_CATEGORY_PUBLIC_KEY = "preference_category_public_key"; 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_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 static final String PREFERENCE_CATEGORY_SEED = "preference_category_seed";
private final DynamicTheme dynamicTheme = new DynamicTheme(); private final DynamicTheme dynamicTheme = new DynamicTheme();
@ -172,15 +165,15 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS)); .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS));
*/ */
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS) this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_NOTIFICATIONS)); .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_NOTIFICATIONS));
this.findPreference(PREFERENCE_CATEGORY_APP_PROTECTION) 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) this.findPreference(PREFERENCE_CATEGORY_APPEARANCE)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE)); .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE));
*/ */
this.findPreference(PREFERENCE_CATEGORY_CHATS) this.findPreference(PREFERENCE_CATEGORY_CHATS)
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_CHATS)); .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CHATS));
/* /*
this.findPreference(PREFERENCE_CATEGORY_DEVICES) this.findPreference(PREFERENCE_CATEGORY_DEVICES)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DEVICES)); .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DEVICES));
@ -188,29 +181,18 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED)); .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
*/ */
this.findPreference(PREFERENCE_CATEGORY_PUBLIC_KEY) 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) 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); Preference linkDevicesPreference = this.findPreference(PREFERENCE_CATEGORY_LINKED_DEVICES);
linkDevicePreference.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_LINK_DEVICE)); linkDevicesPreference.setVisible(isMasterDevice);
linkDevicesPreference.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_LINKED_DEVICES));
// 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 seedPreference = this.findPreference(PREFERENCE_CATEGORY_SEED); Preference seedPreference = this.findPreference(PREFERENCE_CATEGORY_SEED);
// Hide if this is a slave device // Hide if this is a slave device
seedPreference.setVisible(isMasterDevice); 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) { if (VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
tintIcons(getActivity()); tintIcons(getActivity());
@ -299,16 +281,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
// this.findPreference(PREFERENCE_CATEGORY_ADVANCED).setIcon(advanced); // this.findPreference(PREFERENCE_CATEGORY_ADVANCED).setIcon(advanced);
this.findPreference(PREFERENCE_CATEGORY_PUBLIC_KEY).setIcon(publicKey); this.findPreference(PREFERENCE_CATEGORY_PUBLIC_KEY).setIcon(publicKey);
this.findPreference(PREFERENCE_CATEGORY_QR_CODE).setIcon(qrCode); 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); this.findPreference(PREFERENCE_CATEGORY_SEED).setIcon(seed);
} }
private class CategoryClickListener implements Preference.OnPreferenceClickListener, DeviceLinkingDialogDelegate { private class CategoryClickListener implements Preference.OnPreferenceClickListener {
private String category; private String category;
private Context context;
CategoryClickListener(Context context,String category) { CategoryClickListener(String category) {
this.context = context;
this.category = category; this.category = category;
} }
@ -360,8 +340,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
case PREFERENCE_CATEGORY_QR_CODE: case PREFERENCE_CATEGORY_QR_CODE:
QRCodeDialog.INSTANCE.show(getContext()); QRCodeDialog.INSTANCE.show(getContext());
break; break;
case PREFERENCE_CATEGORY_LINK_DEVICE: case PREFERENCE_CATEGORY_LINKED_DEVICES:
DeviceLinkingDialog.Companion.show(getContext(), DeviceLinkingView.Mode.Master, this); Intent intent = new Intent(getActivity(), LinkedDevicesActivity.class);
startActivity(intent);
break; break;
case PREFERENCE_CATEGORY_SEED: case PREFERENCE_CATEGORY_SEED:
Analytics.Companion.getShared().track("Seed Modal Shown"); Analytics.Companion.getShared().track("Seed Modal Shown");
@ -404,12 +385,6 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
return true; 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 { private class ProfileClickListener implements Preference.OnPreferenceClickListener {

@ -22,6 +22,7 @@ import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Outline; import android.graphics.Outline;
import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
@ -38,8 +39,14 @@ import android.view.ViewTreeObserver;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.Toast; 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.RatingManager;
import org.thoughtcrime.securesms.components.SearchToolbar; import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.conversation.ConversationActivity; import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.Address; 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.lock.RegistrationLockDialog;
import org.thoughtcrime.securesms.loki.AddPublicChatActivity; import org.thoughtcrime.securesms.loki.AddPublicChatActivity;
import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable; 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.MarkReadReceiver;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
@ -129,6 +138,18 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
super.onDestroy(); super.onDestroy();
} }
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
public void onStop() {
EventBus.getDefault().unregister(this);
super.onStop();
}
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
MenuInflater inflater = this.getMenuInflater(); MenuInflater inflater = this.getMenuInflater();
@ -197,45 +218,23 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
outline.setOval(0, 0, view.getWidth(), view.getHeight()); outline.setOval(0, 0, view.getWidth(), view.getHeight());
} }
}); });
profilePictureImageView.setClipToOutline(true);
// Display the correct identicon if we're a secondary device // 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 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); Drawable fallback = primaryRecipient.getFallbackContactPhotoDrawable(this, false);
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));
GlideApp.with(this) GlideApp.with(this)
.load(new ProfileContactPhoto(recipient.getAddress(), String.valueOf(TextSecurePreferences.getProfileAvatarId(this)))) .load(primaryRecipient.getContactPhoto())
.fallback(fallback)
.error(fallback) .error(fallback)
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL) .diskCacheStrategy(DiskCacheStrategy.ALL)
.into(icon); .circleCrop()
*/ .into(profilePictureImageView);
profilePictureImageView.setOnClickListener(v -> handleDisplaySettings()); profilePictureImageView.setOnClickListener(v -> handleDisplaySettings());
} }
@ -336,4 +335,12 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
private void addNewPublicChat() { private void addNewPublicChat() {
startActivity(new Intent(this, AddPublicChatActivity.class)); 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);
}
}
} }

@ -39,7 +39,6 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions; 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.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.util.StreamDetails; 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.LokiPublicChatAPI;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.utilities.Analytics; import org.whispersystems.signalservice.loki.utilities.Analytics;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -96,6 +98,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
private View reveal; private View reveal;
private Intent nextIntent; private Intent nextIntent;
private byte[] originalAvatarBytes;
private byte[] avatarBytes; private byte[] avatarBytes;
private File captureFile; private File captureFile;
@ -301,6 +304,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
@Override @Override
protected void onPostExecute(byte[] result) { protected void onPostExecute(byte[] result) {
if (result != null) { if (result != null) {
originalAvatarBytes = result;
avatarBytes = result; avatarBytes = result;
GlideApp.with(CreateProfileActivity.this) GlideApp.with(CreateProfileActivity.this)
.load(result) .load(result)
@ -314,6 +318,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
@Override @Override
public void onSuccess(byte[] result) { public void onSuccess(byte[] result) {
if (result != null) { if (result != null) {
originalAvatarBytes = result;
avatarBytes = result; avatarBytes = result;
GlideApp.with(CreateProfileActivity.this) GlideApp.with(CreateProfileActivity.this)
.load(result) .load(result)
@ -380,7 +385,6 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
@Override @Override
protected Boolean doInBackground(Void... params) { protected Boolean doInBackground(Void... params) {
Context context = CreateProfileActivity.this; Context context = CreateProfileActivity.this;
byte[] profileKey = ProfileKeyUtil.getProfileKey(CreateProfileActivity.this);
Analytics.Companion.getShared().track("Display Name Updated"); Analytics.Companion.getShared().track("Display Name Updated");
@ -393,31 +397,44 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
} }
} }
// Loki - Original code // Loki - Only update avatar if there was a change
// ======== if (!Arrays.equals(originalAvatarBytes, avatarBytes)) {
// try { try {
// accountManager.setProfileName(profileKey, name); // Loki - Original profile photo code
// TextSecurePreferences.setProfileName(context, name); // ========
// } catch (IOException e) { // accountManager.setProfileAvatar(profileKey, avatar);
// Log.w(TAG, e); // ========
// return false;
// }
// ========
try { // Try upload photo with a new profile key
// Loki - Original code String newProfileKey = ProfileKeyUtil.generateEncodedProfileKey(context);
// ======== byte[] profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(newProfileKey);
// accountManager.setProfileAvatar(profileKey, avatar);
// ======== //Loki - Upload the profile photo here
AvatarHelper.setAvatar(CreateProfileActivity.this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), avatarBytes); if (avatar != null) {
TextSecurePreferences.setProfileAvatarId(CreateProfileActivity.this, new SecureRandom().nextInt()); Log.d("Loki", "Start uploading profile photo");
} catch (IOException e) { LokiStorageAPI storageAPI = LokiStorageAPI.shared;
Log.w(TAG, e); LokiDotNetAPI.UploadResult result = storageAPI.uploadProfilePicture(storageAPI.getServer(), profileKey, avatar);
return false; 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; return true;
} }

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.app.ListFragment; import android.support.v4.app.ListFragment;
@ -16,29 +15,32 @@ import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListView; import android.widget.ListView;
import android.widget.Toast;
import com.melnykov.fab.FloatingActionButton; import com.melnykov.fab.FloatingActionButton;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader; import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.devicelist.Device; import org.thoughtcrime.securesms.devicelist.Device;
import org.thoughtcrime.securesms.jobs.RefreshUnidentifiedDeliveryAbilityJob; import org.thoughtcrime.securesms.loki.DeviceListBottomSheetFragment;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.loki.MnemonicUtilities;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.whispersystems.libsignal.util.guava.Function;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import java.io.IOException; import java.io.File;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.inject.Inject; import kotlin.Pair;
import kotlin.Unit;
import network.loki.messenger.R; import network.loki.messenger.R;
import static org.thoughtcrime.securesms.loki.GeneralUtilitiesKt.toPx;
public class DeviceListFragment extends ListFragment public class DeviceListFragment extends ListFragment
implements LoaderManager.LoaderCallbacks<List<Device>>, implements LoaderManager.LoaderCallbacks<List<Device>>,
ListView.OnItemClickListener, InjectableType, Button.OnClickListener ListView.OnItemClickListener, InjectableType, Button.OnClickListener
@ -46,14 +48,14 @@ public class DeviceListFragment extends ListFragment
private static final String TAG = DeviceListFragment.class.getSimpleName(); private static final String TAG = DeviceListFragment.class.getSimpleName();
@Inject private File languageFileDirectory;
SignalServiceAccountManager accountManager;
private Locale locale; private Locale locale;
private View empty; private View empty;
private View progressContainer; private View progressContainer;
private FloatingActionButton addDeviceButton; private FloatingActionButton addDeviceButton;
private Button.OnClickListener addDeviceButtonListener; private Button.OnClickListener addDeviceButtonListener;
private Function<String, Void> handleDisconnectDevice;
private Function<Pair<String, String>, Void> handleDeviceNameChange;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@ -71,10 +73,11 @@ public class DeviceListFragment extends ListFragment
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
View view = inflater.inflate(R.layout.device_list_fragment, container, false); View view = inflater.inflate(R.layout.device_list_fragment, container, false);
this.empty = view.findViewById(R.id.empty); this.empty = view.findViewById(R.id.emptyStateTextView);
this.progressContainer = view.findViewById(R.id.progress_container); this.progressContainer = view.findViewById(R.id.activityIndicator);
this.addDeviceButton = ViewUtil.findById(view, R.id.add_device); this.addDeviceButton = ViewUtil.findById(view, R.id.addDeviceButton);
this.addDeviceButton.setOnClickListener(this); this.addDeviceButton.setOnClickListener(this);
updateAddDeviceButtonVisibility();
return view; return view;
} }
@ -82,6 +85,7 @@ public class DeviceListFragment extends ListFragment
@Override @Override
public void onActivityCreated(Bundle bundle) { public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle); super.onActivityCreated(bundle);
this.languageFileDirectory = MnemonicUtilities.getLanguageFileDirectory(getContext());
getLoaderManager().initLoader(0, null, this); getLoaderManager().initLoader(0, null, this);
getListView().setOnItemClickListener(this); getListView().setOnItemClickListener(this);
} }
@ -90,12 +94,20 @@ public class DeviceListFragment extends ListFragment
this.addDeviceButtonListener = listener; 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 @Override
public @NonNull Loader<List<Device>> onCreateLoader(int id, Bundle args) { public @NonNull Loader<List<Device>> onCreateLoader(int id, Bundle args) {
empty.setVisibility(View.GONE); empty.setVisibility(View.GONE);
progressContainer.setVisibility(View.VISIBLE); progressContainer.setVisibility(View.VISIBLE);
return new DeviceListLoader(getActivity(), accountManager); return new DeviceListLoader(getActivity(), languageFileDirectory);
} }
@Override @Override
@ -124,20 +136,63 @@ public class DeviceListFragment extends ListFragment
@Override @Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 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 String deviceName = ((DeviceListItem)view).getDeviceName();
final long deviceId = ((DeviceListItem)view).getDeviceId(); final String deviceId = ((DeviceListItem)view).getDeviceId();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); DeviceListBottomSheetFragment bottomSheet = new DeviceListBottomSheetFragment();
builder.setTitle(getActivity().getString(R.string.DeviceListActivity_unlink_s, deviceName)); bottomSheet.setOnEditTapped(() -> {
builder.setMessage(R.string.DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive); bottomSheet.dismiss();
builder.setNegativeButton(android.R.string.cancel, null); EditText deviceNameEditText = new EditText(getContext());
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { LinearLayout deviceNameEditTextContainer = new LinearLayout(getContext());
@Override LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
public void onClick(DialogInterface dialog, int which) { layoutParams.setMarginStart(toPx(18, getResources()));
handleDisconnectDevice(deviceId); 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() { private void handleLoaderFailed() {
@ -167,34 +222,6 @@ public class DeviceListFragment extends ListFragment
builder.show(); 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 @Override
public void onClick(View v) { public void onClick(View v) {
if (addDeviceButtonListener != null) addDeviceButtonListener.onClick(v); if (addDeviceButtonListener != null) addDeviceButtonListener.onClick(v);

@ -15,10 +15,9 @@ import network.loki.messenger.R;
public class DeviceListItem extends LinearLayout { public class DeviceListItem extends LinearLayout {
private long deviceId; private String deviceId;
private TextView name; private TextView name;
private TextView created; private TextView shortId;
private TextView lastActive;
public DeviceListItem(Context context) { public DeviceListItem(Context context) {
super(context); super(context);
@ -31,29 +30,19 @@ public class DeviceListItem extends LinearLayout {
@Override @Override
public void onFinishInflate() { public void onFinishInflate() {
super.onFinishInflate(); super.onFinishInflate();
this.name = (TextView) findViewById(R.id.name); this.name = (TextView) findViewById(R.id.name);
this.created = (TextView) findViewById(R.id.created); this.shortId = (TextView) findViewById(R.id.shortId);
this.lastActive = (TextView) findViewById(R.id.active);
} }
public void set(Device deviceInfo, Locale locale) { 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(); 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; return deviceId;
} }
@ -61,4 +50,8 @@ public class DeviceListItem extends LinearLayout {
return name.getText().toString(); return name.getText().toString();
} }
public boolean hasDeviceName() {
return shortId.getVisibility() == VISIBLE;
}
} }

@ -5,6 +5,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.IdRes; import android.support.annotation.IdRes;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;

@ -5,6 +5,7 @@ import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
@ -43,7 +44,7 @@ public final class AvatarSelection {
CropImage.activity(inputFile) CropImage.activity(inputFile)
.setGuidelines(CropImageView.Guidelines.ON) .setGuidelines(CropImageView.Guidelines.ON)
.setAspectRatio(1, 1) .setAspectRatio(1, 1)
.setCropShape(CropImageView.CropShape.OVAL) .setCropShape(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? CropImageView.CropShape.RECTANGLE : CropImageView.CropShape.OVAL)
.setOutputUri(outputFile) .setOutputUri(outputFile)
.setAllowRotation(true) .setAllowRotation(true)
.setAllowFlipping(true) .setAllowFlipping(true)

@ -11,22 +11,23 @@ import android.provider.ContactsContract;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.widget.AppCompatImageView; import android.support.v7.widget.AppCompatImageView;
import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import android.view.ViewOutlineProvider; 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.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.database.Address;
import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientExporter; import org.thoughtcrime.securesms.recipients.RecipientExporter;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ThemeUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Objects;
import network.loki.messenger.R; import network.loki.messenger.R;
@ -52,7 +53,9 @@ public class AvatarImageView extends AppCompatImageView {
private boolean inverted; private boolean inverted;
private Paint outlinePaint; private Paint outlinePaint;
private OnClickListener listener; private OnClickListener listener;
private Recipient recipient;
private @Nullable RecipientContactPhoto recipientContactPhoto;
private @NonNull Drawable unknownRecipientDrawable;
public AvatarImageView(Context context) { public AvatarImageView(Context context) {
super(context); super(context);
@ -75,23 +78,27 @@ public class AvatarImageView extends AppCompatImageView {
outlinePaint = ThemeUtil.isDarkTheme(getContext()) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT; outlinePaint = ThemeUtil.isDarkTheme(getContext()) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT;
setOutlineProvider(new ViewOutlineProvider() { setOutlineProvider(new ViewOutlineProvider() {
@Override @Override
public void getOutline(View view, Outline outline) { public void getOutline(View view, Outline outline) {
outline.setOval(0, 0, view.getWidth(), view.getHeight()); outline.setOval(0, 0, view.getWidth(), view.getHeight());
} }
}); });
setClipToOutline(true); setClipToOutline(true);
unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_default).asDrawable(getContext(), ContactColors.UNKNOWN_COLOR.toConversationColor(getContext()), inverted);
} }
@Override @Override
protected void dispatchDraw(Canvas canvas) { protected void onDraw(Canvas canvas) {
super.dispatchDraw(canvas); super.onDraw(canvas);
float cx = canvas.getWidth() / 2; float width = getWidth() - getPaddingRight() - getPaddingLeft();
float cy = canvas.getHeight() / 2; float height = getHeight() - getPaddingBottom() - getPaddingTop();
float radius = (canvas.getWidth() / 2) - (outlinePaint.getStrokeWidth() / 2); 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); canvas.drawCircle(cx, cy, radius, outlinePaint);
} }
@ -101,39 +108,46 @@ public class AvatarImageView extends AppCompatImageView {
super.setOnClickListener(listener); 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) { public void update(String hexEncodedPublicKey) {
Address address = Address.fromSerialized(hexEncodedPublicKey); Address address = Address.fromSerialized(hexEncodedPublicKey);
if (recipient == null || !address.equals(recipient.getAddress())) { Recipient recipient = Recipient.from(getContext(), address, false);
this.recipient = Recipient.from(getContext(), address, false); updateAvatar(recipient);
updateImage(); }
}
private void updateAvatar(Recipient recipient) {
setAvatar(GlideApp.with(getContext()), recipient, false);
} }
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) { 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) { if (recipient != null) {
requestManager.load(recipient.getContactPhoto()) if (recipient.isLocalNumber()) {
.fallback(recipient.getFallbackContactPhotoDrawable(getContext(), inverted)) setImageDrawable(new ResourceContactPhoto(R.drawable.ic_note_to_self).asDrawable(getContext(), recipient.getColor().toAvatarColor(getContext()), inverted));
.error(recipient.getFallbackContactPhotoDrawable(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) .diskCacheStrategy(DiskCacheStrategy.ALL)
.circleCrop() .circleCrop()
.into(this); .into(this);
setAvatarClickHandler(recipient, quickContactEnabled); } else {
setImageDrawable(fallbackContactPhotoDrawable);
}
}
}
} else { } 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); super.setOnClickListener(listener);
} }
*/
} }
public void clear(@NonNull GlideRequests glideRequests) { 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) { private final @NonNull Recipient recipient;
if (w == 0 || h == 0 || recipient == null) { return; } private final @Nullable ContactPhoto contactPhoto;
private final boolean ready;
Drawable image; RecipientContactPhoto(@NonNull Recipient recipient) {
Context context = this.getContext(); this.recipient = recipient;
this.ready = !recipient.isResolving();
if (recipient.isGroupRecipient()) { this.contactPhoto = recipient.getContactPhoto();
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());
} }
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);
}
}
} }

@ -3052,12 +3052,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
long originalThreadID = lokiMessageDatabase.getOriginalThreadID(friendRequest.id); long originalThreadID = lokiMessageDatabase.getOriginalThreadID(friendRequest.id);
long threadId = originalThreadID < 0 ? this.threadId : originalThreadID; long threadId = originalThreadID < 0 ? this.threadId : originalThreadID;
Address contact = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadId).getAddress(); Recipient contact = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadId);
String contactPubKey = contact.toString(); Address address = contact.getAddress();
String contactPubKey = address.serialize();
DatabaseFactory.getLokiThreadDatabase(this).setFriendRequestStatus(threadId, LokiThreadFriendRequestStatus.FRIENDS); DatabaseFactory.getLokiThreadDatabase(this).setFriendRequestStatus(threadId, LokiThreadFriendRequestStatus.FRIENDS);
lokiMessageDatabase.setFriendRequestStatus(friendRequest.id, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED); lokiMessageDatabase.setFriendRequestStatus(friendRequest.id, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
DatabaseFactory.getRecipientDatabase(this).setProfileSharing(contact, true);
MessageSender.sendBackgroundMessageToAllDevices(this, contactPubKey); MessageSender.sendBackgroundMessageToAllDevices(this, contactPubKey);
MessageSender.syncContact(this, contact); MessageSender.syncContact(this, address);
updateInputPanel(); updateInputPanel();
} }

@ -198,6 +198,10 @@ public class MasterSecretUtil {
return preferences.getBoolean("passphrase_initialized", false); 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) { private static void save(Context context, String key, int value) {
if (!context.getSharedPreferences(PREFERENCES_NAME, 0) if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
.edit() .edit()

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.crypto;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.TextSecurePreferences; 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) { public static synchronized @NonNull byte[] rotateProfileKey(@NonNull Context context) {
TextSecurePreferences.setProfileKey(context, null); TextSecurePreferences.setProfileKey(context, null);
return getProfileKey(context); 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);
}
} }

@ -72,7 +72,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV3 = 24; private static final int lokiV3 = 24;
private static final int lokiV4 = 25; 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 static final String DATABASE_NAME = "signal.db";
private final Context context; private final Context context;

@ -7,10 +7,15 @@ import android.text.TextUtils;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; 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.devicelist.Device;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.MnemonicUtilities;
import org.thoughtcrime.securesms.util.AsyncLoader; import org.thoughtcrime.securesms.util.AsyncLoader;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPrivateKey; 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.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; 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.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.MessageDigest; import java.security.MessageDigest;
@ -33,93 +41,43 @@ import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import static org.thoughtcrime.securesms.devicelist.DeviceNameProtos.*; import static org.thoughtcrime.securesms.devicelist.DeviceNameProtos.*;
import static org.whispersystems.signalservice.loki.utilities.TrimmingKt.removing05PrefixIfNeeded;
public class DeviceListLoader extends AsyncLoader<List<Device>> { public class DeviceListLoader extends AsyncLoader<List<Device>> {
private static final String TAG = DeviceListLoader.class.getSimpleName(); private static final String TAG = DeviceListLoader.class.getSimpleName();
private MnemonicCodec mnemonicCodec;
private final SignalServiceAccountManager accountManager; public DeviceListLoader(Context context, File languageFileDirectory) {
public DeviceListLoader(Context context, SignalServiceAccountManager accountManager) {
super(context); super(context);
this.accountManager = accountManager; this.mnemonicCodec = new MnemonicCodec(languageFileDirectory);
} }
@Override @Override
public List<Device> loadInBackground() { public List<Device> loadInBackground() {
try { try {
List<Device> devices = Stream.of(accountManager.getDevices()) String ourPublicKey = TextSecurePreferences.getLocalNumber(getContext());
.filter(d -> d.getId() != SignalServiceAddress.DEFAULT_DEVICE_ID) List<String> secondaryDevicePublicKeys = LokiStorageAPI.shared.getSecondaryDevicePublicKeys(ourPublicKey).get();
.map(this::mapToDevice) List<Device> devices = Stream.of(secondaryDevicePublicKeys).map(this::mapToDevice).toList();
.toList();
Collections.sort(devices, new DeviceComparator()); Collections.sort(devices, new DeviceComparator());
return devices; return devices;
} catch (IOException e) { } catch (Exception e) {
Log.w(TAG, e); Log.w(TAG, e);
return null; return null;
} }
} }
private Device mapToDevice(@NonNull DeviceInfo deviceInfo) { private Device mapToDevice(@NonNull String hexEncodedPublicKey) {
try { String shortId = MnemonicUtilities.getFirst3Words(mnemonicCodec, hexEncodedPublicKey);
if (TextUtils.isEmpty(deviceInfo.getName()) || deviceInfo.getName().length() < 4) { String name = DatabaseFactory.getLokiUserDatabase(getContext()).getDisplayName(hexEncodedPublicKey);
throw new IOException("Invalid DeviceInfo name."); return new Device(hexEncodedPublicKey, shortId, 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 static class DeviceComparator implements Comparator<Device> { private static class DeviceComparator implements Comparator<Device> {
@Override @Override
public int compare(Device lhs, Device rhs) { public int compare(Device lhs, Device rhs) {
if (lhs.getCreated() < rhs.getCreated()) return -1; return lhs.getName().compareTo(rhs.getName());
else if (lhs.getCreated() != rhs.getCreated()) return 1;
else return 0;
} }
} }
} }

@ -2,31 +2,19 @@ package org.thoughtcrime.securesms.devicelist;
public class Device { public class Device {
private final long id; private final String id;
private final String shortId;
private final String name; private final String name;
private final long created;
private final long lastSeen;
public Device(long id, String name, long created, long lastSeen) { public Device(String id, String shortId, String name) {
this.id = id; this.id = id;
this.name = name; this.shortId = shortId;
this.created = created; this.name = name;
this.lastSeen = lastSeen;
} }
public long getId() { public String getId() {
return id; return id;
} }
public String getShortId() { return shortId; }
public String getName() { public String getName() { return name; }
return name;
}
public long getCreated() {
return created;
}
public long getLastSeen() {
return lastSeen;
}
} }

@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
@ -14,7 +15,6 @@ import android.util.Pair;
import com.annimon.stream.Collectors; import com.annimon.stream.Collectors;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import com.google.android.gms.common.util.IOUtils;
import org.signal.libsignal.metadata.InvalidMetadataMessageException; import org.signal.libsignal.metadata.InvalidMetadataMessageException;
import org.signal.libsignal.metadata.InvalidMetadataVersionException; 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.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.DeviceLinkingSession; import org.whispersystems.signalservice.loki.api.DeviceLinkingSession;
import org.whispersystems.signalservice.loki.api.LokiAPI; 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.LokiStorageAPI;
import org.whispersystems.signalservice.loki.api.PairingAuthorisation; import org.whispersystems.signalservice.loki.api.PairingAuthorisation;
import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher; 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.messaging.LokiThreadSessionResetStatus;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil; import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.SecureRandom; import java.security.SecureRandom;
@ -147,11 +147,13 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set;
import javax.inject.Inject; import javax.inject.Inject;
import kotlin.Unit; import kotlin.Unit;
import network.loki.messenger.R; import network.loki.messenger.R;
import nl.komponents.kovenant.Promise;
public class PushDecryptJob extends BaseJob implements InjectableType { 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 // Loki - Handle friend request acceptance if needed
acceptFriendRequestIfNeeded(envelope, content); 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()) { if (content.lokiServiceMessage.isPresent()) {
LokiServiceMessage lokiMessage = content.lokiServiceMessage.get(); 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) { if (lokiMessage.getAddressMessage() != null) {
// TODO: Loki - Handle address message // TODO: Loki - Handle address message
} }
@ -311,43 +306,58 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Store the sender display name if needed // Loki - Store the sender display name if needed
Optional<String> rawSenderDisplayName = content.senderDisplayName; Optional<String> rawSenderDisplayName = content.senderDisplayName;
if (rawSenderDisplayName.isPresent() && rawSenderDisplayName.get().length() > 0) { if (rawSenderDisplayName.isPresent() && rawSenderDisplayName.get().length() > 0) {
setDisplayName(envelope.getSource(), rawSenderDisplayName.get()); // If we got a name from our primary device then we set our profile name to match it
// If we got a name from our primary device then we also set that
String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context); String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
if (ourPrimaryDevice != null && envelope.getSource().equals(ourPrimaryDevice)) { if (ourPrimaryDevice != null && envelope.getSource().equals(ourPrimaryDevice)) {
TextSecurePreferences.setProfileName(context, rawSenderDisplayName.get()); 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()) { if (content.getPairingAuthorisation().isPresent()) {
handlePairingMessage(content.getPairingAuthorisation().get(), envelope, content); handlePairingMessage(content.getPairingAuthorisation().get(), envelope, content);
} else if (content.getDataMessage().isPresent()) { } else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get(); SignalServiceDataMessage message = content.getDataMessage().get();
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent();
if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId); if (!envelope.isFriendRequest() && message.isUnpairingRequest()) {
else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId); // Make sure we got the request from our primary device
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId); String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId, Optional.absent()); if (ourPrimaryDevice != null && ourPrimaryDevice.equals(content.getSender())) {
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, Optional.absent()); 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))) { if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))) {
handleUnknownGroupMessage(content, message.getGroupInfo().get()); 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()) { } else if (content.getSyncMessage().isPresent()) {
TextSecurePreferences.setMultiDevice(context, true); TextSecurePreferences.setMultiDevice(context, true);
@ -716,6 +726,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get()); handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get());
} }
String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
boolean isSenderMasterDevice = ourMasterDevice != null && ourMasterDevice.equals(content.getSender());
if (message.getMessage().getProfileKey().isPresent()) { if (message.getMessage().getProfileKey().isPresent()) {
Recipient recipient = null; Recipient recipient = null;
@ -726,6 +738,16 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) { if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) {
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); 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) { if (threadId != null) {
@ -859,18 +881,23 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
database.endTransaction(); database.endTransaction();
} }
// Loki - Store message server ID
updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);
// Loki - Update mapping of message to original thread id
if (insertResult.isPresent()) { if (insertResult.isPresent()) {
MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); 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 { 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 // Insert the message into the database
Optional<InsertResult> insertResult = database.insertMessageInbox(textMessage); Optional<InsertResult> insertResult = database.insertMessageInbox(textMessage);
Long messageId = null;
if (insertResult.isPresent()) { if (insertResult.isPresent()) {
threadId = insertResult.get().getThreadId(); threadId = insertResult.get().getThreadId();
messageId = insertResult.get().getMessageId();
} }
if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get()); 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(); boolean isGroupMessage = message.getGroupInfo().isPresent();
if (threadId != null && !isGroupMessage) { if (threadId != null && !isGroupMessage) {
MessageNotifier.updateNotification(context, threadId); 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(); 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) { private void handlePairingMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context); String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
if (authorisation.getType() == PairingAuthorisation.Type.REQUEST) { if (authorisation.getType() == PairingAuthorisation.Type.REQUEST) {
handlePairingRequestMessage(authorisation); handlePairingRequestMessage(authorisation, envelope, content);
} else if (authorisation.getSecondaryDevicePublicKey().equals(userHexEncodedPublicKey)) { } else if (authorisation.getSecondaryDevicePublicKey().equals(userHexEncodedPublicKey)) {
handlePairingAuthorisationMessage(authorisation, envelope, content); 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); boolean isValid = isValidPairingMessage(authorisation);
DeviceLinkingSession linkingSession = DeviceLinkingSession.Companion.getShared(); DeviceLinkingSession linkingSession = DeviceLinkingSession.Companion.getShared();
if (isValid && linkingSession.isListeningForLinkingRequests()) { if (isValid && linkingSession.isListeningForLinkingRequests()) {
// Loki - If we successfully received a request then we should store the PreKeyBundle
storePreKeyBundleIfNeeded(envelope, content);
linkingSession.processLinkingRequest(authorisation); linkingSession.processLinkingRequest(authorisation);
} }
} }
@ -1101,6 +1137,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} }
if (authorisation.getType() != PairingAuthorisation.Type.GRANT) { return; } if (authorisation.getType() != PairingAuthorisation.Type.GRANT) { return; }
Log.d("Loki", "Received pairing authorisation message from: " + authorisation.getPrimaryDevicePublicKey() + "."); Log.d("Loki", "Received pairing authorisation message from: " + authorisation.getPrimaryDevicePublicKey() + ".");
// Save PreKeyBundle if for whatever reason we got one
storePreKeyBundleIfNeeded(envelope, content);
// Process // Process
DeviceLinkingSession.Companion.getShared().processLinkingAuthorization(authorisation); DeviceLinkingSession.Companion.getShared().processLinkingAuthorization(authorisation);
// Store the primary device's public key // 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) { if (content.senderDisplayName.isPresent() && content.senderDisplayName.get().length() > 0) {
TextSecurePreferences.setProfileName(context, content.senderDisplayName.get()); TextSecurePreferences.setProfileName(context, content.senderDisplayName.get());
} }
// Profile avatar updates
if (content.getDataMessage().isPresent()) {
handleProfileKey(content, content.getDataMessage().get());
}
// Contact sync // Contact sync
if (content.getSyncMessage().isPresent() && content.getSyncMessage().get().getContacts().isPresent()) { if (content.getSyncMessage().isPresent() && content.getSyncMessage().get().getContacts().isPresent()) {
handleSynchronizeContactMessage(content.getSyncMessage().get().getContacts().get()); 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) { private void acceptFriendRequestIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
// If we get anything other than a friend request, we can assume that we have a session with the other user // If we get anything other than a friend request, we can assume that we have a session with the other user
if (envelope.isFriendRequest() || isGroupChatMessage(content)) { return; } if (envelope.isFriendRequest() || isGroupChatMessage(content)) { return; }
@ -1159,6 +1217,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (syncContact) { if (syncContact) {
MessageSender.syncContact(context, contactID.getAddress()); MessageSender.syncContact(context, contactID.getAddress());
} }
// Allow profile sharing with contact
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(contactID, true);
// Update the last message if needed // Update the last message if needed
LokiStorageAPI.shared.getPrimaryDevicePublicKey(pubKey).success(primaryDevice -> { LokiStorageAPI.shared.getPrimaryDevicePublicKey(pubKey).success(primaryDevice -> {
Util.runOnMain(() -> { 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) { private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
if (!envelope.isFriendRequest() || message.isGroupUpdate()) { return; } if (!envelope.isFriendRequest() || message.isGroupUpdate()) { return; }
// This handles the case where another user sends us a regular message without authorisation // 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) { if (shouldBecomeFriends) {
// Become friends AND update the message they sent // Become friends AND update the message they sent
becomeFriendsWithContact(content.getSender(), true); becomeFriendsWithContact(content.getSender(), true);
@ -1369,14 +1430,25 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void handleProfileKey(@NonNull SignalServiceContent content, private void handleProfileKey(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message) @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); RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
Address sourceAddress = Address.fromSerialized(content.getSender()); Recipient recipient = Recipient.from(context, Address.fromSerialized(content.getSender()), false);
Recipient recipient = Recipient.from(context, sourceAddress, false);
if (recipient.getProfileKey() == null || !MessageDigest.isEqual(recipient.getProfileKey(), message.getProfileKey().get())) { if (recipient.getProfileKey() == null || !MessageDigest.isEqual(recipient.getProfileKey(), message.getProfileKey().get())) {
database.setProfileKey(recipient, message.getProfileKey().get()); database.setProfileKey(recipient, message.getProfileKey().get());
database.setUnidentifiedAccessMode(recipient, RecipientDatabase.UnidentifiedAccessMode.UNKNOWN); 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) { private Recipient getPrimaryDeviceRecipient(String pubKey) {
try { try {
String primaryDevice = LokiStorageAPI.shared.getPrimaryDevicePublicKey(pubKey).get(); String primaryDevice = PromiseUtil.timeout(LokiStorageAPI.shared.getPrimaryDevicePublicKey(pubKey), 5000).get();
String publicKey = (primaryDevice != null) ? primaryDevice : pubKey; 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) // 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); String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
@ -1696,7 +1768,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} else if (content.getSyncMessage().isPresent()) { } else if (content.getSyncMessage().isPresent()) {
try { try {
// We should ignore a sync message if the sender is not one of our devices // 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) { if (!isOurDevice) {
Log.w(TAG, "Got a sync message from a device that is not ours!."); Log.w(TAG, "Got a sync message from a device that is not ours!.");
} }

@ -25,22 +25,26 @@ public abstract class PushReceivedJob extends BaseJob {
public void processEnvelope(@NonNull SignalServiceEnvelope envelope) { public void processEnvelope(@NonNull SignalServiceEnvelope envelope) {
synchronized (RECEIVE_LOCK) { synchronized (RECEIVE_LOCK) {
if (envelope.hasSource()) { try {
Address source = Address.fromExternal(context, envelope.getSource()); if (envelope.hasSource()) {
Recipient recipient = Recipient.from(context, source, false); Address source = Address.fromExternal(context, envelope.getSource());
Recipient recipient = Recipient.from(context, source, false);
if (!isActiveNumber(recipient)) { if (!isActiveNumber(recipient)) {
DatabaseFactory.getRecipientDatabase(context).setRegistered(recipient, RecipientDatabase.RegisteredState.REGISTERED); DatabaseFactory.getRecipientDatabase(context).setRegistered(recipient, RecipientDatabase.RegisteredState.REGISTERED);
ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(recipient, false)); ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(recipient, false));
}
} }
}
if (envelope.isReceipt()) { if (envelope.isReceipt()) {
handleReceipt(envelope); handleReceipt(envelope);
} else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender() || envelope.isFriendRequest()) { } else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender() || envelope.isFriendRequest()) {
handleMessage(envelope); handleMessage(envelope);
} else { } else {
Log.w(TAG, "Received envelope of unknown type: " + envelope.getType()); Log.w(TAG, "Received envelope of unknown type: " + envelope.getType());
}
} catch (Exception e) {
Log.d("Loki", "Failed to process envelope: " + e);
} }
} }
} }

@ -49,7 +49,7 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
.setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize()) .setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize())
.addConstraint(NetworkConstraint.KEY) .addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.HOURS.toMillis(1)) .setLifespan(TimeUnit.HOURS.toMillis(1))
.setMaxInstances(1) .setMaxAttempts(2)
.build(), .build(),
recipient, recipient,
profileAvatar); profileAvatar);
@ -99,8 +99,8 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
File downloadDestination = File.createTempFile("avatar", "jpg", context.getCacheDir()); File downloadDestination = File.createTempFile("avatar", "jpg", context.getCacheDir());
try { try {
InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, MAX_PROFILE_SIZE_BYTES); InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, MAX_PROFILE_SIZE_BYTES);
File decryptDestination = File.createTempFile("avatar", "jpg", context.getCacheDir()); File decryptDestination = File.createTempFile("avatar", "jpg", context.getCacheDir());
Util.copy(avatarStream, new FileOutputStream(decryptDestination)); Util.copy(avatarStream, new FileOutputStream(decryptDestination));
decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.getAddress())); decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.getAddress()));

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

@ -8,13 +8,12 @@ import org.whispersystems.signalservice.loki.api.DeviceLinkingSession
import org.whispersystems.signalservice.loki.api.DeviceLinkingSessionListener import org.whispersystems.signalservice.loki.api.DeviceLinkingSessionListener
import org.whispersystems.signalservice.loki.api.PairingAuthorisation 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 view: DeviceLinkingView
private lateinit var dialog: AlertDialog private lateinit var dialog: AlertDialog
companion object { companion object {
fun show(context: Context, mode: DeviceLinkingView.Mode, delegate: DeviceLinkingDelegate?): DeviceLinkingDialog {
fun show(context: Context, mode: DeviceLinkingView.Mode, delegate: DeviceLinkingDialogDelegate?): DeviceLinkingDialog {
val dialog = DeviceLinkingDialog(context, mode, delegate) val dialog = DeviceLinkingDialog(context, mode, delegate)
dialog.show() dialog.show()
return dialog return dialog
@ -22,8 +21,10 @@ class DeviceLinkingDialog private constructor(private val context: Context, priv
} }
private fun show() { 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 = AlertDialog.Builder(context).setView(view).show()
dialog.setCanceledOnTouchOutside(false)
view.dismiss = { dismiss() } view.dismiss = { dismiss() }
DeviceLinkingSession.shared.startListeningForLinkingRequests() DeviceLinkingSession.shared.startListeningForLinkingRequests()
DeviceLinkingSession.shared.addListener(this) DeviceLinkingSession.shared.addListener(this)
@ -35,20 +36,11 @@ class DeviceLinkingDialog private constructor(private val context: Context, priv
dialog.dismiss() dialog.dismiss()
} }
override fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
delegate?.handleDeviceLinkAuthorized(pairingAuthorisation)
}
override fun handleDeviceLinkingDialogDismissed() { override fun handleDeviceLinkingDialogDismissed() {
if (mode == DeviceLinkingView.Mode.Master && view.pairingAuthorisation != null) { if (mode == DeviceLinkingView.Mode.Master && view.pairingAuthorisation != null) {
val authorisation = view.pairingAuthorisation!! val authorisation = view.pairingAuthorisation!!
DatabaseFactory.getLokiPreKeyBundleDatabase(context).removePreKeyBundle(authorisation.secondaryDevicePublicKey) DatabaseFactory.getLokiPreKeyBundleDatabase(context).removePreKeyBundle(authorisation.secondaryDevicePublicKey)
} }
delegate?.handleDeviceLinkingDialogDismissed()
}
override fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {
delegate?.sendPairingAuthorizedMessage(pairingAuthorisation)
} }
override fun requestUserAuthorization(authorisation: PairingAuthorisation) { override fun requestUserAuthorization(authorisation: PairingAuthorisation) {

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

@ -5,19 +5,23 @@ import android.graphics.Color
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.os.Handler import android.os.Handler
import android.util.AttributeSet import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_device_linking.view.* 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 network.loki.messenger.R
import org.thoughtcrime.securesms.qr.QrCode
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.api.PairingAuthorisation import org.whispersystems.signalservice.loki.api.PairingAuthorisation
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded
import java.io.File 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) { class DeviceLinkingView private constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, private val mode: Mode, private var delegate: DeviceLinkingDelegate) : LinearLayout(context, attrs, defStyleAttr) {
private lateinit var languageFileDirectory: File private val languageFileDirectory: File = MnemonicUtilities.getLanguageFileDirectory(context)
var dismiss: (() -> Unit)? = null var dismiss: (() -> Unit)? = null
var pairingAuthorisation: PairingAuthorisation? = null var pairingAuthorisation: PairingAuthorisation? = null
private set private set
@ -27,36 +31,14 @@ class DeviceLinkingView private constructor(context: Context, attrs: AttributeSe
// endregion // endregion
// region Lifecycle // region Lifecycle
constructor(context: Context, mode: Mode, delegate: DeviceLinkingViewDelegate) : this(context, null, 0, mode, delegate) 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 : DeviceLinkingViewDelegate { }) // Just pass in a dummy mode 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) private constructor(context: Context) : this(context, null)
init { init {
setUpLanguageFileDirectory()
setUpViewHierarchy() 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() { private fun setUpViewHierarchy() {
inflate(context, R.layout.view_device_linking, this) inflate(context, R.layout.view_device_linking, this)
spinner.indeterminateDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN) 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) explanationTextView.text = resources.getString(explanationID)
mnemonicTextView.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE mnemonicTextView.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE
if (mode == Mode.Slave) { if (mode == Mode.Slave) {
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded() val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ") mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), hexEncodedPublicKey)
} }
authorizeButton.visibility = View.GONE authorizeButton.visibility = View.GONE
authorizeButton.setOnClickListener { authorizePairing() } 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() } cancelButton.setOnClickListener { cancel() }
} }
// endregion // 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 } if (mode != Mode.Master || pairingAuthorisation.type != PairingAuthorisation.Type.REQUEST || this.pairingAuthorisation != null) { return }
this.pairingAuthorisation = pairingAuthorisation this.pairingAuthorisation = pairingAuthorisation
spinner.visibility = View.GONE spinner.visibility = View.GONE
qrCodeImageView.visibility = View.GONE
val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams
titleTextViewLayoutParams.topMargin = toPx(16, resources) titleTextViewLayoutParams.topMargin = toPx(16, resources)
titleTextView.layoutParams = titleTextViewLayoutParams titleTextView.layoutParams = titleTextViewLayoutParams
titleTextView.text = resources.getString(R.string.view_device_linking_title_3) titleTextView.text = resources.getString(R.string.view_device_linking_title_3)
explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_2) explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_2)
mnemonicTextView.visibility = View.VISIBLE mnemonicTextView.visibility = View.VISIBLE
val hexEncodedPublicKey = pairingAuthorisation.secondaryDevicePublicKey.removing05PrefixIfNeeded() mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), pairingAuthorisation.secondaryDevicePublicKey)
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
authorizeButton.visibility = View.VISIBLE authorizeButton.visibility = View.VISIBLE
} }

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

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

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

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

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

@ -189,6 +189,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
database.delete(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey )) 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 // region Convenience

@ -19,7 +19,7 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
private val friendRequestStatus = "friend_request_status" private val friendRequestStatus = "friend_request_status"
private val threadID = "thread_id" 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 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? { override fun getQuoteServerID(quoteID: Long, quoteeHexEncodedPublicKey: String): Long? {

@ -36,11 +36,6 @@ class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) :
"$signedPreKeySignature TEXT," + "$identityKey TEXT NOT NULL," + "$deviceID INTEGER," + "$registrationID INTEGER" + ");" "$signedPreKeySignature TEXT," + "$identityKey TEXT NOT NULL," + "$deviceID INTEGER," + "$registrationID INTEGER" + ");"
} }
fun resetAllPreKeyBundleInfo() {
TextSecurePreferences.removeLocalRegistrationId(context)
TextSecurePreferences.setSignedPreKeyRegistered(context, false)
}
fun generatePreKeyBundle(hexEncodedPublicKey: String): PreKeyBundle? { fun generatePreKeyBundle(hexEncodedPublicKey: String): PreKeyBundle? {
var registrationID = TextSecurePreferences.getLocalRegistrationId(context) var registrationID = TextSecurePreferences.getLocalRegistrationId(context)
if (registrationID == 0) { if (registrationID == 0) {

@ -6,9 +6,14 @@ import android.util.Log
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.then import nl.komponents.kovenant.then
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.jobs.PushDecryptJob 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.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.libsignal.util.guava.Optional import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer 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.LokiPublicChatAPI
import org.whispersystems.signalservice.loki.api.LokiPublicChatMessage import org.whispersystems.signalservice.loki.api.LokiPublicChatMessage
import org.whispersystems.signalservice.loki.api.LokiStorageAPI import org.whispersystems.signalservice.loki.api.LokiStorageAPI
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
import org.whispersystems.signalservice.loki.utilities.successBackground import org.whispersystems.signalservice.loki.utilities.successBackground
import java.security.MessageDigest
import java.util.* import java.util.*
class LokiPublicChatPoller(private val context: Context, private val group: LokiPublicChat) { 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)})" val senderDisplayName = "${message.displayName} (...${message.hexEncodedPublicKey.takeLast(8)})"
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.hexEncodedPublicKey, senderDisplayName) DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.hexEncodedPublicKey, senderDisplayName)
} }
val senderPublicKey = primaryDevice ?: message.hexEncodedPublicKey val senderPublicKey = primaryDevice ?: message.hexEncodedPublicKey
val serviceDataMessage = getDataMessage(message) val serviceDataMessage = getDataMessage(message)
val serviceContent = SignalServiceContent(serviceDataMessage, senderPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.timestamp, false) 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 { } else {
PushDecryptJob(context).handleTextMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID)) 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) { fun processOutgoingMessage(message: LokiPublicChatMessage) {
val messageServerID = message.serverID ?: return val messageServerID = message.serverID ?: return
@ -178,6 +205,19 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
} else { } else {
PushDecryptJob(context).handleSynchronizeSentTextMessage(transcript) 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 userDevices = setOf<String>()
var uniqueDevices = setOf<String>() var uniqueDevices = setOf<String>()

@ -16,6 +16,7 @@ class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database
companion object { companion object {
// Shared // Shared
private val displayName = "display_name" private val displayName = "display_name"
private val profileAvatarUrl = "profile_avatar_url"
// Display name cache // Display name cache
private val displayNameTable = "loki_user_display_name_database" private val displayNameTable = "loki_user_display_name_database"
private val hexEncodedPublicKey = "hex_encoded_public_key" 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.") 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
}
}
} }

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

@ -6,9 +6,13 @@ import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all import nl.komponents.kovenant.all
import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import nl.komponents.kovenant.then
import nl.komponents.kovenant.toFailVoid import nl.komponents.kovenant.toFailVoid
import nl.komponents.kovenant.ui.successUi
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil 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.Address
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.logging.Log 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.LokiStorageAPI
import org.whispersystems.signalservice.loki.api.PairingAuthorisation import org.whispersystems.signalservice.loki.api.PairingAuthorisation
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus 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.recover
import org.whispersystems.signalservice.loki.utilities.retryIfNeeded import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
import java.util.* import java.util.*
import kotlin.concurrent.schedule 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> { fun getAllDeviceFriendRequestStatuses(context: Context, hexEncodedPublicKey: String): Promise<Map<String, LokiThreadFriendRequestStatus>, Exception> {
val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context) val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context)
return LokiStorageAPI.shared.getAllDevicePublicKeys(hexEncodedPublicKey).map { keys -> 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> { fun sendPairingAuthorisationMessage(context: Context, contactHexEncodedPublicKey: String, authorisation: PairingAuthorisation): Promise<Unit, Exception> {
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender() val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(contactHexEncodedPublicKey) 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. // 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) { if (authorisation.type == PairingAuthorisation.Type.REQUEST) {
val preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.number) val preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.number)
message.asFriendRequest(true).withPreKeyBundle(preKeyBundle) 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 { return try {
Log.d("Loki", "Sending authorisation message to: $contactHexEncodedPublicKey.") Log.d("Loki", "Sending authorisation message to: $contactHexEncodedPublicKey.")
val result = messageSender.sendMessage(0, address, Optional.absent<UnidentifiedAccessPair>(), message.build()) val result = messageSender.sendMessage(0, address, Optional.absent<UnidentifiedAccessPair>(), message.build())

@ -11,39 +11,58 @@ import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.util.JsonUtil
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit 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( class PushBackgroundMessageSendJob private constructor(
parameters: Parameters, parameters: Parameters,
private val recipient: String, private val message: BackgroundMessage
private val messageBody: String?,
private val friendRequest: Boolean
) : BaseJob(parameters) { ) : BaseJob(parameters) {
companion object { companion object {
const val KEY = "PushBackgroundMessageSendJob" const val KEY = "PushBackgroundMessageSendJob"
private val TAG = PushBackgroundMessageSendJob::class.java.simpleName private val TAG = PushBackgroundMessageSendJob::class.java.simpleName
private val KEY_RECIPIENT = "recipient" private val KEY_MESSAGE = "message"
private val KEY_MESSAGE_BODY = "message_body"
private val KEY_FRIEND_REQUEST = "asFriendRequest"
} }
constructor(recipient: String): this(recipient, null, false) constructor(message: BackgroundMessage) : this(Parameters.Builder()
constructor(recipient: String, messageBody: String?, friendRequest: Boolean) : this(Parameters.Builder()
.addConstraint(NetworkConstraint.KEY) .addConstraint(NetworkConstraint.KEY)
.setQueue(KEY) .setQueue(KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1)) .setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(1) .setMaxAttempts(1)
.build(), .build(),
recipient, messageBody, friendRequest) message)
override fun serialize(): Data { override fun serialize(): Data {
return Data.Builder() return Data.Builder()
.putString(KEY_RECIPIENT, recipient) .putString(KEY_MESSAGE, message.serialize())
.putString(KEY_MESSAGE_BODY, messageBody)
.putBoolean(KEY_FRIEND_REQUEST, friendRequest)
.build() .build()
} }
@ -52,22 +71,24 @@ class PushBackgroundMessageSendJob private constructor(
} }
public override fun onRun() { public override fun onRun() {
val message = SignalServiceDataMessage.newBuilder() val dataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(System.currentTimeMillis()) .withTimestamp(System.currentTimeMillis())
.withBody(messageBody) .withBody(message.body)
if (friendRequest) { if (message.friendRequest) {
val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(recipient) val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(message.recipient)
message.withPreKeyBundle(bundle) dataMessage.withPreKeyBundle(bundle)
.asFriendRequest(true) .asFriendRequest(true)
} else if (message.unpairingRequest) {
dataMessage.asUnpairingRequest(true)
} }
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender() val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(recipient) val address = SignalServiceAddress(message.recipient)
try { 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) { } 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 throw e
} }
} }
@ -82,10 +103,8 @@ class PushBackgroundMessageSendJob private constructor(
class Factory : Job.Factory<PushBackgroundMessageSendJob> { class Factory : Job.Factory<PushBackgroundMessageSendJob> {
override fun create(parameters: Parameters, data: Data): PushBackgroundMessageSendJob { override fun create(parameters: Parameters, data: Data): PushBackgroundMessageSendJob {
try { try {
val recipient = data.getString(KEY_RECIPIENT) val messageJSON = data.getString(KEY_MESSAGE)
val messageBody = if (data.hasString(KEY_MESSAGE_BODY)) data.getString(KEY_MESSAGE_BODY) else null return PushBackgroundMessageSendJob(parameters, BackgroundMessage.parse(messageJSON))
val friendRequest = data.getBooleanOrDefault(KEY_FRIEND_REQUEST, false)
return PushBackgroundMessageSendJob(parameters, recipient, messageBody, friendRequest)
} catch (e: IOException) { } catch (e: IOException) {
throw AssertionError(e) throw AssertionError(e)
} }

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

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

@ -1,11 +1,13 @@
package org.thoughtcrime.securesms.loki package org.thoughtcrime.securesms.loki
import android.Manifest
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.AsyncTask import android.os.AsyncTask
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.FragmentManager
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast 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.DatabaseFactory
import org.thoughtcrime.securesms.database.IdentityDatabase import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.logging.Log 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.Hex
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.curve25519.Curve25519 import org.whispersystems.curve25519.Curve25519
@ -32,7 +36,7 @@ import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate { class SeedActivity : BaseActionBarActivity(), DeviceLinkingDelegate, ScanListener {
private lateinit var languageFileDirectory: File private lateinit var languageFileDirectory: File
private var mode = Mode.Register private var mode = Mode.Register
set(newValue) { field = newValue; updateUI() } set(newValue) { field = newValue; updateUI() }
@ -57,6 +61,23 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
toggleRestoreModeButton.setOnClickListener { mode = Mode.Restore } toggleRestoreModeButton.setOnClickListener { mode = Mode.Restore }
toggleLinkModeButton.setOnClickListener { mode = Mode.Link } toggleLinkModeButton.setOnClickListener { mode = Mode.Link }
mainButton.setOnClickListener { handleMainButtonTapped() } 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") Analytics.shared.track("Seed Screen Viewed")
} }
// endregion // endregion
@ -106,6 +127,7 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
mnemonicEditText.visibility = restoreModeVisibility mnemonicEditText.visibility = restoreModeVisibility
linkExplanationTextView.visibility = linkModeVisibility linkExplanationTextView.visibility = linkModeVisibility
publicKeyEditText.visibility = linkModeVisibility publicKeyEditText.visibility = linkModeVisibility
scanQRButton.visibility = linkModeVisibility
toggleRegisterModeButton.visibility = if (mode != Mode.Register) View.VISIBLE else View.GONE toggleRegisterModeButton.visibility = if (mode != Mode.Register) View.VISIBLE else View.GONE
toggleRestoreModeButton.visibility = if (mode != Mode.Restore) 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 toggleLinkModeButton.visibility = if (mode != Mode.Link) View.VISIBLE else View.GONE
@ -230,10 +252,19 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
private fun resetForRegistration() { private fun resetForRegistration() {
IdentityKeyUtil.delete(this, IdentityKeyUtil.lokiSeedKey) IdentityKeyUtil.delete(this, IdentityKeyUtil.lokiSeedKey)
DatabaseFactory.getLokiPreKeyBundleDatabase(this).resetAllPreKeyBundleInfo()
TextSecurePreferences.removeLocalNumber(this) TextSecurePreferences.removeLocalNumber(this)
TextSecurePreferences.setHasSeenWelcomeScreen(this, false) TextSecurePreferences.setHasSeenWelcomeScreen(this, false)
TextSecurePreferences.setPromptedPushRegistration(this, false) TextSecurePreferences.setPromptedPushRegistration(this, false)
} }
// endregion // 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
} }

@ -4,6 +4,13 @@ import android.app.Notification;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; 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.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@ -237,11 +244,34 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
Bitmap recipientPhotoBitmap = BitmapUtil.createFromDrawable(drawable, largeIconTargetSize, largeIconTargetSize); Bitmap recipientPhotoBitmap = BitmapUtil.createFromDrawable(drawable, largeIconTargetSize, largeIconTargetSize);
if (recipientPhotoBitmap != null) { 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) { private boolean hasBigPictureSlide(@Nullable SlideDeck slideDeck) {
if (slideDeck == null) { if (slideDeck == null) {
return false; return false;

@ -5,6 +5,7 @@ import android.content.ClipData;
import android.content.ClipboardManager; import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.graphics.Outline; import android.graphics.Outline;
import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.support.annotation.RequiresApi; import android.support.annotation.RequiresApi;
import android.support.v7.preference.Preference; import android.support.v7.preference.Preference;
@ -13,14 +14,20 @@ import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import android.view.ViewOutlineProvider; import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; 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.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.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec;
import network.loki.messenger.R; import network.loki.messenger.R;
@ -31,6 +38,7 @@ public class ProfilePreference extends Preference {
private TextView profileNameView; private TextView profileNameView;
private TextView profileNumberView; private TextView profileNumberView;
private TextView profileTagView; private TextView profileTagView;
private String ourDeviceWords;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public ProfilePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { public ProfilePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
@ -73,13 +81,14 @@ public class ProfilePreference extends Preference {
public void refresh() { public void refresh() {
if (profileNumberView == null) return; if (profileNumberView == null) return;
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext()); Context context = getContext();
String primaryDevicePublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(getContext()); String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
String primaryDevicePublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
String publicKey = primaryDevicePublicKey != null ? primaryDevicePublicKey : userHexEncodedPublicKey; String publicKey = primaryDevicePublicKey != null ? primaryDevicePublicKey : userHexEncodedPublicKey;
final Address localAddress = Address.fromSerialized(publicKey); 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 -> { containerView.setOnLongClickListener(v -> {
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("Public Key", publicKey); ClipData clip = ClipData.newPlainText("Public Key", publicKey);
@ -96,28 +105,16 @@ public class ProfilePreference extends Preference {
} }
}); });
avatarView.setClipToOutline(true); avatarView.setClipToOutline(true);
avatarView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override Drawable fallback = recipient.getFallbackContactPhotoDrawable(context, false);
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;
}
});
/*
GlideApp.with(getContext().getApplicationContext()) GlideApp.with(getContext().getApplicationContext())
.load(new ProfileContactPhoto(localAddress, String.valueOf(TextSecurePreferences.getProfileAvatarId(getContext())))) .load(recipient.getContactPhoto())
.error(new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp).asDrawable(getContext(), getContext().getResources().getColor(R.color.grey_400))) .fallback(fallback)
.error(fallback)
.circleCrop() .circleCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL) .diskCacheStrategy(DiskCacheStrategy.ALL)
.into(avatarView); .into(avatarView);
*/
if (!TextUtils.isEmpty(profileName)) { if (!TextUtils.isEmpty(profileName)) {
profileNameView.setText(profileName); profileNameView.setText(profileName);
@ -127,6 +124,12 @@ public class ProfilePreference extends Preference {
profileNumberView.setText(localAddress.toPhoneString()); profileNumberView.setText(localAddress.toPhoneString());
profileTagView.setVisibility(primaryDevicePublicKey == null ? View.GONE : View.VISIBLE); 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 : "-"));
} }
} }

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

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

@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.registration; package org.thoughtcrime.securesms.registration;
import android.Manifest; import android.Manifest;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
@ -8,6 +10,7 @@ import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.BaseActionBarActivity; import org.thoughtcrime.securesms.BaseActionBarActivity;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.loki.utilities.Analytics; import org.whispersystems.signalservice.loki.utilities.Analytics;
import network.loki.messenger.R; import network.loki.messenger.R;
@ -23,6 +26,24 @@ public class WelcomeActivity extends BaseActionBarActivity {
Analytics.Companion.getShared().track("Landing Screen Viewed"); 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 @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);

@ -76,6 +76,10 @@ public class KeyCachingService extends Service {
private static MasterSecret masterSecret; private static MasterSecret masterSecret;
// Loki - Caching
private static MasterSecret cachedSecret;
private static long cacheTime = 0;
public KeyCachingService() {} public KeyCachingService() {}
public static synchronized boolean isLocked(Context context) { public static synchronized boolean isLocked(Context context) {
@ -85,7 +89,13 @@ public class KeyCachingService extends Service {
public static synchronized @Nullable MasterSecret getMasterSecret(Context context) { public static synchronized @Nullable MasterSecret getMasterSecret(Context context) {
if (masterSecret == null && (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context))) { if (masterSecret == null && (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context))) {
try { 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) { } catch (InvalidPassphraseException e) {
Log.w("KeyCachingService", e); Log.w("KeyCachingService", e);
} }

@ -17,6 +17,7 @@
package org.thoughtcrime.securesms.sms; package org.thoughtcrime.securesms.sms;
import android.content.Context; import android.content.Context;
import android.os.AsyncTask;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import org.thoughtcrime.securesms.ApplicationContext; 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.LinkPreviewRepository;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.BackgroundMessage;
import org.thoughtcrime.securesms.loki.FriendRequestHandler; import org.thoughtcrime.securesms.loki.FriendRequestHandler;
import org.thoughtcrime.securesms.loki.GeneralUtilitiesKt; import org.thoughtcrime.securesms.loki.GeneralUtilitiesKt;
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities; import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
@ -58,7 +60,11 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.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.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.LokiStorageAPI; import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus; import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil; 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 // 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 // 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) { 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) { 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 // endregion

@ -11,7 +11,7 @@ import java.io.File;
public class FileProviderUtil { 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) { public static Uri getUriFor(@NonNull Context context, @NonNull File file) {
if (Build.VERSION.SDK_INT >= 24) return FileProvider.getUriForFile(context, AUTHORITY, file); if (Build.VERSION.SDK_INT >= 24) return FileProvider.getUriForFile(context, AUTHORITY, file);

@ -121,6 +121,7 @@ public class TextSecurePreferences {
private static final String PROFILE_KEY_PREF = "pref_profile_key"; 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_NAME_PREF = "pref_profile_name";
private static final String PROFILE_AVATAR_ID_PREF = "pref_profile_avatar_id"; 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 READ_RECEIPTS_PREF = "pref_read_receipts";
public static final String INCOGNITO_KEYBORAD_PREF = "pref_incognito_keyboard"; public static final String INCOGNITO_KEYBORAD_PREF = "pref_incognito_keyboard";
private static final String UNAUTHORIZED_RECEIVED = "pref_unauthorized_received"; private static final String UNAUTHORIZED_RECEIVED = "pref_unauthorized_received";
@ -401,6 +402,14 @@ public class TextSecurePreferences {
return getIntegerPreference(context, PROFILE_AVATAR_ID_PREF, 0); 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) { public static int getNotificationPriority(Context context) {
return Integer.valueOf(getStringPreference(context, NOTIFICATION_PRIORITY_PREF, String.valueOf(NotificationCompat.PRIORITY_HIGH))); 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) { public static void setMasterHexEncodedPublicKey(Context context, String masterHexEncodedPublicKey) {
setStringPreference(context, "master_hex_encoded_public_key", masterHexEncodedPublicKey.toLowerCase()); 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 // endregion
public static void clearAll(Context context) {
PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit();
}
} }