mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
commit
d64d34bd25
1441
AndroidManifest.xml
1441
AndroidManifest.xml
File diff suppressed because it is too large
Load Diff
5
res/drawable/ic_edit_white_24dp.xml
Normal file
5
res/drawable/ic_edit_white_24dp.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||
</vector>
|
5
res/drawable/ic_phonelink_erase_white_24dp.xml
Normal file
5
res/drawable/ic_phonelink_erase_white_24dp.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M13,8.2l-1,-1 -4,4 -4,-4 -1,1 4,4 -4,4 1,1 4,-4 4,4 1,-1 -4,-4 4,-4zM19,1H9c-1.1,0 -2,0.9 -2,2v3h2V4h10v16H9v-2H7v3c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3c0,-1.1 -0.9,-2 -2,-2z"/>
|
||||
</vector>
|
@ -96,6 +96,17 @@
|
||||
app:labeledEditText_background="@color/loki_darkest_gray"
|
||||
app:labeledEditText_label="@string/activity_key_pair_public_key_edit_text_label"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/scanQRButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:background="@color/transparent"
|
||||
android:elevation="0dp"
|
||||
android:stateListAnimator="@null"
|
||||
android:text="@string/fragment_scan_qr_code_title"
|
||||
android:textColor="@color/signal_primary"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/toggleRestoreModeButton"
|
||||
android:layout_width="match_parent"
|
||||
@ -123,7 +134,7 @@
|
||||
android:layout_height="50dp"
|
||||
android:background="@color/transparent"
|
||||
android:textColor="@color/signal_primary"
|
||||
android:text="Link Device"
|
||||
android:text="@string/activity_key_pair_toggle_mode_button_title_3"
|
||||
android:elevation="0dp"
|
||||
android:stateListAnimator="@null" />
|
||||
|
||||
|
@ -1,58 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:fab="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:fab="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<LinearLayout android:id="@+id/progress_container"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone" >
|
||||
<ProgressBar
|
||||
android:id="@+id/activityIndicator"
|
||||
android:indeterminate="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ProgressBar android:id="@+id/progress"
|
||||
android:indeterminate="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" >
|
||||
</ProgressBar>
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/emptyStateTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone"
|
||||
android:text="@string/device_list_fragment__no_devices_linked"
|
||||
android:paddingStart="16dip"
|
||||
android:paddingEnd="16dip"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<TextView android:id="@+id/empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center|center_vertical"
|
||||
android:gravity="center|center_vertical"
|
||||
android:textSize="20sp"
|
||||
android:visibility="gone"
|
||||
android:text="@string/device_list_fragment__no_devices_linked"
|
||||
android:paddingStart="16dip"
|
||||
android:paddingEnd="16dip"
|
||||
android:layout_weight="1"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<ListView android:id="@id/android:list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:drawSelectorOnTop="false"
|
||||
android:paddingStart="16dip"
|
||||
android:paddingEnd="16dip"
|
||||
tools:visibility="gone"/>
|
||||
<ListView
|
||||
android:id="@id/android:list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:drawSelectorOnTop="false" />
|
||||
|
||||
<com.melnykov.fab.FloatingActionButton
|
||||
android:id="@+id/add_device"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|right"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_add_white_original_24dp"
|
||||
android:focusable="true"
|
||||
android:contentDescription="@string/device_list_fragment__link_new_device"
|
||||
fab:fab_colorNormal="?fab_color"
|
||||
fab:fab_colorPressed="@color/textsecure_primary_dark"
|
||||
fab:fab_colorRipple="@color/textsecure_primary_dark" />
|
||||
android:id="@+id/addDeviceButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_add_white_original_24dp"
|
||||
android:focusable="true"
|
||||
android:contentDescription="@string/device_list_fragment__link_new_device"
|
||||
fab:fab_colorNormal="?fab_color"
|
||||
fab:fab_colorPressed="@color/textsecure_primary_dark"
|
||||
fab:fab_colorRipple="@color/textsecure_primary_dark" />
|
||||
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
@ -1,33 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.DeviceListItem xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp">
|
||||
<org.thoughtcrime.securesms.DeviceListItem
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView android:id="@+id/name"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?attr/conversation_list_item_contact_color"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
<TextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:singleLine="true"
|
||||
android:text="Name"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?attr/conversation_list_item_contact_color"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<TextView android:id="@+id/created"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?attr/conversation_list_item_subject_color"
|
||||
android:fontFamily="sans-serif-light" />
|
||||
|
||||
<TextView android:id="@+id/active"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?attr/conversation_list_item_subject_color"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:layout_marginBottom="8dp" />
|
||||
<TextView
|
||||
android:id="@+id/shortId"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:ellipsize="marquee"
|
||||
android:singleLine="true"
|
||||
android:text="shortId"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="#A2A2A2"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</org.thoughtcrime.securesms.DeviceListItem>
|
22
res/layout/fragment_device_list_bottom_sheet.xml
Normal file
22
res/layout/fragment_device_list_bottom_sheet.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:behavior_hideable="true"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/editDisplayNameText"
|
||||
style="@style/ActionItem"
|
||||
android:drawableStart="@drawable/ic_edit_white_24dp"
|
||||
android:text="@string/fragment_device_list_edit_device_name_title"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/unlinkDeviceText"
|
||||
style="@style/ActionItem"
|
||||
android:drawableStart="@drawable/ic_phonelink_erase_white_24dp"
|
||||
android:text="@string/fragment_device_list_unlink_device_title" />
|
||||
|
||||
</LinearLayout>
|
@ -24,13 +24,14 @@
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/descriptionTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:padding="32dp"
|
||||
android:gravity="center"
|
||||
android:background="@color/loki_darkest_gray"
|
||||
android:text="@string/fragment_scan_qr_code_explanation"
|
||||
android:text="@string/fragment_scan_qr_code_explanation_new_conversation"
|
||||
android:textColor="?android:textColorPrimary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -43,7 +43,6 @@
|
||||
android:id="@+id/avatar_background"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:visibility="gone"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:src="@drawable/circle_tintable"
|
||||
@ -56,7 +55,6 @@
|
||||
android:id="@+id/avatar_placeholder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/ic_profile_default"
|
||||
@ -71,7 +69,6 @@
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/avatar_background"
|
||||
app:layout_constraintEnd_toEndOf="@+id/avatar_background"
|
||||
app:layout_constraintStart_toStartOf="@+id/avatar_background"
|
||||
@ -81,7 +78,6 @@
|
||||
android:id="@+id/camera_icon"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:visibility="gone"
|
||||
android:layout_marginStart="35dp"
|
||||
android:layout_marginTop="35dp"
|
||||
android:cropToPadding="false"
|
||||
@ -94,7 +90,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="49dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_weight="1"
|
||||
@ -105,7 +101,7 @@
|
||||
app:layout_constraintBottom_toTopOf="@+id/description_text"
|
||||
app:layout_constraintEnd_toStartOf="@+id/emoji_toggle"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/avatar_background"
|
||||
app:layout_constraintStart_toEndOf="@+id/avatar_background"
|
||||
app:layout_constraintTop_toBottomOf="@+id/title" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiToggle
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
@ -15,6 +16,13 @@
|
||||
android:indeterminate="true"
|
||||
android:progressTint="@color/white" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/qrCodeImageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@color/white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleTextView"
|
||||
style="@style/Signal.Text.Headline"
|
||||
|
@ -115,4 +115,10 @@
|
||||
|
||||
<dimen name="recording_voice_lock_target">-150dp</dimen>
|
||||
|
||||
<dimen name="default_margin">16dp</dimen>
|
||||
<dimen name="drawable_padding">24dp</dimen>
|
||||
<dimen name="text_size">16sp</dimen>
|
||||
<dimen name="normal_padding">16dp</dimen>
|
||||
<dimen name="action_item_height">56dp</dimen>
|
||||
|
||||
</resources>
|
||||
|
@ -293,12 +293,14 @@
|
||||
|
||||
<!-- DeviceListActivity -->
|
||||
<string name="DeviceListActivity_unlink_s">Unlink \'%s\'?</string>
|
||||
<string name="DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive">By unlinking this device, it will no longer be able to send or receive messages.</string>
|
||||
<string name="DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive">This device will no longer be able to send or receive messages.</string>
|
||||
<string name="DeviceListActivity_network_connection_failed">Network connection failed</string>
|
||||
<string name="DeviceListActivity_try_again">Try again</string>
|
||||
<string name="DeviceListActivity_unlinking_device">Unlinking device...</string>
|
||||
<string name="DeviceListActivity_unlinking_device_no_ellipsis">Unlinking device</string>
|
||||
<string name="DeviceListActivity_network_failed">Network failed!</string>
|
||||
<string name="DeviceListActivity_unlinked_device">Successfully unlinked device</string>
|
||||
<string name="DeviceListActivity_edit_device_name">Edit device name</string>
|
||||
|
||||
<!-- DeviceListItem -->
|
||||
<string name="DeviceListItem_unnamed_device">Unnamed device</string>
|
||||
@ -946,7 +948,7 @@
|
||||
<string name="device_link_fragment__link_device">Link device</string>
|
||||
|
||||
<!-- device_list_fragment -->
|
||||
<string name="device_list_fragment__no_devices_linked">No devices linked</string>
|
||||
<string name="device_list_fragment__no_devices_linked">You don\'t have any linked devices yet</string>
|
||||
<string name="device_list_fragment__link_new_device">Link new device</string>
|
||||
|
||||
<!-- experience_upgrade_activity -->
|
||||
@ -1157,7 +1159,7 @@
|
||||
<string name="AndroidManifest__log_submit">Submit debug log</string>
|
||||
<string name="AndroidManifest__media_preview">Media preview</string>
|
||||
<string name="AndroidManifest__message_details">Message details</string>
|
||||
<string name="AndroidManifest__linked_devices">Linked devices</string>
|
||||
<string name="AndroidManifest__linked_devices">Linked Devices</string>
|
||||
<string name="AndroidManifest__invite_friends">Invite friends</string>
|
||||
<string name="AndroidManifest_archived_conversations">Archived conversations</string>
|
||||
<string name="AndroidManifest_remove_photo">Remove photo</string>
|
||||
@ -1572,11 +1574,11 @@
|
||||
<!-- Conversation list activity -->
|
||||
<string name="activity_conversation_list_empty_state_message">Looks like you don\'t have any conversations yet. Get started by messaging a friend.</string>
|
||||
<!-- Settings activity -->
|
||||
<string name="activity_settings_secondary_device_tag">Secondary device</string>
|
||||
<string name="activity_settings_linked_device_tag">Linked device (%s)</string>
|
||||
<string name="activity_settings_public_key_copied_message">Copied to clipboard</string>
|
||||
<string name="activity_settings_share_public_key_button_title">Share Public Key</string>
|
||||
<string name="activity_settings_show_qr_code_button_title">Show QR Code</string>
|
||||
<string name="activity_settings_link_device_button_title">Link Device</string>
|
||||
<string name="activity_settings_linked_devices_button_title">Linked Devices</string>
|
||||
<string name="activity_settings_show_seed_button_title">Show Seed</string>
|
||||
<string name="activity_settings_seed_dialog_title">Your Seed</string>
|
||||
<string name="activity_settings_seed_dialog_copy_button_title">Copy</string>
|
||||
@ -1636,11 +1638,18 @@
|
||||
<string name="view_device_linking_cancel_button_title">Cancel</string>
|
||||
<!-- Scan QR code fragment -->
|
||||
<string name="fragment_scan_qr_code_title">Scan QR Code</string>
|
||||
<string name="fragment_scan_qr_code_explanation">Scan the QR code of the person you\'d like to securely message. They can find their QR code by going into Loki Messenger\'s in-app settings and clicking \"Show QR Code\".</string>
|
||||
<string name="fragment_scan_qr_code_explanation_new_conversation">Scan the QR code of the person you\'d like to securely message. They can find their QR code by going into Loki Messenger\'s in-app settings and clicking \"Show QR Code\".</string>
|
||||
<string name="fragment_scan_qr_code_explanation_link_device">Link to an existing device by going into its in-app settings and clicking \"Link Device\".</string>
|
||||
<string name="fragment_scan_qr_code_camera_permission_dialog_message">Loki Messenger needs camera access to scan QR codes.</string>
|
||||
<!-- Conversation activity -->
|
||||
<string name="activity_conversation_copy_public_key_button_title">Copy public key</string>
|
||||
<!-- Conversation list activity -->
|
||||
<string name="activity_conversation_list_add_public_chat_button_title">Add Public Chat</string>
|
||||
<!-- Device list bottom sheet fragment -->
|
||||
<string name="fragment_device_list_edit_device_name_title">Edit device name</string>
|
||||
<string name="fragment_device_list_unlink_device_title">Unlink device</string>
|
||||
<!-- Device unlink dialog -->
|
||||
<string name="dialog_device_unlink_title">Device unlinked</string>
|
||||
<string name="dialog_device_unlink_message">This device has been successfully unlinked</string>
|
||||
|
||||
</resources>
|
||||
|
@ -242,4 +242,15 @@
|
||||
<item name="colorControlActivated">@color/white</item>
|
||||
</style>
|
||||
|
||||
<style name="ActionItem">
|
||||
<item name="android:textSize">@dimen/text_size</item>
|
||||
<item name="android:drawablePadding">@dimen/drawable_padding</item>
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">@dimen/action_item_height</item>
|
||||
<item name="android:padding">@dimen/normal_padding</item>
|
||||
<item name="android:gravity">center_vertical</item>
|
||||
<item name="android:selectable">true</item>
|
||||
<item name="android:foreground">?attr/selectableItemBackground</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
@ -133,6 +133,7 @@
|
||||
<item name="android:windowBackground">@color/loki_darkest_gray</item>
|
||||
<item name="alertDialogTheme">@style/AppCompatAlertDialogStyleLight</item>
|
||||
<item name="android:alertDialogTheme">@style/AppCompatDialogStyleLight</item>
|
||||
<item name="bottomSheetDialogTheme">@style/Theme.MaterialComponents.Light.BottomSheetDialog</item>
|
||||
<!--<item name="android:windowContentOverlay">@drawable/compat_actionbar_shadow_background</item>-->
|
||||
|
||||
<item name="attachment_type_selector_background">@color/white</item>
|
||||
@ -317,6 +318,7 @@
|
||||
<item name="android:windowBackground">@color/loki_darkest_gray</item>
|
||||
<item name="alertDialogTheme">@style/AppCompatAlertDialogStyleDark</item>
|
||||
<item name="android:alertDialogTheme">@style/AppCompatDialogStyleDark</item>
|
||||
<item name="bottomSheetDialogTheme">@style/Theme.MaterialComponents.BottomSheetDialog</item>
|
||||
|
||||
<item name="attachment_type_selector_background">@color/gray95</item>
|
||||
<item name="attachment_document_icon_small">@drawable/ic_document_small_dark</item>
|
||||
|
@ -33,6 +33,10 @@
|
||||
android:title="@string/preferences__advanced"
|
||||
android:icon="@drawable/ic_advanced_24dp"/> -->
|
||||
|
||||
<Preference android:key="preference_category_linked_devices"
|
||||
android:title="@string/activity_settings_linked_devices_button_title"
|
||||
android:icon="@drawable/icon_link"/>
|
||||
|
||||
<Preference android:key="preference_category_public_key"
|
||||
android:title="@string/activity_settings_share_public_key_button_title"
|
||||
android:icon="@drawable/icon_share"/>
|
||||
@ -41,10 +45,6 @@
|
||||
android:title="@string/activity_settings_show_qr_code_button_title"
|
||||
android:icon="@drawable/icon_qr_code"/>
|
||||
|
||||
<Preference android:key="preference_category_link_device"
|
||||
android:title="Link Device"
|
||||
android:icon="@drawable/icon_link"/>
|
||||
|
||||
<Preference android:key="preference_category_seed"
|
||||
android:title="@string/activity_settings_show_seed_button_title"
|
||||
android:icon="@drawable/icon_seedling"/>
|
||||
|
@ -20,10 +20,13 @@ import android.annotation.SuppressLint;
|
||||
import android.arch.lifecycle.DefaultLifecycleObserver;
|
||||
import android.arch.lifecycle.LifecycleOwner;
|
||||
import android.arch.lifecycle.ProcessLifecycleOwner;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.ContentObserver;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.multidex.MultiDexApplication;
|
||||
@ -38,6 +41,9 @@ import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule;
|
||||
@ -65,10 +71,12 @@ import org.thoughtcrime.securesms.loki.LokiAPIDatabase;
|
||||
import org.thoughtcrime.securesms.loki.LokiPublicChatManager;
|
||||
import org.thoughtcrime.securesms.loki.LokiRSSFeedPoller;
|
||||
import org.thoughtcrime.securesms.loki.LokiUserDatabase;
|
||||
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
|
||||
@ -88,11 +96,11 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol;
|
||||
import org.whispersystems.signalservice.loki.api.LokiDotNetAPI;
|
||||
import org.whispersystems.signalservice.loki.api.LokiPublicChat;
|
||||
import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI;
|
||||
import org.whispersystems.signalservice.loki.api.LokiLongPoller;
|
||||
import org.whispersystems.signalservice.loki.api.LokiP2PAPI;
|
||||
import org.whispersystems.signalservice.loki.api.LokiP2PAPIDelegate;
|
||||
import org.whispersystems.signalservice.loki.api.LokiPublicChat;
|
||||
import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI;
|
||||
import org.whispersystems.signalservice.loki.api.LokiRSSFeed;
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
|
||||
import org.whispersystems.signalservice.loki.utilities.Analytics;
|
||||
@ -154,8 +162,9 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
startKovenant();
|
||||
Log.i(TAG, "onCreate()");
|
||||
checkNeedsDatabaseReset();
|
||||
startKovenant();
|
||||
initializeSecurityProvider();
|
||||
initializeLogging();
|
||||
initializeCrashHandling();
|
||||
@ -196,7 +205,11 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
// Loki - Update device mappings
|
||||
if (setUpStorageAPIIfNeeded()) {
|
||||
LokiStorageAPI.Companion.getShared().updateUserDeviceMappings();
|
||||
if (TextSecurePreferences.needsRevocationCheck(this)) {
|
||||
checkNeedsRevocation();
|
||||
}
|
||||
}
|
||||
updatePublicChatProfileAvatarIfNeeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -587,5 +600,54 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
if (lokiNewsFeedPoller != null) lokiNewsFeedPoller.startIfNeeded();
|
||||
if (lokiMessengerUpdatesFeedPoller != null) lokiMessengerUpdatesFeedPoller.startIfNeeded();
|
||||
}
|
||||
|
||||
public void updatePublicChatProfileAvatarIfNeeded() {
|
||||
AsyncTask.execute(() -> {
|
||||
LokiPublicChatAPI publicChatAPI = getLokiPublicChatAPI();
|
||||
if (publicChatAPI != null) {
|
||||
byte[] profileKey = ProfileKeyUtil.getProfileKey(this);
|
||||
String url = TextSecurePreferences.getProfileAvatarUrl(this);
|
||||
String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(this);
|
||||
if (ourMasterDevice != null) {
|
||||
Recipient masterDevice = Recipient.from(this, Address.fromSerialized(ourMasterDevice), false).resolve();
|
||||
profileKey = masterDevice.getProfileKey();
|
||||
url = masterDevice.getProfileAvatar();
|
||||
}
|
||||
Set<String> servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers();
|
||||
for (String server : servers) {
|
||||
publicChatAPI.setProfilePicture(server, profileKey, url);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// endregion
|
||||
|
||||
public void checkNeedsRevocation() {
|
||||
MultiDeviceUtilities.checkForRevocation(this);
|
||||
}
|
||||
|
||||
public void checkNeedsDatabaseReset() {
|
||||
if (TextSecurePreferences.resetDatabase(this)) {
|
||||
boolean wasUnlinked = TextSecurePreferences.databaseResetFromUnpair(this);
|
||||
TextSecurePreferences.clearAll(this);
|
||||
TextSecurePreferences.setDatabaseResetFromUnpair(this, wasUnlinked); // Loki - Re-set the preference so we can use it in the starting screen to determine whether device was unlinked or not
|
||||
MasterSecretUtil.clear(this);
|
||||
if (this.deleteDatabase("signal.db")) {
|
||||
Log.d("Loki", "Deleted database");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clearData() {
|
||||
TextSecurePreferences.setResetDatabase(this, true);
|
||||
new Handler().postDelayed(this::restartApplication, 200);
|
||||
}
|
||||
|
||||
public void restartApplication() {
|
||||
Intent intent = new Intent(this, ConversationListActivity.class);
|
||||
ComponentName componentName = intent.getComponent();
|
||||
Intent mainIntent = Intent.makeRestartActivityTask(componentName);
|
||||
this.startActivity(mainIntent);
|
||||
Runtime.getRuntime().exit(0);
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Bundle;
|
||||
@ -40,13 +39,8 @@ import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.preference.Preference;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.loki.DeviceLinkingDialog;
|
||||
import org.thoughtcrime.securesms.loki.DeviceLinkingDialogDelegate;
|
||||
import org.thoughtcrime.securesms.loki.DeviceLinkingView;
|
||||
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
|
||||
import org.thoughtcrime.securesms.loki.LinkedDevicesActivity;
|
||||
import org.thoughtcrime.securesms.loki.QRCodeDialog;
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
|
||||
@ -57,7 +51,6 @@ import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation;
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec;
|
||||
import org.whispersystems.signalservice.loki.utilities.Analytics;
|
||||
import org.whispersystems.signalservice.loki.utilities.SerializationKt;
|
||||
@ -89,7 +82,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
// private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
||||
private static final String PREFERENCE_CATEGORY_PUBLIC_KEY = "preference_category_public_key";
|
||||
private static final String PREFERENCE_CATEGORY_QR_CODE = "preference_category_qr_code";
|
||||
private static final String PREFERENCE_CATEGORY_LINK_DEVICE = "preference_category_link_device";
|
||||
private static final String PREFERENCE_CATEGORY_LINKED_DEVICES = "preference_category_linked_devices";
|
||||
private static final String PREFERENCE_CATEGORY_SEED = "preference_category_seed";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
@ -172,15 +165,15 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS));
|
||||
*/
|
||||
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_NOTIFICATIONS));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_NOTIFICATIONS));
|
||||
this.findPreference(PREFERENCE_CATEGORY_APP_PROTECTION)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_APP_PROTECTION));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APP_PROTECTION));
|
||||
/*
|
||||
this.findPreference(PREFERENCE_CATEGORY_APPEARANCE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE));
|
||||
*/
|
||||
this.findPreference(PREFERENCE_CATEGORY_CHATS)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_CHATS));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CHATS));
|
||||
/*
|
||||
this.findPreference(PREFERENCE_CATEGORY_DEVICES)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DEVICES));
|
||||
@ -188,29 +181,18 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
|
||||
*/
|
||||
this.findPreference(PREFERENCE_CATEGORY_PUBLIC_KEY)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_PUBLIC_KEY));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PUBLIC_KEY));
|
||||
this.findPreference(PREFERENCE_CATEGORY_QR_CODE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_QR_CODE));
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_QR_CODE));
|
||||
|
||||
Preference linkDevicePreference = this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE);
|
||||
linkDevicePreference.setOnPreferenceClickListener(new CategoryClickListener(getContext(), PREFERENCE_CATEGORY_LINK_DEVICE));
|
||||
|
||||
// Disable if we hit the cap of 1 linked device
|
||||
if (isMasterDevice) {
|
||||
Context context = getContext();
|
||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
boolean isDeviceLinkingEnabled = DatabaseFactory.getLokiAPIDatabase(context).getPairingAuthorisations(userHexEncodedPublicKey).size() <= 1;
|
||||
linkDevicePreference.setEnabled(isDeviceLinkingEnabled);
|
||||
linkDevicePreference.getIcon().setAlpha(isDeviceLinkingEnabled ? 255 : 124);
|
||||
} else {
|
||||
// Hide if this is a slave device
|
||||
linkDevicePreference.setVisible(false);
|
||||
}
|
||||
Preference linkDevicesPreference = this.findPreference(PREFERENCE_CATEGORY_LINKED_DEVICES);
|
||||
linkDevicesPreference.setVisible(isMasterDevice);
|
||||
linkDevicesPreference.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_LINKED_DEVICES));
|
||||
|
||||
Preference seedPreference = this.findPreference(PREFERENCE_CATEGORY_SEED);
|
||||
// Hide if this is a slave device
|
||||
seedPreference.setVisible(isMasterDevice);
|
||||
seedPreference.setOnPreferenceClickListener(new CategoryClickListener(getContext(), (PREFERENCE_CATEGORY_SEED)));
|
||||
seedPreference.setOnPreferenceClickListener(new CategoryClickListener((PREFERENCE_CATEGORY_SEED)));
|
||||
|
||||
if (VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
tintIcons(getActivity());
|
||||
@ -299,16 +281,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
// this.findPreference(PREFERENCE_CATEGORY_ADVANCED).setIcon(advanced);
|
||||
this.findPreference(PREFERENCE_CATEGORY_PUBLIC_KEY).setIcon(publicKey);
|
||||
this.findPreference(PREFERENCE_CATEGORY_QR_CODE).setIcon(qrCode);
|
||||
this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE).setIcon(linkDevice);
|
||||
this.findPreference(PREFERENCE_CATEGORY_LINKED_DEVICES).setIcon(linkDevice);
|
||||
this.findPreference(PREFERENCE_CATEGORY_SEED).setIcon(seed);
|
||||
}
|
||||
|
||||
private class CategoryClickListener implements Preference.OnPreferenceClickListener, DeviceLinkingDialogDelegate {
|
||||
private class CategoryClickListener implements Preference.OnPreferenceClickListener {
|
||||
private String category;
|
||||
private Context context;
|
||||
|
||||
CategoryClickListener(Context context,String category) {
|
||||
this.context = context;
|
||||
CategoryClickListener(String category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
@ -360,8 +340,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
case PREFERENCE_CATEGORY_QR_CODE:
|
||||
QRCodeDialog.INSTANCE.show(getContext());
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_LINK_DEVICE:
|
||||
DeviceLinkingDialog.Companion.show(getContext(), DeviceLinkingView.Mode.Master, this);
|
||||
case PREFERENCE_CATEGORY_LINKED_DEVICES:
|
||||
Intent intent = new Intent(getActivity(), LinkedDevicesActivity.class);
|
||||
startActivity(intent);
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_SEED:
|
||||
Analytics.Companion.getShared().track("Seed Modal Shown");
|
||||
@ -404,12 +385,6 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override public void sendPairingAuthorizedMessage(@NotNull PairingAuthorisation pairingAuthorisation) {
|
||||
AsyncTask.execute(() -> MultiDeviceUtilities.signAndSendPairingAuthorisationMessage(context, pairingAuthorisation));
|
||||
}
|
||||
@Override public void handleDeviceLinkAuthorized(@NotNull PairingAuthorisation pairingAuthorisation) {}
|
||||
@Override public void handleDeviceLinkingDialogDismissed() {}
|
||||
}
|
||||
|
||||
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
|
||||
|
@ -22,6 +22,7 @@ import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
@ -38,8 +39,14 @@ import android.view.ViewTreeObserver;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.components.RatingManager;
|
||||
import org.thoughtcrime.securesms.components.SearchToolbar;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
@ -48,6 +55,8 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
|
||||
import org.thoughtcrime.securesms.loki.AddPublicChatActivity;
|
||||
import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable;
|
||||
import org.thoughtcrime.securesms.loki.RecipientAvatarModifiedEvent;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
@ -129,6 +138,18 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
EventBus.getDefault().unregister(this);
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
@ -197,45 +218,23 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
outline.setOval(0, 0, view.getWidth(), view.getHeight());
|
||||
}
|
||||
});
|
||||
profilePictureImageView.setClipToOutline(true);
|
||||
|
||||
// Display the correct identicon if we're a secondary device
|
||||
String currentUser = TextSecurePreferences.getLocalNumber(this);
|
||||
String recipientAddress = recipient.getAddress().serialize();
|
||||
String primaryAddress = TextSecurePreferences.getMasterHexEncodedPublicKey(this);
|
||||
String profileAddress = (recipientAddress.equalsIgnoreCase(currentUser) && primaryAddress != null) ? primaryAddress : recipientAddress;
|
||||
String profileAddress = (recipient.isLocalNumber() && primaryAddress != null) ? primaryAddress : recipient.getAddress().serialize();
|
||||
Recipient primaryRecipient = Recipient.from(this, Address.fromSerialized(profileAddress), false);
|
||||
|
||||
profilePictureImageView.setClipToOutline(true);
|
||||
profilePictureImageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
|
||||
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
int width = profilePictureImageView.getWidth();
|
||||
int height = profilePictureImageView.getHeight();
|
||||
if (width == 0 || height == 0) return true;
|
||||
profilePictureImageView.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, profileAddress.toLowerCase());
|
||||
profilePictureImageView.setImageDrawable(identicon);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
String name = Optional.fromNullable(recipient.getName()).or(Optional.fromNullable(TextSecurePreferences.getProfileName(this))).or("");
|
||||
MaterialColor fallbackColor = recipient.getColor();
|
||||
|
||||
if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
|
||||
fallbackColor = ContactColors.generateFor(name);
|
||||
}
|
||||
|
||||
Drawable fallback = new GeneratedContactPhoto(name, R.drawable.ic_profile_default).asDrawable(this, fallbackColor.toAvatarColor(this));
|
||||
Drawable fallback = primaryRecipient.getFallbackContactPhotoDrawable(this, false);
|
||||
|
||||
GlideApp.with(this)
|
||||
.load(new ProfileContactPhoto(recipient.getAddress(), String.valueOf(TextSecurePreferences.getProfileAvatarId(this))))
|
||||
.load(primaryRecipient.getContactPhoto())
|
||||
.fallback(fallback)
|
||||
.error(fallback)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(icon);
|
||||
*/
|
||||
.circleCrop()
|
||||
.into(profilePictureImageView);
|
||||
|
||||
|
||||
profilePictureImageView.setOnClickListener(v -> handleDisplaySettings());
|
||||
}
|
||||
@ -336,4 +335,12 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
private void addNewPublicChat() {
|
||||
startActivity(new Intent(this, AddPublicChatActivity.class));
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onAvatarModified(RecipientAvatarModifiedEvent event) {
|
||||
Recipient recipient = event.getRecipient();
|
||||
if (recipient.isLocalNumber() || recipient.isOurMasterDevice()) {
|
||||
initializeProfileIcon(recipient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,6 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
@ -58,13 +57,16 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
import org.whispersystems.signalservice.loki.api.LokiDotNetAPI;
|
||||
import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI;
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
|
||||
import org.whispersystems.signalservice.loki.utilities.Analytics;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.regex.Matcher;
|
||||
@ -96,6 +98,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
|
||||
private View reveal;
|
||||
|
||||
private Intent nextIntent;
|
||||
private byte[] originalAvatarBytes;
|
||||
private byte[] avatarBytes;
|
||||
private File captureFile;
|
||||
|
||||
@ -301,6 +304,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
|
||||
@Override
|
||||
protected void onPostExecute(byte[] result) {
|
||||
if (result != null) {
|
||||
originalAvatarBytes = result;
|
||||
avatarBytes = result;
|
||||
GlideApp.with(CreateProfileActivity.this)
|
||||
.load(result)
|
||||
@ -314,6 +318,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
|
||||
@Override
|
||||
public void onSuccess(byte[] result) {
|
||||
if (result != null) {
|
||||
originalAvatarBytes = result;
|
||||
avatarBytes = result;
|
||||
GlideApp.with(CreateProfileActivity.this)
|
||||
.load(result)
|
||||
@ -380,7 +385,6 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
Context context = CreateProfileActivity.this;
|
||||
byte[] profileKey = ProfileKeyUtil.getProfileKey(CreateProfileActivity.this);
|
||||
|
||||
Analytics.Companion.getShared().track("Display Name Updated");
|
||||
|
||||
@ -393,31 +397,44 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
|
||||
}
|
||||
}
|
||||
|
||||
// Loki - Original code
|
||||
// ========
|
||||
// try {
|
||||
// accountManager.setProfileName(profileKey, name);
|
||||
// TextSecurePreferences.setProfileName(context, name);
|
||||
// } catch (IOException e) {
|
||||
// Log.w(TAG, e);
|
||||
// return false;
|
||||
// }
|
||||
// ========
|
||||
// Loki - Only update avatar if there was a change
|
||||
if (!Arrays.equals(originalAvatarBytes, avatarBytes)) {
|
||||
try {
|
||||
// Loki - Original profile photo code
|
||||
// ========
|
||||
// accountManager.setProfileAvatar(profileKey, avatar);
|
||||
// ========
|
||||
|
||||
try {
|
||||
// Loki - Original code
|
||||
// ========
|
||||
// accountManager.setProfileAvatar(profileKey, avatar);
|
||||
// ========
|
||||
AvatarHelper.setAvatar(CreateProfileActivity.this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), avatarBytes);
|
||||
TextSecurePreferences.setProfileAvatarId(CreateProfileActivity.this, new SecureRandom().nextInt());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
// Try upload photo with a new profile key
|
||||
String newProfileKey = ProfileKeyUtil.generateEncodedProfileKey(context);
|
||||
byte[] profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(newProfileKey);
|
||||
|
||||
//Loki - Upload the profile photo here
|
||||
if (avatar != null) {
|
||||
Log.d("Loki", "Start uploading profile photo");
|
||||
LokiStorageAPI storageAPI = LokiStorageAPI.shared;
|
||||
LokiDotNetAPI.UploadResult result = storageAPI.uploadProfilePicture(storageAPI.getServer(), profileKey, avatar);
|
||||
Log.d("Loki", "Profile photo uploaded, the url is " + result.getUrl());
|
||||
TextSecurePreferences.setProfileAvatarUrl(context, result.getUrl());
|
||||
} else {
|
||||
TextSecurePreferences.setProfileAvatarUrl(context, null);
|
||||
}
|
||||
|
||||
AvatarHelper.setAvatar(context, Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)), avatarBytes);
|
||||
TextSecurePreferences.setProfileAvatarId(context, new SecureRandom().nextInt());
|
||||
|
||||
// Upload was successful with this new profile key, we should set it so the other users know to re-fetch profiles
|
||||
ProfileKeyUtil.setEncodedProfileKey(context, newProfileKey);
|
||||
|
||||
// Update profile key on the public chat server
|
||||
ApplicationContext.getInstance(context).updatePublicChatProfileAvatarIfNeeded();
|
||||
} catch (Exception e) {
|
||||
Log.d("Loki", "Failed to upload profile photo: " + e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceProfileKeyUpdateJob());
|
||||
|
||||
// ApplicationContext.getInstance(context).getJobManager().add(new MultiDeviceProfileKeyUpdateJob());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.ListFragment;
|
||||
@ -16,29 +15,32 @@ import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.melnykov.fab.FloatingActionButton;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.devicelist.Device;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshUnidentifiedDeliveryAbilityJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.DeviceListBottomSheetFragment;
|
||||
import org.thoughtcrime.securesms.loki.MnemonicUtilities;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import kotlin.Pair;
|
||||
import kotlin.Unit;
|
||||
import network.loki.messenger.R;
|
||||
|
||||
import static org.thoughtcrime.securesms.loki.GeneralUtilitiesKt.toPx;
|
||||
|
||||
public class DeviceListFragment extends ListFragment
|
||||
implements LoaderManager.LoaderCallbacks<List<Device>>,
|
||||
ListView.OnItemClickListener, InjectableType, Button.OnClickListener
|
||||
@ -46,14 +48,14 @@ public class DeviceListFragment extends ListFragment
|
||||
|
||||
private static final String TAG = DeviceListFragment.class.getSimpleName();
|
||||
|
||||
@Inject
|
||||
SignalServiceAccountManager accountManager;
|
||||
|
||||
private File languageFileDirectory;
|
||||
private Locale locale;
|
||||
private View empty;
|
||||
private View progressContainer;
|
||||
private FloatingActionButton addDeviceButton;
|
||||
private Button.OnClickListener addDeviceButtonListener;
|
||||
private Function<String, Void> handleDisconnectDevice;
|
||||
private Function<Pair<String, String>, Void> handleDeviceNameChange;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
@ -71,10 +73,11 @@ public class DeviceListFragment extends ListFragment
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||
View view = inflater.inflate(R.layout.device_list_fragment, container, false);
|
||||
|
||||
this.empty = view.findViewById(R.id.empty);
|
||||
this.progressContainer = view.findViewById(R.id.progress_container);
|
||||
this.addDeviceButton = ViewUtil.findById(view, R.id.add_device);
|
||||
this.empty = view.findViewById(R.id.emptyStateTextView);
|
||||
this.progressContainer = view.findViewById(R.id.activityIndicator);
|
||||
this.addDeviceButton = ViewUtil.findById(view, R.id.addDeviceButton);
|
||||
this.addDeviceButton.setOnClickListener(this);
|
||||
updateAddDeviceButtonVisibility();
|
||||
|
||||
return view;
|
||||
}
|
||||
@ -82,6 +85,7 @@ public class DeviceListFragment extends ListFragment
|
||||
@Override
|
||||
public void onActivityCreated(Bundle bundle) {
|
||||
super.onActivityCreated(bundle);
|
||||
this.languageFileDirectory = MnemonicUtilities.getLanguageFileDirectory(getContext());
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
getListView().setOnItemClickListener(this);
|
||||
}
|
||||
@ -90,12 +94,20 @@ public class DeviceListFragment extends ListFragment
|
||||
this.addDeviceButtonListener = listener;
|
||||
}
|
||||
|
||||
public void setHandleDisconnectDevice(Function<String, Void> handler) {
|
||||
this.handleDisconnectDevice = handler;
|
||||
}
|
||||
|
||||
public void setHandleDeviceNameChange(Function<Pair<String, String>, Void> handler) {
|
||||
this.handleDeviceNameChange = handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<List<Device>> onCreateLoader(int id, Bundle args) {
|
||||
empty.setVisibility(View.GONE);
|
||||
progressContainer.setVisibility(View.VISIBLE);
|
||||
|
||||
return new DeviceListLoader(getActivity(), accountManager);
|
||||
return new DeviceListLoader(getActivity(), languageFileDirectory);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -124,20 +136,63 @@ public class DeviceListFragment extends ListFragment
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
final boolean hasDeviceName = ((DeviceListItem)view).hasDeviceName(); // Tells us whether the name is set to shortId or the device name
|
||||
final String deviceName = ((DeviceListItem)view).getDeviceName();
|
||||
final long deviceId = ((DeviceListItem)view).getDeviceId();
|
||||
final String deviceId = ((DeviceListItem)view).getDeviceId();
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setTitle(getActivity().getString(R.string.DeviceListActivity_unlink_s, deviceName));
|
||||
builder.setMessage(R.string.DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive);
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
handleDisconnectDevice(deviceId);
|
||||
}
|
||||
DeviceListBottomSheetFragment bottomSheet = new DeviceListBottomSheetFragment();
|
||||
bottomSheet.setOnEditTapped(() -> {
|
||||
bottomSheet.dismiss();
|
||||
EditText deviceNameEditText = new EditText(getContext());
|
||||
LinearLayout deviceNameEditTextContainer = new LinearLayout(getContext());
|
||||
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
layoutParams.setMarginStart(toPx(18, getResources()));
|
||||
layoutParams.setMarginEnd(toPx(18, getResources()));
|
||||
deviceNameEditText.setLayoutParams(layoutParams);
|
||||
deviceNameEditTextContainer.addView(deviceNameEditText);
|
||||
deviceNameEditText.setText(hasDeviceName ? deviceName : "");
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setTitle(R.string.DeviceListActivity_edit_device_name);
|
||||
builder.setView(deviceNameEditTextContainer);
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (handleDeviceNameChange != null) { handleDeviceNameChange.apply(new Pair<>(deviceId, deviceNameEditText.getText().toString().trim())); }
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
builder.show();
|
||||
bottomSheet.setOnUnlinkTapped(() -> {
|
||||
bottomSheet.dismiss();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setTitle(getActivity().getString(R.string.DeviceListActivity_unlink_s, deviceName));
|
||||
builder.setMessage(R.string.DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive);
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (handleDisconnectDevice != null) { handleDisconnectDevice.apply(deviceId); }
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
bottomSheet.show(getFragmentManager(), bottomSheet.getTag());
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
updateAddDeviceButtonVisibility();
|
||||
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
|
||||
}
|
||||
|
||||
private void updateAddDeviceButtonVisibility() {
|
||||
if (addDeviceButton != null) {
|
||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
|
||||
boolean isDeviceLinkingEnabled = DatabaseFactory.getLokiAPIDatabase(getContext()).getPairingAuthorisations(userHexEncodedPublicKey).isEmpty();
|
||||
addDeviceButton.setVisibility(isDeviceLinkingEnabled ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLoaderFailed() {
|
||||
@ -167,34 +222,6 @@ public class DeviceListFragment extends ListFragment
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void handleDisconnectDevice(final long deviceId) {
|
||||
new ProgressDialogAsyncTask<Void, Void, Void>(getActivity(),
|
||||
R.string.DeviceListActivity_unlinking_device_no_ellipsis,
|
||||
R.string.DeviceListActivity_unlinking_device)
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try {
|
||||
accountManager.removeDevice(deviceId);
|
||||
|
||||
ApplicationContext.getInstance(getContext())
|
||||
.getJobManager()
|
||||
.add(new RefreshUnidentifiedDeliveryAbilityJob());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
super.onPostExecute(result);
|
||||
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (addDeviceButtonListener != null) addDeviceButtonListener.onClick(v);
|
||||
|
@ -15,10 +15,9 @@ import network.loki.messenger.R;
|
||||
|
||||
public class DeviceListItem extends LinearLayout {
|
||||
|
||||
private long deviceId;
|
||||
private String deviceId;
|
||||
private TextView name;
|
||||
private TextView created;
|
||||
private TextView lastActive;
|
||||
private TextView shortId;
|
||||
|
||||
public DeviceListItem(Context context) {
|
||||
super(context);
|
||||
@ -31,29 +30,19 @@ public class DeviceListItem extends LinearLayout {
|
||||
@Override
|
||||
public void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
this.name = (TextView) findViewById(R.id.name);
|
||||
this.created = (TextView) findViewById(R.id.created);
|
||||
this.lastActive = (TextView) findViewById(R.id.active);
|
||||
this.name = (TextView) findViewById(R.id.name);
|
||||
this.shortId = (TextView) findViewById(R.id.shortId);
|
||||
}
|
||||
|
||||
public void set(Device deviceInfo, Locale locale) {
|
||||
if (TextUtils.isEmpty(deviceInfo.getName())) this.name.setText(R.string.DeviceListItem_unnamed_device);
|
||||
else this.name.setText(deviceInfo.getName());
|
||||
|
||||
this.created.setText(getContext().getString(R.string.DeviceListItem_linked_s,
|
||||
DateUtils.getDayPrecisionTimeSpanString(getContext(),
|
||||
locale,
|
||||
deviceInfo.getCreated())));
|
||||
|
||||
this.lastActive.setText(getContext().getString(R.string.DeviceListItem_last_active_s,
|
||||
DateUtils.getDayPrecisionTimeSpanString(getContext(),
|
||||
locale,
|
||||
deviceInfo.getLastSeen())));
|
||||
|
||||
this.deviceId = deviceInfo.getId();
|
||||
boolean hasName = !TextUtils.isEmpty(deviceInfo.getName());
|
||||
this.name.setText(hasName ? deviceInfo.getName() : deviceInfo.getShortId());
|
||||
this.shortId.setText(deviceInfo.getShortId());
|
||||
this.shortId.setVisibility(hasName ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
public long getDeviceId() {
|
||||
public String getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
@ -61,4 +50,8 @@ public class DeviceListItem extends LinearLayout {
|
||||
return name.getText().toString();
|
||||
}
|
||||
|
||||
public boolean hasDeviceName() {
|
||||
return shortId.getVisibility() == VISIBLE;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
@ -5,6 +5,7 @@ import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
@ -43,7 +44,7 @@ public final class AvatarSelection {
|
||||
CropImage.activity(inputFile)
|
||||
.setGuidelines(CropImageView.Guidelines.ON)
|
||||
.setAspectRatio(1, 1)
|
||||
.setCropShape(CropImageView.CropShape.OVAL)
|
||||
.setCropShape(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? CropImageView.CropShape.RECTANGLE : CropImageView.CropShape.OVAL)
|
||||
.setOutputUri(outputFile)
|
||||
.setAllowRotation(true)
|
||||
.setAllowFlipping(true)
|
||||
|
@ -11,22 +11,23 @@ import android.provider.ContactsContract;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.AppCompatImageView;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewOutlineProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientExporter;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
@ -52,7 +53,9 @@ public class AvatarImageView extends AppCompatImageView {
|
||||
private boolean inverted;
|
||||
private Paint outlinePaint;
|
||||
private OnClickListener listener;
|
||||
private Recipient recipient;
|
||||
|
||||
private @Nullable RecipientContactPhoto recipientContactPhoto;
|
||||
private @NonNull Drawable unknownRecipientDrawable;
|
||||
|
||||
public AvatarImageView(Context context) {
|
||||
super(context);
|
||||
@ -75,23 +78,27 @@ public class AvatarImageView extends AppCompatImageView {
|
||||
|
||||
outlinePaint = ThemeUtil.isDarkTheme(getContext()) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT;
|
||||
setOutlineProvider(new ViewOutlineProvider() {
|
||||
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline) {
|
||||
outline.setOval(0, 0, view.getWidth(), view.getHeight());
|
||||
}
|
||||
});
|
||||
setClipToOutline(true);
|
||||
|
||||
unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_default).asDrawable(getContext(), ContactColors.UNKNOWN_COLOR.toConversationColor(getContext()), inverted);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchDraw(Canvas canvas) {
|
||||
super.dispatchDraw(canvas);
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
float cx = canvas.getWidth() / 2;
|
||||
float cy = canvas.getHeight() / 2;
|
||||
float radius = (canvas.getWidth() / 2) - (outlinePaint.getStrokeWidth() / 2);
|
||||
|
||||
float width = getWidth() - getPaddingRight() - getPaddingLeft();
|
||||
float height = getHeight() - getPaddingBottom() - getPaddingTop();
|
||||
float cx = width / 2f;
|
||||
float cy = height / 2f;
|
||||
float radius = Math.min(cx, cy) - (outlinePaint.getStrokeWidth() / 2f);
|
||||
|
||||
canvas.translate(getPaddingLeft(), getPaddingTop());
|
||||
canvas.drawCircle(cx, cy, radius, outlinePaint);
|
||||
}
|
||||
|
||||
@ -101,39 +108,46 @@ public class AvatarImageView extends AppCompatImageView {
|
||||
super.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
updateImage(w, h);
|
||||
}
|
||||
|
||||
public void update(String hexEncodedPublicKey) {
|
||||
Address address = Address.fromSerialized(hexEncodedPublicKey);
|
||||
if (recipient == null || !address.equals(recipient.getAddress())) {
|
||||
this.recipient = Recipient.from(getContext(), address, false);
|
||||
updateImage();
|
||||
}
|
||||
Recipient recipient = Recipient.from(getContext(), address, false);
|
||||
updateAvatar(recipient);
|
||||
}
|
||||
|
||||
private void updateAvatar(Recipient recipient) {
|
||||
setAvatar(GlideApp.with(getContext()), recipient, false);
|
||||
}
|
||||
|
||||
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
|
||||
if (this.recipient == null || !this.recipient.equals(recipient)) {
|
||||
this.recipient = recipient;
|
||||
updateImage();
|
||||
}
|
||||
/*
|
||||
if (recipient != null) {
|
||||
requestManager.load(recipient.getContactPhoto())
|
||||
.fallback(recipient.getFallbackContactPhotoDrawable(getContext(), inverted))
|
||||
.error(recipient.getFallbackContactPhotoDrawable(getContext(), inverted))
|
||||
if (recipient.isLocalNumber()) {
|
||||
setImageDrawable(new ResourceContactPhoto(R.drawable.ic_note_to_self).asDrawable(getContext(), recipient.getColor().toAvatarColor(getContext()), inverted));
|
||||
} else {
|
||||
RecipientContactPhoto photo = new RecipientContactPhoto(recipient);
|
||||
if (!photo.equals(recipientContactPhoto)) {
|
||||
requestManager.clear(this);
|
||||
recipientContactPhoto = photo;
|
||||
|
||||
Drawable fallbackContactPhotoDrawable = photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted);
|
||||
|
||||
if (photo.contactPhoto != null) {
|
||||
requestManager.load(photo.contactPhoto)
|
||||
.fallback(fallbackContactPhotoDrawable)
|
||||
.error(fallbackContactPhotoDrawable)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.circleCrop()
|
||||
.into(this);
|
||||
setAvatarClickHandler(recipient, quickContactEnabled);
|
||||
} else {
|
||||
setImageDrawable(fallbackContactPhotoDrawable);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setImageDrawable(new ResourceContactPhoto(R.drawable.ic_profile_default).asDrawable(getContext(), ContactColors.UNKNOWN_COLOR.toConversationColor(getContext()), inverted));
|
||||
recipientContactPhoto = null;
|
||||
requestManager.clear(this);
|
||||
setImageDrawable(unknownRecipientDrawable);
|
||||
super.setOnClickListener(listener);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
public void clear(@NonNull GlideRequests glideRequests) {
|
||||
@ -154,32 +168,25 @@ public class AvatarImageView extends AppCompatImageView {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateImage() { updateImage(getWidth(), getHeight()); }
|
||||
private static class RecipientContactPhoto {
|
||||
|
||||
private void updateImage(int w, int h) {
|
||||
if (w == 0 || h == 0 || recipient == null) { return; }
|
||||
private final @NonNull Recipient recipient;
|
||||
private final @Nullable ContactPhoto contactPhoto;
|
||||
private final boolean ready;
|
||||
|
||||
Drawable image;
|
||||
Context context = this.getContext();
|
||||
|
||||
if (recipient.isGroupRecipient()) {
|
||||
String name = Optional.fromNullable(recipient.getName()).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context))).or("");
|
||||
MaterialColor fallbackColor = recipient.getColor();
|
||||
|
||||
if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
|
||||
fallbackColor = ContactColors.generateFor(name);
|
||||
}
|
||||
|
||||
image = new GeneratedContactPhoto(name, R.drawable.ic_profile_default).asDrawable(context, fallbackColor.toAvatarColor(context));
|
||||
} else {
|
||||
// Default to primary device image
|
||||
String ourPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
|
||||
String recipientAddress = recipient.getAddress().serialize();
|
||||
String profileAddress = (ourPrimaryDevice != null && ourPublicKey.equals(recipientAddress)) ? ourPrimaryDevice : recipientAddress;
|
||||
image = new JazzIdenticonDrawable(w, h, profileAddress.toLowerCase());
|
||||
RecipientContactPhoto(@NonNull Recipient recipient) {
|
||||
this.recipient = recipient;
|
||||
this.ready = !recipient.isResolving();
|
||||
this.contactPhoto = recipient.getContactPhoto();
|
||||
}
|
||||
setImageDrawable(image);
|
||||
}
|
||||
|
||||
public boolean equals(@Nullable RecipientContactPhoto other) {
|
||||
if (other == null) return false;
|
||||
|
||||
return other.recipient.equals(recipient) &&
|
||||
other.recipient.getColor().equals(recipient.getColor()) &&
|
||||
other.ready == ready &&
|
||||
Objects.equals(other.contactPhoto, contactPhoto);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3052,12 +3052,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
long originalThreadID = lokiMessageDatabase.getOriginalThreadID(friendRequest.id);
|
||||
long threadId = originalThreadID < 0 ? this.threadId : originalThreadID;
|
||||
|
||||
Address contact = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadId).getAddress();
|
||||
String contactPubKey = contact.toString();
|
||||
Recipient contact = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadId);
|
||||
Address address = contact.getAddress();
|
||||
String contactPubKey = address.serialize();
|
||||
DatabaseFactory.getLokiThreadDatabase(this).setFriendRequestStatus(threadId, LokiThreadFriendRequestStatus.FRIENDS);
|
||||
lokiMessageDatabase.setFriendRequestStatus(friendRequest.id, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED);
|
||||
DatabaseFactory.getRecipientDatabase(this).setProfileSharing(contact, true);
|
||||
MessageSender.sendBackgroundMessageToAllDevices(this, contactPubKey);
|
||||
MessageSender.syncContact(this, contact);
|
||||
MessageSender.syncContact(this, address);
|
||||
updateInputPanel();
|
||||
}
|
||||
|
||||
|
@ -198,6 +198,10 @@ public class MasterSecretUtil {
|
||||
return preferences.getBoolean("passphrase_initialized", false);
|
||||
}
|
||||
|
||||
public static void clear(Context context) {
|
||||
context.getSharedPreferences(PREFERENCES_NAME, 0).edit().clear().commit();
|
||||
}
|
||||
|
||||
private static void save(Context context, String key, int value) {
|
||||
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
|
||||
.edit()
|
||||
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@ -31,8 +32,24 @@ public class ProfileKeyUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized @NonNull byte[] getProfileKeyFromEncodedString(String encodedProfileKey) {
|
||||
try {
|
||||
return Base64.decode(encodedProfileKey);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized @NonNull byte[] rotateProfileKey(@NonNull Context context) {
|
||||
TextSecurePreferences.setProfileKey(context, null);
|
||||
return getProfileKey(context);
|
||||
}
|
||||
|
||||
public static synchronized @NonNull String generateEncodedProfileKey(@NonNull Context context) {
|
||||
return Util.getSecret(32);
|
||||
}
|
||||
|
||||
public static synchronized void setEncodedProfileKey(@NonNull Context context, @Nullable String key) {
|
||||
TextSecurePreferences.setProfileKey(context, key);
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int lokiV3 = 24;
|
||||
private static final int lokiV4 = 25;
|
||||
|
||||
private static final int DATABASE_VERSION = lokiV3; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||
private static final int DATABASE_VERSION = lokiV4; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
|
@ -7,10 +7,15 @@ import android.text.TextUtils;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.Database;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.devicelist.Device;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.MnemonicUtilities;
|
||||
import org.thoughtcrime.securesms.util.AsyncLoader;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECPrivateKey;
|
||||
@ -19,7 +24,10 @@ import org.whispersystems.libsignal.util.ByteUtil;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.MessageDigest;
|
||||
@ -33,93 +41,43 @@ import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import static org.thoughtcrime.securesms.devicelist.DeviceNameProtos.*;
|
||||
import static org.whispersystems.signalservice.loki.utilities.TrimmingKt.removing05PrefixIfNeeded;
|
||||
|
||||
public class DeviceListLoader extends AsyncLoader<List<Device>> {
|
||||
|
||||
private static final String TAG = DeviceListLoader.class.getSimpleName();
|
||||
private MnemonicCodec mnemonicCodec;
|
||||
|
||||
private final SignalServiceAccountManager accountManager;
|
||||
|
||||
public DeviceListLoader(Context context, SignalServiceAccountManager accountManager) {
|
||||
public DeviceListLoader(Context context, File languageFileDirectory) {
|
||||
super(context);
|
||||
this.accountManager = accountManager;
|
||||
this.mnemonicCodec = new MnemonicCodec(languageFileDirectory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Device> loadInBackground() {
|
||||
try {
|
||||
List<Device> devices = Stream.of(accountManager.getDevices())
|
||||
.filter(d -> d.getId() != SignalServiceAddress.DEFAULT_DEVICE_ID)
|
||||
.map(this::mapToDevice)
|
||||
.toList();
|
||||
|
||||
String ourPublicKey = TextSecurePreferences.getLocalNumber(getContext());
|
||||
List<String> secondaryDevicePublicKeys = LokiStorageAPI.shared.getSecondaryDevicePublicKeys(ourPublicKey).get();
|
||||
List<Device> devices = Stream.of(secondaryDevicePublicKeys).map(this::mapToDevice).toList();
|
||||
Collections.sort(devices, new DeviceComparator());
|
||||
|
||||
return devices;
|
||||
} catch (IOException e) {
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Device mapToDevice(@NonNull DeviceInfo deviceInfo) {
|
||||
try {
|
||||
if (TextUtils.isEmpty(deviceInfo.getName()) || deviceInfo.getName().length() < 4) {
|
||||
throw new IOException("Invalid DeviceInfo name.");
|
||||
}
|
||||
|
||||
DeviceName deviceName = DeviceName.parseFrom(Base64.decode(deviceInfo.getName()));
|
||||
|
||||
if (!deviceName.hasCiphertext() || !deviceName.hasEphemeralPublic() || !deviceName.hasSyntheticIv()) {
|
||||
throw new IOException("Got a DeviceName that wasn't properly populated.");
|
||||
}
|
||||
|
||||
byte[] syntheticIv = deviceName.getSyntheticIv().toByteArray();
|
||||
byte[] cipherText = deviceName.getCiphertext().toByteArray();
|
||||
ECPrivateKey identityKey = IdentityKeyUtil.getIdentityKeyPair(getContext()).getPrivateKey();
|
||||
ECPublicKey ephemeralPublic = Curve.decodePoint(deviceName.getEphemeralPublic().toByteArray(), 0);
|
||||
byte[] masterSecret = Curve.calculateAgreement(ephemeralPublic, identityKey);
|
||||
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(masterSecret, "HmacSHA256"));
|
||||
byte[] cipherKeyPart1 = mac.doFinal("cipher".getBytes());
|
||||
|
||||
mac.init(new SecretKeySpec(cipherKeyPart1, "HmacSHA256"));
|
||||
byte[] cipherKey = mac.doFinal(syntheticIv);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(new byte[16]));
|
||||
final byte[] plaintext = cipher.doFinal(cipherText);
|
||||
|
||||
mac.init(new SecretKeySpec(masterSecret, "HmacSHA256"));
|
||||
byte[] verificationPart1 = mac.doFinal("auth".getBytes());
|
||||
|
||||
mac.init(new SecretKeySpec(verificationPart1, "HmacSHA256"));
|
||||
byte[] verificationPart2 = mac.doFinal(plaintext);
|
||||
byte[] ourSyntheticIv = ByteUtil.trim(verificationPart2, 16);
|
||||
|
||||
if (!MessageDigest.isEqual(ourSyntheticIv, syntheticIv)) {
|
||||
throw new GeneralSecurityException("The computed syntheticIv didn't match the actual syntheticIv.");
|
||||
}
|
||||
|
||||
return new Device(deviceInfo.getId(), new String(plaintext), deviceInfo.getCreated(), deviceInfo.getLastSeen());
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed while reading the protobuf.", e);
|
||||
} catch (GeneralSecurityException | InvalidKeyException e) {
|
||||
Log.w(TAG, "Failed during decryption.", e);
|
||||
}
|
||||
|
||||
return new Device(deviceInfo.getId(), deviceInfo.getName(), deviceInfo.getCreated(), deviceInfo.getLastSeen());
|
||||
private Device mapToDevice(@NonNull String hexEncodedPublicKey) {
|
||||
String shortId = MnemonicUtilities.getFirst3Words(mnemonicCodec, hexEncodedPublicKey);
|
||||
String name = DatabaseFactory.getLokiUserDatabase(getContext()).getDisplayName(hexEncodedPublicKey);
|
||||
return new Device(hexEncodedPublicKey, shortId, name);
|
||||
}
|
||||
|
||||
private static class DeviceComparator implements Comparator<Device> {
|
||||
|
||||
@Override
|
||||
public int compare(Device lhs, Device rhs) {
|
||||
if (lhs.getCreated() < rhs.getCreated()) return -1;
|
||||
else if (lhs.getCreated() != rhs.getCreated()) return 1;
|
||||
else return 0;
|
||||
return lhs.getName().compareTo(rhs.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,31 +2,19 @@ package org.thoughtcrime.securesms.devicelist;
|
||||
|
||||
public class Device {
|
||||
|
||||
private final long id;
|
||||
private final String id;
|
||||
private final String shortId;
|
||||
private final String name;
|
||||
private final long created;
|
||||
private final long lastSeen;
|
||||
|
||||
public Device(long id, String name, long created, long lastSeen) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.created = created;
|
||||
this.lastSeen = lastSeen;
|
||||
public Device(String id, String shortId, String name) {
|
||||
this.id = id;
|
||||
this.shortId = shortId;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public long getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public long getLastSeen() {
|
||||
return lastSeen;
|
||||
}
|
||||
public String getShortId() { return shortId; }
|
||||
public String getName() { return name; }
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
@ -14,7 +15,6 @@ import android.util.Pair;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.gms.common.util.IOUtils;
|
||||
|
||||
import org.signal.libsignal.metadata.InvalidMetadataMessageException;
|
||||
import org.signal.libsignal.metadata.InvalidMetadataVersionException;
|
||||
@ -130,6 +130,7 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.loki.api.DeviceLinkingSession;
|
||||
import org.whispersystems.signalservice.loki.api.LokiAPI;
|
||||
import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI;
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation;
|
||||
import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher;
|
||||
@ -139,7 +140,6 @@ import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestSt
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiThreadSessionResetStatus;
|
||||
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
@ -147,11 +147,13 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import kotlin.Unit;
|
||||
import network.loki.messenger.R;
|
||||
import nl.komponents.kovenant.Promise;
|
||||
|
||||
public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
|
||||
@ -288,21 +290,14 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
// Loki - Handle friend request acceptance if needed
|
||||
acceptFriendRequestIfNeeded(envelope, content);
|
||||
|
||||
// Loki - Store pre key bundle if needed
|
||||
// Loki - Store pre key bundle
|
||||
// We shouldn't store it if it's a pairing message
|
||||
if (!content.getPairingAuthorisation().isPresent()) {
|
||||
storePreKeyBundleIfNeeded(envelope, content);
|
||||
}
|
||||
|
||||
if (content.lokiServiceMessage.isPresent()) {
|
||||
LokiServiceMessage lokiMessage = content.lokiServiceMessage.get();
|
||||
if (lokiMessage.getPreKeyBundleMessage() != null) {
|
||||
int registrationID = TextSecurePreferences.getLocalRegistrationId(context);
|
||||
LokiPreKeyBundleDatabase lokiPreKeyBundleDatabase = DatabaseFactory.getLokiPreKeyBundleDatabase(context);
|
||||
|
||||
// Only store the pre key bundle if we don't have one in our database
|
||||
if (registrationID > 0 && !lokiPreKeyBundleDatabase.hasPreKeyBundle(envelope.getSource())) {
|
||||
Log.d("Loki", "Received a pre key bundle from: " + envelope.getSource() + ".");
|
||||
PreKeyBundle preKeyBundle = lokiMessage.getPreKeyBundleMessage().getPreKeyBundle(registrationID);
|
||||
lokiPreKeyBundleDatabase.setPreKeyBundle(envelope.getSource(), preKeyBundle);
|
||||
}
|
||||
}
|
||||
|
||||
if (lokiMessage.getAddressMessage() != null) {
|
||||
// TODO: Loki - Handle address message
|
||||
}
|
||||
@ -311,43 +306,58 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
// Loki - Store the sender display name if needed
|
||||
Optional<String> rawSenderDisplayName = content.senderDisplayName;
|
||||
if (rawSenderDisplayName.isPresent() && rawSenderDisplayName.get().length() > 0) {
|
||||
setDisplayName(envelope.getSource(), rawSenderDisplayName.get());
|
||||
|
||||
// If we got a name from our primary device then we also set that
|
||||
// If we got a name from our primary device then we set our profile name to match it
|
||||
String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
|
||||
if (ourPrimaryDevice != null && envelope.getSource().equals(ourPrimaryDevice)) {
|
||||
TextSecurePreferences.setProfileName(context, rawSenderDisplayName.get());
|
||||
}
|
||||
|
||||
// If we receive a message from our device then don't set the display name in the database (as we probably have a alias set for them)
|
||||
MultiDeviceUtilities.isOneOfOurDevices(context, Address.fromSerialized(content.getSender())).success(isOneOfOurDevice -> {
|
||||
if (!isOneOfOurDevice) { setDisplayName(envelope.getSource(), rawSenderDisplayName.get()); }
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Deleting the display name
|
||||
if (content.getPairingAuthorisation().isPresent()) {
|
||||
handlePairingMessage(content.getPairingAuthorisation().get(), envelope, content);
|
||||
} else if (content.getDataMessage().isPresent()) {
|
||||
SignalServiceDataMessage message = content.getDataMessage().get();
|
||||
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent();
|
||||
|
||||
if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
|
||||
else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId);
|
||||
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId);
|
||||
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId, Optional.absent());
|
||||
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, Optional.absent());
|
||||
if (!envelope.isFriendRequest() && message.isUnpairingRequest()) {
|
||||
// Make sure we got the request from our primary device
|
||||
String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
|
||||
if (ourPrimaryDevice != null && ourPrimaryDevice.equals(content.getSender())) {
|
||||
TextSecurePreferences.setDatabaseResetFromUnpair(context, true);
|
||||
MultiDeviceUtilities.checkForRevocation(context);
|
||||
}
|
||||
} else {
|
||||
if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
|
||||
else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId);
|
||||
else if (message.isExpirationUpdate())
|
||||
handleExpirationUpdate(content, message, smsMessageId);
|
||||
else if (isMediaMessage)
|
||||
handleMediaMessage(content, message, smsMessageId, Optional.absent());
|
||||
else if (message.getBody().isPresent())
|
||||
handleTextMessage(content, message, smsMessageId, Optional.absent());
|
||||
|
||||
if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))) {
|
||||
handleUnknownGroupMessage(content, message.getGroupInfo().get());
|
||||
if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))) {
|
||||
handleUnknownGroupMessage(content, message.getGroupInfo().get());
|
||||
}
|
||||
|
||||
if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
|
||||
handleProfileKey(content, message);
|
||||
}
|
||||
|
||||
// Loki - This doesn't get invoked for group chats
|
||||
if (content.isNeedsReceipt()) {
|
||||
handleNeedsDeliveryReceipt(content, message);
|
||||
}
|
||||
|
||||
// Loki - Handle friend request logic if needed
|
||||
updateFriendRequestStatusIfNeeded(envelope, content, message);
|
||||
}
|
||||
|
||||
if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
|
||||
handleProfileKey(content, message);
|
||||
}
|
||||
|
||||
// Loki - This doesn't get invoked for group chats
|
||||
if (content.isNeedsReceipt()) {
|
||||
handleNeedsDeliveryReceipt(content, message);
|
||||
}
|
||||
|
||||
// Loki - Handle friend request logic if needed
|
||||
updateFriendRequestStatusIfNeeded(envelope, content, message);
|
||||
} else if (content.getSyncMessage().isPresent()) {
|
||||
TextSecurePreferences.setMultiDevice(context, true);
|
||||
|
||||
@ -716,6 +726,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get());
|
||||
}
|
||||
|
||||
String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
|
||||
boolean isSenderMasterDevice = ourMasterDevice != null && ourMasterDevice.equals(content.getSender());
|
||||
if (message.getMessage().getProfileKey().isPresent()) {
|
||||
Recipient recipient = null;
|
||||
|
||||
@ -726,6 +738,16 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
|
||||
}
|
||||
|
||||
// Loki - If we received a sync message from our master device then we need to extract the avatar url
|
||||
if (isSenderMasterDevice) {
|
||||
handleProfileKey(content, message.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Loki - Update display name from master device
|
||||
if (isSenderMasterDevice && content.senderDisplayName.isPresent() && content.senderDisplayName.get().length() > 0) {
|
||||
TextSecurePreferences.setProfileName(context, content.senderDisplayName.get());
|
||||
}
|
||||
|
||||
if (threadId != null) {
|
||||
@ -859,18 +881,23 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
database.endTransaction();
|
||||
}
|
||||
|
||||
// Loki - Store message server ID
|
||||
updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);
|
||||
|
||||
// Loki - Update mapping of message to original thread id
|
||||
if (insertResult.isPresent()) {
|
||||
MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
|
||||
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
||||
long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient);
|
||||
lokiMessageDatabase.setOriginalThreadID(insertResult.get().getMessageId(), originalThreadId);
|
||||
}
|
||||
|
||||
// Loki - Run db updates in the background, we should look into fixing this in the future
|
||||
AsyncTask.execute(() -> {
|
||||
// Loki - Store message server ID
|
||||
updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);
|
||||
|
||||
// Loki - Update mapping of message to original thread id
|
||||
if (insertResult.isPresent()) {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
||||
long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient);
|
||||
lokiMessageDatabase.setOriginalThreadID(insertResult.get().getMessageId(), originalThreadId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private long handleSynchronizeSentExpirationUpdate(@NonNull SentTranscriptMessage message) throws MmsException {
|
||||
@ -1014,35 +1041,37 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
// Insert the message into the database
|
||||
Optional<InsertResult> insertResult = database.insertMessageInbox(textMessage);
|
||||
|
||||
Long messageId = null;
|
||||
if (insertResult.isPresent()) {
|
||||
threadId = insertResult.get().getThreadId();
|
||||
messageId = insertResult.get().getMessageId();
|
||||
}
|
||||
|
||||
if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get());
|
||||
|
||||
// Loki - Cache the user hex encoded public key (for mentions)
|
||||
if (threadId != null) {
|
||||
LokiAPIUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(threadId, context);
|
||||
LokiAPI.Companion.cache(textMessage.getSender().serialize(), threadId);
|
||||
}
|
||||
|
||||
// Loki - Store message server ID
|
||||
updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);
|
||||
|
||||
// Loki - Update mapping of message to original thread id
|
||||
if (messageId != null) {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
||||
long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient);
|
||||
lokiMessageDatabase.setOriginalThreadID(messageId, originalThreadId);
|
||||
}
|
||||
|
||||
boolean isGroupMessage = message.getGroupInfo().isPresent();
|
||||
if (threadId != null && !isGroupMessage) {
|
||||
MessageNotifier.updateNotification(context, threadId);
|
||||
}
|
||||
|
||||
// Loki - Run db updates in background, we should look into fixing this in the future
|
||||
AsyncTask.execute(() -> {
|
||||
if (insertResult.isPresent()) {
|
||||
InsertResult result = insertResult.get();
|
||||
// Loki - Cache the user hex encoded public key (for mentions)
|
||||
LokiAPIUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(result.getThreadId(), context);
|
||||
LokiAPI.Companion.cache(textMessage.getSender().serialize(), result.getThreadId());
|
||||
|
||||
// Loki - Store message server ID
|
||||
updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);
|
||||
|
||||
// Loki - Update mapping of message to original thread id
|
||||
if (result.getMessageId() > -1) {
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
|
||||
long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient);
|
||||
lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1066,19 +1095,26 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
return authorisation.verify();
|
||||
}
|
||||
|
||||
private void handleProfileAvatar(SignalServiceContent content, String url) {
|
||||
Recipient primaryDevice = getPrimaryDeviceRecipient(content.getSender());
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new RetrieveProfileAvatarJob(primaryDevice, url));
|
||||
}
|
||||
|
||||
private void handlePairingMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
|
||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
if (authorisation.getType() == PairingAuthorisation.Type.REQUEST) {
|
||||
handlePairingRequestMessage(authorisation);
|
||||
handlePairingRequestMessage(authorisation, envelope, content);
|
||||
} else if (authorisation.getSecondaryDevicePublicKey().equals(userHexEncodedPublicKey)) {
|
||||
handlePairingAuthorisationMessage(authorisation, envelope, content);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePairingRequestMessage(@NonNull PairingAuthorisation authorisation) {
|
||||
private void handlePairingRequestMessage(@NonNull PairingAuthorisation authorisation, @NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
|
||||
boolean isValid = isValidPairingMessage(authorisation);
|
||||
DeviceLinkingSession linkingSession = DeviceLinkingSession.Companion.getShared();
|
||||
if (isValid && linkingSession.isListeningForLinkingRequests()) {
|
||||
// Loki - If we successfully received a request then we should store the PreKeyBundle
|
||||
storePreKeyBundleIfNeeded(envelope, content);
|
||||
linkingSession.processLinkingRequest(authorisation);
|
||||
}
|
||||
}
|
||||
@ -1101,6 +1137,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
}
|
||||
if (authorisation.getType() != PairingAuthorisation.Type.GRANT) { return; }
|
||||
Log.d("Loki", "Received pairing authorisation message from: " + authorisation.getPrimaryDevicePublicKey() + ".");
|
||||
// Save PreKeyBundle if for whatever reason we got one
|
||||
storePreKeyBundleIfNeeded(envelope, content);
|
||||
// Process
|
||||
DeviceLinkingSession.Companion.getShared().processLinkingAuthorization(authorisation);
|
||||
// Store the primary device's public key
|
||||
@ -1118,7 +1156,10 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
if (content.senderDisplayName.isPresent() && content.senderDisplayName.get().length() > 0) {
|
||||
TextSecurePreferences.setProfileName(context, content.senderDisplayName.get());
|
||||
}
|
||||
|
||||
// Profile avatar updates
|
||||
if (content.getDataMessage().isPresent()) {
|
||||
handleProfileKey(content, content.getDataMessage().get());
|
||||
}
|
||||
// Contact sync
|
||||
if (content.getSyncMessage().isPresent() && content.getSyncMessage().get().getContacts().isPresent()) {
|
||||
handleSynchronizeContactMessage(content.getSyncMessage().get().getContacts().get());
|
||||
@ -1138,6 +1179,23 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
}
|
||||
}
|
||||
|
||||
private void storePreKeyBundleIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
|
||||
if (content.lokiServiceMessage.isPresent()) {
|
||||
LokiServiceMessage lokiMessage = content.lokiServiceMessage.get();
|
||||
if (lokiMessage.getPreKeyBundleMessage() != null) {
|
||||
int registrationID = TextSecurePreferences.getLocalRegistrationId(context);
|
||||
LokiPreKeyBundleDatabase lokiPreKeyBundleDatabase = DatabaseFactory.getLokiPreKeyBundleDatabase(context);
|
||||
|
||||
// Store the latest PreKeyBundle
|
||||
if (registrationID > 0) {
|
||||
Log.d("Loki", "Received a pre key bundle from: " + envelope.getSource() + ".");
|
||||
PreKeyBundle preKeyBundle = lokiMessage.getPreKeyBundleMessage().getPreKeyBundle(registrationID);
|
||||
lokiPreKeyBundleDatabase.setPreKeyBundle(envelope.getSource(), preKeyBundle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptFriendRequestIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content) {
|
||||
// If we get anything other than a friend request, we can assume that we have a session with the other user
|
||||
if (envelope.isFriendRequest() || isGroupChatMessage(content)) { return; }
|
||||
@ -1159,6 +1217,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
if (syncContact) {
|
||||
MessageSender.syncContact(context, contactID.getAddress());
|
||||
}
|
||||
// Allow profile sharing with contact
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(contactID, true);
|
||||
// Update the last message if needed
|
||||
LokiStorageAPI.shared.getPrimaryDevicePublicKey(pubKey).success(primaryDevice -> {
|
||||
Util.runOnMain(() -> {
|
||||
@ -1172,7 +1232,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
|
||||
if (!envelope.isFriendRequest() || message.isGroupUpdate()) { return; }
|
||||
// This handles the case where another user sends us a regular message without authorisation
|
||||
boolean shouldBecomeFriends = PromiseUtil.get(MultiDeviceUtilities.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context), false);
|
||||
Promise<Boolean, Exception> promise = PromiseUtil.timeout(MultiDeviceUtilities.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context), 8000);
|
||||
boolean shouldBecomeFriends = PromiseUtil.get(promise, false);
|
||||
if (shouldBecomeFriends) {
|
||||
// Become friends AND update the message they sent
|
||||
becomeFriendsWithContact(content.getSender(), true);
|
||||
@ -1369,14 +1430,25 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
private void handleProfileKey(@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceDataMessage message)
|
||||
{
|
||||
if (!message.getProfileKey().isPresent()) { return; }
|
||||
|
||||
/*
|
||||
If we get a profile key then we don't need to map it to the primary device.
|
||||
For now a profile key is mapped one-to-one to avoid secondary devices setting the incorrect avatar for a primary device.
|
||||
*/
|
||||
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
|
||||
Address sourceAddress = Address.fromSerialized(content.getSender());
|
||||
Recipient recipient = Recipient.from(context, sourceAddress, false);
|
||||
Recipient recipient = Recipient.from(context, Address.fromSerialized(content.getSender()), false);
|
||||
|
||||
if (recipient.getProfileKey() == null || !MessageDigest.isEqual(recipient.getProfileKey(), message.getProfileKey().get())) {
|
||||
database.setProfileKey(recipient, message.getProfileKey().get());
|
||||
database.setUnidentifiedAccessMode(recipient, RecipientDatabase.UnidentifiedAccessMode.UNKNOWN);
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new RetrieveProfileJob(recipient));
|
||||
String url = content.senderProfileAvatarUrl.or("");
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new RetrieveProfileAvatarJob(recipient, url));
|
||||
|
||||
// Loki - If the recipient is our master device then we need to go and update our avatar mappings on the public chats
|
||||
if (recipient.isOurMasterDevice()) {
|
||||
ApplicationContext.getInstance(context).updatePublicChatProfileAvatarIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1631,7 +1703,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
*/
|
||||
private Recipient getPrimaryDeviceRecipient(String pubKey) {
|
||||
try {
|
||||
String primaryDevice = LokiStorageAPI.shared.getPrimaryDevicePublicKey(pubKey).get();
|
||||
String primaryDevice = PromiseUtil.timeout(LokiStorageAPI.shared.getPrimaryDevicePublicKey(pubKey), 5000).get();
|
||||
String publicKey = (primaryDevice != null) ? primaryDevice : pubKey;
|
||||
// If the public key matches our primary device then we need to forward the message to ourselves (Note to self)
|
||||
String ourPrimaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
|
||||
@ -1696,7 +1768,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
} else if (content.getSyncMessage().isPresent()) {
|
||||
try {
|
||||
// We should ignore a sync message if the sender is not one of our devices
|
||||
boolean isOurDevice = MultiDeviceUtilities.isOneOfOurDevices(context, sender.getAddress()).get();
|
||||
boolean isOurDevice = PromiseUtil.timeout(MultiDeviceUtilities.isOneOfOurDevices(context, sender.getAddress()), 5000).get();
|
||||
if (!isOurDevice) {
|
||||
Log.w(TAG, "Got a sync message from a device that is not ours!.");
|
||||
}
|
||||
|
@ -25,22 +25,26 @@ public abstract class PushReceivedJob extends BaseJob {
|
||||
|
||||
public void processEnvelope(@NonNull SignalServiceEnvelope envelope) {
|
||||
synchronized (RECEIVE_LOCK) {
|
||||
if (envelope.hasSource()) {
|
||||
Address source = Address.fromExternal(context, envelope.getSource());
|
||||
Recipient recipient = Recipient.from(context, source, false);
|
||||
try {
|
||||
if (envelope.hasSource()) {
|
||||
Address source = Address.fromExternal(context, envelope.getSource());
|
||||
Recipient recipient = Recipient.from(context, source, false);
|
||||
|
||||
if (!isActiveNumber(recipient)) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setRegistered(recipient, RecipientDatabase.RegisteredState.REGISTERED);
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(recipient, false));
|
||||
if (!isActiveNumber(recipient)) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setRegistered(recipient, RecipientDatabase.RegisteredState.REGISTERED);
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new DirectoryRefreshJob(recipient, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (envelope.isReceipt()) {
|
||||
handleReceipt(envelope);
|
||||
} else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender() || envelope.isFriendRequest()) {
|
||||
handleMessage(envelope);
|
||||
} else {
|
||||
Log.w(TAG, "Received envelope of unknown type: " + envelope.getType());
|
||||
if (envelope.isReceipt()) {
|
||||
handleReceipt(envelope);
|
||||
} else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender() || envelope.isFriendRequest()) {
|
||||
handleMessage(envelope);
|
||||
} else {
|
||||
Log.w(TAG, "Received envelope of unknown type: " + envelope.getType());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d("Loki", "Failed to process envelope: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
|
||||
.setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize())
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.HOURS.toMillis(1))
|
||||
.setMaxInstances(1)
|
||||
.setMaxAttempts(2)
|
||||
.build(),
|
||||
recipient,
|
||||
profileAvatar);
|
||||
@ -99,8 +99,8 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType
|
||||
File downloadDestination = File.createTempFile("avatar", "jpg", context.getCacheDir());
|
||||
|
||||
try {
|
||||
InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, MAX_PROFILE_SIZE_BYTES);
|
||||
File decryptDestination = File.createTempFile("avatar", "jpg", context.getCacheDir());
|
||||
InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, MAX_PROFILE_SIZE_BYTES);
|
||||
File decryptDestination = File.createTempFile("avatar", "jpg", context.getCacheDir());
|
||||
|
||||
Util.copy(avatarStream, new FileOutputStream(decryptDestination));
|
||||
decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.getAddress()));
|
||||
|
29
src/org/thoughtcrime/securesms/loki/DeviceLinkingDelegate.kt
Normal file
29
src/org/thoughtcrime/securesms/loki/DeviceLinkingDelegate.kt
Normal file
@ -0,0 +1,29 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
|
||||
// Loki - TODO: Remove this yucky delegate pattern for device linking dialog once we have the redesign
|
||||
interface DeviceLinkingDelegate {
|
||||
companion object {
|
||||
fun combine(vararg delegates: DeviceLinkingDelegate?): DeviceLinkingDelegate {
|
||||
val validDelegates = delegates.filterNotNull()
|
||||
return object : DeviceLinkingDelegate {
|
||||
override fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
|
||||
for (delegate in validDelegates) { delegate.handleDeviceLinkAuthorized(pairingAuthorisation) }
|
||||
}
|
||||
|
||||
override fun handleDeviceLinkingDialogDismissed() {
|
||||
for (delegate in validDelegates) { delegate.handleDeviceLinkingDialogDismissed() }
|
||||
}
|
||||
|
||||
override fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {
|
||||
for (delegate in validDelegates) { delegate.sendPairingAuthorizedMessage(pairingAuthorisation) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {}
|
||||
fun handleDeviceLinkingDialogDismissed() {}
|
||||
fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {}
|
||||
}
|
@ -8,13 +8,12 @@ import org.whispersystems.signalservice.loki.api.DeviceLinkingSession
|
||||
import org.whispersystems.signalservice.loki.api.DeviceLinkingSessionListener
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
|
||||
class DeviceLinkingDialog private constructor(private val context: Context, private val mode: DeviceLinkingView.Mode, private val delegate: DeviceLinkingDialogDelegate?) : DeviceLinkingViewDelegate, DeviceLinkingSessionListener {
|
||||
class DeviceLinkingDialog private constructor(private val context: Context, private val mode: DeviceLinkingView.Mode, private val delegate: DeviceLinkingDelegate?) : DeviceLinkingDelegate, DeviceLinkingSessionListener {
|
||||
private lateinit var view: DeviceLinkingView
|
||||
private lateinit var dialog: AlertDialog
|
||||
|
||||
companion object {
|
||||
|
||||
fun show(context: Context, mode: DeviceLinkingView.Mode, delegate: DeviceLinkingDialogDelegate?): DeviceLinkingDialog {
|
||||
fun show(context: Context, mode: DeviceLinkingView.Mode, delegate: DeviceLinkingDelegate?): DeviceLinkingDialog {
|
||||
val dialog = DeviceLinkingDialog(context, mode, delegate)
|
||||
dialog.show()
|
||||
return dialog
|
||||
@ -22,8 +21,10 @@ class DeviceLinkingDialog private constructor(private val context: Context, priv
|
||||
}
|
||||
|
||||
private fun show() {
|
||||
view = DeviceLinkingView(context, mode, this)
|
||||
val delegate = DeviceLinkingDelegate.combine(this, this.delegate)
|
||||
view = DeviceLinkingView(context, mode, delegate)
|
||||
dialog = AlertDialog.Builder(context).setView(view).show()
|
||||
dialog.setCanceledOnTouchOutside(false)
|
||||
view.dismiss = { dismiss() }
|
||||
DeviceLinkingSession.shared.startListeningForLinkingRequests()
|
||||
DeviceLinkingSession.shared.addListener(this)
|
||||
@ -35,20 +36,11 @@ class DeviceLinkingDialog private constructor(private val context: Context, priv
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
override fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
|
||||
delegate?.handleDeviceLinkAuthorized(pairingAuthorisation)
|
||||
}
|
||||
|
||||
override fun handleDeviceLinkingDialogDismissed() {
|
||||
if (mode == DeviceLinkingView.Mode.Master && view.pairingAuthorisation != null) {
|
||||
val authorisation = view.pairingAuthorisation!!
|
||||
DatabaseFactory.getLokiPreKeyBundleDatabase(context).removePreKeyBundle(authorisation.secondaryDevicePublicKey)
|
||||
}
|
||||
delegate?.handleDeviceLinkingDialogDismissed()
|
||||
}
|
||||
|
||||
override fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {
|
||||
delegate?.sendPairingAuthorizedMessage(pairingAuthorisation)
|
||||
}
|
||||
|
||||
override fun requestUserAuthorization(authorisation: PairingAuthorisation) {
|
||||
|
@ -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.os.Handler
|
||||
import android.util.AttributeSet
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_device_linking.view.*
|
||||
import kotlinx.android.synthetic.main.view_device_linking.view.cancelButton
|
||||
import kotlinx.android.synthetic.main.view_device_linking.view.explanationTextView
|
||||
import kotlinx.android.synthetic.main.view_device_linking.view.titleTextView
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.qr.QrCode
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||
import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class DeviceLinkingView private constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, private val mode: Mode, private var delegate: DeviceLinkingViewDelegate) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
private lateinit var languageFileDirectory: File
|
||||
class DeviceLinkingView private constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, private val mode: Mode, private var delegate: DeviceLinkingDelegate) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
private val languageFileDirectory: File = MnemonicUtilities.getLanguageFileDirectory(context)
|
||||
var dismiss: (() -> Unit)? = null
|
||||
var pairingAuthorisation: PairingAuthorisation? = null
|
||||
private set
|
||||
@ -27,36 +31,14 @@ class DeviceLinkingView private constructor(context: Context, attrs: AttributeSe
|
||||
// endregion
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context, mode: Mode, delegate: DeviceLinkingViewDelegate) : this(context, null, 0, mode, delegate)
|
||||
private constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0, Mode.Master, object : DeviceLinkingViewDelegate { }) // Just pass in a dummy mode
|
||||
constructor(context: Context, mode: Mode, delegate: DeviceLinkingDelegate) : this(context, null, 0, mode, delegate)
|
||||
private constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0, Mode.Master, object : DeviceLinkingDelegate { }) // Just pass in a dummy mode
|
||||
private constructor(context: Context) : this(context, null)
|
||||
|
||||
init {
|
||||
setUpLanguageFileDirectory()
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private fun setUpLanguageFileDirectory() {
|
||||
val languages = listOf( "english", "japanese", "portuguese", "spanish" )
|
||||
val directory = File(context.applicationInfo.dataDir)
|
||||
for (language in languages) {
|
||||
val fileName = "$language.txt"
|
||||
if (directory.list().contains(fileName)) { continue }
|
||||
val inputStream = context.assets.open("mnemonic/$fileName")
|
||||
val file = File(directory, fileName)
|
||||
val outputStream = FileOutputStream(file)
|
||||
val buffer = ByteArray(1024)
|
||||
while (true) {
|
||||
val count = inputStream.read(buffer)
|
||||
if (count < 0) { break }
|
||||
outputStream.write(buffer, 0, count)
|
||||
}
|
||||
inputStream.close()
|
||||
outputStream.close()
|
||||
}
|
||||
languageFileDirectory = directory
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
inflate(context, R.layout.view_device_linking, this)
|
||||
spinner.indeterminateDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)
|
||||
@ -72,11 +54,24 @@ class DeviceLinkingView private constructor(context: Context, attrs: AttributeSe
|
||||
explanationTextView.text = resources.getString(explanationID)
|
||||
mnemonicTextView.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE
|
||||
if (mode == Mode.Slave) {
|
||||
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded()
|
||||
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
|
||||
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), hexEncodedPublicKey)
|
||||
}
|
||||
authorizeButton.visibility = View.GONE
|
||||
authorizeButton.setOnClickListener { authorizePairing() }
|
||||
|
||||
// QR Code
|
||||
spinner.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE
|
||||
qrCodeImageView.visibility = if (mode == Mode.Master) View.VISIBLE else View.GONE
|
||||
if (mode == Mode.Master) {
|
||||
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val displayMetrics = DisplayMetrics()
|
||||
ServiceUtil.getWindowManager(context).defaultDisplay.getMetrics(displayMetrics)
|
||||
val size = displayMetrics.widthPixels - 2 * toPx(96, resources)
|
||||
val qrCode = QrCode.create(hexEncodedPublicKey, size)
|
||||
qrCodeImageView.setImageBitmap(qrCode)
|
||||
}
|
||||
|
||||
cancelButton.setOnClickListener { cancel() }
|
||||
}
|
||||
// endregion
|
||||
@ -86,14 +81,14 @@ class DeviceLinkingView private constructor(context: Context, attrs: AttributeSe
|
||||
if (mode != Mode.Master || pairingAuthorisation.type != PairingAuthorisation.Type.REQUEST || this.pairingAuthorisation != null) { return }
|
||||
this.pairingAuthorisation = pairingAuthorisation
|
||||
spinner.visibility = View.GONE
|
||||
qrCodeImageView.visibility = View.GONE
|
||||
val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams
|
||||
titleTextViewLayoutParams.topMargin = toPx(16, resources)
|
||||
titleTextView.layoutParams = titleTextViewLayoutParams
|
||||
titleTextView.text = resources.getString(R.string.view_device_linking_title_3)
|
||||
explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_2)
|
||||
mnemonicTextView.visibility = View.VISIBLE
|
||||
val hexEncodedPublicKey = pairingAuthorisation.secondaryDevicePublicKey.removing05PrefixIfNeeded()
|
||||
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
|
||||
mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), pairingAuthorisation.secondaryDevicePublicKey)
|
||||
authorizeButton.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
|
||||
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)
|
||||
}
|
||||
}
|
85
src/org/thoughtcrime/securesms/loki/LinkedDevicesActivity.kt
Normal file
85
src/org/thoughtcrime/securesms/loki/LinkedDevicesActivity.kt
Normal file
@ -0,0 +1,85 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import org.thoughtcrime.securesms.*
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.then
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
|
||||
class LinkedDevicesActivity : PassphraseRequiredActionBarActivity(), DeviceLinkingDelegate {
|
||||
|
||||
companion object {
|
||||
private val TAG = DeviceActivity::class.java.simpleName
|
||||
}
|
||||
|
||||
private val dynamicTheme = DynamicTheme()
|
||||
private val dynamicLanguage = DynamicLanguage()
|
||||
private lateinit var deviceListFragment: DeviceListFragment
|
||||
|
||||
public override fun onPreCreate() {
|
||||
dynamicTheme.onCreate(this)
|
||||
dynamicLanguage.onCreate(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setTitle(R.string.AndroidManifest__linked_devices)
|
||||
this.deviceListFragment = DeviceListFragment()
|
||||
this.deviceListFragment.setAddDeviceButtonListener {
|
||||
DeviceLinkingDialog.show(this, DeviceLinkingView.Mode.Master, this)
|
||||
}
|
||||
this.deviceListFragment.setHandleDisconnectDevice { devicePublicKey ->
|
||||
// Purge the device pairing from our database
|
||||
val ourPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
val database = DatabaseFactory.getLokiAPIDatabase(this)
|
||||
database.removePairingAuthorisation(ourPublicKey, devicePublicKey)
|
||||
// Update mapping on the file server
|
||||
LokiStorageAPI.shared.updateUserDeviceMappings().success {
|
||||
// Send an unpair request to let the device know that it has been revoked
|
||||
MessageSender.sendUnpairRequest(this, devicePublicKey)
|
||||
}
|
||||
// Refresh the list
|
||||
this.deviceListFragment.refresh()
|
||||
Toast.makeText(this, R.string.DeviceListActivity_unlinked_device, Toast.LENGTH_LONG).show()
|
||||
return@setHandleDisconnectDevice null
|
||||
}
|
||||
this.deviceListFragment.setHandleDeviceNameChange { pair ->
|
||||
DatabaseFactory.getLokiUserDatabase(this).setDisplayName(pair.first, pair.second)
|
||||
this.deviceListFragment.refresh()
|
||||
return@setHandleDeviceNameChange null
|
||||
}
|
||||
initFragment(android.R.id.content, deviceListFragment, dynamicLanguage.currentLocale)
|
||||
}
|
||||
|
||||
public override fun onResume() {
|
||||
super.onResume()
|
||||
dynamicTheme.onResume(this)
|
||||
dynamicLanguage.onResume(this)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {
|
||||
AsyncTask.execute {
|
||||
signAndSendPairingAuthorisationMessage(this, pairingAuthorisation)
|
||||
Util.runOnMain { this.deviceListFragment.refresh() }
|
||||
}
|
||||
}
|
||||
}
|
@ -189,6 +189,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
val database = databaseHelper.readableDatabase
|
||||
database.delete(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey ))
|
||||
}
|
||||
|
||||
fun removePairingAuthorisation(primaryDevicePublicKey: String, secondaryDevicePublicKey: String) {
|
||||
val database = databaseHelper.readableDatabase
|
||||
database.delete(pairingAuthorisationCache, "${Companion.primaryDevicePublicKey} = ? OR ${Companion.secondaryDevicePublicKey} = ?", arrayOf( primaryDevicePublicKey, secondaryDevicePublicKey ))
|
||||
}
|
||||
}
|
||||
|
||||
// region Convenience
|
||||
|
@ -19,7 +19,7 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
||||
private val friendRequestStatus = "friend_request_status"
|
||||
private val threadID = "thread_id"
|
||||
@JvmStatic val createMessageFriendRequestTableCommand = "CREATE TABLE $messageFriendRequestTableName ($messageID INTEGER PRIMARY KEY, $serverID INTEGER DEFAULT 0, $friendRequestStatus INTEGER DEFAULT 0);"
|
||||
@JvmStatic val createMessageToThreadMappingTableCommand = "CREATE TABLE $messageThreadMappingTableName ($messageID INTEGER PRIMARY KEY, $threadID INTEGER);"
|
||||
@JvmStatic val createMessageToThreadMappingTableCommand = "CREATE TABLE IF NOT EXISTS $messageThreadMappingTableName ($messageID INTEGER PRIMARY KEY, $threadID INTEGER);"
|
||||
}
|
||||
|
||||
override fun getQuoteServerID(quoteID: Long, quoteeHexEncodedPublicKey: String): Long? {
|
||||
|
@ -36,11 +36,6 @@ class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) :
|
||||
"$signedPreKeySignature TEXT," + "$identityKey TEXT NOT NULL," + "$deviceID INTEGER," + "$registrationID INTEGER" + ");"
|
||||
}
|
||||
|
||||
fun resetAllPreKeyBundleInfo() {
|
||||
TextSecurePreferences.removeLocalRegistrationId(context)
|
||||
TextSecurePreferences.setSignedPreKeyRegistered(context, false)
|
||||
}
|
||||
|
||||
fun generatePreKeyBundle(hexEncodedPublicKey: String): PreKeyBundle? {
|
||||
var registrationID = TextSecurePreferences.getLocalRegistrationId(context)
|
||||
if (registrationID == 0) {
|
||||
|
@ -6,9 +6,14 @@ import android.util.Log
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.then
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
@ -21,7 +26,9 @@ import org.whispersystems.signalservice.loki.api.LokiPublicChat
|
||||
import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI
|
||||
import org.whispersystems.signalservice.loki.api.LokiPublicChatMessage
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
|
||||
import org.whispersystems.signalservice.loki.utilities.successBackground
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
|
||||
class LokiPublicChatPoller(private val context: Context, private val group: LokiPublicChat) {
|
||||
@ -155,6 +162,7 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
|
||||
val senderDisplayName = "${message.displayName} (...${message.hexEncodedPublicKey.takeLast(8)})"
|
||||
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.hexEncodedPublicKey, senderDisplayName)
|
||||
}
|
||||
|
||||
val senderPublicKey = primaryDevice ?: message.hexEncodedPublicKey
|
||||
val serviceDataMessage = getDataMessage(message)
|
||||
val serviceContent = SignalServiceContent(serviceDataMessage, senderPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.timestamp, false)
|
||||
@ -163,6 +171,25 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
|
||||
} else {
|
||||
PushDecryptJob(context).handleTextMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID))
|
||||
}
|
||||
|
||||
// Update profile avatar if needed
|
||||
val senderRecipient = Recipient.from(context, Address.fromSerialized(senderPublicKey), false)
|
||||
if (message.avatar != null && message.avatar!!.url.isNotEmpty()) {
|
||||
val profileKey = message.avatar!!.profileKey
|
||||
val url = message.avatar!!.url
|
||||
if (senderRecipient.profileKey == null || !MessageDigest.isEqual(senderRecipient.profileKey, profileKey)) {
|
||||
val database = DatabaseFactory.getRecipientDatabase(context)
|
||||
database.setProfileKey(senderRecipient, profileKey)
|
||||
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderRecipient, url))
|
||||
}
|
||||
} else if (senderRecipient.profileAvatar.orEmpty().isNotEmpty()) {
|
||||
// Unset the avatar if we had an avatar before and we're not friends with the person
|
||||
val threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(senderRecipient)
|
||||
val friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId)
|
||||
if (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) {
|
||||
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderRecipient, ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
fun processOutgoingMessage(message: LokiPublicChatMessage) {
|
||||
val messageServerID = message.serverID ?: return
|
||||
@ -178,6 +205,19 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
|
||||
} else {
|
||||
PushDecryptJob(context).handleSynchronizeSentTextMessage(transcript)
|
||||
}
|
||||
|
||||
// Loki - If we got a message from our master device then make sure our mappings stay in sync
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(message.hexEncodedPublicKey), false)
|
||||
if (recipient.isOurMasterDevice && message.avatar != null) {
|
||||
val profileKey = message.avatar!!.profileKey
|
||||
val url = message.avatar!!.url
|
||||
if (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, profileKey)) {
|
||||
val database = DatabaseFactory.getRecipientDatabase(context)
|
||||
database.setProfileKey(recipient, profileKey)
|
||||
database.setProfileAvatar(recipient, url)
|
||||
ApplicationContext.getInstance(context).updatePublicChatProfileAvatarIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
var userDevices = setOf<String>()
|
||||
var uniqueDevices = setOf<String>()
|
||||
|
@ -16,6 +16,7 @@ class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database
|
||||
companion object {
|
||||
// Shared
|
||||
private val displayName = "display_name"
|
||||
private val profileAvatarUrl = "profile_avatar_url"
|
||||
// Display name cache
|
||||
private val displayNameTable = "loki_user_display_name_database"
|
||||
private val hexEncodedPublicKey = "hex_encoded_public_key"
|
||||
@ -66,4 +67,12 @@ class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database
|
||||
Log.d("Loki", "Couldn't save server display name due to exception: $e.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getProfileAvatarUrl(hexEncodedPublicKey: String): String? {
|
||||
return if (hexEncodedPublicKey == TextSecurePreferences.getLocalNumber(context)) {
|
||||
TextSecurePreferences.getProfileAvatarUrl(context)
|
||||
} else {
|
||||
Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false).resolve().profileAvatar
|
||||
}
|
||||
}
|
||||
}
|
37
src/org/thoughtcrime/securesms/loki/MnemonicUtilities.kt
Normal file
37
src/org/thoughtcrime/securesms/loki/MnemonicUtilities.kt
Normal file
@ -0,0 +1,37 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.content.Context
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||
import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
object MnemonicUtilities {
|
||||
|
||||
@JvmStatic
|
||||
public fun getLanguageFileDirectory(context: Context): File {
|
||||
val languages = listOf( "english", "japanese", "portuguese", "spanish" )
|
||||
val directory = File(context.applicationInfo.dataDir)
|
||||
for (language in languages) {
|
||||
val fileName = "$language.txt"
|
||||
if (directory.list().contains(fileName)) { continue }
|
||||
val inputStream = context.assets.open("mnemonic/$fileName")
|
||||
val file = File(directory, fileName)
|
||||
val outputStream = FileOutputStream(file)
|
||||
val buffer = ByteArray(1024)
|
||||
while (true) {
|
||||
val count = inputStream.read(buffer)
|
||||
if (count < 0) { break }
|
||||
outputStream.write(buffer, 0, count)
|
||||
}
|
||||
inputStream.close()
|
||||
outputStream.close()
|
||||
}
|
||||
return directory
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun getFirst3Words(codec: MnemonicCodec, hexEncodedPublicKey: String): String {
|
||||
return codec.encode(hexEncodedPublicKey.removing05PrefixIfNeeded()).split(" ").slice(0 until 3).joinToString(" ")
|
||||
}
|
||||
}
|
@ -6,9 +6,13 @@ import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.all
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import nl.komponents.kovenant.then
|
||||
import nl.komponents.kovenant.toFailVoid
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
@ -22,11 +26,32 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
|
||||
import org.whispersystems.signalservice.loki.utilities.Analytics
|
||||
import org.whispersystems.signalservice.loki.utilities.recover
|
||||
import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
fun checkForRevocation(context: Context) {
|
||||
val primaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context) ?: return
|
||||
val ourDevice = TextSecurePreferences.getLocalNumber(context)
|
||||
|
||||
LokiStorageAPI.shared.fetchDeviceMappings(primaryDevice).bind { mappings ->
|
||||
val ourMapping = mappings.find { it.secondaryDevicePublicKey == ourDevice }
|
||||
if (ourMapping != null) throw Error("Device has not been revoked")
|
||||
// remove pairing authorisations for our device
|
||||
DatabaseFactory.getLokiAPIDatabase(context).removePairingAuthorisations(ourDevice)
|
||||
LokiStorageAPI.shared.updateUserDeviceMappings()
|
||||
}.successUi {
|
||||
Analytics.shared.track("Secondary Device Unlinked")
|
||||
TextSecurePreferences.setNeedsRevocationCheck(context, false)
|
||||
ApplicationContext.getInstance(context).clearData()
|
||||
}.fail { error ->
|
||||
TextSecurePreferences.setNeedsRevocationCheck(context, true)
|
||||
Log.d("Loki", "Revocation check failed: ${error.message ?: error}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllDeviceFriendRequestStatuses(context: Context, hexEncodedPublicKey: String): Promise<Map<String, LokiThreadFriendRequestStatus>, Exception> {
|
||||
val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context)
|
||||
return LokiStorageAPI.shared.getAllDevicePublicKeys(hexEncodedPublicKey).map { keys ->
|
||||
@ -91,12 +116,16 @@ fun shouldAutomaticallyBecomeFriendsWithDevice(publicKey: String, context: Conte
|
||||
fun sendPairingAuthorisationMessage(context: Context, contactHexEncodedPublicKey: String, authorisation: PairingAuthorisation): Promise<Unit, Exception> {
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(contactHexEncodedPublicKey)
|
||||
val message = SignalServiceDataMessage.newBuilder().withBody(null).withPairingAuthorisation(authorisation)
|
||||
val message = SignalServiceDataMessage.newBuilder().withPairingAuthorisation(authorisation)
|
||||
// A REQUEST should always act as a friend request. A GRANT should always be replying back as a normal message.
|
||||
if (authorisation.type == PairingAuthorisation.Type.REQUEST) {
|
||||
val preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.number)
|
||||
message.asFriendRequest(true).withPreKeyBundle(preKeyBundle)
|
||||
} else {
|
||||
// Send over our profile key so that our linked device can get our profile picture
|
||||
message.withProfileKey(ProfileKeyUtil.getProfileKey(context))
|
||||
}
|
||||
|
||||
return try {
|
||||
Log.d("Loki", "Sending authorisation message to: $contactHexEncodedPublicKey.")
|
||||
val result = messageSender.sendMessage(0, address, Optional.absent<UnidentifiedAccessPair>(), message.build())
|
||||
|
@ -11,39 +11,58 @@ import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class BackgroundMessage private constructor(val recipient: String, val body: String?, val friendRequest: Boolean, val unpairingRequest: Boolean) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun create(recipient: String) = BackgroundMessage(recipient, null, false, false)
|
||||
@JvmStatic
|
||||
fun createFriendRequest(recipient: String, messageBody: String) = BackgroundMessage(recipient, messageBody, true, false)
|
||||
@JvmStatic
|
||||
fun createUnpairingRequest(recipient: String) = BackgroundMessage(recipient, null, false, true)
|
||||
|
||||
internal fun parse(serialized: String): BackgroundMessage {
|
||||
val node = JsonUtil.fromJson(serialized)
|
||||
val recipient = node.get("recipient").asText()
|
||||
val body = if (node.hasNonNull("body")) node.get("body").asText() else null
|
||||
val friendRequest = node.get("friendRequest").asBoolean(false)
|
||||
val unpairingRequest = node.get("unpairingRequest").asBoolean(false)
|
||||
return BackgroundMessage(recipient, body, friendRequest, unpairingRequest)
|
||||
}
|
||||
}
|
||||
|
||||
fun serialize(): String {
|
||||
val map = mapOf("recipient" to recipient, "body" to body, "friendRequest" to friendRequest, "unpairingRequest" to unpairingRequest)
|
||||
return JsonUtil.toJson(map)
|
||||
}
|
||||
}
|
||||
|
||||
class PushBackgroundMessageSendJob private constructor(
|
||||
parameters: Parameters,
|
||||
private val recipient: String,
|
||||
private val messageBody: String?,
|
||||
private val friendRequest: Boolean
|
||||
private val message: BackgroundMessage
|
||||
) : BaseJob(parameters) {
|
||||
companion object {
|
||||
const val KEY = "PushBackgroundMessageSendJob"
|
||||
|
||||
private val TAG = PushBackgroundMessageSendJob::class.java.simpleName
|
||||
|
||||
private val KEY_RECIPIENT = "recipient"
|
||||
private val KEY_MESSAGE_BODY = "message_body"
|
||||
private val KEY_FRIEND_REQUEST = "asFriendRequest"
|
||||
private val KEY_MESSAGE = "message"
|
||||
}
|
||||
|
||||
constructor(recipient: String): this(recipient, null, false)
|
||||
constructor(recipient: String, messageBody: String?, friendRequest: Boolean) : this(Parameters.Builder()
|
||||
constructor(message: BackgroundMessage) : this(Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(1)
|
||||
.build(),
|
||||
recipient, messageBody, friendRequest)
|
||||
message)
|
||||
|
||||
override fun serialize(): Data {
|
||||
return Data.Builder()
|
||||
.putString(KEY_RECIPIENT, recipient)
|
||||
.putString(KEY_MESSAGE_BODY, messageBody)
|
||||
.putBoolean(KEY_FRIEND_REQUEST, friendRequest)
|
||||
.putString(KEY_MESSAGE, message.serialize())
|
||||
.build()
|
||||
}
|
||||
|
||||
@ -52,22 +71,24 @@ class PushBackgroundMessageSendJob private constructor(
|
||||
}
|
||||
|
||||
public override fun onRun() {
|
||||
val message = SignalServiceDataMessage.newBuilder()
|
||||
val dataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(System.currentTimeMillis())
|
||||
.withBody(messageBody)
|
||||
.withBody(message.body)
|
||||
|
||||
if (friendRequest) {
|
||||
val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(recipient)
|
||||
message.withPreKeyBundle(bundle)
|
||||
if (message.friendRequest) {
|
||||
val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(message.recipient)
|
||||
dataMessage.withPreKeyBundle(bundle)
|
||||
.asFriendRequest(true)
|
||||
} else if (message.unpairingRequest) {
|
||||
dataMessage.asUnpairingRequest(true)
|
||||
}
|
||||
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(recipient)
|
||||
val address = SignalServiceAddress(message.recipient)
|
||||
try {
|
||||
messageSender.sendMessage(-1, address, Optional.absent<UnidentifiedAccessPair>(), message.build()) // The message ID doesn't matter
|
||||
messageSender.sendMessage(-1, address, Optional.absent<UnidentifiedAccessPair>(), dataMessage.build()) // The message ID doesn't matter
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send background message to: $recipient.")
|
||||
Log.d("Loki", "Failed to send background message to: ${message.recipient}.")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@ -82,10 +103,8 @@ class PushBackgroundMessageSendJob private constructor(
|
||||
class Factory : Job.Factory<PushBackgroundMessageSendJob> {
|
||||
override fun create(parameters: Parameters, data: Data): PushBackgroundMessageSendJob {
|
||||
try {
|
||||
val recipient = data.getString(KEY_RECIPIENT)
|
||||
val messageBody = if (data.hasString(KEY_MESSAGE_BODY)) data.getString(KEY_MESSAGE_BODY) else null
|
||||
val friendRequest = data.getBooleanOrDefault(KEY_FRIEND_REQUEST, false)
|
||||
return PushBackgroundMessageSendJob(parameters, recipient, messageBody, friendRequest)
|
||||
val messageJSON = data.getString(KEY_MESSAGE)
|
||||
return PushBackgroundMessageSendJob(parameters, BackgroundMessage.parse(messageJSON))
|
||||
} catch (e: IOException) {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
|
@ -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.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -14,8 +15,15 @@ import org.thoughtcrime.securesms.qr.ScanningThread
|
||||
|
||||
class ScanQRCodeFragment : Fragment() {
|
||||
private val scanningThread = ScanningThread()
|
||||
private var viewCreated = false
|
||||
var scanListener: ScanListener? = null
|
||||
set(value) { field = value; scanningThread.setScanListener(scanListener) }
|
||||
var mode: Mode = Mode.NewConversation
|
||||
set(value) { field = value; updateDescription(); }
|
||||
|
||||
// region Types
|
||||
enum class Mode { NewConversation, LinkDevice }
|
||||
// endregion
|
||||
|
||||
override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? {
|
||||
return layoutInflater.inflate(R.layout.fragment_scan_qr_code, viewGroup, false)
|
||||
@ -23,10 +31,12 @@ class ScanQRCodeFragment : Fragment() {
|
||||
|
||||
override fun onViewCreated(view: View, bundle: Bundle?) {
|
||||
super.onViewCreated(view, bundle)
|
||||
viewCreated = true
|
||||
when (resources.configuration.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> overlayView.orientation = LinearLayout.HORIZONTAL
|
||||
else -> overlayView.orientation = LinearLayout.VERTICAL
|
||||
}
|
||||
updateDescription()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@ -35,8 +45,10 @@ class ScanQRCodeFragment : Fragment() {
|
||||
this.cameraView.onResume()
|
||||
this.cameraView.setPreviewCallback(scanningThread)
|
||||
this.scanningThread.start()
|
||||
val activity = activity as NewConversationActivity
|
||||
activity.supportActionBar!!.setTitle(R.string.fragment_scan_qr_code_title)
|
||||
if (activity is AppCompatActivity) {
|
||||
val activity = activity as AppCompatActivity
|
||||
activity.supportActionBar?.setTitle(R.string.fragment_scan_qr_code_title)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@ -55,4 +67,13 @@ class ScanQRCodeFragment : Fragment() {
|
||||
cameraView.onResume()
|
||||
cameraView.setPreviewCallback(scanningThread)
|
||||
}
|
||||
|
||||
fun updateDescription() {
|
||||
if (!viewCreated) { return }
|
||||
val text = when (mode) {
|
||||
Mode.NewConversation -> R.string.fragment_scan_qr_code_explanation_new_conversation
|
||||
Mode.LinkDevice -> R.string.fragment_scan_qr_code_explanation_link_device
|
||||
}
|
||||
descriptionTextView.setText(text)
|
||||
}
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
@ -19,6 +21,8 @@ import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.qr.ScanListener
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
@ -32,7 +36,7 @@ import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
|
||||
class SeedActivity : BaseActionBarActivity(), DeviceLinkingDelegate, ScanListener {
|
||||
private lateinit var languageFileDirectory: File
|
||||
private var mode = Mode.Register
|
||||
set(newValue) { field = newValue; updateUI() }
|
||||
@ -57,6 +61,23 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
|
||||
toggleRestoreModeButton.setOnClickListener { mode = Mode.Restore }
|
||||
toggleLinkModeButton.setOnClickListener { mode = Mode.Link }
|
||||
mainButton.setOnClickListener { handleMainButtonTapped() }
|
||||
scanQRButton.setOnClickListener {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.fragment_scan_qr_code_camera_permission_dialog_message))
|
||||
.onAllGranted {
|
||||
val fragment = ScanQRCodeFragment()
|
||||
fragment.mode = ScanQRCodeFragment.Mode.LinkDevice
|
||||
fragment.scanListener = this
|
||||
supportFragmentManager.beginTransaction().replace(android.R.id.content, fragment).addToBackStack("QR").commitAllowingStateLoss()
|
||||
publicKeyEditText.clearFocus()
|
||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(publicKeyEditText.windowToken, 0)
|
||||
}
|
||||
.onAnyDenied { Toast.makeText(this, R.string.fragment_scan_qr_code_camera_permission_dialog_message, Toast.LENGTH_SHORT).show() }
|
||||
.execute()
|
||||
}
|
||||
Analytics.shared.track("Seed Screen Viewed")
|
||||
}
|
||||
// endregion
|
||||
@ -106,6 +127,7 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
|
||||
mnemonicEditText.visibility = restoreModeVisibility
|
||||
linkExplanationTextView.visibility = linkModeVisibility
|
||||
publicKeyEditText.visibility = linkModeVisibility
|
||||
scanQRButton.visibility = linkModeVisibility
|
||||
toggleRegisterModeButton.visibility = if (mode != Mode.Register) View.VISIBLE else View.GONE
|
||||
toggleRestoreModeButton.visibility = if (mode != Mode.Restore) View.VISIBLE else View.GONE
|
||||
toggleLinkModeButton.visibility = if (mode != Mode.Link) View.VISIBLE else View.GONE
|
||||
@ -230,10 +252,19 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
|
||||
|
||||
private fun resetForRegistration() {
|
||||
IdentityKeyUtil.delete(this, IdentityKeyUtil.lokiSeedKey)
|
||||
DatabaseFactory.getLokiPreKeyBundleDatabase(this).resetAllPreKeyBundleInfo()
|
||||
TextSecurePreferences.removeLocalNumber(this)
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, false)
|
||||
TextSecurePreferences.setPromptedPushRegistration(this, false)
|
||||
}
|
||||
// endregion
|
||||
override fun onQrDataFound(data: String?) {
|
||||
runOnUiThread {
|
||||
if (data != null && PublicKeyValidation.isValid(data.trim())) {
|
||||
publicKeyEditText.setText(data.trim())
|
||||
supportFragmentManager.popBackStackImmediate("QR", FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
handleMainButtonTapped()
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -4,6 +4,13 @@ import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
@ -237,11 +244,34 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
||||
Bitmap recipientPhotoBitmap = BitmapUtil.createFromDrawable(drawable, largeIconTargetSize, largeIconTargetSize);
|
||||
|
||||
if (recipientPhotoBitmap != null) {
|
||||
setLargeIcon(recipientPhotoBitmap);
|
||||
setLargeIcon(getCircleBitmap(recipientPhotoBitmap));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap getCircleBitmap(Bitmap bitmap) {
|
||||
final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
|
||||
bitmap.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
final Canvas canvas = new Canvas(output);
|
||||
|
||||
final int color = Color.RED;
|
||||
final Paint paint = new Paint();
|
||||
final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
final RectF rectF = new RectF(rect);
|
||||
|
||||
paint.setAntiAlias(true);
|
||||
canvas.drawARGB(0, 0, 0, 0);
|
||||
paint.setColor(color);
|
||||
canvas.drawOval(rectF, paint);
|
||||
|
||||
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
|
||||
canvas.drawBitmap(bitmap, rect, rect, paint);
|
||||
|
||||
bitmap.recycle();
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private boolean hasBigPictureSlide(@Nullable SlideDeck slideDeck) {
|
||||
if (slideDeck == null) {
|
||||
return false;
|
||||
|
@ -5,6 +5,7 @@ import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.support.v7.preference.Preference;
|
||||
@ -13,14 +14,20 @@ import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable;
|
||||
import org.thoughtcrime.securesms.loki.MnemonicUtilities;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
|
||||
@ -31,6 +38,7 @@ public class ProfilePreference extends Preference {
|
||||
private TextView profileNameView;
|
||||
private TextView profileNumberView;
|
||||
private TextView profileTagView;
|
||||
private String ourDeviceWords;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public ProfilePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
@ -73,13 +81,14 @@ public class ProfilePreference extends Preference {
|
||||
public void refresh() {
|
||||
if (profileNumberView == null) return;
|
||||
|
||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
|
||||
String primaryDevicePublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(getContext());
|
||||
Context context = getContext();
|
||||
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
String primaryDevicePublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
|
||||
String publicKey = primaryDevicePublicKey != null ? primaryDevicePublicKey : userHexEncodedPublicKey;
|
||||
final Address localAddress = Address.fromSerialized(publicKey);
|
||||
final String profileName = TextSecurePreferences.getProfileName(getContext());
|
||||
final Recipient recipient = Recipient.from(context, localAddress, false);
|
||||
final String profileName = TextSecurePreferences.getProfileName(context);
|
||||
|
||||
Context context = getContext();
|
||||
containerView.setOnLongClickListener(v -> {
|
||||
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("Public Key", publicKey);
|
||||
@ -96,28 +105,16 @@ public class ProfilePreference extends Preference {
|
||||
}
|
||||
});
|
||||
avatarView.setClipToOutline(true);
|
||||
avatarView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
|
||||
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
int width = avatarView.getWidth();
|
||||
int height = avatarView.getHeight();
|
||||
if (width == 0 || height == 0) return true;
|
||||
avatarView.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, publicKey.toLowerCase());
|
||||
avatarView.setImageDrawable(identicon);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
Drawable fallback = recipient.getFallbackContactPhotoDrawable(context, false);
|
||||
GlideApp.with(getContext().getApplicationContext())
|
||||
.load(new ProfileContactPhoto(localAddress, String.valueOf(TextSecurePreferences.getProfileAvatarId(getContext()))))
|
||||
.error(new ResourceContactPhoto(R.drawable.ic_camera_alt_white_24dp).asDrawable(getContext(), getContext().getResources().getColor(R.color.grey_400)))
|
||||
.load(recipient.getContactPhoto())
|
||||
.fallback(fallback)
|
||||
.error(fallback)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(avatarView);
|
||||
*/
|
||||
|
||||
|
||||
if (!TextUtils.isEmpty(profileName)) {
|
||||
profileNameView.setText(profileName);
|
||||
@ -127,6 +124,12 @@ public class ProfilePreference extends Preference {
|
||||
profileNumberView.setText(localAddress.toPhoneString());
|
||||
|
||||
profileTagView.setVisibility(primaryDevicePublicKey == null ? View.GONE : View.VISIBLE);
|
||||
profileTagView.setText(R.string.activity_settings_secondary_device_tag);
|
||||
if (primaryDevicePublicKey != null && ourDeviceWords == null) {
|
||||
MnemonicCodec codec = new MnemonicCodec(MnemonicUtilities.getLanguageFileDirectory(context));
|
||||
ourDeviceWords = MnemonicUtilities.getFirst3Words(codec, userHexEncodedPublicKey);
|
||||
}
|
||||
|
||||
String tag = context.getResources().getString(R.string.activity_settings_linked_device_tag);
|
||||
profileTagView.setText(String.format(tag, ourDeviceWords != null ? ourDeviceWords : "-"));
|
||||
}
|
||||
}
|
||||
|
@ -5,12 +5,15 @@ import android.graphics.Color;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.EncodeHintType;
|
||||
import com.google.zxing.WriterException;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class QrCode {
|
||||
|
||||
public static final String TAG = QrCode.class.getSimpleName();
|
||||
@ -18,10 +21,12 @@ public class QrCode {
|
||||
public static @NonNull Bitmap create(String data) {
|
||||
return create(data, 1024);
|
||||
}
|
||||
|
||||
public static @NonNull Bitmap create(String data, int size) {
|
||||
public static @NonNull Bitmap create(String data, int size) { return create(data, size, 2); }
|
||||
public static @NonNull Bitmap create(String data, int size, int margin) {
|
||||
try {
|
||||
BitMatrix result = new QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, size, size);
|
||||
HashMap<EncodeHintType, Integer> hintMap = new HashMap<>();
|
||||
hintMap.put(EncodeHintType.MARGIN, margin);
|
||||
BitMatrix result = new QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, size, size, hintMap);
|
||||
Bitmap bitmap = Bitmap.createBitmap(result.getWidth(), result.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
|
||||
for (int y = 0; y < result.getHeight(); y++) {
|
||||
|
@ -26,6 +26,7 @@ import android.text.TextUtils;
|
||||
|
||||
import com.annimon.stream.function.Consumer;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
@ -44,10 +45,13 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.JazzIdenticonContactPhoto;
|
||||
import org.thoughtcrime.securesms.loki.RecipientAvatarModifiedEvent;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientProvider.RecipientDetails;
|
||||
import org.thoughtcrime.securesms.util.FutureTaskListener;
|
||||
import org.thoughtcrime.securesms.util.ListenableFutureTask;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
@ -275,6 +279,11 @@ public class Recipient implements RecipientModifiedListener {
|
||||
return isLocalNumber;
|
||||
}
|
||||
|
||||
public boolean isOurMasterDevice() {
|
||||
String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
|
||||
return ourMasterDevice != null && ourMasterDevice.equals(getAddress().serialize());
|
||||
}
|
||||
|
||||
public synchronized @Nullable Uri getContactUri() {
|
||||
return this.contactUri;
|
||||
}
|
||||
@ -392,6 +401,7 @@ public class Recipient implements RecipientModifiedListener {
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
EventBus.getDefault().post(new RecipientAvatarModifiedEvent(this));
|
||||
}
|
||||
|
||||
public synchronized boolean isProfileSharing() {
|
||||
@ -455,15 +465,19 @@ public class Recipient implements RecipientModifiedListener {
|
||||
}
|
||||
|
||||
public synchronized @NonNull FallbackContactPhoto getFallbackContactPhoto() {
|
||||
if (isLocalNumber) return new ResourceContactPhoto(R.drawable.ic_note_to_self);
|
||||
if (isResolving()) return new TransparentContactPhoto();
|
||||
else if (isGroupRecipient()) return new ResourceContactPhoto(R.drawable.ic_group_white_24dp, R.drawable.ic_group_large);
|
||||
else if (!TextUtils.isEmpty(name)) return new GeneratedContactPhoto(name, R.drawable.ic_profile_default);
|
||||
else return new ResourceContactPhoto(R.drawable.ic_profile_default, R.drawable.ic_person_large);
|
||||
else if (isGroupRecipient()) return new GeneratedContactPhoto(name, R.drawable.ic_profile_default);
|
||||
else {
|
||||
String currentUser = TextSecurePreferences.getLocalNumber(context);
|
||||
String recipientAddress = address.serialize();
|
||||
String primaryAddress = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
|
||||
String profileAddress = (recipientAddress.equalsIgnoreCase(currentUser) && primaryAddress != null) ? primaryAddress : recipientAddress;
|
||||
return new JazzIdenticonContactPhoto(profileAddress);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized @Nullable ContactPhoto getContactPhoto() {
|
||||
if (isLocalNumber) return null;
|
||||
if (isLocalNumber) return new ProfileContactPhoto(address, String.valueOf(TextSecurePreferences.getProfileAvatarId(context)));
|
||||
else if (isGroupRecipient() && groupAvatarId != null) return new GroupRecordContactPhoto(address, groupAvatarId);
|
||||
else if (systemContactPhoto != null) return new SystemContactPhoto(address, systemContactPhoto, 0);
|
||||
else if (profileAvatar != null) return new ProfileContactPhoto(address, profileAvatar);
|
||||
|
@ -1,6 +1,8 @@
|
||||
package org.thoughtcrime.securesms.registration;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
@ -8,6 +10,7 @@ import android.support.annotation.NonNull;
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.loki.utilities.Analytics;
|
||||
|
||||
import network.loki.messenger.R;
|
||||
@ -23,6 +26,24 @@ public class WelcomeActivity extends BaseActionBarActivity {
|
||||
Analytics.Companion.getShared().track("Landing Screen Viewed");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (TextSecurePreferences.databaseResetFromUnpair(this)) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.dialog_device_unlink_title);
|
||||
builder.setMessage(R.string.dialog_device_unlink_message);
|
||||
builder.setPositiveButton(R.string.ok, null);
|
||||
builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialog) {
|
||||
TextSecurePreferences.setDatabaseResetFromUnpair(getBaseContext(), false);
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
|
@ -76,6 +76,10 @@ public class KeyCachingService extends Service {
|
||||
|
||||
private static MasterSecret masterSecret;
|
||||
|
||||
// Loki - Caching
|
||||
private static MasterSecret cachedSecret;
|
||||
private static long cacheTime = 0;
|
||||
|
||||
public KeyCachingService() {}
|
||||
|
||||
public static synchronized boolean isLocked(Context context) {
|
||||
@ -85,7 +89,13 @@ public class KeyCachingService extends Service {
|
||||
public static synchronized @Nullable MasterSecret getMasterSecret(Context context) {
|
||||
if (masterSecret == null && (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context))) {
|
||||
try {
|
||||
return MasterSecretUtil.getMasterSecret(context, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
|
||||
// Loki - Cache the secret.
|
||||
// Don't know if this will affect any other signal code :( but it makes it so we're not wasting time re-fetching the same secret from the database
|
||||
if (cachedSecret == null || cacheTime < System.currentTimeMillis()) {
|
||||
cachedSecret = MasterSecretUtil.getMasterSecret(context, MasterSecretUtil.UNENCRYPTED_PASSPHRASE);
|
||||
cacheTime = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5);
|
||||
}
|
||||
return cachedSecret;
|
||||
} catch (InvalidPassphraseException e) {
|
||||
Log.w("KeyCachingService", e);
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
package org.thoughtcrime.securesms.sms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
@ -44,6 +45,7 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.BackgroundMessage;
|
||||
import org.thoughtcrime.securesms.loki.FriendRequestHandler;
|
||||
import org.thoughtcrime.securesms.loki.GeneralUtilitiesKt;
|
||||
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
|
||||
@ -58,7 +60,11 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
|
||||
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
|
||||
@ -123,11 +129,15 @@ public class MessageSender {
|
||||
// We don't call the message sender here directly and instead we just opt to create a specific job for the send
|
||||
// This is because calling message sender directly would cause the application to freeze in some cases as it was blocking the thread when waiting for a response from the send
|
||||
public static void sendBackgroundMessage(Context context, String contactHexEncodedPublicKey) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(contactHexEncodedPublicKey));
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.create(contactHexEncodedPublicKey)));
|
||||
}
|
||||
|
||||
public static void sendBackgroundFriendRequest(Context context, String contactHexEncodedPublicKey, String messageBody) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(contactHexEncodedPublicKey, messageBody, true));
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createFriendRequest(contactHexEncodedPublicKey, messageBody)));
|
||||
}
|
||||
|
||||
public static void sendUnpairRequest(Context context, String contactHexEncodedPublicKey) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createUnpairingRequest(contactHexEncodedPublicKey)));
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
@ -11,7 +11,7 @@ import java.io.File;
|
||||
|
||||
public class FileProviderUtil {
|
||||
|
||||
private static final String AUTHORITY = "org.thoughtcrime.securesms.fileprovider";
|
||||
private static final String AUTHORITY = "network.loki.securesms.fileprovider";
|
||||
|
||||
public static Uri getUriFor(@NonNull Context context, @NonNull File file) {
|
||||
if (Build.VERSION.SDK_INT >= 24) return FileProvider.getUriForFile(context, AUTHORITY, file);
|
||||
|
@ -121,6 +121,7 @@ public class TextSecurePreferences {
|
||||
private static final String PROFILE_KEY_PREF = "pref_profile_key";
|
||||
private static final String PROFILE_NAME_PREF = "pref_profile_name";
|
||||
private static final String PROFILE_AVATAR_ID_PREF = "pref_profile_avatar_id";
|
||||
private static final String PROFILE_AVATAR_URL_PREF = "pref_profile_avatar_url";
|
||||
public static final String READ_RECEIPTS_PREF = "pref_read_receipts";
|
||||
public static final String INCOGNITO_KEYBORAD_PREF = "pref_incognito_keyboard";
|
||||
private static final String UNAUTHORIZED_RECEIVED = "pref_unauthorized_received";
|
||||
@ -401,6 +402,14 @@ public class TextSecurePreferences {
|
||||
return getIntegerPreference(context, PROFILE_AVATAR_ID_PREF, 0);
|
||||
}
|
||||
|
||||
public static void setProfileAvatarUrl(Context context, String url) {
|
||||
setStringPreference(context, PROFILE_AVATAR_URL_PREF, url);
|
||||
}
|
||||
|
||||
public static String getProfileAvatarUrl(Context context) {
|
||||
return getStringPreference(context, PROFILE_AVATAR_URL_PREF, null);
|
||||
}
|
||||
|
||||
public static int getNotificationPriority(Context context) {
|
||||
return Integer.valueOf(getStringPreference(context, NOTIFICATION_PRIORITY_PREF, String.valueOf(NotificationCompat.PRIORITY_HIGH)));
|
||||
}
|
||||
@ -1185,5 +1194,35 @@ public class TextSecurePreferences {
|
||||
public static void setMasterHexEncodedPublicKey(Context context, String masterHexEncodedPublicKey) {
|
||||
setStringPreference(context, "master_hex_encoded_public_key", masterHexEncodedPublicKey.toLowerCase());
|
||||
}
|
||||
|
||||
public static void setResetDatabase(Context context, boolean resetDatabase) {
|
||||
// We do it this way so that it gets persisted in storage straight away
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("database_reset", resetDatabase).commit();
|
||||
}
|
||||
|
||||
public static boolean resetDatabase(Context context) {
|
||||
return getBooleanPreference(context, "database_reset", false);
|
||||
}
|
||||
|
||||
public static void setDatabaseResetFromUnpair(Context context, boolean value) {
|
||||
// We do it this way so that it gets persisted in storage straight away
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("database_reset_unpair", value).commit();
|
||||
}
|
||||
|
||||
public static boolean databaseResetFromUnpair(Context context) {
|
||||
return getBooleanPreference(context, "database_reset_unpair", false);
|
||||
}
|
||||
|
||||
public static void setNeedsRevocationCheck(Context context, boolean needsCheck) {
|
||||
setBooleanPreference(context, "needs_revocation", needsCheck);
|
||||
}
|
||||
|
||||
public static boolean needsRevocationCheck(Context context) {
|
||||
return getBooleanPreference(context, "needs_revocation", false);
|
||||
}
|
||||
// endregion
|
||||
|
||||
public static void clearAll(Context context) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().clear().commit();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user