Merge pull request #260 from loki-project/localization

Integrate Persian Translation
This commit is contained in:
Niels Andriesse 2020-07-17 12:12:47 +10:00 committed by GitHub
commit 3e8e4326bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
94 changed files with 1258 additions and 1813 deletions

View File

@ -73,7 +73,7 @@
android:layout_width="0dp"
android:layout_height="@dimen/medium_button_height"
android:layout_weight="1"
android:layout_marginLeft="@dimen/medium_spacing"
android:layout_marginStart="@dimen/medium_spacing"
android:text="@string/share" />
</LinearLayout>

View File

@ -150,7 +150,7 @@
android:layout_width="0dp"
android:layout_height="@dimen/medium_button_height"
android:layout_weight="1"
android:layout_marginLeft="@dimen/medium_spacing"
android:layout_marginStart="@dimen/medium_spacing"
android:text="@string/share" />
</LinearLayout>

View File

@ -96,6 +96,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textAlignment="viewStart"
android:layout_weight="1"
android:textColorHint="#99FFFFFF"
android:textSize="@dimen/small_font_size"
@ -137,7 +138,7 @@
android:id="@+id/recorder_view"
android:layout_height="match_parent"
android:layout_width="36dp"
android:layout_gravity="center_vertical"
android:layout_gravity="center"
android:clipChildren="false"
android:clipToPadding="false">

View File

@ -208,12 +208,6 @@
</LinearLayout>
<org.thoughtcrime.securesms.loki.views.FriendRequestView
android:id="@+id/friend_request_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
</LinearLayout>
<org.thoughtcrime.securesms.components.AlertView
@ -225,4 +219,5 @@
android:gravity="center_vertical"/>
</RelativeLayout>
</org.thoughtcrime.securesms.conversation.ConversationItem>
</org.thoughtcrime.securesms.conversation.ConversationItem>

View File

@ -161,12 +161,6 @@
</LinearLayout>
<org.thoughtcrime.securesms.loki.views.FriendRequestView
android:id="@+id/friend_request_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
</LinearLayout>
<org.thoughtcrime.securesms.components.AlertView
@ -181,4 +175,4 @@
</RelativeLayout>
</org.thoughtcrime.securesms.conversation.ConversationItem>
</org.thoughtcrime.securesms.conversation.ConversationItem>

View File

@ -49,7 +49,7 @@
android:layout_width="0dp"
android:layout_height="@dimen/small_button_height"
android:layout_weight="1"
android:layout_marginLeft="@dimen/medium_spacing"
android:layout_marginStart="@dimen/medium_spacing"
android:text="@string/delete" />
</LinearLayout>

View File

@ -87,7 +87,7 @@
android:layout_width="0dp"
android:layout_height="@dimen/small_button_height"
android:layout_weight="1"
android:layout_marginLeft="@dimen/medium_spacing"
android:layout_marginStart="@dimen/medium_spacing"
android:text="@string/dialog_link_device_master_mode_authorize_button_title"
android:visibility="gone" />

View File

@ -60,7 +60,7 @@
android:layout_width="0dp"
android:layout_height="@dimen/small_button_height"
android:layout_weight="1"
android:layout_marginLeft="@dimen/medium_spacing"
android:layout_marginStart="@dimen/medium_spacing"
android:text="@string/copy" />
</LinearLayout>

View File

@ -0,0 +1,28 @@
<?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"
android:background="@color/dialog_background">
<TextView
android:id="@+id/blockOrUnblockTextView"
style="@style/ActionItem"
android:drawableStart="@drawable/ic_block_white_24dp"
android:textSize="@dimen/medium_font_size"
android:textColor="@color/text"
android:text="@string/RecipientPreferenceActivity_block"/>
<TextView
android:id="@+id/deleteTextView"
style="@style/ActionItem"
android:drawableStart="@drawable/ic_delete_white_24dp"
android:textSize="@dimen/medium_font_size"
android:textColor="@color/text"
android:text="@string/delete" />
</LinearLayout>

View File

@ -73,7 +73,7 @@
android:layout_width="0dp"
android:layout_height="@dimen/medium_button_height"
android:layout_weight="1"
android:layout_marginLeft="@dimen/medium_spacing"
android:layout_marginStart="@dimen/medium_spacing"
android:text="@string/share" />
</LinearLayout>

View File

@ -8,11 +8,11 @@
android:id="@+id/quick_audio_toggle"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="2dp"
android:layout_gravity="center"
android:background="@null"
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_record_and_send_audio_description"
android:scaleType="centerInside"
android:layout_marginEnd="4dp"
android:tint="@color/text"
app:srcCompat="@drawable/ic_microphone" />

View File

@ -8,7 +8,7 @@
android:gravity="center_vertical">
<View
android:id="@+id/unreadMessagesIndicatorView"
android:id="@+id/accentView"
android:layout_width="@dimen/accent_line_thickness"
android:layout_height="match_parent"
android:background="@color/accent" />
@ -18,14 +18,14 @@
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size"
android:layout_marginTop="@dimen/medium_spacing"
android:layout_marginLeft="@dimen/medium_spacing"
android:layout_marginStart="@dimen/medium_spacing"
android:layout_marginBottom="@dimen/medium_spacing" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/medium_spacing"
android:layout_marginRight="@dimen/medium_spacing"
android:layout_marginStart="@dimen/medium_spacing"
android:layout_marginEnd="@dimen/medium_spacing"
android:orientation="vertical">
<LinearLayout
@ -54,7 +54,7 @@
android:id="@+id/timestampTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/medium_spacing"
android:layout_marginStart="@dimen/medium_spacing"
android:maxLines="1"
android:ellipsize="end"
android:textSize="@dimen/small_font_size"
@ -76,7 +76,7 @@
android:layout_height="12dp"
android:src="@drawable/ic_mute"
android:layout_marginTop="1dp"
android:layout_marginRight="4dp" />
android:layout_marginEnd="4dp" />
<RelativeLayout
android:layout_width="wrap_content"
@ -110,7 +110,7 @@
android:id="@+id/statusIndicatorImageView"
android:layout_width="@dimen/conversation_view_status_indicator_size"
android:layout_height="@dimen/conversation_view_status_indicator_size"
android:layout_marginLeft="@dimen/medium_spacing" />
android:layout_marginStart="@dimen/medium_spacing" />
</LinearLayout>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="@string/recipient_preferences__block"
android:id="@+id/menu_block"
android:icon="@drawable/ic_block_white_24dp" />
</menu>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="@string/ConversationActivity_unblock"
android:id="@+id/menu_unblock"
android:icon="@drawable/ic_check_white_24dp" />
</menu>

View File

@ -1214,5 +1214,215 @@
<string name="prompt_passphrase_activity__signal_is_locked">Session قفل شده است</string>
<string name="RegistrationLockDialog_reminder">یادآور:</string>
<string name="recipient_preferences__about">درباره ی ما</string>
<!--EOF-->
<!-- Session -->
<string name="continue_2">ادامه </string>
<string name="copy">رونویسی کردن</string>
<string name="invalid_url">URL نامعتبر است</string>
<string name="copied_to_clipboard">در کلیپ بورد کپی شد</string>
<string name="device_linking_failed">پیوند دستگاه امکان پذیر نیست.</string>
<string name="next">بعد</string>
<string name="share">اشتراک گذاری</string>
<string name="invalid_session_id">شناسه نامعتبر جلسه</string>
<string name="cancel">لغو</string>
<string name="your_session_id">شناسه جلسه شما</string>
<string name="activity_landing_title_2">جلسه شما از اینجا شروع می شود...</string>
<string name="activity_landing_register_button_title">شناسه جلسه را ایجاد کنید</string>
<string name="activity_landing_restore_button_title">جلسه خود را ادامه دهید</string>
<string name="activity_landing_link_button_title">پیوند به یک حساب کاربری موجود</string>
<string name="activity_landing_device_unlinked_dialog_title">ارتباط دستگاه شما با موفقیت انجام نشد</string>
<string name="view_fake_chat_bubble_1">جلسه چیست؟</string>
<string name="view_fake_chat_bubble_2">این یک برنامه پیام رسانی غیرمتمرکز و رمزگذاری شده است</string>
<string name="view_fake_chat_bubble_3">بنابراین اطلاعات شخصی یا ابرداده گفتگوی من جمع نمی شود؟ چگونه کار می کند؟</string>
<string name="view_fake_chat_bubble_4">استفاده از ترکیبی از فن آوری های رمزگذاری پیشرفته مسیریابی ناشناس و پایان به پایان.</string>
<string name="view_fake_chat_bubble_5">دوستان اجازه نمی دهند تا دوستان از پیام رسان های مصالحه استفاده کنند. خواهش میکنم.</string>
<string name="activity_register_title">سلام به شناسه جلسه خود</string>
<string name="activity_register_explanation">شناسه جلسه شما آدرس منحصر به فردی است که افراد می توانند از آنها برای تماس با شما در جلسه استفاده کنند. بدون ارتباط با هویت واقعی شما ، شناسه جلسه شما کاملاً ناشناس و خصوصی است.</string>
<string name="activity_register_public_key_copied_message">در کلیپ بورد کپی شد</string>
<string name="activity_restore_title">حساب خود را بازیابی کنید</string>
<string name="activity_restore_explanation">عبارت بازیابی را هنگام ثبت نام برای بازیابی حساب خود وارد کنید.</string>
<string name="activity_restore_seed_edit_text_hint">عبارت بازیابی خود را وارد کنید</string>
<string name="activity_link_device_title">دستگاه پیوند</string>
<string name="activity_link_device_enter_session_id_tab_title">شناسه جلسه را وارد کنید</string>
<string name="activity_link_device_scan_qr_code_tab_title">اسکن کد QR</string>
<string name="activity_link_device_scan_qr_code_explanation">به تنظیمات &gt; دستگاه ها &gt; پیوند دستگاه در دستگاه دیگر خود بروید و سپس کد QR را که برای شروع کار پیوند داده است ، اسکن کنید.</string>
<string name="fragment_enter_session_id_title">دستگاه خود را پیوند دهید</string>
<string name="fragment_enter_session_id_explanation">به تنظیمات &gt; دستگاه ها &gt; پیوند دستگاه در دستگاه دیگر خود بروید و سپس شناسه جلسه خود را در اینجا وارد کنید تا فرایند پیوند آغاز شود.</string>
<string name="fragment_enter_session_id_edit_text_hint">شناسه جلسه خود را وارد کنید</string>
<string name="activity_display_name_title_2">نام صفحه نمایش خود را انتخاب کنید</string>
<string name="activity_display_name_explanation">این نام شما هنگام استفاده از جلسه خواهد بود. این می تواند نام واقعی شما باشد ، نام مستعار یا هر چیز دیگری که دوست دارید.</string>
<string name="activity_display_name_edit_text_hint">نام نمایشگر را وارد کنید</string>
<string name="activity_display_name_display_name_missing_error">لطفاً نام نمایشگر را انتخاب کنید</string>
<string name="activity_display_name_display_name_invalid_error">لطفاً یک نام نمایشگر را انتخاب کنید که فقط از نویسه های Az ، AZ ، 0-9 و _ تشکیل شده باشد</string>
<string name="activity_display_name_display_name_too_long_error">لطفاً نام نمایشگر کوتاه تری انتخاب کنید</string>
<string name="activity_pn_mode_title">اعلانهای فشار</string>
<string name="activity_pn_mode_explanation">دو راه وجود دارد که جلسه می تواند اعلان های فشار را کنترل کند. حتما قبل از انتخاب توضیحات را با دقت بخوانید.</string>
<string name="activity_pn_mode_fcm_option_title">پیام ابری فایربیس</string>
<string name="activity_pn_mode_fcm_option_explanation">جلسه برای دریافت اعلان های فشار از سرویس پیام ابری فایربیس استفاده می کند. با اطمینان و بلافاصله از پیامهای جدید مطلع خواهید شد. استفاده از FCM بدان معنا است که آدرس IP و نشانه دستگاه شما در معرض گوگل قرار خواهد گرفت. اگر از اعلانهای فشار برای سایر برنامه ها استفاده می کنید ، این مورد در حال حاضر اینگونه خواهد بود. آدرس IP و نشانه دستگاه شما نیز در معرض Loki قرار خواهد گرفت ، اما پیام های شما هنوز هم از طریق رمزگذاری شده توسط اونیون و رمزگذاری نهایی به پایان می رسد ، بنابراین محتوای پیام های شما کاملاً خصوصی خواهد بود.</string>
<string name="activity_pn_mode_background_polling_option_title">زمینه رای گیری</string>
<string name="activity_pn_mode_background_polling_option_explanation">جلسه گاه به گاه پیام های جدید را در پس زمینه بررسی می کند. این محافظت کامل از ابرداده را تضمین می کند ، اما اعلان های پیام ممکن است به میزان قابل توجهی به تأخیر بیفتند.</string>
<string name="activity_pn_mode_recommended_option_tag">توصیه شده</string>
<string name="activity_pn_mode_no_option_picked_dialog_title">لطفا گزینه ای را انتخاب کنید</string>
<string name="activity_home_empty_state_message">شما هنوز هیچ تماسی ندارید</string>
<string name="activity_home_empty_state_button_title">شروع جلسه</string>
<string name="activity_home_leave_group_dialog_message">آیا مطمئن هستید که می خواهید این گروه را ترک کنید؟</string>
<string name="activity_home_leaving_group_failed_message">نمی توان گروه را ترک کرد</string>
<string name="activity_home_delete_conversation_dialog_message">آیا مطمئن هستید که می خواهید این مکالمه را حذف کنید؟</string>
<string name="activity_home_conversation_deleted_message">مکالمه حذف شد</string>
<string name="sheet_pn_mode_title">اعلانهای فشار</string>
<string name="sheet_pn_mode_explanation">جلسه اکنون دو راه برای رسیدگی به اعلان های فشار دارد. حتما قبل از انتخاب توضیحات را با دقت بخوانید.</string>
<string name="sheet_pn_mode_fcm_option_title">پیام ابری فایربیس</string>
<string name="sheet_pn_mode_fcm_option_explanation">جلسه برای دریافت اعلان های فشار از سرویس پیام ابری فایربیس استفاده می کند. با اطمینان و بلافاصله از پیامهای جدید مطلع خواهید شد. استفاده از FCM بدان معنا است که آدرس IP و نشانه دستگاه شما در معرض گوگل قرار خواهد گرفت. اگر از اعلانهای فشار برای سایر برنامه ها استفاده می کنید ، این مورد در حال حاضر اینگونه خواهد بود. آدرس IP و نشانه دستگاه شما نیز در معرض Loki قرار خواهد گرفت ، اما پیام های شما هنوز هم از طریق رمزگذاری شده توسط پیاز و رمزگذاری نهایی به پایان می رسد ، بنابراین محتوای پیام های شما کاملاً خصوصی خواهد بود.</string>
<string name="sheet_pn_mode_background_polling_option_title">زمینه رای گیری</string>
<string name="sheet_pn_mode_background_polling_option_explanation">جلسه گاه به گاه پیام های جدید را در پس زمینه بررسی می کند. این محافظت کامل از ابرداده را تضمین می کند ، اما اعلان های پیام ممکن است به میزان قابل توجهی به تأخیر بیفتند.</string>
<string name="sheet_pn_mode_recommended_option_tag">توصیه شده</string>
<string name="sheet_pn_mode_no_option_picked_dialog_title">لطفا گزینه ای را انتخاب کنید</string>
<string name="sheet_pn_mode_confirm_button_title">تایید</string>
<string name="sheet_pn_mode_skip_button_title">رد</string>
<string name="activity_seed_title">عبارت بازیابی شما</string>
<string name="activity_seed_title_2">با عبارت بازیابی خود مطابقت داشته باشید</string>
<string name="activity_seed_explanation">عبارت بازیابی شما کلید اصلی شناسه جلسه شما است - در صورت عدم دسترسی به دستگاه خود می توانید از آن برای بازگرداندن شناسه جلسه استفاده کنید. عبارت بازیابی خود را در مکانی امن ذخیره کنید و آن را به کسی ندهید.</string>
<string name="activity_seed_reveal_button_title">نگه دارید تا فاش شود</string>
<string name="view_seed_reminder_subtitle_1">با ذخیره کردن عبارت بازیابی ، حساب خود را ایمن کنید</string>
<string name="view_seed_reminder_subtitle_2">برای فاش کردن عبارت بازیابی ، بر روی کلمات redacted ضربه زده و نگه دارید ، سپس با خیال راحت آن را ذخیره کنید تا شناسه جلسه خود را ایمن نمایید.</string>
<string name="view_seed_reminder_subtitle_3">حتماً عبارت بازیابی خود را در مکانی امن ذخیره کنید</string>
<string name="activity_path_title">مسیر</string>
<string name="activity_path_explanation">جلسه IP شما را با گزاف گویی پیام های خود از طریق چندین گره سرویس در شبکه غیرمتمرکز جلسه مخفی می کند. اینها کشورهایی هستند که اتصال شما در حال حاضر از طریق آن فراخوانی می شوند:</string>
<string name="activity_path_device_row_title">شما</string>
<string name="activity_path_guard_node_row_title">گره ورود</string>
<string name="activity_path_service_node_row_title">گره سرویس</string>
<string name="activity_path_destination_row_title">مقصد</string>
<string name="activity_path_learn_more_button_title">بیشتر بدانید</string>
<string name="activity_create_private_chat_title">جلسه جدید</string>
<string name="activity_create_private_chat_enter_session_id_tab_title">شناسه جلسه را وارد کنید</string>
<string name="activity_create_private_chat_scan_qr_code_tab_title">اسکن کد QR</string>
<string name="activity_create_private_chat_scan_qr_code_explanation">برای شروع جلسه ، کد QR کاربر را اسکن کنید. با ضربه زدن روی نماد کد QR در تنظیمات حساب ، کدهای QR را می توان یافت.</string>
<string name="fragment_enter_public_key_edit_text_hint">شناسه گیرنده را وارد کنید</string>
<string name="fragment_enter_public_key_explanation">کاربران می توانند با وارد کردن به تنظیمات حساب خود و ضربه زدن به Share Share Session ID یا با به اشتراک گذاشتن کد QR خود ، شناسه جلسه خود را به اشتراک بگذارند.</string>
<string name="fragment_scan_qr_code_camera_access_explanation">جلسه برای اسکن کدهای QR به دوربین دسترسی دارد</string>
<string name="fragment_scan_qr_code_grant_camera_access_button_title">دسترسی به کمک دوربین</string>
<string name="activity_create_closed_group_title">گروه بسته شده جدید</string>
<string name="activity_create_closed_group_edit_text_hint">نام گروه را وارد کنید</string>
<string name="activity_create_closed_group_explanation">گروه های بسته تا 10 عضو را پشتیبانی می کنند و همان محافظت از حریم خصوصی را به عنوان جلسات یک به یک ارائه می دهند.</string>
<string name="activity_create_closed_group_empty_state_message">شما هنوز هیچ تماسی ندارید</string>
<string name="activity_create_closed_group_empty_state_button_title">شروع جلسه</string>
<string name="activity_create_closed_group_group_name_missing_error">لطفاً یک نام گروه وارد کنید</string>
<string name="activity_create_closed_group_group_name_too_long_error">لطفاً نام گروه کوتاه تری وارد کنید</string>
<string name="activity_create_closed_group_not_enough_group_members_error">لطفا حداقل 2 عضو گروه را انتخاب کنید</string>
<string name="activity_create_closed_group_too_many_group_members_error">یک گروه بسته نمی تواند بیش از 10 عضو داشته باشد</string>
<string name="activity_create_closed_group_invalid_session_id_error">یکی از اعضای گروه شما دارای شناسه نامعتبر است</string>
<string name="activity_join_public_chat_title">به گروه باز بپیوندید</string>
<string name="activity_join_public_chat_error">امکان پیوستن به گروه نیست</string>
<string name="activity_join_public_chat_enter_group_url_tab_title">آدرس اینترنتی گروه را باز کنید</string>
<string name="activity_join_public_chat_scan_qr_code_tab_title">اسکن کد QR</string>
<string name="activity_join_public_chat_scan_qr_code_explanation">کد QR گروه باز را که می خواهید بپیوندید اسکن کنید</string>
<string name="fragment_enter_chat_url_edit_text_hint">یک URL گروه باز وارد کنید</string>
<string name="fragment_enter_chat_url_privacy_warning">گروه های باز می توانند توسط هر کسی بپیوندند و محافظت کامل از حریم خصوصی ارائه نمی دهند</string>
<string name="activity_settings_title">تنظیمات</string>
<string name="activity_settings_display_name_edit_text_hint">نام نمایشگر را وارد کنید</string>
<string name="activity_settings_display_name_missing_error">لطفاً نام نمایشگر را انتخاب کنید</string>
<string name="activity_settings_invalid_display_name_error">لطفاً یک نام نمایشگر را انتخاب کنید که فقط از نویسه های Az ، AZ ، 0-9 و _ تشکیل شده باشد</string>
<string name="activity_settings_display_name_too_long_error">لطفاً نام نمایشگر کوتاه تری انتخاب کنید</string>
<string name="activity_settings_privacy_button_title">حریم خصوصی</string>
<string name="activity_settings_notifications_button_title">اطلاعیه</string>
<string name="activity_settings_chats_button_title">چت</string>
<string name="activity_settings_devices_button_title">دستگاه ها</string>
<string name="activity_settings_recovery_phrase_button_title">عبارت بازیابی</string>
<string name="activity_settings_clear_all_data_button_title">اطلاعات روشن</string>
<string name="activity_notification_settings_title">اطلاعیه</string>
<string name="activity_notification_settings_style_section_title">سبک اطلاع رسانی</string>
<string name="activity_notification_settings_content_section_title">محتوای اطلاع رسانی</string>
<string name="activity_privacy_settings_title">حریم خصوصی</string>
<string name="activity_chat_settings_title">چت</string>
<string name="activity_linked_devices_title">دستگاه ها</string>
<string name="activity_linked_devices_multi_device_limit_reached_dialog_title">دستگاه محدود شد</string>
<string name="activity_linked_devices_multi_device_limit_reached_dialog_explanation">در حال حاضر مجاز به پیوند بیش از یک دستگاه نیست.</string>
<string name="activity_linked_devices_unlinking_failed_message">دستگاه را نمی توان ارتباط برقرار کرد.</string>
<string name="activity_linked_devices_unlinking_successful_message">دستگاه شما با موفقیت ارتباط برقرار نشد</string>
<string name="activity_linked_devices_linking_failed_message">پیوند دستگاه امکان پذیر نیست.</string>
<string name="activity_linked_devices_empty_state_message">شما هنوز هیچ دستگاهی را پیوند نداده اید</string>
<string name="activity_linked_devices_empty_state_button_title">پیوند یک دستگاه (بتا)</string>
<string name="preferences_notifications_strategy_category_title">استراتژی اعلان</string>
<string name="preferences_notifications_use_fcm_option_title">از FCM استفاده کنید</string>
<string name="preferences_notifications_use_fcm_option_explanation">با استفاده از پیام ابری فایربیس، اعلان های فشار قابل اطمینان تر امکان پذیر است ، اما نشانگر IP و دستگاه شما را در گوگل و لوکی قرار می دهد.</string>
<string name="dialog_link_device_slave_mode_title_1">در انتظار مجوز</string>
<string name="dialog_link_device_slave_mode_title_2">پیوند دستگاه مجاز است</string>
<string name="dialog_link_device_slave_mode_explanation_1">لطفاً بررسی کنید که کلمات زیر با کلمات نشان داده شده در دستگاه دیگر شما مطابقت دارند.</string>
<string name="dialog_link_device_slave_mode_explanation_2">دستگاه شما با موفقیت پیوند خورده است</string>
<string name="dialog_link_device_master_mode_title_1">منتظر دستگاه</string>
<string name="dialog_link_device_master_mode_title_2">درخواست پیوند دریافت شد</string>
<string name="dialog_link_device_master_mode_title_3">پیوند دستگاه مجاز</string>
<string name="dialog_link_device_master_mode_explanation_1">جلسه را در دستگاه دیگر خود دانلود کرده و روی پیوند به یک حساب موجود در پایین صفحه فرود ضربه بزنید. اگر در حال حاضر دارای یک حساب کاربری در دستگاه دیگر خود هستید ، ابتدا باید آن حساب را حذف کنید.</string>
<string name="dialog_link_device_master_mode_explanation_2">لطفاً بررسی کنید که کلمات زیر با کلمات نشان داده شده در دستگاه دیگر شما مطابقت دارند.</string>
<string name="dialog_link_device_master_mode_explanation_3">لطفاً منتظر بمانید تا پیوند دستگاه ایجاد شود. این ممکن است تا یک دقیقه طول بکشد.</string>
<string name="dialog_link_device_master_mode_authorize_button_title">اجازه دهید</string>
<string name="fragment_device_list_bottom_sheet_change_name_button_title">تغییر نام</string>
<string name="fragment_device_list_bottom_sheet_unlink_device_button_title">دستگاه را جدا کنید</string>
<string name="dialog_edit_device_name_edit_text_hint">یک نام وارد کنید</string>
<string name="dialog_seed_title">عبارت بازیابی شما</string>
<string name="dialog_seed_explanation">این عبارت بازیابی شماست. با استفاده از آن ، می توانید شناسه جلسه خود را به دستگاه جدید بازیابی یا انتقال دهید.</string>
<string name="dialog_clear_all_data_title">پاک کردن همه داده ها</string>
<string name="dialog_clear_all_data_explanation">این به طور دائم پیام ها، جلسات و مخاطبین شما را حذف می کند.</string>
<string name="activity_qr_code_title">کد QR</string>
<string name="activity_qr_code_view_my_qr_code_tab_title">مشاهده کد QR من</string>
<string name="activity_qr_code_view_scan_qr_code_tab_title">اسکن کد QR</string>
<string name="activity_qr_code_view_scan_qr_code_explanation">برای شروع مکالمه با آنها ، کد QR شخصی را اسکن کنید</string>
<string name="fragment_view_my_qr_code_explanation">این کد QR شماست. سایر کاربران می توانند برای شروع جلسه با شما آن را اسکن کنند.</string>
<string name="fragment_view_my_qr_code_share_title">کد QR را به اشتراک بگذارید</string>
<string name="view_friend_request_accept_button_title">تایید کنید</string>
<string name="view_friend_request_reject_button_title">کاهش می یابد</string>
<string name="view_friend_request_incoming_pending_message">%1$s برای شما درخواست جلسه ارسال کرد</string>
<string name="view_friend_request_incoming_accepted_message">شما قبول کرده اید %1$s درخواست جلسه</string>
<string name="view_friend_request_incoming_declined_message">شما کاهش داده اید %1$s درخواست جلسه</string>
<string name="view_friend_request_incoming_expired_message">%1$s درخواست جلسه منقضی شده است</string>
<string name="view_friend_request_outgoing_pending_message">شما ارسال کرده اید %1$s درخواست جلسه</string>
<string name="view_friend_request_outgoing_accepted_message">%1$s درخواست جلسه خود را پذیرفته اید</string>
<string name="view_friend_request_outgoing_expired_message">درخواست جلسه شما به %1$s منقضی شده است</string>
<string name="session_reset_banner_message">آیا می خواهید جلسه خود را با آن بازیابی کنید %s ؟</string>
<string name="session_reset_banner_dismiss_button_title">رد</string>
<string name="session_reset_banner_restore_button_title">بازگرداندن</string>
<string name="fragment_contact_selection_contacts_title">مخاطبین</string>
<string name="fragment_contact_selection_closed_groups_title">گروه های بسته</string>
<string name="fragment_contact_selection_open_groups_title">باز کردن گروه ها</string>
</resources>

View File

@ -1561,4 +1561,5 @@ Otrzymano wiadomość wymiany klucz dla niepoprawnej wersji protokołu.</string>
<string name="fragment_contact_selection_contacts_title">Łączność</string>
<string name="fragment_contact_selection_closed_groups_title">Grupy zamknięte</string>
<string name="fragment_contact_selection_open_groups_title">Otwórz grupy</string>
</resources>

View File

@ -1358,6 +1358,7 @@
<!-- Session -->
<string name="continue_2">Продолжить</string>
<string name="copy">Копировать</string>
<string name="invalid_url">Неверная ссылка</string>

View File

@ -1655,7 +1655,6 @@
<!-- Session -->
<string name="continue_2">Continue</string>
<string name="copy">Copy</string>
<string name="invalid_url">Invalid URL</string>

View File

@ -139,6 +139,7 @@
<item name="android:textSize">@dimen/large_font_size</item>
<item name="android:textColor">@color/text</item>
<item name="android:fontFamily">@font/space_mono_regular</item>
<item name="android:textAlignment">viewStart</item>
</style>
<style name="SessionEditText">
@ -150,6 +151,7 @@
<item name="android:textSize">@dimen/small_font_size</item>
<item name="android:textColor">@color/text</item>
<item name="android:textCursorDrawable">@drawable/session_edit_text_cursor</item>
<item name="android:textAlignment">viewStart</item>
<item name="android:maxLines">1</item>
</style>
@ -162,6 +164,7 @@
<item name="android:textSize">@dimen/small_font_size</item>
<item name="android:textColor">@color/text</item>
<item name="android:textCursorDrawable">@drawable/session_edit_text_cursor</item>
<item name="android:textAlignment">viewStart</item>
<item name="android:maxLines">1</item>
</style>

View File

@ -61,14 +61,13 @@ import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
import org.thoughtcrime.securesms.loki.api.BackgroundPollWorker;
import org.thoughtcrime.securesms.loki.api.LokiPublicChatManager;
import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager;
import org.thoughtcrime.securesms.loki.api.PublicChatManager;
import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.protocol.EphemeralMessage;
import org.thoughtcrime.securesms.loki.protocol.LokiSessionResetImplementation;
import org.thoughtcrime.securesms.loki.protocol.PushEphemeralMessageSendJob;
import org.thoughtcrime.securesms.loki.protocol.PushSessionRequestMessageSendJob;
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation;
import org.thoughtcrime.securesms.loki.utilities.Broadcaster;
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
@ -102,14 +101,14 @@ import org.whispersystems.signalservice.loki.api.Poller;
import org.whispersystems.signalservice.loki.api.PushNotificationAcknowledgement;
import org.whispersystems.signalservice.loki.api.SnodeAPI;
import org.whispersystems.signalservice.loki.api.SwarmAPI;
import org.whispersystems.signalservice.loki.api.fileserver.LokiFileServerAPI;
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChatAPI;
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI;
import org.whispersystems.signalservice.loki.api.opengroups.PublicChatAPI;
import org.whispersystems.signalservice.loki.api.shelved.p2p.LokiP2PAPI;
import org.whispersystems.signalservice.loki.api.shelved.p2p.LokiP2PAPIDelegate;
import org.whispersystems.signalservice.loki.database.LokiAPIDatabaseProtocol;
import org.whispersystems.signalservice.loki.protocol.friendrequests.FriendRequestProtocol;
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager;
import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol;
import org.whispersystems.signalservice.loki.protocol.meta.TTLUtilities;
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLink;
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol;
import org.whispersystems.signalservice.loki.protocol.sessionmanagement.SessionManagementProtocol;
@ -154,9 +153,9 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
// Loki
public MessageNotifier messageNotifier = null;
public Poller lokiPoller = null;
public LokiPublicChatManager lokiPublicChatManager = null;
private LokiPublicChatAPI lokiPublicChatAPI = null;
public Poller poller = null;
public PublicChatManager publicChatManager = null;
private PublicChatAPI publicChatAPI = null;
public Broadcaster broadcaster = null;
public SignalCommunicationModule communicationModule;
@ -185,11 +184,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(this);
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
LokiSessionResetImplementation sessionResetImpl = new LokiSessionResetImplementation(this);
SessionResetImplementation sessionResetImpl = new SessionResetImplementation(this);
if (userPublicKey != null) {
SwarmAPI.Companion.configureIfNeeded(apiDB);
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
FriendRequestProtocol.Companion.configureIfNeeded(apiDB, userPublicKey);
MentionsManager.Companion.configureIfNeeded(userPublicKey, threadDB, userDB);
SessionMetaProtocol.Companion.configureIfNeeded(apiDB, userPublicKey);
SyncMessagesProtocol.Companion.configureIfNeeded(apiDB, userPublicKey);
@ -201,11 +199,11 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
if (setUpStorageAPIIfNeeded()) {
if (userPublicKey != null) {
Set<DeviceLink> deviceLinks = DatabaseFactory.getLokiAPIDatabase(this).getDeviceLinks(userPublicKey);
LokiFileServerAPI.shared.setDeviceLinks(deviceLinks);
FileServerAPI.shared.setDeviceLinks(deviceLinks);
}
}
resubmitProfilePictureIfNeeded();
lokiPublicChatManager = new LokiPublicChatManager(this);
publicChatManager = new PublicChatManager(this);
updateOpenGroupProfilePicturesIfNeeded();
registerForFCMIfNeeded(false);
// ========
@ -229,10 +227,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
executePendingContactSync();
KeyCachingService.onAppForegrounded(this);
// Loki
if (lokiPoller != null) { lokiPoller.setCaughtUp(false); }
if (poller != null) { poller.setCaughtUp(false); }
startPollingIfNeeded();
lokiPublicChatManager.markAllAsNotCaughtUp();
lokiPublicChatManager.startPollersIfNeeded();
publicChatManager.markAllAsNotCaughtUp();
publicChatManager.startPollersIfNeeded();
}
@Override
@ -242,8 +240,8 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
KeyCachingService.onAppBackgrounded(this);
messageNotifier.setVisibleThread(-1);
// Loki
if (lokiPoller != null) { lokiPoller.stopIfNeeded(); }
if (lokiPublicChatManager != null) { lokiPublicChatManager.stopPollers(); }
if (poller != null) { poller.stopIfNeeded(); }
if (publicChatManager != null) { publicChatManager.stopPollers(); }
}
@Override
@ -284,15 +282,15 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
}
// Loki
public @Nullable LokiPublicChatAPI getLokiPublicChatAPI() {
if (lokiPublicChatAPI != null || !IdentityKeyUtil.hasIdentityKey(this)) { return lokiPublicChatAPI; }
public @Nullable PublicChatAPI getPublicChatAPI() {
if (publicChatAPI != null || !IdentityKeyUtil.hasIdentityKey(this)) { return publicChatAPI; }
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey== null) { return lokiPublicChatAPI; }
if (userPublicKey== null) { return publicChatAPI; }
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
lokiPublicChatAPI = new LokiPublicChatAPI(userPublicKey, userPrivateKey, apiDB, userDB);
return lokiPublicChatAPI;
publicChatAPI = new PublicChatAPI(userPublicKey, userPrivateKey, apiDB, userDB);
return publicChatAPI;
}
private void initializeSecurityProvider() {
@ -446,8 +444,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
}
private static class ProviderInitializationException extends RuntimeException {
}
private static class ProviderInitializationException extends RuntimeException { }
// region Loki
public boolean setUpStorageAPIIfNeeded() {
@ -456,7 +453,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
boolean isDebugMode = BuildConfig.DEBUG;
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize();
LokiAPIDatabaseProtocol apiDB = DatabaseFactory.getLokiAPIDatabase(this);
LokiFileServerAPI.Companion.configure(isDebugMode, userPublicKey, userPrivateKey, apiDB);
FileServerAPI.Companion.configure(userPublicKey, userPrivateKey, apiDB);
return true;
}
@ -495,16 +492,16 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
private void setUpPollingIfNeeded() {
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (userPublicKey == null) return;
if (lokiPoller != null) {
if (poller != null) {
SnodeAPI.shared.setUserPublicKey(userPublicKey);
lokiPoller.setUserPublicKey(userPublicKey);
poller.setUserPublicKey(userPublicKey);
return;
}
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
Context context = this;
SwarmAPI.Companion.configureIfNeeded(apiDB);
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
lokiPoller = new Poller(userPublicKey, apiDB, protos -> {
poller = new Poller(userPublicKey, apiDB, protos -> {
for (SignalServiceProtos.Envelope proto : protos) {
new PushContentReceiveJob(context).processEnvelope(new SignalServiceEnvelope(proto), false);
}
@ -514,12 +511,12 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
public void startPollingIfNeeded() {
setUpPollingIfNeeded();
if (lokiPoller != null) { lokiPoller.startIfNeeded(); }
if (poller != null) { poller.startIfNeeded(); }
}
public void stopPolling() {
if (lokiPoller == null) { return; }
lokiPoller.stopIfNeeded();
if (poller == null) { return; }
poller.stopIfNeeded();
}
private void resubmitProfilePictureIfNeeded() {
@ -534,7 +531,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
try {
File profilePicture = AvatarHelper.getAvatarFile(this, Address.fromSerialized(userPublicKey));
StreamDetails stream = new StreamDetails(new FileInputStream(profilePicture), "image/jpeg", profilePicture.length());
LokiFileServerAPI.shared.uploadProfilePicture(LokiFileServerAPI.shared.getServer(), profileKey, stream, () -> {
FileServerAPI.shared.uploadProfilePicture(FileServerAPI.shared.getServer(), profileKey, stream, () -> {
TextSecurePreferences.setLastProfilePictureUpload(this, new Date().getTime());
TextSecurePreferences.setProfileAvatarId(this, new SecureRandom().nextInt());
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey);
@ -548,9 +545,9 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
public void updateOpenGroupProfilePicturesIfNeeded() {
AsyncTask.execute(() -> {
LokiPublicChatAPI publicChatAPI = null;
PublicChatAPI publicChatAPI = null;
try {
publicChatAPI = getLokiPublicChatAPI();
publicChatAPI = getPublicChatAPI();
} catch (Exception e) {
// Do nothing
}
@ -589,8 +586,19 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
Runtime.getRuntime().exit(0);
}
public boolean hasSentSessionRequestExpired(@NotNull String publicKey) {
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
Long timestamp = apiDB.getSessionRequestSentTimestamp(publicKey);
if (timestamp != null) {
long expiration = timestamp + TTLUtilities.getTTL(TTLUtilities.MessageType.SessionRequest);
return new Date().getTime() > expiration;
} else {
return false;
}
}
@Override
public void sendSessionRequest(@NotNull String publicKey) {
public void sendSessionRequestIfNeeded(@NotNull String publicKey) {
// It's never necessary to establish a session with self
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
if (publicKey.equals(userPublicKey)) { return; }
@ -598,14 +606,19 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
SignalProtocolAddress address = new SignalProtocolAddress(publicKey, SignalServiceAddress.DEFAULT_DEVICE_ID);
boolean hasSession = new TextSecureSessionStore(this).containsSession(address);
if (hasSession) { return; }
// Check that we didn't already send or process a session request
// Check that we didn't already send a session request
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
boolean hasSentOrProcessedSessionRequest = (apiDB.getSessionRequestTimestamp(publicKey) != null);
if (hasSentOrProcessedSessionRequest) { return; }
boolean hasSentSessionRequest = (apiDB.getSessionRequestSentTimestamp(publicKey) != null);
boolean hasSentSessionRequestExpired = hasSentSessionRequestExpired(publicKey);
if (hasSentSessionRequestExpired) {
apiDB.setSessionRequestSentTimestamp(publicKey, 0);
}
if (hasSentSessionRequest && !hasSentSessionRequestExpired) { return; }
// Send the session request
DatabaseFactory.getLokiAPIDatabase(this).setSessionRequestTimestamp(publicKey, new Date().getTime());
EphemeralMessage sessionRequest = EphemeralMessage.createSessionRequest(publicKey);
jobManager.add(new PushEphemeralMessageSendJob(sessionRequest));
long timestamp = new Date().getTime();
apiDB.setSessionRequestSentTimestamp(publicKey, timestamp);
PushSessionRequestMessageSendJob job = new PushSessionRequestMessageSendJob(publicKey, timestamp);
jobManager.add(job);
}
// endregion
}

View File

@ -58,8 +58,8 @@ 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.fileserver.LokiFileServerAPI;
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChatAPI;
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI;
import org.whispersystems.signalservice.loki.api.opengroups.PublicChatAPI;
import java.io.ByteArrayInputStream;
import java.io.File;
@ -386,7 +386,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
Context context = CreateProfileActivity.this;
TextSecurePreferences.setProfileName(context, name);
LokiPublicChatAPI publicChatAPI = ApplicationContext.getInstance(context).getLokiPublicChatAPI();
PublicChatAPI publicChatAPI = ApplicationContext.getInstance(context).getPublicChatAPI();
if (publicChatAPI != null) {
Set<String> servers = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChatServers();
for (String server : servers) {
@ -409,7 +409,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
// Loki - Upload the profile photo here
if (avatar != null) {
Log.d("Loki", "Start uploading profile photo");
LokiFileServerAPI storageAPI = LokiFileServerAPI.shared;
FileServerAPI storageAPI = FileServerAPI.shared;
LokiDotNetAPI.UploadResult result = storageAPI.uploadProfilePicture(storageAPI.getServer(), profileKey, avatar, () -> {
TextSecurePreferences.setLastProfilePictureUpload(CreateProfileActivity.this, new Date().getTime());
return Unit.INSTANCE;

View File

@ -322,7 +322,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
@Override
public void onClick(View v) {
Intent intent = new Intent(GroupCreateActivity.this, PushContactSelectionActivity.class);
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_FRIENDS);
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_CONTACTS);
startActivityForResult(intent, PICK_CONTACT);
}
}

View File

@ -53,7 +53,7 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_FRIENDS);
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, DisplayMode.FLAG_CONTACTS);
getIntent().putExtra(ContactSelectionListFragment.MULTI_SELECT, true);
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);

View File

@ -31,7 +31,7 @@ import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChat;
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat;
import java.util.List;
@ -197,7 +197,7 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener
long threadID = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(conversationRecipient);
String senderHexEncodedPublicKey = author.getAddress().serialize();
LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadID);
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadID);
if (senderHexEncodedPublicKey.equalsIgnoreCase(TextSecurePreferences.getLocalNumber(getContext()))) {
quoteeDisplayName = TextSecurePreferences.getProfileName(getContext());
} else if (publicChat != null) {

View File

@ -84,7 +84,7 @@ public class TypingStatusSender {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
Recipient recipient = threadDatabase.getRecipientForThreadId(threadId);
// Loki - Check whether we want to send a typing indicator to this user
if (!SessionMetaProtocol.shouldSendTypingIndicator(recipient, context)) { return; }
if (recipient != null && !SessionMetaProtocol.shouldSendTypingIndicator(recipient.getAddress())) { return; }
// Loki - Take into account multi device
Set<String> linkedDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(recipient.getAddress().serialize());
for (String device : linkedDevices) {

View File

@ -68,7 +68,6 @@ import android.view.View.OnFocusChangeListener;
import android.view.View.OnKeyListener;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
@ -81,7 +80,6 @@ import com.annimon.stream.Stream;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.jetbrains.annotations.NotNull;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ExpirationDialog;
import org.thoughtcrime.securesms.GroupCreateActivity;
@ -159,10 +157,8 @@ import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabaseDelegate;
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
import org.thoughtcrime.securesms.loki.protocol.FriendRequestProtocol;
import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol;
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities;
import org.thoughtcrime.securesms.loki.views.FriendRequestViewDelegate;
import org.thoughtcrime.securesms.loki.views.MentionCandidateSelectionView;
import org.thoughtcrime.securesms.loki.views.SessionRestoreBannerView;
import org.thoughtcrime.securesms.mediasend.Media;
@ -187,7 +183,6 @@ import org.thoughtcrime.securesms.mms.StickerSlide;
import org.thoughtcrime.securesms.mms.TextSlide;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
@ -227,12 +222,11 @@ import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChat;
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat;
import org.whispersystems.signalservice.loki.protocol.mentions.Mention;
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager;
import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol;
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol;
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation;
import java.io.IOException;
@ -273,8 +267,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
ComposeText.CursorPositionChangedListener,
ConversationSearchBottomBar.EventListener,
StickerKeyboardProvider.StickerEventListener,
LokiThreadDatabaseDelegate,
FriendRequestViewDelegate
LokiThreadDatabaseDelegate
{
private static final String TAG = ConversationActivity.class.getSimpleName();
@ -356,14 +349,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
// Message status bar
private ArrayList<BroadcastReceiver> broadcastReceivers = new ArrayList<>();
private String messageStatus = null;
private String messageStatus = null;
// Mentions
private View mentionCandidateSelectionViewContainer;
private View mentionCandidateSelectionViewContainer;
private MentionCandidateSelectionView mentionCandidateSelectionView;
private int currentMentionStartIndex = -1;
private ArrayList<Mention> mentions = new ArrayList<>();
private String oldText = "";
private int currentMentionStartIndex = -1;
private ArrayList<Mention> mentions = new ArrayList<>();
private String oldText = "";
// Restoration
protected SessionRestoreBannerView sessionRestoreBannerView;
@ -387,7 +380,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
getWindow().getDecorView().setBackgroundColor(color);
fragment = initFragment(R.id.fragment_content, new ConversationFragment(), dynamicLanguage.getCurrentLocale());
fragment.friendRequestViewDelegate = this;
registerMessageStatusObserver("calculatingPoW");
registerMessageStatusObserver("contactingNetwork");
@ -465,9 +457,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(threadId, this);
LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
if (publicChat != null) {
ApplicationContext.getInstance(this).getLokiPublicChatAPI().getChannelInfo(publicChat.getChannel(), publicChat.getServer()).success( displayName -> {
ApplicationContext.getInstance(this).getPublicChatAPI().getChannelInfo(publicChat.getChannel(), publicChat.getServer()).success(displayName -> {
updateSubtitleTextView();
return Unit.INSTANCE;
});
@ -555,7 +547,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
DatabaseFactory.getLokiThreadDatabase(this).setDelegate(this);
updateInputPanel();
inputPanel.setHint("Message");
updateSessionRestoreBanner();
@ -761,10 +753,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
if (isSingleConversation()) {
/*
if (isSecureText) inflater.inflate(R.menu.conversation_callable_secure, menu);
else inflater.inflate(R.menu.conversation_callable_insecure, menu);
*/
if (recipient.isBlocked()) {
inflater.inflate(R.menu.conversation_unblock, menu);
} else {
inflater.inflate(R.menu.conversation_block, menu);
}
} else if (isGroupConversation() && !isOpenGroupOrRSSFeed) {
inflater.inflate(R.menu.conversation_group_options, menu);
@ -868,8 +861,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case R.id.menu_call_secure: handleDial(getRecipient(), true); return true;
case R.id.menu_call_insecure: handleDial(getRecipient(), false); return true;
case R.id.menu_call_secure: handleDial(getRecipient(), true); return true;
case R.id.menu_call_insecure: handleDial(getRecipient(), false); return true;
case R.id.menu_unblock: handleUnblock(); return true;
case R.id.menu_block: handleBlock(); return true;
case R.id.menu_view_media: handleViewMedia(); return true;
case R.id.menu_add_shortcut: handleAddShortcut(); return true;
case R.id.menu_search: handleSearch(); return true;
@ -994,12 +989,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
int titleRes = R.string.ConversationActivity_unblock_this_contact_question;
int bodyRes = R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact;
if (recipient.isGroupRecipient()) {
titleRes = R.string.ConversationActivity_unblock_this_group_question;
bodyRes = R.string.ConversationActivity_unblock_this_group_description;
}
//noinspection CodeBlock2Expr
new AlertDialog.Builder(this)
.setTitle(titleRes)
.setMessage(bodyRes)
@ -1078,6 +1067,33 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
builder.show();
}
private void handleBlock() {
int titleRes = R.string.RecipientPreferenceActivity_block_this_contact_question;
int bodyRes = R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact;
new AlertDialog.Builder(this)
.setTitle(titleRes)
.setMessage(bodyRes)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.RecipientPreferenceActivity_block, (dialog, which) -> {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
DatabaseFactory.getRecipientDatabase(ConversationActivity.this)
.setBlocked(recipient, true);
ApplicationContext.getInstance(ConversationActivity.this)
.getJobManager()
.add(new MultiDeviceBlockedUpdateJob());
Util.runOnMain(() -> finish());
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}).show();
}
private void handleViewMedia() {
Intent intent = new Intent(this, MediaOverviewActivity.class);
intent.putExtra(MediaOverviewActivity.ADDRESS_EXTRA, recipient.getAddress());
@ -2172,7 +2188,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
try {
int startIndex = result.indexOf("@" + mention.getDisplayName());
int endIndex = startIndex + mention.getDisplayName().length() + 1; // + 1 to include the @
result = result.substring(0, startIndex) + "@" + mention.getHexEncodedPublicKey() + result.substring(endIndex);
result = result.substring(0, startIndex) + "@" + mention.getPublicKey() + result.substring(endIndex);
} catch (Exception exception) {
Log.d("Loki", "Couldn't process mention due to error: " + exception.toString() + ".");
}
@ -2253,26 +2269,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
updateLinkPreviewState();
}
@Override
public void handleThreadFriendRequestStatusChanged(long threadID) {
if (recipient.isGroupRecipient()) { return; }
boolean isUpdateNeeded = false;
if (threadID == this.threadId) {
isUpdateNeeded = true;
} else {
String thisThreadPublicKey = recipient.getAddress().serialize();
Set<String> thisThreadAssociatedDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(thisThreadPublicKey);
Recipient changedThreadRecipient = DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadID);
String changedThreadPublicKey = changedThreadRecipient.getAddress().serialize();
for (String device : thisThreadAssociatedDevices) {
if (device.equals(changedThreadPublicKey)) { isUpdateNeeded = true; }
}
}
if (isUpdateNeeded) {
updateInputPanel();
}
}
@Override
public void handleSessionRestoreDevicesChanged(long threadID) {
if (threadID == this.threadId) {
@ -2280,21 +2276,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private void updateInputPanel() {
boolean shouldInputPanelBeEnabled = FriendRequestProtocol.shouldInputPanelBeEnabled(this, recipient);
Util.runOnMain(() -> {
updateToggleButtonState();
String hint = shouldInputPanelBeEnabled ? "Message" : "Pending session request";
inputPanel.setHint(hint);
inputPanel.setEnabled(shouldInputPanelBeEnabled);
if (shouldInputPanelBeEnabled && inputPanel.getVisibility() == View.VISIBLE) {
inputPanel.composeText.requestFocus();
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
inputMethodManager.showSoftInput(inputPanel.composeText, 0);
}
});
}
private void sendMessage() {
if (inputPanel.isRecordingInLockedMode()) {
inputPanel.releaseRecordingLock();
@ -2396,11 +2377,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
outgoingMessage = outgoingMessageCandidate;
}
// Loki - Send a friend request if we're not yet friends with the user in question
LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId);
outgoingMessage.isFriendRequest = !isGroupConversation() && friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS
&& !SessionMetaProtocol.shared.isNoteToSelf(recipient.getAddress().serialize()); // Needed for stageOutgoingMessage(...)
if (clearComposeBox) {
inputPanel.clearQuote();
attachmentManager.clear(glideRequests, false);
@ -2409,6 +2385,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
final long id = fragment.stageOutgoingMessage(outgoingMessage);
if (!recipient.isGroupRecipient()) {
ApplicationContext.getInstance(this).sendSessionRequestIfNeeded(recipient.getAddress().serialize());
}
new AsyncTask<Void, Void, Long>() {
@Override
protected Long doInBackground(Void... param) {
@ -2449,14 +2429,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
message = new OutgoingTextMessage(recipient, messageBody, expiresIn, subscriptionId);
}
// Loki - Send a friend request if we're not yet friends with the user in question
LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId);
message.isFriendRequest = !isGroupConversation() && friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS
&& !SessionMetaProtocol.shared.isNoteToSelf(recipient.getAddress().serialize()); // Needed for stageOutgoingMessage(...)
silentlySetComposeText("");
final long id = fragment.stageOutgoingMessage(message);
if (!recipient.isGroupRecipient()) {
ApplicationContext.getInstance(this).sendSessionRequestIfNeeded(recipient.getAddress().serialize());
}
new AsyncTask<OutgoingTextMessage, Void, Long>() {
@Override
protected Long doInBackground(OutgoingTextMessage... messages) {
@ -2483,13 +2462,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void updateToggleButtonState() {
if (!FriendRequestProtocol.shouldAttachmentButtonBeEnabled(this, recipient)) {
buttonToggle.display(sendButton);
quickAttachmentToggle.hide();
inlineAttachmentToggle.hide();
return;
}
if (inputPanel.isRecordingInLockedMode()) {
buttonToggle.display(sendButton);
quickAttachmentToggle.show();
@ -3107,7 +3079,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
muteIndicatorImageView.setVisibility(View.VISIBLE);
subtitleTextView.setText("Muted until " + DateUtils.getFormattedDateTime(recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()));
} else if (recipient.isGroupRecipient() && recipient.getName() != null && !recipient.getName().equals("Session Updates") && !recipient.getName().equals("Loki News")) {
LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(this).getPublicChat(threadId);
if (publicChat != null) {
Integer userCount = DatabaseFactory.getLokiAPIDatabase(this).getUserCount(publicChat.getChannel(), publicChat.getServer());
if (userCount == null) { userCount = 0; }
@ -3193,19 +3165,5 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
updateSubtitleTextView();
updateMessageStatusProgressBar();
}
@Override
public void acceptFriendRequest(@NotNull MessageRecord friendRequest) {
if (recipient.isGroupRecipient()) { return; }
FriendRequestProtocol.acceptFriendRequest(this, recipient);
updateInputPanel();
}
@Override
public void rejectFriendRequest(@NotNull MessageRecord friendRequest) {
if (recipient.isGroupRecipient()) { return; }
FriendRequestProtocol.rejectFriendRequest(this, recipient);
updateInputPanel();
}
// endregion
}

View File

@ -40,7 +40,6 @@ import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.views.FriendRequestViewDelegate;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -108,8 +107,6 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
private MessageRecord recordToPulseHighlight;
private String searchQuery;
public FriendRequestViewDelegate friendRequestViewDelegate; // Loki
protected static class ViewHolder extends RecyclerView.ViewHolder {
public <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
super(itemView);
@ -202,11 +199,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
MessageRecord previousRecord = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getRecordForPositionOrThrow(adapterPosition + 1) : null;
MessageRecord nextRecord = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getRecordForPositionOrThrow(adapterPosition - 1) : null;
BindableConversationItem conversationItem = viewHolder.getView();
if (conversationItem instanceof ConversationItem) {
((ConversationItem)conversationItem).friendRequestViewDelegate = this.friendRequestViewDelegate;
}
conversationItem.bind(messageRecord,
viewHolder.getView().bind(messageRecord,
Optional.fromNullable(previousRecord),
Optional.fromNullable(nextRecord),
glideRequests,

View File

@ -79,7 +79,6 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.views.FriendRequestViewDelegate;
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.GlideApp;
@ -101,8 +100,8 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChat;
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChatAPI;
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat;
import org.whispersystems.signalservice.loki.api.opengroups.PublicChatAPI;
import java.io.IOException;
import java.io.InputStream;
@ -151,7 +150,6 @@ public class ConversationFragment extends Fragment
private View composeDivider;
private View scrollToBottomButton;
private TextView scrollDateHeader;
public FriendRequestViewDelegate friendRequestViewDelegate; // Loki
@Override
public void onCreate(Bundle icicle) {
@ -399,7 +397,7 @@ public class ConversationFragment extends Fragment
boolean isGroupChat = recipient.isGroupRecipient();
if (isGroupChat) {
LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
boolean isPublicChat = (publicChat != null);
int selectedMessageCount = messageRecords.size();
boolean areAllSentByUser = true;
@ -409,7 +407,7 @@ public class ConversationFragment extends Fragment
menu.findItem(R.id.menu_context_copy_public_key).setVisible(isPublicChat && selectedMessageCount == 1 && !areAllSentByUser);
menu.findItem(R.id.menu_context_reply).setVisible(selectedMessageCount == 1);
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
boolean userCanModerate = isPublicChat && LokiPublicChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer());
boolean userCanModerate = isPublicChat && PublicChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer());
boolean isDeleteOptionVisible = !isPublicChat || (areAllSentByUser || userCanModerate);
menu.findItem(R.id.menu_context_delete_message).setVisible(isDeleteOptionVisible);
} else {
@ -502,7 +500,7 @@ public class ConversationFragment extends Fragment
builder.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount));
builder.setCancelable(true);
LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
@Override
@ -518,7 +516,7 @@ public class ConversationFragment extends Fragment
ArrayList<Long> ignoredMessages = new ArrayList<>();
ArrayList<Long> failedMessages = new ArrayList<>();
boolean isSentByUser = true;
LokiPublicChatAPI publicChatAPI = ApplicationContext.getInstance(getContext()).getLokiPublicChatAPI();
PublicChatAPI publicChatAPI = ApplicationContext.getInstance(getContext()).getPublicChatAPI();
for (MessageRecord messageRecord : messageRecords) {
isSentByUser = isSentByUser && messageRecord.isOutgoing();
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id);
@ -706,7 +704,6 @@ public class ConversationFragment extends Fragment
if (adapter == null) {
return;
}
adapter.friendRequestViewDelegate = this.friendRequestViewDelegate;
if (cursor.getCount() >= PARTIAL_CONVERSATION_LIMIT && loader.hasLimit()) {
adapter.setFooterView(topLoadMoreView);

View File

@ -86,10 +86,7 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities;
import org.thoughtcrime.securesms.loki.views.FriendRequestView;
import org.thoughtcrime.securesms.loki.views.FriendRequestViewDelegate;
import org.thoughtcrime.securesms.loki.views.ProfilePictureView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
@ -112,8 +109,8 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChat;
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChatAPI;
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat;
import org.whispersystems.signalservice.loki.api.opengroups.PublicChatAPI;
import java.util.Collections;
import java.util.HashSet;
@ -158,7 +155,6 @@ public class ConversationItem extends LinearLayout
private ViewGroup contactPhotoHolder;
private AlertView alertView;
private ViewGroup container;
private FriendRequestView friendRequestView;
private @NonNull Set<MessageRecord> batchSelected = new HashSet<>();
private Recipient conversationRecipient;
@ -182,8 +178,6 @@ public class ConversationItem extends LinearLayout
private final Context context;
public FriendRequestViewDelegate friendRequestViewDelegate; // Loki
public ConversationItem(Context context) {
this(context, null);
}
@ -223,7 +217,6 @@ public class ConversationItem extends LinearLayout
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
this.quoteView = findViewById(R.id.quote_view);
this.container = findViewById(R.id.container);
this.friendRequestView = findViewById(R.id.friend_request_view);
setOnClickListener(new ClickListener(null));
@ -269,7 +262,6 @@ public class ConversationItem extends LinearLayout
setQuote(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setMessageSpacing(context, messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setFooter(messageRecord, nextMessageRecord, locale, groupThread);
setFriendRequestView(messageRecord);
adjustMarginsIfNeeded(messageRecord);
}
@ -807,8 +799,8 @@ public class ConversationItem extends LinearLayout
layoutParams.setMarginStart((groupThread && !isRSSFeed) ? groupThreadMargin : defaultMargin);
bodyBubble.setLayoutParams(layoutParams);
if (profilePictureView == null) return;
profilePictureView.setHexEncodedPublicKey(recipient.getAddress().toString());
profilePictureView.setAdditionalHexEncodedPublicKey(null);
profilePictureView.setPublicKey(recipient.getAddress().toString());
profilePictureView.setAdditionalPublicKey(null);
profilePictureView.setRSSFeed(false);
profilePictureView.setGlide(glideRequests);
profilePictureView.update();
@ -920,11 +912,6 @@ public class ConversationItem extends LinearLayout
}
}
private void setFriendRequestView(@NonNull MessageRecord record) {
friendRequestView.setDelegate(friendRequestViewDelegate);
friendRequestView.update(record);
}
private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) {
if (hasSticker(messageRecord)) {
return stickerFooter;
@ -1001,9 +988,9 @@ public class ConversationItem extends LinearLayout
profilePictureView.setVisibility(VISIBLE);
int visibility = View.GONE;
LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId());
PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId());
if (publicChat != null) {
boolean isModerator = LokiPublicChatAPI.Companion.isUserModerator(current.getRecipient().getAddress().toString(), publicChat.getChannel(), publicChat.getServer());
boolean isModerator = PublicChatAPI.Companion.isUserModerator(current.getRecipient().getAddress().toString(), publicChat.getChannel(), publicChat.getServer());
visibility = isModerator ? View.VISIBLE : View.GONE;
}
@ -1066,14 +1053,6 @@ public class ConversationItem extends LinearLayout
int spacingTop = readDimen(context, R.dimen.conversation_vertical_message_spacing_collapse);
int spacingBottom = spacingTop;
boolean isOutgoingStack = current.isOutgoing() && previous.orNull() != null && previous.get().isOutgoing();
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
boolean isPreviousMessageFriendRequest = previous.orNull() != null && lokiMessageDatabase.isFriendRequest(previous.get().id);
if (isOutgoingStack && isPreviousMessageFriendRequest) {
spacingTop = readDimen(context, R.dimen.conversation_vertical_message_spacing_default);
}
if (isStartOfMessageCluster(current, previous, isGroupThread)) {
spacingTop = readDimen(context, R.dimen.conversation_vertical_message_spacing_default);
}

View File

@ -27,15 +27,14 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
public class RecipientDatabase extends Database {
private static final String TAG = RecipientDatabase.class.getSimpleName();
static final String TABLE_NAME = "recipient_preferences";
static final String TABLE_NAME = "recipient_preferences";
private static final String ID = "_id";
static final String ADDRESS = "recipient_ids";
public static final String ADDRESS = "recipient_ids";
private static final String BLOCK = "block";
private static final String NOTIFICATION = "notification";
private static final String VIBRATE = "vibrate";

View File

@ -885,7 +885,7 @@ public class SmsDatabase extends MessagingDatabase {
0, message.isSecureMessage() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(),
threadId, 0, new LinkedList<IdentityKeyMismatch>(),
message.getSubscriptionId(), message.getExpiresIn(),
System.currentTimeMillis(), 0, false, message.isFriendRequest);
System.currentTimeMillis(), 0, false);
}
}
@ -934,15 +934,12 @@ public class SmsDatabase extends MessagingDatabase {
List<IdentityKeyMismatch> mismatches = getMismatches(mismatchDocument);
Recipient recipient = Recipient.from(context, address, true);
// Loki - Check to see if this message was a friend request
boolean isFriendRequest = DatabaseFactory.getLokiMessageDatabase(context).isFriendRequest(messageId);
return new SmsMessageRecord(messageId, body, recipient,
recipient,
addressDeviceId,
dateSent, dateReceived, deliveryReceiptCount, type,
threadId, status, mismatches, subscriptionId,
expiresIn, expireStarted, readReceiptCount, unidentified, isFriendRequest);
expiresIn, expireStarted, readReceiptCount, unidentified);
}
private List<IdentityKeyMismatch> getMismatches(String document) {

View File

@ -45,7 +45,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChat;
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat;
import java.io.File;
@ -83,8 +83,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV7 = 28;
private static final int lokiV8 = 29;
private static final int lokiV9 = 30;
private static final int lokiV10 = 31;
private static final int DATABASE_VERSION = lokiV9; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
private static final int DATABASE_VERSION = lokiV10; // 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;
@ -143,12 +144,13 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiAPIDatabase.getCreateDeviceLinkCacheCommand());
db.execSQL(LokiAPIDatabase.getCreateUserCountCacheCommand());
db.execSQL(LokiAPIDatabase.getCreateSessionRequestTimestampCacheCommand());
db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampCacheCommand());
db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampCacheCommand());
db.execSQL(LokiPreKeyBundleDatabase.getCreateTableCommand());
db.execSQL(LokiPreKeyRecordDatabase.getCreateTableCommand());
db.execSQL(LokiMessageDatabase.getCreateMessageFriendRequestTableCommand());
db.execSQL(LokiMessageDatabase.getCreateMessageIDTableCommand());
db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand());
db.execSQL(LokiMessageDatabase.getCreateErrorMessageTableCommand());
db.execSQL(LokiThreadDatabase.getCreateFriendRequestTableCommand());
db.execSQL(LokiThreadDatabase.getCreateSessionResetTableCommand());
db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand());
db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand());
@ -545,7 +547,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
try (Cursor lokiPublicChatCursor = db.rawQuery("SELECT public_chat FROM loki_public_chat_database", null)) {
while (lokiPublicChatCursor != null && lokiPublicChatCursor.moveToNext()) {
String chatString = lokiPublicChatCursor.getString(0);
LokiPublicChat publicChat = LokiPublicChat.fromJSON(chatString);
PublicChat publicChat = PublicChat.fromJSON(chatString);
if (publicChat != null) {
byte[] groupId = publicChat.getId().getBytes();
String oldId = GroupUtil.getEncodedId(groupId, false);
@ -590,6 +592,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathCacheCommand());
}
if (oldVersion < lokiV10) {
db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampCacheCommand());
db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampCacheCommand());
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@ -39,23 +39,6 @@ import network.loki.messenger.R;
*/
public class SmsMessageRecord extends MessageRecord {
// Loki
private final boolean isFriendRequest;
public SmsMessageRecord(long id,
String body, Recipient recipient,
Recipient individualRecipient,
int recipientDeviceId,
long dateSent, long dateReceived,
int deliveryReceiptCount,
long type, long threadId,
int status, List<IdentityKeyMismatch> mismatches,
int subscriptionId, long expiresIn, long expireStarted,
int readReceiptCount, boolean unidentified)
{
this(id, body, recipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, deliveryReceiptCount, type, threadId, status, mismatches, subscriptionId, expiresIn, expireStarted, readReceiptCount, unidentified, false);
}
public SmsMessageRecord(long id,
String body, Recipient recipient,
Recipient individualRecipient,
@ -65,22 +48,18 @@ public class SmsMessageRecord extends MessageRecord {
long type, long threadId,
int status, List<IdentityKeyMismatch> mismatches,
int subscriptionId, long expiresIn, long expireStarted,
int readReceiptCount, boolean unidentified, boolean isFriendRequest)
int readReceiptCount, boolean unidentified)
{
super(id, body, recipient, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, status, deliveryReceiptCount, type,
mismatches, new LinkedList<>(), subscriptionId,
expiresIn, expireStarted, readReceiptCount, unidentified);
this.isFriendRequest = isFriendRequest;
}
public long getType() {
return type;
}
// Loki
public boolean isFriendRequest() { return isFriendRequest; }
@Override
public SpannableString getDisplayBody(@NonNull Context context) {
Recipient recipient = getRecipient();

View File

@ -45,7 +45,7 @@ import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
import org.thoughtcrime.securesms.jobs.TypingSendJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.protocol.LokiSessionResetImplementation;
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation;
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceOpenGroupUpdateJob;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
import org.thoughtcrime.securesms.push.MessageSenderEventListener;
@ -152,12 +152,11 @@ public class SignalCommunicationModule {
Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()),
Optional.of(new MessageSenderEventListener(context)),
TextSecurePreferences.getLocalNumber(context),
TextSecurePreferences.getMasterHexEncodedPublicKey(context),
DatabaseFactory.getLokiAPIDatabase(context),
DatabaseFactory.getLokiThreadDatabase(context),
DatabaseFactory.getLokiMessageDatabase(context),
DatabaseFactory.getLokiPreKeyBundleDatabase(context),
new LokiSessionResetImplementation(context),
new SessionResetImplementation(context),
DatabaseFactory.getLokiUserDatabase(context),
((ApplicationContext)context.getApplicationContext()).broadcaster);
} else {

View File

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

View File

@ -14,8 +14,8 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceOpenGroupUpdateJob;
import org.thoughtcrime.securesms.loki.protocol.PushEphemeralMessageSendJob;
import org.thoughtcrime.securesms.loki.protocol.PushNullMessageSendJob;
import org.thoughtcrime.securesms.loki.protocol.PushSessionRequestMessageSendJob;
import java.util.Arrays;
import java.util.HashMap;
@ -72,7 +72,7 @@ public final class JobManagerFactories {
put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
put(TypingSendJob.KEY, new TypingSendJob.Factory());
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
put(PushEphemeralMessageSendJob.KEY, new PushEphemeralMessageSendJob.Factory());
put(PushSessionRequestMessageSendJob.KEY, new PushSessionRequestMessageSendJob.Factory());
put(MultiDeviceOpenGroupUpdateJob.KEY, new MultiDeviceOpenGroupUpdateJob.Factory());
}};
}

View File

@ -122,7 +122,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
}
if (address == null) generateFullContactUpdate();
else if (SyncMessagesProtocol.shouldSyncContact(context, Address.fromSerialized(address))) generateSingleContactUpdate(Address.fromSerialized(address));
else if (SyncMessagesProtocol.shouldSyncContact(context, address)) generateSingleContactUpdate(Address.fromSerialized(address));
}
private void generateSingleContactUpdate(@NonNull Address address)
@ -139,8 +139,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
Optional<IdentityDatabase.IdentityRecord> identityRecord = DatabaseFactory.getIdentityDatabase(context).getIdentity(address);
Optional<VerifiedMessage> verifiedMessage = getVerifiedMessage(recipient, identityRecord);
// Loki - Only sync contacts we are friends with
if (SyncMessagesProtocol.shouldSyncContact(context, address)) {
if (SyncMessagesProtocol.shouldSyncContact(context, address.serialize())) {
out.write(new DeviceContact(address.toPhoneString(),
Optional.fromNullable(recipient.getName()),
getAvatar(recipient.getContactUri()),

View File

@ -48,7 +48,6 @@ import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
@ -66,16 +65,11 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.activities.HomeActivity;
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
import org.thoughtcrime.securesms.loki.protocol.EphemeralMessage;
import org.thoughtcrime.securesms.loki.protocol.FriendRequestProtocol;
import org.thoughtcrime.securesms.loki.protocol.LokiSessionResetImplementation;
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol;
import org.thoughtcrime.securesms.loki.protocol.PushEphemeralMessageSendJob;
import org.thoughtcrime.securesms.loki.protocol.PushNullMessageSendJob;
import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol;
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation;
import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol;
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities;
import org.thoughtcrime.securesms.loki.utilities.PromiseUtilities;
@ -103,7 +97,7 @@ import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.loki.LokiSessionResetProtocol;
import org.whispersystems.libsignal.loki.SessionResetProtocol;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
@ -128,13 +122,11 @@ import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOper
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.fileserver.LokiFileServerAPI;
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI;
import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher;
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager;
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
@ -146,8 +138,6 @@ import javax.inject.Inject;
import network.loki.messenger.R;
import static org.thoughtcrime.securesms.loki.utilities.RecipientUtilitiesKt.recipient;
public class PushDecryptJob extends BaseJob implements InjectableType {
public static final String KEY = "PushDecryptJob";
@ -226,9 +216,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
@Override
public void onCanceled() {
}
public void onCanceled() { }
public void processMessage(@NonNull SignalServiceEnvelope envelope, boolean isPushNotification) {
synchronized (PushReceivedJob.RECEIVE_LOCK) {
@ -263,32 +251,21 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
private void handleMessage(@NonNull SignalServiceEnvelope envelope, @NonNull Optional<Long> smsMessageId, boolean isPushNotification) {
try {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context);
LokiSessionResetProtocol lokiSessionResetProtocol = new LokiSessionResetImplementation(context);
SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context));
LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, lokiSessionResetProtocol, UnidentifiedAccessUtil.getCertificateValidator());
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context);
SessionResetProtocol sessionResetProtocol = new SessionResetImplementation(context);
SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context));
LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, sessionResetProtocol, UnidentifiedAccessUtil.getCertificateValidator());
SignalServiceContent content = cipher.decrypt(envelope);
// Loki - Ignore any friend requests from before restoration
if (FriendRequestProtocol.isFriendRequestFromBeforeRestoration(context, content)) {
Log.d("Loki", "Ignoring friend request from before restoration.");
return;
}
if (shouldIgnore(content)) {
Log.i(TAG, "Ignoring message.");
return;
}
// Loki - Handle pre key bundle message if needed
SessionManagementProtocol.handlePreKeyBundleMessageIfNeeded(context, content);
// Loki - Handle session request if needed
if (SessionManagementProtocol.handleSessionRequestIfNeeded(context, content)) { return; } // Don't process the message any further
// Loki - Handle profile update if needed
SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content);
if (content.getDeviceLink().isPresent()) {
@ -297,15 +274,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
SignalServiceDataMessage message = content.getDataMessage().get();
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent();
// Loki - Handle unlinking request if needed
if (message.isUnlinkingRequest()) {
if (message.isDeviceUnlinkingRequest()) {
MultiDeviceProtocol.handleUnlinkingRequestIfNeeded(context, content);
} else {
// Loki - Don't process session restoration requests any further
if (message.isSessionRestorationRequest()) { return; }
// Loki - Handle friend request acceptance if needed
FriendRequestProtocol.handleFriendRequestAcceptanceIfNeeded(context, content.getSender(), content);
if (message.isEndSession()) {
handleEndSessionMessage(content, smsMessageId);
@ -315,14 +286,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
handleExpirationUpdate(content, message, smsMessageId);
} else if (isMediaMessage) {
handleMediaMessage(content, message, smsMessageId, Optional.absent());
// Loki - Handle friend request message if needed
FriendRequestProtocol.handleFriendRequestMessageIfNeeded(context, content.getSender(), content);
} else if (message.getBody().isPresent()) {
handleTextMessage(content, message, smsMessageId, Optional.absent());
// Loki - Handle friend request message if needed
FriendRequestProtocol.handleFriendRequestMessageIfNeeded(context, content.getSender(), content);
}
if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get()))) {
@ -330,7 +295,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
}
if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
handleProfileKey(content, message);
SessionMetaProtocol.handleProfileKeyUpdate(context, content);
}
if (content.isNeedsReceipt()) {
@ -350,6 +315,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
else if (syncMessage.getContacts().isPresent()) SyncMessagesProtocol.handleContactSyncMessage(context, content, syncMessage.getContacts().get());
else if (syncMessage.getGroups().isPresent()) SyncMessagesProtocol.handleClosedGroupSyncMessage(context, content, syncMessage.getGroups().get());
else if (syncMessage.getOpenGroups().isPresent()) SyncMessagesProtocol.handleOpenGroupSyncMessage(context, content, syncMessage.getOpenGroups().get());
else if (syncMessage.getBlockedList().isPresent()) SyncMessagesProtocol.handleBlockedContactsSyncMessage(context, content, syncMessage.getBlockedList().get());
else Log.w(TAG, "Contains no known sync types...");
} else if (content.getCallMessage().isPresent()) {
Log.i(TAG, "Got call message...");
@ -367,28 +333,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
else if (message.isDeliveryReceipt()) handleDeliveryReceipt(content, message);
} else if (content.getTypingMessage().isPresent()) {
handleTypingMessage(content, content.getTypingMessage().get());
} else if (content.getNullMessage().isPresent()) {
// Loki - This is needed for compatibility with refactored desktop clients
// ========
if (content.isFriendRequest()) {
ApplicationContext.getInstance(context).getJobManager().add(new PushNullMessageSendJob(content.getSender()));
} else {
Log.w(TAG, "Got unrecognized message...");
}
Recipient recipient = recipient(context, content.getSender());
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(context);
LokiThreadFriendRequestStatus threadFriendRequestStatus = threadDB.getFriendRequestStatus(threadID);
if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.NONE || threadFriendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_EXPIRED) {
threadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_RECEIVED);
} else if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_SENT) {
threadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS);
EphemeralMessage ephemeralMessage = EphemeralMessage.create(content.getSender());
ApplicationContext.getInstance(context).getJobManager().add(new PushEphemeralMessageSendJob(ephemeralMessage));
SyncMessagesProtocol.syncContact(context, Address.fromSerialized(content.getSender()));
}
// ========
} else {
Log.w(TAG, "Got unrecognized message...");
}
resetRecipientToPush(Recipient.from(context, Address.fromSerialized(content.getSender()), false));
@ -685,11 +631,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
}
// Loki - Handle profile key update if needed
handleProfileKey(content, message.getMessage());
SessionMetaProtocol.handleProfileKeyUpdate(context, content);
}
// Loki - Update profile if needed
SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content);
if (threadId != null) {
@ -794,7 +738,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
database.beginTransaction();
// Loki - Ignore message if it has no body and no attachments
// Ignore message if it has no body and no attachments
if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) {
return;
}
@ -831,20 +775,28 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
messageNotifier.updateNotification(context, insertResult.get().getThreadId());
}
// Loki - Store message open group server ID if needed
if (insertResult.isPresent() && messageServerIDOrNull.isPresent()) {
long messageID = insertResult.get().getMessageId();
long messageServerID = messageServerIDOrNull.get();
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
lokiMessageDatabase.setServerID(messageID, messageServerID);
}
// Loki - Update mapping of message ID 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);
InsertResult result = insertResult.get();
// Loki - Cache the user hex encoded public key (for mentions)
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context);
MentionsManager.shared.cache(content.getSender(), result.getThreadId());
// Loki - Store message open group server ID if needed
if (messageServerIDOrNull.isPresent()) {
long messageID = result.getMessageId();
long messageServerID = messageServerIDOrNull.get();
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
lokiMessageDatabase.setServerID(messageID, messageServerID);
}
// Loki - Update mapping of message ID 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);
}
}
}
@ -1008,17 +960,17 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Cache the user hex encoded public key (for mentions)
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context);
MentionsManager.shared.cache(textMessage.getSender().serialize(), result.getThreadId());
MentionsManager.shared.cache(content.getSender(), result.getThreadId());
// Loki - Store message server ID
if (insertResult.isPresent() && messageServerIDOrNull.isPresent()) {
long messageID = insertResult.get().getMessageId();
// Loki - Store message open group server ID if needed
if (messageServerIDOrNull.isPresent()) {
long messageID = result.getMessageId();
long messageServerID = messageServerIDOrNull.get();
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
lokiMessageDatabase.setServerID(messageID, messageServerID);
}
// Loki - Update mapping of message to original thread ID
// Loki - Update mapping of message ID to original thread ID
if (result.getMessageId() > -1) {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
@ -1177,28 +1129,6 @@ 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);
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);
String url = content.senderProfilePictureURL.or("");
ApplicationContext.getInstance(context).getJobManager().add(new RetrieveProfileAvatarJob(recipient, url));
SessionMetaProtocol.handleProfileKeyUpdateIfNeeded(context, content);
}
}
private void handleNeedsDeliveryReceipt(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message)
{
@ -1432,7 +1362,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} else {
try {
// TODO: Burn this with fire when we can
PromiseUtilities.timeout(LokiFileServerAPI.shared.getDeviceLinks(publicKey, false), 6000).get();
PromiseUtilities.timeout(FileServerAPI.shared.getDeviceLinks(publicKey, false), 6000).get();
String masterPublicKey = org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol.shared.getMasterDevice(publicKey);
if (masterPublicKey == null) {
masterPublicKey = publicKey;
@ -1464,7 +1394,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} else {
try {
// TODO: Burn this with fire when we can
PromiseUtilities.timeout(LokiFileServerAPI.shared.getDeviceLinks(publicKey, false), 6000).get();
PromiseUtilities.timeout(FileServerAPI.shared.getDeviceLinks(publicKey, false), 6000).get();
String masterPublicKey = org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol.shared.getMasterDevice(publicKey);
if (masterPublicKey == null) {
masterPublicKey = publicKey;

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import com.annimon.stream.Stream;
@ -31,7 +30,6 @@ import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.state.PreKeyBundle;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
@ -64,46 +62,34 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
private static final String KEY_TEMPLATE_MESSAGE_ID = "template_message_id";
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_DESTINATION = "destination";
private static final String KEY_IS_LOKI_PRE_KEY_BUNDLE_MESSAGE = "is_friend_request";
private static final String KEY_CUSTOM_FR_MESSAGE = "custom_friend_request_message";
@Inject SignalServiceMessageSender messageSender;
private long messageId; // The message ID
private long templateMessageId; // The message ID of the message to template this send job from
// Loki - Multi device
private Address destination; // Used to check whether this is another device we're sending to
private boolean isLokiPreKeyBundleMessage; // Whether this is a friend request / session request / device link message
private String customFriendRequestMessage; // If this isn't set then we use the message body
private long messageId;
private long templateMessageId;
private Address destination;
public PushMediaSendJob(long messageId, Address destination) {
this(messageId, messageId, destination);
}
public PushMediaSendJob(long templateMessageId, long messageId, Address destination) {
this(templateMessageId, messageId, destination, false, null);
this(constructParameters(destination), templateMessageId, messageId, destination);
}
public PushMediaSendJob(long templateMessageId, long messageId, Address destination, boolean isLokiPreKeyBundleMessage, String customFriendRequestMessage) {
this(constructParameters(destination), templateMessageId, messageId, destination, isLokiPreKeyBundleMessage, customFriendRequestMessage);
}
private PushMediaSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination, boolean isLokiPreKeyBundleMessage, String customFriendRequestMessage) {
private PushMediaSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination) {
super(parameters);
this.templateMessageId = templateMessageId;
this.messageId = messageId;
this.destination = destination;
this.isLokiPreKeyBundleMessage = isLokiPreKeyBundleMessage;
this.customFriendRequestMessage = customFriendRequestMessage;
}
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long messageId, @NonNull Address destination) {
enqueue(context, jobManager, messageId, messageId, destination, false, null);
enqueue(context, jobManager, messageId, messageId, destination);
}
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long templateMessageId, long messageId, @NonNull Address destination, Boolean isFriendRequest, @Nullable String customFriendRequestMessage) {
enqueue(context, jobManager, Collections.singletonList(new PushMediaSendJob(templateMessageId, messageId, destination, isFriendRequest, customFriendRequestMessage)));
public static void enqueue(@NonNull Context context, @NonNull JobManager jobManager, long templateMessageId, long messageId, @NonNull Address destination) {
enqueue(context, jobManager, Collections.singletonList(new PushMediaSendJob(templateMessageId, messageId, destination)));
}
@WorkerThread
@ -144,14 +130,10 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
@Override
public @NonNull Data serialize() {
Data.Builder builder = new Data.Builder()
.putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId)
.putLong(KEY_MESSAGE_ID, messageId)
.putString(KEY_DESTINATION, destination.serialize())
.putBoolean(KEY_IS_LOKI_PRE_KEY_BUNDLE_MESSAGE, isLokiPreKeyBundleMessage);
if (customFriendRequestMessage != null) { builder.putString(KEY_CUSTOM_FR_MESSAGE, customFriendRequestMessage); }
return builder.build();
return new Data.Builder()
.putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId)
.putLong(KEY_MESSAGE_ID, messageId)
.putString(KEY_DESTINATION, destination.serialize()).build();
}
@Override
@ -270,17 +252,8 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
List<SharedContact> sharedContacts = getSharedContactsFor(message);
List<Preview> previews = getPreviewsFor(message);
// Loki - Include a pre key bundle if needed
PreKeyBundle preKeyBundle;
if (isLokiPreKeyBundleMessage) {
preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.getNumber());
} else {
preKeyBundle = null;
}
String body = (isLokiPreKeyBundleMessage && customFriendRequestMessage != null) ? customFriendRequestMessage : message.getBody();
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
.withBody(body)
.withBody(message.getBody())
.withAttachments(serviceAttachments)
.withTimestamp(message.getSentTimeMillis())
.withExpiration((int)(message.getExpiresIn() / 1000))
@ -290,8 +263,6 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
.withSharedContacts(sharedContacts)
.withPreviews(previews)
.asExpirationUpdate(message.isExpirationUpdate())
.withPreKeyBundle(preKeyBundle)
.asFriendRequest(isLokiPreKeyBundleMessage)
.build();
if (SessionMetaProtocol.shared.isNoteToSelf(address.getNumber())) {
@ -327,9 +298,7 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
long templateMessageID = data.getLong(KEY_TEMPLATE_MESSAGE_ID);
long messageID = data.getLong(KEY_MESSAGE_ID);
Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION));
boolean isLokiPreKeyBundleMessage = data.getBoolean(KEY_IS_LOKI_PRE_KEY_BUNDLE_MESSAGE);
String customFRMessage = data.hasString(KEY_CUSTOM_FR_MESSAGE) ? data.getString(KEY_CUSTOM_FR_MESSAGE) : null;
return new PushMediaSendJob(parameters, templateMessageID, messageID, destination, isLokiPreKeyBundleMessage, customFRMessage);
return new PushMediaSendJob(parameters, templateMessageID, messageID, destination);
}
}
}

View File

@ -36,7 +36,7 @@ public abstract class PushReceivedJob extends BaseJob {
if (envelope.isReceipt()) {
handleReceipt(envelope);
} else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender() || envelope.isFriendRequest()) {
} else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender() || envelope.isFallbackMessage()) {
handleMessage(envelope, isPushNotification);
} else {
Log.w(TAG, "Received envelope of unknown type: " + envelope.getType());

View File

@ -16,7 +16,6 @@ import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
@ -48,50 +47,34 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
private static final String KEY_TEMPLATE_MESSAGE_ID = "template_message_id";
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_DESTINATION = "destination";
private static final String KEY_IS_LOKI_PRE_KEY_BUNDLE_MESSAGE = "is_friend_request";
private static final String KEY_CUSTOM_FR_MESSAGE = "custom_friend_request_message";
@Inject SignalServiceMessageSender messageSender;
private long messageId; // The message ID
private long templateMessageId; // The message ID of the message to template this send job from
// Loki - Multi device
private Address destination; // Used to check whether this is another device we're sending to
private boolean isLokiPreKeyBundleMessage; // Whether this is a friend request / session request / device link message
private String customFriendRequestMessage; // If this isn't set then we use the message body
private long messageId;
private long templateMessageId;
private Address destination;
public PushTextSendJob(long messageId, Address destination) {
this(messageId, messageId, destination);
}
public PushTextSendJob(long templateMessageId, long messageId, Address destination) {
this(templateMessageId, messageId, destination, false, null);
this(constructParameters(destination), templateMessageId, messageId, destination);
}
public PushTextSendJob(long templateMessageId, long messageId, Address destination, boolean isLokiPreKeyBundleMessage, String customFriendRequestMessage) {
this(constructParameters(destination), templateMessageId, messageId, destination, isLokiPreKeyBundleMessage, customFriendRequestMessage);
}
private PushTextSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination, boolean isLokiPreKeyBundleMessage, String customFriendRequestMessage) {
private PushTextSendJob(@NonNull Job.Parameters parameters, long templateMessageId, long messageId, Address destination) {
super(parameters);
this.templateMessageId = templateMessageId;
this.messageId = messageId;
this.destination = destination;
this.isLokiPreKeyBundleMessage = isLokiPreKeyBundleMessage;
this.customFriendRequestMessage = customFriendRequestMessage;
}
@Override
public @NonNull Data serialize() {
Data.Builder builder = new Data.Builder()
.putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId)
.putLong(KEY_MESSAGE_ID, messageId)
.putString(KEY_DESTINATION, destination.serialize())
.putBoolean(KEY_IS_LOKI_PRE_KEY_BUNDLE_MESSAGE, isLokiPreKeyBundleMessage);
if (customFriendRequestMessage != null) { builder.putString(KEY_CUSTOM_FR_MESSAGE, customFriendRequestMessage); }
return builder.build();
return new Data.Builder()
.putLong(KEY_TEMPLATE_MESSAGE_ID, templateMessageId)
.putLong(KEY_MESSAGE_ID, messageId)
.putString(KEY_DESTINATION, destination.serialize()).build();
}
@Override
@ -214,23 +197,18 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
log(TAG, "Have access key to use: " + unidentifiedAccess.isPresent());
// Loki - Include a pre key bundle if needed
PreKeyBundle preKeyBundle;
if (isLokiPreKeyBundleMessage || message.isEndSession()) {
preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.getNumber());
} else {
preKeyBundle = null;
PreKeyBundle preKeyBundle = null;
if (message.isEndSession()) {
preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(destination.serialize());
}
String body = (isLokiPreKeyBundleMessage && customFriendRequestMessage != null) ? customFriendRequestMessage : message.getBody();
SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getDateSent())
.withBody(body)
.withBody(message.getBody())
.withExpiration((int)(message.getExpiresIn() / 1000))
.withProfileKey(profileKey.orNull())
.asEndSessionMessage(message.isEndSession())
.asFriendRequest(isLokiPreKeyBundleMessage)
.withPreKeyBundle(preKeyBundle)
.asEndSessionMessage(message.isEndSession())
.build();
if (SessionMetaProtocol.shared.isNoteToSelf(address.getNumber())) {
@ -263,9 +241,7 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
long templateMessageID = data.getLong(KEY_TEMPLATE_MESSAGE_ID);
long messageID = data.getLong(KEY_MESSAGE_ID);
Address destination = Address.fromSerialized(data.getString(KEY_DESTINATION));
boolean isLokiPreKeyBundleMessage = data.getBoolean(KEY_IS_LOKI_PRE_KEY_BUNDLE_MESSAGE);
String customFRMessage = data.hasString(KEY_CUSTOM_FR_MESSAGE) ? data.getString(KEY_CUSTOM_FR_MESSAGE) : null;
return new PushTextSendJob(parameters, templateMessageID, messageID, destination, isLokiPreKeyBundleMessage, customFRMessage);
return new PushTextSendJob(parameters, templateMessageID, messageID, destination);
}
}
}

View File

@ -8,9 +8,8 @@ class CreateClosedGroupLoader(context: Context) : AsyncLoader<List<String>>(cont
override fun loadInBackground(): List<String> {
val contacts = ContactUtilities.getAllContacts(context)
// Only show the master devices of the users we are friends with
return contacts.filter { contact ->
!contact.recipient.isGroupRecipient && contact.isFriend && !contact.isOurDevice && !contact.isSlave
!contact.recipient.isGroupRecipient && !contact.isOurDevice && !contact.isSlave
}.map {
it.recipient.address.toPhoneString()
}

View File

@ -1,21 +1,19 @@
package org.thoughtcrime.securesms.loki.activities
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.arch.lifecycle.Observer
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.database.Cursor
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Paint
import android.os.AsyncTask
import android.os.Bundle
import android.os.Handler
import android.support.v4.app.LoaderManager
import android.support.v4.content.Loader
import android.support.v4.content.LocalBroadcastManager
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.helper.ItemTouchHelper
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
@ -31,36 +29,35 @@ import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob
import org.thoughtcrime.securesms.loki.dialogs.ConversationOptionsBottomSheet
import org.thoughtcrime.securesms.loki.dialogs.PNModeBottomSheet
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol
import org.thoughtcrime.securesms.loki.protocol.LokiSessionResetImplementation
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation
import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.loki.views.ConversationView
import org.thoughtcrime.securesms.loki.views.NewConversationButtonSetViewDelegate
import org.thoughtcrime.securesms.loki.views.SeedReminderViewDelegate
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.notifications.MessageNotifier
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.api.fileserver.LokiFileServerAPI
import org.whispersystems.signalservice.loki.protocol.friendrequests.FriendRequestProtocol
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager
import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
import org.whispersystems.signalservice.loki.protocol.sessionmanagement.SessionManagementProtocol
import org.whispersystems.signalservice.loki.protocol.syncmessages.SyncMessagesProtocol
import org.whispersystems.signalservice.loki.protocol.todo.LokiMessageFriendRequestStatus
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
import kotlin.math.abs
class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate {
private lateinit var glide: GlideRequests
private var broadcastReceiver: BroadcastReceiver? = null
private val hexEncodedPublicKey: String
private val publicKey: String
get() {
val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(this)
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)
return masterHexEncodedPublicKey ?: userHexEncodedPublicKey
val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(this)
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
return masterPublicKey ?: userPublicKey
}
// region Lifecycle
@ -96,7 +93,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
glide = GlideApp.with(this)
// Set up toolbar buttons
profileButton.glide = glide
profileButton.hexEncodedPublicKey = hexEncodedPublicKey
profileButton.publicKey = publicKey
profileButton.update()
profileButton.setOnClickListener { openSettings() }
pathStatusViewContainer.setOnClickListener { showPath() }
@ -120,7 +117,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
homeAdapter.conversationClickListener = this
recyclerView.adapter = homeAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
ItemTouchHelper(SwipeCallback(this)).attachToRecyclerView(recyclerView)
// Set up empty state view
createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
// This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will)
@ -158,46 +154,32 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
val threadDB = DatabaseFactory.getLokiThreadDatabase(this)
val userDB = DatabaseFactory.getLokiUserDatabase(this)
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
val sessionResetImpl = LokiSessionResetImplementation(this)
val sessionResetImpl = SessionResetImplementation(this)
if (userPublicKey != null) {
FriendRequestProtocol.configureIfNeeded(apiDB, userPublicKey)
MentionsManager.configureIfNeeded(userPublicKey, threadDB, userDB)
SessionMetaProtocol.configureIfNeeded(apiDB, userPublicKey)
SyncMessagesProtocol.configureIfNeeded(apiDB, userPublicKey)
application.lokiPublicChatManager.startPollersIfNeeded()
application.publicChatManager.startPollersIfNeeded()
}
SessionManagementProtocol.configureIfNeeded(sessionResetImpl, threadDB, application)
MultiDeviceProtocol.configureIfNeeded(apiDB)
IP2Country.configureIfNeeded(this)
// Preload device links to make message sending quicker
val publicKeys = ContactUtilities.getAllContacts(this).filter { contact ->
!contact.recipient.isGroupRecipient && contact.isFriend && !contact.isOurDevice && !contact.isSlave
!contact.recipient.isGroupRecipient && !contact.isOurDevice && !contact.isSlave
}.map {
it.recipient.address.toPhoneString()
}.toSet()
LokiFileServerAPI.shared.getDeviceLinks(publicKeys)
// TODO: Temporary hack to unbork existing clients
val allContacts = DatabaseFactory.getRecipientDatabase(this).allAddresses.map {
MultiDeviceProtocol.shared.getMasterDevice(it.serialize()) ?: it.serialize()
}.toSet()
val lokiMessageDB = DatabaseFactory.getLokiMessageDatabase(this)
for (contact in allContacts) {
val slaveDeviceHasPendingFR = MultiDeviceProtocol.shared.getSlaveDevices(contact).any {
val slaveDeviceThreadID = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(recipient(this, it))
DatabaseFactory.getLokiThreadDatabase(this).getFriendRequestStatus(slaveDeviceThreadID) == LokiThreadFriendRequestStatus.REQUEST_RECEIVED
}
val masterDeviceThreadID = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(recipient(this, contact))
val masterDeviceHasNoPendingFR = (DatabaseFactory.getLokiThreadDatabase(this).getFriendRequestStatus(masterDeviceThreadID) == LokiThreadFriendRequestStatus.NONE)
if (slaveDeviceHasPendingFR && masterDeviceHasNoPendingFR) {
val lastMessageID = org.thoughtcrime.securesms.loki.protocol.FriendRequestProtocol.getLastMessageID(this, masterDeviceThreadID)
if (lastMessageID != null) {
val lastMessageFRStatus = lokiMessageDB.getFriendRequestStatus(lastMessageID)
if (lastMessageFRStatus != LokiMessageFriendRequestStatus.REQUEST_PENDING) {
lokiMessageDB.setFriendRequestStatus(lastMessageID, LokiMessageFriendRequestStatus.REQUEST_PENDING)
}
}
FileServerAPI.shared.getDeviceLinks(publicKeys)
// Observe blocked contacts changed events
val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
recyclerView.adapter!!.notifyDataSetChanged()
}
}
this.broadcastReceiver = broadcastReceiver
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged"))
}
override fun onResume() {
@ -230,6 +212,14 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
createNewPrivateChat()
}
}
override fun onDestroy() {
val broadcastReceiver = this.broadcastReceiver
if (broadcastReceiver != null) {
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
}
super.onDestroy()
}
// endregion
// region Updating
@ -251,7 +241,104 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
}
override fun onLongConversationClick(view: ConversationView) {
// Do nothing
val thread = view.thread ?: return
val bottomSheet = ConversationOptionsBottomSheet()
bottomSheet.recipient = thread.recipient
bottomSheet.onBlockOrUnblockTapped = {
bottomSheet.dismiss()
if (thread.recipient.isBlocked) {
unblockConversation(thread)
} else {
blockConversation(thread)
}
}
bottomSheet.onDeleteTapped = {
bottomSheet.dismiss()
deleteConversation(thread)
}
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
}
private fun blockConversation(thread: ThreadRecord) {
AlertDialog.Builder(this)
.setTitle(R.string.RecipientPreferenceActivity_block_this_contact_question)
.setMessage(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ ->
Thread {
DatabaseFactory.getRecipientDatabase(this).setBlocked(thread.recipient, true)
ApplicationContext.getInstance(this).jobManager.add(MultiDeviceBlockedUpdateJob())
Util.runOnMain {
recyclerView.adapter!!.notifyDataSetChanged()
dialog.dismiss()
}
}.start()
}.show()
}
private fun unblockConversation(thread: ThreadRecord) {
AlertDialog.Builder(this)
.setTitle(R.string.RecipientPreferenceActivity_unblock_this_contact_question)
.setMessage(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ ->
Thread {
DatabaseFactory.getRecipientDatabase(this).setBlocked(thread.recipient, false)
ApplicationContext.getInstance(this).jobManager.add(MultiDeviceBlockedUpdateJob())
Util.runOnMain {
recyclerView.adapter!!.notifyDataSetChanged()
dialog.dismiss()
}
}.start()
}.show()
}
private fun deleteConversation(thread: ThreadRecord) {
val threadID = thread.threadId
val recipient = thread.recipient
val threadDB = DatabaseFactory.getThreadDatabase(this)
val deleteThread = object : Runnable {
override fun run() {
AsyncTask.execute {
val publicChat = DatabaseFactory.getLokiThreadDatabase(this@HomeActivity).getPublicChat(threadID)
if (publicChat != null) {
val apiDB = DatabaseFactory.getLokiAPIDatabase(this@HomeActivity)
apiDB.removeLastMessageServerID(publicChat.channel, publicChat.server)
apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server)
ApplicationContext.getInstance(this@HomeActivity).publicChatAPI!!.leave(publicChat.channel, publicChat.server)
}
threadDB.deleteConversation(threadID)
ApplicationContext.getInstance(this@HomeActivity).messageNotifier.updateNotification(this@HomeActivity)
}
}
}
val dialogMessage = if (recipient.isGroupRecipient) R.string.activity_home_leave_group_dialog_message else R.string.activity_home_delete_conversation_dialog_message
val dialog = AlertDialog.Builder(this)
dialog.setMessage(dialogMessage)
dialog.setPositiveButton(R.string.yes) { _, _ ->
val isClosedGroup = recipient.address.isClosedGroup
// Send a leave group message if this is an active closed group
if (isClosedGroup && DatabaseFactory.getGroupDatabase(this).isActive(recipient.address.toGroupString())) {
if (!ClosedGroupsProtocol.leaveGroup(this, recipient)) {
Toast.makeText(this, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()
return@setPositiveButton
}
}
// Archive the conversation and then delete it after 10 seconds (the case where the
// app was closed before the conversation could be deleted is handled in onCreate)
threadDB.archiveConversation(threadID)
val delay = if (isClosedGroup) 10000L else 1000L
val handler = Handler()
handler.postDelayed(deleteThread, delay)
// Notify the user
val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message
Toast.makeText(this, toastMessage, Toast.LENGTH_LONG).show()
}
dialog.setNegativeButton(R.string.no) { _, _ ->
// Do nothing
}
dialog.create().show()
}
private fun openConversation(thread: ThreadRecord) {
@ -289,92 +376,5 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
val intent = Intent(this, JoinPublicChatActivity::class.java)
show(intent)
}
private class SwipeCallback(val activity: HomeActivity) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
@SuppressLint("StaticFieldLeak")
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
viewHolder as HomeAdapter.ViewHolder
val threadID = viewHolder.view.thread!!.threadId
val recipient = viewHolder.view.thread!!.recipient
val threadDatabase = DatabaseFactory.getThreadDatabase(activity)
val deleteThread = object : Runnable {
override fun run() {
AsyncTask.execute {
val publicChat = DatabaseFactory.getLokiThreadDatabase(activity).getPublicChat(threadID)
if (publicChat != null) {
val apiDatabase = DatabaseFactory.getLokiAPIDatabase(activity)
apiDatabase.removeLastMessageServerID(publicChat.channel, publicChat.server)
apiDatabase.removeLastDeletionServerID(publicChat.channel, publicChat.server)
ApplicationContext.getInstance(activity).lokiPublicChatAPI!!.leave(publicChat.channel, publicChat.server)
}
threadDatabase.deleteConversation(threadID)
ApplicationContext.getInstance(activity).messageNotifier.updateNotification(activity)
}
}
}
val dialogMessage = if (recipient.isGroupRecipient) R.string.activity_home_leave_group_dialog_message else R.string.activity_home_delete_conversation_dialog_message
val dialog = AlertDialog.Builder(activity)
dialog.setMessage(dialogMessage)
dialog.setPositiveButton(R.string.yes) { _, _ ->
val isClosedGroup = recipient.address.isClosedGroup
// Send a leave group message if this is an active closed group
if (isClosedGroup && DatabaseFactory.getGroupDatabase(activity).isActive(recipient.address.toGroupString())) {
if (!ClosedGroupsProtocol.leaveGroup(activity, recipient)) {
Toast.makeText(activity, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()
clearView(activity.recyclerView, viewHolder)
return@setPositiveButton
}
}
// Archive the conversation and then delete it after 10 seconds (the case where the
// app was closed before the conversation could be deleted is handled in onCreate)
threadDatabase.archiveConversation(threadID)
val delay = if (isClosedGroup) 10000L else 1000L
val handler = Handler()
handler.postDelayed(deleteThread, delay)
// Notify the user
val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message
Toast.makeText(activity, toastMessage, Toast.LENGTH_LONG).show()
}
dialog.setNegativeButton(R.string.no) { _, _ ->
clearView(activity.recyclerView, viewHolder)
}
dialog.create().show()
}
override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dx: Float, dy: Float, actionState: Int, isCurrentlyActive: Boolean) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && dx < 0) {
val itemView = viewHolder.itemView
animate(viewHolder, dx)
val backgroundPaint = Paint()
backgroundPaint.color = activity.resources.getColorWithID(R.color.destructive, activity.theme)
c.drawRect(itemView.right.toFloat() - abs(dx), itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat(), backgroundPaint)
val icon = BitmapFactory.decodeResource(activity.resources, R.drawable.ic_trash_filled_32)
val iconPaint = Paint()
val left = itemView.right.toFloat() - abs(dx) + activity.resources.getDimension(R.dimen.medium_spacing)
val top = itemView.top.toFloat() + (itemView.bottom.toFloat() - itemView.top.toFloat() - icon.height) / 2
c.drawBitmap(icon, left, top, iconPaint)
} else {
super.onChildDraw(c, recyclerView, viewHolder, dx, dy, actionState, isCurrentlyActive)
}
}
private fun animate(viewHolder: RecyclerView.ViewHolder, dx: Float) {
val alpha = 1.0f - abs(dx) / viewHolder.itemView.width.toFloat()
viewHolder.itemView.alpha = alpha
viewHolder.itemView.translationX = dx
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
viewHolder.itemView.alpha = 1.0f
viewHolder.itemView.translationX = 0.0f
}
}
// endregion
}

View File

@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.logging.Log
import org.thoughtcrime.securesms.loki.dialogs.LinkDeviceSlaveModeDialog
import org.thoughtcrime.securesms.loki.dialogs.LinkDeviceSlaveModeDialogDelegate
import org.thoughtcrime.securesms.loki.protocol.LokiSessionResetImplementation
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
import org.thoughtcrime.securesms.loki.utilities.push
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
@ -27,7 +27,6 @@ import org.whispersystems.curve25519.Curve25519
import org.whispersystems.libsignal.ecc.Curve
import org.whispersystems.libsignal.ecc.ECKeyPair
import org.whispersystems.libsignal.util.KeyHelper
import org.whispersystems.signalservice.loki.protocol.friendrequests.FriendRequestProtocol
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager
import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLink
@ -110,8 +109,7 @@ class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelega
val threadDB = DatabaseFactory.getLokiThreadDatabase(this)
val userDB = DatabaseFactory.getLokiUserDatabase(this)
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
val sessionResetImpl = LokiSessionResetImplementation(this)
FriendRequestProtocol.configureIfNeeded(apiDB, userPublicKey)
val sessionResetImpl = SessionResetImplementation(this)
MentionsManager.configureIfNeeded(userPublicKey, threadDB, userDB)
SessionMetaProtocol.configureIfNeeded(apiDB, userPublicKey)
org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol.configureIfNeeded(apiDB)
@ -124,13 +122,13 @@ class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelega
linkDeviceDialog.show(supportFragmentManager, "Link Device Dialog")
AsyncTask.execute {
retryIfNeeded(8) {
MultiDeviceProtocol.sendDeviceLinkMessage(this@LandingActivity, deviceLink.masterHexEncodedPublicKey, deviceLink)
MultiDeviceProtocol.sendDeviceLinkMessage(this@LandingActivity, deviceLink.masterPublicKey, deviceLink)
}
}
}
override fun onDeviceLinkRequestAuthorized(deviceLink: DeviceLink) {
TextSecurePreferences.setMasterHexEncodedPublicKey(this, deviceLink.masterHexEncodedPublicKey)
TextSecurePreferences.setMasterHexEncodedPublicKey(this, deviceLink.masterPublicKey)
val intent = Intent(this, HomeActivity::class.java)
show(intent)
finish()

View File

@ -26,7 +26,7 @@ import org.thoughtcrime.securesms.loki.utilities.recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.loki.api.fileserver.LokiFileServerAPI
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI
import java.util.*
import kotlin.concurrent.schedule
@ -126,30 +126,30 @@ class LinkedDevicesActivity : PassphraseRequiredActionBarActivity, LoaderManager
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
val apiDB = DatabaseFactory.getLokiAPIDatabase(this)
val deviceLinks = apiDB.getDeviceLinks(userPublicKey)
val deviceLink = deviceLinks.find { it.masterHexEncodedPublicKey == userPublicKey && it.slaveHexEncodedPublicKey == slaveDevicePublicKey }
val deviceLink = deviceLinks.find { it.masterPublicKey == userPublicKey && it.slavePublicKey == slaveDevicePublicKey }
if (deviceLink == null) {
return Toast.makeText(this, R.string.activity_linked_devices_unlinking_failed_message, Toast.LENGTH_LONG).show()
}
LokiFileServerAPI.shared.setDeviceLinks(setOf()).successUi {
FileServerAPI.shared.setDeviceLinks(setOf()).successUi {
DatabaseFactory.getLokiAPIDatabase(this).clearDeviceLinks(userPublicKey)
deviceLinks.forEach { deviceLink ->
// We don't use PushEphemeralMessageJob because want these messages to send before the pre key and
// session associated with the slave device have been deleted
val unlinkingRequest = SignalServiceDataMessage.newBuilder()
.withTimestamp(System.currentTimeMillis())
.asUnlinkingRequest(true)
.asDeviceUnlinkingRequest(true)
val messageSender = ApplicationContext.getInstance(this@LinkedDevicesActivity).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(deviceLink.slaveHexEncodedPublicKey)
val address = SignalServiceAddress(deviceLink.slavePublicKey)
try {
val udAccess = UnidentifiedAccessUtil.getAccessFor(this@LinkedDevicesActivity, recipient(this@LinkedDevicesActivity, deviceLink.slaveHexEncodedPublicKey))
val udAccess = UnidentifiedAccessUtil.getAccessFor(this@LinkedDevicesActivity, recipient(this@LinkedDevicesActivity, deviceLink.slavePublicKey))
messageSender.sendMessage(0, address, udAccess, unlinkingRequest.build()) // The message ID doesn't matter
} catch (e: Exception) {
Log.d("Loki", "Failed to send unlinking request due to error: $e.")
throw e
}
DatabaseFactory.getLokiPreKeyBundleDatabase(this).removePreKeyBundle(deviceLink.slaveHexEncodedPublicKey)
DatabaseFactory.getLokiPreKeyBundleDatabase(this).removePreKeyBundle(deviceLink.slavePublicKey)
val sessionStore = TextSecureSessionStore(this@LinkedDevicesActivity)
sessionStore.deleteAllSessions(deviceLink.slaveHexEncodedPublicKey)
sessionStore.deleteAllSessions(deviceLink.slavePublicKey)
}
LoaderManager.getInstance(this).restartLoader(0, null, this)
Toast.makeText(this, R.string.activity_linked_devices_unlinking_successful_message, Toast.LENGTH_LONG).show()

View File

@ -121,6 +121,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
titleTextView.setTextColor(resources.getColorWithID(R.color.text, theme))
titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.medium_font_size))
titleTextView.text = title
titleTextView.textAlignment = TextView.TEXT_ALIGNMENT_VIEW_START
val titleContainer = LinearLayout(this)
titleContainer.orientation = LinearLayout.VERTICAL
titleContainer.addView(titleTextView)
@ -133,6 +134,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
subtitleTextView.setTextColor(resources.getColorWithID(R.color.text, theme))
subtitleTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.small_font_size))
subtitleTextView.text = subtitle
subtitleTextView.textAlignment = TextView.TEXT_ALIGNMENT_VIEW_START
titleContainer.addView(subtitleTextView)
}
return mainContainer

View File

@ -42,7 +42,7 @@ import org.thoughtcrime.securesms.util.BitmapUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.crypto.ProfileCipher
import org.whispersystems.signalservice.api.util.StreamDetails
import org.whispersystems.signalservice.loki.api.fileserver.LokiFileServerAPI
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI
import java.io.ByteArrayInputStream
import java.io.File
import java.security.SecureRandom
@ -73,7 +73,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
showQRCodeButton.setOnClickListener { showQRCode() }
glide = GlideApp.with(this)
profilePictureView.glide = glide
profilePictureView.hexEncodedPublicKey = hexEncodedPublicKey
profilePictureView.publicKey = hexEncodedPublicKey
profilePictureView.isLarge = true
profilePictureView.update()
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
@ -151,7 +151,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
val promises = mutableListOf<Promise<*, Exception>>()
val displayName = displayNameToBeUploaded
if (displayName != null) {
val publicChatAPI = ApplicationContext.getInstance(this).lokiPublicChatAPI
val publicChatAPI = ApplicationContext.getInstance(this).publicChatAPI
if (publicChatAPI != null) {
val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
promises.addAll(servers.map { publicChatAPI.setDisplayName(displayName, it) })
@ -162,7 +162,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey)
if (isUpdatingProfilePicture && profilePicture != null) {
val storageAPI = LokiFileServerAPI.shared
val storageAPI = FileServerAPI.shared
val deferred = deferred<Unit, Exception>()
AsyncTask.execute {
val stream = StreamDetails(ByteArrayInputStream(profilePicture), "image/jpeg", profilePicture.size.toLong())

View File

@ -15,7 +15,7 @@ import java.util.concurrent.TimeUnit
class BackgroundPollWorker : PersistentAlarmManagerListener() {
companion object {
private val pollInterval = TimeUnit.MINUTES.toMillis(15)
private val pollInterval = TimeUnit.MINUTES.toMillis(30)
@JvmStatic
fun schedule(context: Context) {
@ -29,7 +29,7 @@ class BackgroundPollWorker : PersistentAlarmManagerListener() {
override fun onAlarm(context: Context, scheduledTime: Long): Long {
if (scheduledTime != 0L) {
if (TextSecurePreferences.isUsingFCM(context)) {
if (!TextSecurePreferences.isUsingFCM(context)) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(context)
try {
@ -47,7 +47,7 @@ class BackgroundPollWorker : PersistentAlarmManagerListener() {
}
val openGroups = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().map { it.value }
for (openGroup in openGroups) {
val poller = LokiPublicChatPoller(context, openGroup)
val poller = PublicChatPoller(context, openGroup)
poller.stop()
poller.pollForNewMessages()
}

View File

@ -47,11 +47,11 @@ object LokiPushNotificationManager {
}
@JvmStatic
fun register(token: String, hexEncodedPublicKey: String, context: Context?, force: Boolean) {
fun register(token: String, publicKey: String, context: Context?, force: Boolean) {
val oldToken = TextSecurePreferences.getFCMToken(context)
val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context)
if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return }
val parameters = mapOf( "token" to token, "pubKey" to hexEncodedPublicKey )
val parameters = mapOf( "token" to token, "pubKey" to publicKey )
val url = "$server/register"
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body).build()

View File

@ -12,11 +12,11 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChat
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat
class LokiPublicChatManager(private val context: Context) {
private var chats = mutableMapOf<Long, LokiPublicChat>()
private val pollers = mutableMapOf<Long, LokiPublicChatPoller>()
class PublicChatManager(private val context: Context) {
private var chats = mutableMapOf<Long, PublicChat>()
private val pollers = mutableMapOf<Long, PublicChatPoller>()
private val observers = mutableMapOf<Long, ContentObserver>()
private var isPolling = false
@ -24,7 +24,7 @@ class LokiPublicChatManager(private val context: Context) {
var areAllCaughtUp = true
refreshChatsAndPollers()
for ((threadID, chat) in chats) {
val poller = pollers[threadID] ?: LokiPublicChatPoller(context, chat)
val poller = pollers[threadID] ?: PublicChatPoller(context, chat)
areAllCaughtUp = areAllCaughtUp && poller.isCaughtUp
}
return areAllCaughtUp
@ -33,7 +33,7 @@ class LokiPublicChatManager(private val context: Context) {
public fun markAllAsNotCaughtUp() {
refreshChatsAndPollers()
for ((threadID, chat) in chats) {
val poller = pollers[threadID] ?: LokiPublicChatPoller(context, chat)
val poller = pollers[threadID] ?: PublicChatPoller(context, chat)
poller.isCaughtUp = false
}
}
@ -42,7 +42,7 @@ class LokiPublicChatManager(private val context: Context) {
refreshChatsAndPollers()
for ((threadId, chat) in chats) {
val poller = pollers[threadId] ?: LokiPublicChatPoller(context, chat)
val poller = pollers[threadId] ?: PublicChatPoller(context, chat)
poller.startIfNeeded()
listenToThreadDeletion(threadId)
if (!pollers.containsKey(threadId)) { pollers[threadId] = poller }
@ -55,8 +55,8 @@ class LokiPublicChatManager(private val context: Context) {
isPolling = false
}
public fun addChat(server: String, channel: Long): Promise<LokiPublicChat, Exception> {
val groupChatAPI = ApplicationContext.getInstance(context).lokiPublicChatAPI ?: return Promise.ofFail(IllegalStateException("LokiPublicChatAPI is not set!"))
public fun addChat(server: String, channel: Long): Promise<PublicChat, Exception> {
val groupChatAPI = ApplicationContext.getInstance(context).publicChatAPI ?: return Promise.ofFail(IllegalStateException("LokiPublicChatAPI is not set!"))
return groupChatAPI.getAuthToken(server).bind {
groupChatAPI.getChannelInfo(channel, server)
}.map {
@ -64,8 +64,8 @@ class LokiPublicChatManager(private val context: Context) {
}
}
public fun addChat(server: String, channel: Long, name: String): LokiPublicChat {
val chat = LokiPublicChat(channel, server, name, true)
public fun addChat(server: String, channel: Long, name: String): PublicChat {
val chat = PublicChat(channel, server, name, true)
var threadID = GroupManager.getOpenGroupThreadID(chat.id, context)
// Create the group if we don't have one
if (threadID < 0) {
@ -76,7 +76,7 @@ class LokiPublicChatManager(private val context: Context) {
// Set our name on the server
val displayName = TextSecurePreferences.getProfileName(context)
if (!TextUtils.isEmpty(displayName)) {
ApplicationContext.getInstance(context).lokiPublicChatAPI?.setDisplayName(displayName, server)
ApplicationContext.getInstance(context).publicChatAPI?.setDisplayName(displayName, server)
}
// Start polling
Util.runOnMain{ startPollersIfNeeded() }

View File

@ -22,16 +22,15 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.messages.SignalServiceGroup
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.loki.api.fileserver.LokiFileServerAPI
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChat
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChatAPI
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChatMessage
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat
import org.whispersystems.signalservice.loki.api.opengroups.PublicChatAPI
import org.whispersystems.signalservice.loki.api.opengroups.PublicChatMessage
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
import java.security.MessageDigest
import java.util.*
class LokiPublicChatPoller(private val context: Context, private val group: LokiPublicChat) {
class PublicChatPoller(private val context: Context, private val group: PublicChat) {
private val handler = Handler()
private var hasStarted = false
public var isCaughtUp = false
@ -40,12 +39,12 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
private val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
private var displayNameUpdatees = setOf<String>()
private val api: LokiPublicChatAPI
private val api: PublicChatAPI
get() = {
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
val lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(context)
val lokiUserDatabase = DatabaseFactory.getLokiUserDatabase(context)
LokiPublicChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase)
PublicChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase)
}()
// endregion
@ -112,16 +111,16 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
// endregion
// region Polling
private fun getDataMessage(message: LokiPublicChatMessage): SignalServiceDataMessage {
private fun getDataMessage(message: PublicChatMessage): SignalServiceDataMessage {
val id = group.id.toByteArray()
val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.PUBLIC_CHAT, null, null, null, null)
val quote = if (message.quote != null) {
SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteeHexEncodedPublicKey), message.quote!!.quotedMessageBody, listOf())
SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteePublicKey), message.quote!!.quotedMessageBody, listOf())
} else {
null
}
val attachments = message.attachments.mapNotNull { attachment ->
if (attachment.kind != LokiPublicChatMessage.Attachment.Kind.Attachment) { return@mapNotNull null }
if (attachment.kind != PublicChatMessage.Attachment.Kind.Attachment) { return@mapNotNull null }
SignalServiceAttachmentPointer(
attachment.serverID,
attachment.contentType,
@ -135,7 +134,7 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
Optional.fromNullable(attachment.caption),
attachment.url)
}
val linkPreview = message.attachments.firstOrNull { it.kind == LokiPublicChatMessage.Attachment.Kind.LinkPreview }
val linkPreview = message.attachments.firstOrNull { it.kind == PublicChatMessage.Attachment.Kind.LinkPreview }
val signalLinkPreviews = mutableListOf<SignalServiceDataMessage.Preview>()
if (linkPreview != null) {
val attachment = SignalServiceAttachmentPointer(
@ -157,16 +156,16 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
}
fun pollForNewMessages() {
fun processIncomingMessage(message: LokiPublicChatMessage) {
fun processIncomingMessage(message: PublicChatMessage) {
// If the sender of the current message is not a slave device, set the display name in the database
val masterHexEncodedPublicKey = MultiDeviceProtocol.shared.getMasterDevice(message.hexEncodedPublicKey)
val masterHexEncodedPublicKey = MultiDeviceProtocol.shared.getMasterDevice(message.publicKey)
if (masterHexEncodedPublicKey == null) {
val senderDisplayName = "${message.displayName} (...${message.hexEncodedPublicKey.takeLast(8)})"
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.hexEncodedPublicKey, senderDisplayName)
val senderDisplayName = "${message.displayName} (...${message.publicKey.takeLast(8)})"
DatabaseFactory.getLokiUserDatabase(context).setServerDisplayName(group.id, message.publicKey, senderDisplayName)
}
val senderHexEncodedPublicKey = masterHexEncodedPublicKey ?: message.hexEncodedPublicKey
val senderHexEncodedPublicKey = masterHexEncodedPublicKey ?: message.publicKey
val serviceDataMessage = getDataMessage(message)
val serviceContent = SignalServiceContent(serviceDataMessage, senderHexEncodedPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.timestamp, false, false, false, false)
val serviceContent = SignalServiceContent(serviceDataMessage, senderHexEncodedPublicKey, SignalServiceAddress.DEFAULT_DEVICE_ID, message.timestamp, false, false)
if (serviceDataMessage.quote.isPresent || (serviceDataMessage.attachments.isPresent && serviceDataMessage.attachments.get().size > 0) || serviceDataMessage.previews.isPresent) {
PushDecryptJob(context).handleMediaMessage(serviceContent, serviceDataMessage, Optional.absent(), Optional.of(message.serverID))
} else {
@ -182,16 +181,9 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
database.setProfileKey(senderAsRecipient, profileKey)
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderAsRecipient, url))
}
} else if (senderAsRecipient.profileAvatar.orEmpty().isNotEmpty()) {
// Clear the profile picture if we had a profile picture before and we're not friends with the person
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(senderAsRecipient)
val friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID)
if (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) {
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(senderAsRecipient, ""))
}
}
}
fun processOutgoingMessage(message: LokiPublicChatMessage) {
fun processOutgoingMessage(message: PublicChatMessage) {
val messageServerID = message.serverID ?: return
val isDuplicate = DatabaseFactory.getLokiMessageDatabase(context).getMessageID(messageServerID) != null
if (isDuplicate) { return }
@ -206,7 +198,7 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
PushDecryptJob(context).handleSynchronizeSentTextMessage(transcript)
}
// If we got a message from our master device then make sure our mapping stays in sync
val recipient = Recipient.from(context, Address.fromSerialized(message.hexEncodedPublicKey), false)
val recipient = Recipient.from(context, Address.fromSerialized(message.publicKey), false)
if (recipient.isUserMasterDevice && message.profilePicture != null) {
val profileKey = message.profilePicture!!.profileKey
val url = message.profilePicture!!.url
@ -222,15 +214,15 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
var uniqueDevices = setOf<String>()
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
LokiFileServerAPI.configure(false, userHexEncodedPublicKey, userPrivateKey, apiDB)
FileServerAPI.configure(userHexEncodedPublicKey, userPrivateKey, apiDB)
// Kovenant propagates a context to chained promises, so LokiPublicChatAPI.sharedContext should be used for all of the below
api.getMessages(group.channel, group.server).bind(LokiPublicChatAPI.sharedContext) { messages ->
api.getMessages(group.channel, group.server).bind(PublicChatAPI.sharedContext) { messages ->
if (messages.isNotEmpty()) {
// We need to fetch the device mapping for any devices we don't have
uniqueDevices = messages.map { it.hexEncodedPublicKey }.toSet()
val devicesToUpdate = uniqueDevices.filter { !userDevices.contains(it) && LokiFileServerAPI.shared.hasDeviceLinkCacheExpired(hexEncodedPublicKey = it) }
uniqueDevices = messages.map { it.publicKey }.toSet()
val devicesToUpdate = uniqueDevices.filter { !userDevices.contains(it) && FileServerAPI.shared.hasDeviceLinkCacheExpired(publicKey = it) }
if (devicesToUpdate.isNotEmpty()) {
return@bind LokiFileServerAPI.shared.getDeviceLinks(devicesToUpdate.toSet()).then { messages }
return@bind FileServerAPI.shared.getDeviceLinks(devicesToUpdate.toSet()).then { messages }
}
}
Promise.of(messages)
@ -244,7 +236,7 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
}.successBackground { messages ->
// Process messages in the background
messages.forEach { message ->
if (userDevices.contains(message.hexEncodedPublicKey)) {
if (userDevices.contains(message.publicKey)) {
processOutgoingMessage(message)
} else {
processIncomingMessage(message)

View File

@ -17,6 +17,9 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
private val userPublicKey get() = TextSecurePreferences.getLocalNumber(context)
companion object {
// Shared
private val publicKey = "public_key"
private val timestamp = "timestamp"
// Snode pool cache
private val snodePoolCache = "loki_snode_pool_cache"
private val dummyKey = "dummy_key"
@ -29,9 +32,9 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
@JvmStatic val createOnionRequestPathCacheCommand = "CREATE TABLE $onionRequestPathCache ($indexPath TEXT PRIMARY KEY, $snode TEXT);"
// Swarm cache
private val swarmCache = "loki_api_swarm_cache"
private val hexEncodedPublicKey = "hex_encoded_public_key"
private val swarmPublicKey = "hex_encoded_public_key"
private val swarm = "swarm"
@JvmStatic val createSwarmCacheCommand = "CREATE TABLE $swarmCache ($hexEncodedPublicKey TEXT PRIMARY KEY, $swarm TEXT);"
@JvmStatic val createSwarmCacheCommand = "CREATE TABLE $swarmCache ($swarmPublicKey TEXT PRIMARY KEY, $swarm TEXT);"
// Last message hash value cache
private val lastMessageHashValueCache = "loki_api_last_message_hash_value_cache"
private val target = "target"
@ -59,22 +62,30 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
@JvmStatic val createLastDeletionServerIDCacheCommand = "CREATE TABLE $lastDeletionServerIDCache ($lastDeletionServerIDCacheIndex STRING PRIMARY KEY, $lastDeletionServerID INTEGER DEFAULT 0);"
// Device link cache
private val deviceLinkCache = "loki_pairing_authorisation_cache"
private val masterHexEncodedPublicKey = "primary_device"
private val slaveHexEncodedPublicKey = "secondary_device"
private val masterPublicKey = "primary_device"
private val slavePublicKey = "secondary_device"
private val requestSignature = "request_signature"
private val authorizationSignature = "grant_signature"
@JvmStatic val createDeviceLinkCacheCommand = "CREATE TABLE $deviceLinkCache ($masterHexEncodedPublicKey TEXT, $slaveHexEncodedPublicKey TEXT, " +
"$requestSignature TEXT NULLABLE DEFAULT NULL, $authorizationSignature TEXT NULLABLE DEFAULT NULL, PRIMARY KEY ($masterHexEncodedPublicKey, $slaveHexEncodedPublicKey));"
@JvmStatic val createDeviceLinkCacheCommand = "CREATE TABLE $deviceLinkCache ($masterPublicKey TEXT, $slavePublicKey TEXT, " +
"$requestSignature TEXT NULLABLE DEFAULT NULL, $authorizationSignature TEXT NULLABLE DEFAULT NULL, PRIMARY KEY ($masterPublicKey, $slavePublicKey));"
// User count cache
private val userCountCache = "loki_user_count_cache"
private val publicChatID = "public_chat_id"
private val userCount = "user_count"
@JvmStatic val createUserCountCacheCommand = "CREATE TABLE $userCountCache ($publicChatID STRING PRIMARY KEY, $userCount INTEGER DEFAULT 0);"
// Session request timestamp cache
// Session request sent timestamp cache
private val sessionRequestSentTimestampCache = "session_request_sent_timestamp_cache"
@JvmStatic val createSessionRequestSentTimestampCacheCommand = "CREATE TABLE $sessionRequestSentTimestampCache ($publicKey STRING PRIMARY KEY, $timestamp INTEGER DEFAULT 0);"
// Session request processed timestamp cache
private val sessionRequestProcessedTimestampCache = "session_request_processed_timestamp_cache"
@JvmStatic val createSessionRequestProcessedTimestampCacheCommand = "CREATE TABLE $sessionRequestProcessedTimestampCache ($publicKey STRING PRIMARY KEY, $timestamp INTEGER DEFAULT 0);"
// region Deprecated
private val sessionRequestTimestampCache = "session_request_timestamp_cache"
private val publicKey = "public_key"
private val timestamp = "timestamp"
@JvmStatic val createSessionRequestTimestampCacheCommand = "CREATE TABLE $sessionRequestTimestampCache ($publicKey STRING PRIMARY KEY, $timestamp INTEGER DEFAULT 0);"
// endregion
}
override fun getSnodePool(): Set<Snode> {
@ -161,9 +172,9 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
set("1-1", path1[1]); set("1-2", path1[2])
}
override fun getSwarm(hexEncodedPublicKey: String): Set<Snode>? {
override fun getSwarm(publicKey: String): Set<Snode>? {
val database = databaseHelper.readableDatabase
return database.get(swarmCache, "${Companion.hexEncodedPublicKey} = ?", wrap(hexEncodedPublicKey)) { cursor ->
return database.get(swarmCache, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) { cursor ->
val swarmAsString = cursor.getString(cursor.getColumnIndexOrThrow(swarm))
swarmAsString.split(", ").mapNotNull { targetAsString ->
val components = targetAsString.split("-")
@ -176,7 +187,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
}?.toSet()
}
override fun setSwarm(hexEncodedPublicKey: String, newValue: Set<Snode>) {
override fun setSwarm(publicKey: String, newValue: Set<Snode>) {
val database = databaseHelper.writableDatabase
val swarmAsString = newValue.joinToString(", ") { target ->
var string = "${target.address}-${target.port}"
@ -186,21 +197,21 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
}
string
}
val row = wrap(mapOf(Companion.hexEncodedPublicKey to hexEncodedPublicKey, swarm to swarmAsString))
database.insertOrUpdate(swarmCache, row, "${Companion.hexEncodedPublicKey} = ?", wrap(hexEncodedPublicKey))
val row = wrap(mapOf(Companion.swarmPublicKey to publicKey, swarm to swarmAsString))
database.insertOrUpdate(swarmCache, row, "${Companion.swarmPublicKey} = ?", wrap(publicKey))
}
override fun getLastMessageHashValue(target: Snode): String? {
override fun getLastMessageHashValue(snode: Snode): String? {
val database = databaseHelper.readableDatabase
return database.get(lastMessageHashValueCache, "${Companion.target} = ?", wrap(target.address)) { cursor ->
return database.get(lastMessageHashValueCache, "${Companion.target} = ?", wrap(snode.address)) { cursor ->
cursor.getString(cursor.getColumnIndexOrThrow(lastMessageHashValue))
}
}
override fun setLastMessageHashValue(target: Snode, newValue: String) {
override fun setLastMessageHashValue(snode: Snode, newValue: String) {
val database = databaseHelper.writableDatabase
val row = wrap(mapOf(Companion.target to target.address, lastMessageHashValue to newValue))
database.insertOrUpdate(lastMessageHashValueCache, row, "${Companion.target} = ?", wrap(target.address))
val row = wrap(mapOf(Companion.target to snode.address, lastMessageHashValue to newValue))
database.insertOrUpdate(lastMessageHashValueCache, row, "${Companion.target} = ?", wrap(snode.address))
}
override fun getReceivedMessageHashValues(): Set<String>? {
@ -277,35 +288,35 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.delete(lastDeletionServerIDCache,"$lastDeletionServerIDCacheIndex = ?", wrap(index))
}
override fun getDeviceLinks(hexEncodedPublicKey: String): Set<DeviceLink> {
override fun getDeviceLinks(publicKey: String): Set<DeviceLink> {
val database = databaseHelper.readableDatabase
return database.getAll(deviceLinkCache, "$masterHexEncodedPublicKey = ? OR $slaveHexEncodedPublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey )) { cursor ->
val masterHexEncodedPublicKey = cursor.getString(masterHexEncodedPublicKey)
val slaveHexEncodedPublicKey = cursor.getString(slaveHexEncodedPublicKey)
return database.getAll(deviceLinkCache, "$masterPublicKey = ? OR $slavePublicKey = ?", arrayOf( publicKey, publicKey )) { cursor ->
val masterHexEncodedPublicKey = cursor.getString(masterPublicKey)
val slaveHexEncodedPublicKey = cursor.getString(slavePublicKey)
val requestSignature: ByteArray? = if (cursor.isNull(cursor.getColumnIndexOrThrow(requestSignature))) null else cursor.getBase64EncodedData(requestSignature)
val authorizationSignature: ByteArray? = if (cursor.isNull(cursor.getColumnIndexOrThrow(authorizationSignature))) null else cursor.getBase64EncodedData(authorizationSignature)
DeviceLink(masterHexEncodedPublicKey, slaveHexEncodedPublicKey, requestSignature, authorizationSignature)
}.toSet()
}
override fun clearDeviceLinks(hexEncodedPublicKey: String) {
override fun clearDeviceLinks(publicKey: String) {
val database = databaseHelper.writableDatabase
database.delete(deviceLinkCache, "$masterHexEncodedPublicKey = ? OR $slaveHexEncodedPublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey ))
database.delete(deviceLinkCache, "$masterPublicKey = ? OR $slavePublicKey = ?", arrayOf( publicKey, publicKey ))
}
override fun addDeviceLink(deviceLink: DeviceLink) {
val database = databaseHelper.writableDatabase
val values = ContentValues()
values.put(masterHexEncodedPublicKey, deviceLink.masterHexEncodedPublicKey)
values.put(slaveHexEncodedPublicKey, deviceLink.slaveHexEncodedPublicKey)
values.put(masterPublicKey, deviceLink.masterPublicKey)
values.put(slavePublicKey, deviceLink.slavePublicKey)
if (deviceLink.requestSignature != null) { values.put(requestSignature, Base64.encodeBytes(deviceLink.requestSignature)) }
if (deviceLink.authorizationSignature != null) { values.put(authorizationSignature, Base64.encodeBytes(deviceLink.authorizationSignature)) }
database.insertOrUpdate(deviceLinkCache, values, "$masterHexEncodedPublicKey = ? AND $slaveHexEncodedPublicKey = ?", arrayOf( deviceLink.masterHexEncodedPublicKey, deviceLink.slaveHexEncodedPublicKey ))
database.insertOrUpdate(deviceLinkCache, values, "$masterPublicKey = ? AND $slavePublicKey = ?", arrayOf( deviceLink.masterPublicKey, deviceLink.slavePublicKey ))
}
override fun removeDeviceLink(deviceLink: DeviceLink) {
val database = databaseHelper.writableDatabase
database.delete(deviceLinkCache, "$masterHexEncodedPublicKey = ? OR $slaveHexEncodedPublicKey = ?", arrayOf( deviceLink.masterHexEncodedPublicKey, deviceLink.slaveHexEncodedPublicKey ))
database.delete(deviceLinkCache, "$masterPublicKey = ? OR $slavePublicKey = ?", arrayOf( deviceLink.masterPublicKey, deviceLink.slavePublicKey ))
}
fun getUserCount(group: Long, server: String): Int? {
@ -323,17 +334,30 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.insertOrUpdate(userCountCache, row, "$publicChatID = ?", wrap(index))
}
override fun getSessionRequestTimestamp(publicKey: String): Long? {
override fun getSessionRequestSentTimestamp(publicKey: String): Long? {
val database = databaseHelper.readableDatabase
return database.get(sessionRequestTimestampCache, "$LokiAPIDatabase.publicKey = ?", wrap(publicKey)) { cursor ->
return database.get(sessionRequestSentTimestampCache, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor ->
cursor.getInt(LokiAPIDatabase.timestamp)
}?.toLong()
}
override fun setSessionRequestTimestamp(publicKey: String, timestamp: Long) {
override fun setSessionRequestSentTimestamp(publicKey: String, timestamp: Long) {
val database = databaseHelper.writableDatabase
val row = wrap(mapOf(LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to timestamp.toString()))
database.insertOrUpdate(sessionRequestTimestampCache, row, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey))
database.insertOrUpdate(sessionRequestSentTimestampCache, row, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey))
}
override fun getSessionRequestProcessedTimestamp(publicKey: String): Long? {
val database = databaseHelper.readableDatabase
return database.get(sessionRequestProcessedTimestampCache, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor ->
cursor.getInt(LokiAPIDatabase.timestamp)
}?.toLong()
}
override fun setSessionRequestProcessedTimestamp(publicKey: String, timestamp: Long) {
val database = databaseHelper.writableDatabase
val row = wrap(mapOf(LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to timestamp.toString()))
database.insertOrUpdate(sessionRequestProcessedTimestampCache, row, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey))
}
}

View File

@ -11,39 +11,38 @@ import org.thoughtcrime.securesms.loki.utilities.getInt
import org.thoughtcrime.securesms.loki.utilities.getString
import org.thoughtcrime.securesms.loki.utilities.insertOrUpdate
import org.whispersystems.signalservice.loki.database.LokiMessageDatabaseProtocol
import org.whispersystems.signalservice.loki.protocol.todo.LokiMessageFriendRequestStatus
class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiMessageDatabaseProtocol {
companion object {
private val messageFriendRequestTableName = "loki_message_friend_request_database"
private val messageThreadMappingTableName = "loki_message_thread_mapping_database"
private val errorMessageTableName = "loki_error_message_database"
private val messageIDTable = "loki_message_friend_request_database"
private val messageThreadMappingTable = "loki_message_thread_mapping_database"
private val errorMessageTable = "loki_error_message_database"
private val messageID = "message_id"
private val serverID = "server_id"
private val friendRequestStatus = "friend_request_status"
private val threadID = "thread_id"
private val errorMessage = "error_message"
@JvmStatic val createMessageFriendRequestTableCommand = "CREATE TABLE $messageFriendRequestTableName ($messageID INTEGER PRIMARY KEY, $serverID INTEGER DEFAULT 0, $friendRequestStatus INTEGER DEFAULT 0);"
@JvmStatic val createMessageToThreadMappingTableCommand = "CREATE TABLE IF NOT EXISTS $messageThreadMappingTableName ($messageID INTEGER PRIMARY KEY, $threadID INTEGER);"
@JvmStatic val createErrorMessageTableCommand = "CREATE TABLE IF NOT EXISTS $errorMessageTableName ($messageID INTEGER PRIMARY KEY, $errorMessage STRING);"
@JvmStatic val createMessageIDTableCommand = "CREATE TABLE $messageIDTable ($messageID INTEGER PRIMARY KEY, $serverID INTEGER DEFAULT 0, $friendRequestStatus INTEGER DEFAULT 0);"
@JvmStatic val createMessageToThreadMappingTableCommand = "CREATE TABLE IF NOT EXISTS $messageThreadMappingTable ($messageID INTEGER PRIMARY KEY, $threadID INTEGER);"
@JvmStatic val createErrorMessageTableCommand = "CREATE TABLE IF NOT EXISTS $errorMessageTable ($messageID INTEGER PRIMARY KEY, $errorMessage STRING);"
}
override fun getQuoteServerID(quoteID: Long, quoteeHexEncodedPublicKey: String): Long? {
val message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteID, Address.fromSerialized(quoteeHexEncodedPublicKey))
override fun getQuoteServerID(quoteID: Long, quoteePublicKey: String): Long? {
val message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteID, Address.fromSerialized(quoteePublicKey))
return if (message != null) getServerID(message.getId()) else null
}
fun getServerID(messageID: Long): Long? {
val database = databaseHelper.readableDatabase
return database.get(messageFriendRequestTableName, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
return database.get(messageIDTable, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
cursor.getInt(serverID)
}?.toLong()
}
fun getMessageID(serverID: Long): Long? {
val database = databaseHelper.readableDatabase
return database.get(messageFriendRequestTableName, "${Companion.serverID} = ?", arrayOf( serverID.toString() )) { cursor ->
return database.get(messageIDTable, "${Companion.serverID} = ?", arrayOf( serverID.toString() )) { cursor ->
cursor.getInt(messageID)
}?.toLong()
}
@ -53,12 +52,12 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
val contentValues = ContentValues(2)
contentValues.put(Companion.messageID, messageID)
contentValues.put(Companion.serverID, serverID)
database.insertOrUpdate(messageFriendRequestTableName, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
database.insertOrUpdate(messageIDTable, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
}
fun getOriginalThreadID(messageID: Long): Long {
val database = databaseHelper.readableDatabase
return database.get(messageThreadMappingTableName, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
return database.get(messageThreadMappingTable, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
cursor.getInt(threadID)
}?.toLong() ?: -1L
}
@ -68,38 +67,12 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
val contentValues = ContentValues(2)
contentValues.put(Companion.messageID, messageID)
contentValues.put(Companion.threadID, threadID)
database.insertOrUpdate(messageThreadMappingTableName, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
}
fun getFriendRequestStatus(messageID: Long): LokiMessageFriendRequestStatus {
val database = databaseHelper.readableDatabase
val result = database.get(messageFriendRequestTableName, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
cursor.getInt(friendRequestStatus)
}
return if (result != null) {
LokiMessageFriendRequestStatus.values().first { it.rawValue == result }
} else {
LokiMessageFriendRequestStatus.NONE
}
}
override fun setFriendRequestStatus(messageID: Long, friendRequestStatus: LokiMessageFriendRequestStatus) {
val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2)
contentValues.put(Companion.messageID, messageID)
contentValues.put(Companion.friendRequestStatus, friendRequestStatus.rawValue)
database.insertOrUpdate(messageFriendRequestTableName, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
val threadID = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageID)
notifyConversationListeners(threadID)
}
fun isFriendRequest(messageID: Long): Boolean {
return getFriendRequestStatus(messageID) != LokiMessageFriendRequestStatus.NONE
database.insertOrUpdate(messageThreadMappingTable, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
}
fun getErrorMessage(messageID: Long): String? {
val database = databaseHelper.readableDatabase
return database.get(errorMessageTableName, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
return database.get(errorMessageTable, "${Companion.messageID} = ?", arrayOf( messageID.toString() )) { cursor ->
cursor.getString(errorMessage)
}
}
@ -109,6 +82,6 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
val contentValues = ContentValues(2)
contentValues.put(Companion.messageID, messageID)
contentValues.put(Companion.errorMessage, errorMessage)
database.insertOrUpdate(errorMessageTableName, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
database.insertOrUpdate(errorMessageTable, contentValues, "${Companion.messageID} = ?", arrayOf( messageID.toString() ))
}
}

View File

@ -26,8 +26,8 @@ import org.whispersystems.signalservice.loki.database.LokiPreKeyBundleDatabasePr
class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiPreKeyBundleDatabaseProtocol {
companion object {
private val tableName = "loki_pre_key_bundle_database"
private val hexEncodedPublicKey = "public_key"
private val table = "loki_pre_key_bundle_database"
private val publicKey = "public_key"
private val preKeyID = "pre_key_id"
private val preKeyPublic = "pre_key_public"
private val signedPreKeyID = "signed_pre_key_id"
@ -36,16 +36,16 @@ class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) :
private val identityKey = "identity_key"
private val deviceID = "device_id"
private val registrationID = "registration_id"
@JvmStatic val createTableCommand = "CREATE TABLE $tableName (" + "$hexEncodedPublicKey TEXT PRIMARY KEY," + "$preKeyID INTEGER," +
@JvmStatic val createTableCommand = "CREATE TABLE $table (" + "$publicKey TEXT PRIMARY KEY," + "$preKeyID INTEGER," +
"$preKeyPublic TEXT NOT NULL," + "$signedPreKeyID INTEGER," + "$signedPreKeyPublic TEXT NOT NULL," +
"$signedPreKeySignature TEXT," + "$identityKey TEXT NOT NULL," + "$deviceID INTEGER," + "$registrationID INTEGER" + ");"
}
fun generatePreKeyBundle(hexEncodedPublicKey: String): PreKeyBundle? {
fun generatePreKeyBundle(publicKey: String): PreKeyBundle? {
var failureCount = 0
while (failureCount < 3) {
try {
val preKey = generatePreKeyBundle(hexEncodedPublicKey, failureCount > 0) ?: return null
val preKey = generatePreKeyBundle(publicKey, failureCount > 0) ?: return null
// Verify the bundle is correct
if (!Curve.verifySignature(preKey.identityKey.publicKey, preKey.signedPreKey.serialize(), preKey.signedPreKeySignature)) {
throw InvalidKeyException()
@ -55,19 +55,19 @@ class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) :
failureCount += 1
}
}
Log.w("Loki", "Failed to generate a valid pre key bundle for: $hexEncodedPublicKey.")
Log.w("Loki", "Failed to generate a valid pre key bundle for: $publicKey.")
return null
}
private fun generatePreKeyBundle(hexEncodedPublicKey: String, forceClean: Boolean): PreKeyBundle? {
if (hexEncodedPublicKey.isEmpty()) return null
private fun generatePreKeyBundle(publicKey: String, forceClean: Boolean): PreKeyBundle? {
if (publicKey.isEmpty()) return null
var registrationID = TextSecurePreferences.getLocalRegistrationId(context)
if (registrationID == 0) {
registrationID = KeyHelper.generateRegistrationId(false)
TextSecurePreferences.setLocalRegistrationId(context, registrationID)
}
val deviceID = SignalServiceAddress.DEFAULT_DEVICE_ID
val preKeyRecord = DatabaseFactory.getLokiPreKeyRecordDatabase(context).getOrCreatePreKeyRecord(hexEncodedPublicKey)
val preKeyRecord = DatabaseFactory.getLokiPreKeyRecordDatabase(context).getOrCreatePreKeyRecord(publicKey)
val identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context)
if (!forceClean && TextSecurePreferences.isSignedPreKeyRegistered(context)) {
Log.d("Loki", "A signed pre key has already been registered.")
@ -80,9 +80,9 @@ class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) :
return PreKeyBundle(registrationID, deviceID, preKeyRecord.id, preKeyRecord.keyPair.publicKey, activeSignedPreKey.id, activeSignedPreKey.keyPair.publicKey, activeSignedPreKey.signature, identityKeyPair.publicKey)
}
override fun getPreKeyBundle(hexEncodedPublicKey: String): PreKeyBundle? {
override fun getPreKeyBundle(publicKey: String): PreKeyBundle? {
val database = databaseHelper.readableDatabase
return database.get(tableName, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey )) { cursor ->
return database.get(table, "${Companion.publicKey} = ?", arrayOf( publicKey )) { cursor ->
val registrationID = cursor.getInt(registrationID)
val deviceID = cursor.getInt(deviceID)
val preKeyID = cursor.getInt(preKeyID)
@ -95,7 +95,7 @@ class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) :
}
}
fun setPreKeyBundle(hexEncodedPublicKey: String, preKeyBundle: PreKeyBundle) {
fun setPreKeyBundle(publicKey: String, preKeyBundle: PreKeyBundle) {
val database = databaseHelper.writableDatabase
val values = ContentValues(9)
values.put(registrationID, preKeyBundle.registrationId)
@ -106,20 +106,20 @@ class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) :
values.put(signedPreKeyPublic, Base64.encodeBytes(preKeyBundle.signedPreKey.serialize()))
values.put(signedPreKeySignature, Base64.encodeBytes(preKeyBundle.signedPreKeySignature))
values.put(identityKey, Base64.encodeBytes(preKeyBundle.identityKey.serialize()))
values.put(Companion.hexEncodedPublicKey, hexEncodedPublicKey)
database.insertOrUpdate(tableName, values, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey ))
values.put(Companion.publicKey, publicKey)
database.insertOrUpdate(table, values, "${Companion.publicKey} = ?", arrayOf( publicKey ))
}
override fun removePreKeyBundle(hexEncodedPublicKey: String) {
override fun removePreKeyBundle(publicKey: String) {
val database = databaseHelper.writableDatabase
database.delete(tableName, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey ))
database.delete(table, "${Companion.publicKey} = ?", arrayOf( publicKey ))
}
fun hasPreKeyBundle(hexEncodedPublicKey: String): Boolean {
fun hasPreKeyBundle(publicKey: String): Boolean {
val database = databaseHelper.readableDatabase
var cursor: Cursor? = null
return try {
cursor = database.query(tableName, null, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey ), null, null, null)
cursor = database.query(table, null, "${Companion.publicKey} = ?", arrayOf( publicKey ), null, null, null)
cursor != null && cursor.count > 0
} catch (e: Exception) {
false

View File

@ -14,38 +14,38 @@ import org.whispersystems.signalservice.loki.database.LokiPreKeyRecordDatabasePr
class LokiPreKeyRecordDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiPreKeyRecordDatabaseProtocol {
companion object {
private val tableName = "loki_pre_key_record_database"
private val hexEncodedPublicKey = "public_key"
private val table = "loki_pre_key_record_database"
private val publicKey = "public_key"
private val preKeyID = "pre_key_id"
@JvmStatic val createTableCommand = "CREATE TABLE $tableName ($hexEncodedPublicKey TEXT PRIMARY KEY, $preKeyID INTEGER);"
@JvmStatic val createTableCommand = "CREATE TABLE $table ($publicKey TEXT PRIMARY KEY, $preKeyID INTEGER);"
}
fun hasPreKey(hexEncodedPublicKey: String): Boolean {
fun hasPreKey(publicKey: String): Boolean {
val database = databaseHelper.readableDatabase
return database.get(tableName, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey )) { it.count > 0 } ?: false
return database.get(table, "${Companion.publicKey} = ?", arrayOf( publicKey )) { it.count > 0 } ?: false
}
override fun getPreKeyRecord(hexEncodedPublicKey: String): PreKeyRecord? {
override fun getPreKeyRecord(publicKey: String): PreKeyRecord? {
val database = databaseHelper.readableDatabase
return database.get(tableName, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey )) { cursor ->
return database.get(table, "${Companion.publicKey} = ?", arrayOf( publicKey )) { cursor ->
val preKeyID = cursor.getInt(preKeyID)
PreKeyUtil.loadPreKey(context, preKeyID)
}
}
fun getOrCreatePreKeyRecord(hexEncodedPublicKey: String): PreKeyRecord {
return getPreKeyRecord(hexEncodedPublicKey) ?: generateAndStorePreKeyRecord(hexEncodedPublicKey)
fun getOrCreatePreKeyRecord(publicKey: String): PreKeyRecord {
return getPreKeyRecord(publicKey) ?: generateAndStorePreKeyRecord(publicKey)
}
private fun generateAndStorePreKeyRecord(hexEncodedPublicKey: String): PreKeyRecord {
private fun generateAndStorePreKeyRecord(publicKey: String): PreKeyRecord {
val records = PreKeyUtil.generatePreKeyRecords(context, 1)
PreKeyUtil.storePreKeyRecords(context, records)
val record = records.first()
val database = databaseHelper.writableDatabase
val values = ContentValues(2)
values.put(Companion.hexEncodedPublicKey, hexEncodedPublicKey)
values.put(Companion.publicKey, publicKey)
values.put(preKeyID, record.id)
database.insertOrUpdate(tableName, values, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey ))
database.insertOrUpdate(table, values, "${Companion.publicKey} = ?", arrayOf( publicKey ))
return record
}
}

View File

@ -11,27 +11,24 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.libsignal.loki.LokiSessionResetStatus
import org.whispersystems.libsignal.loki.SessionResetStatus
import org.whispersystems.signalservice.internal.util.JsonUtil
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChat
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat
import org.whispersystems.signalservice.loki.database.LokiThreadDatabaseProtocol
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiThreadDatabaseProtocol {
var delegate: LokiThreadDatabaseDelegate? = null
companion object {
private val friendRequestTableName = "loki_thread_friend_request_database"
private val sessionResetTableName = "loki_thread_session_reset_database"
val publicChatTableName = "loki_public_chat_database"
private val sessionResetTable = "loki_thread_session_reset_database"
val publicChatTable = "loki_public_chat_database"
val threadID = "thread_id"
private val friendRequestStatus = "friend_request_status"
private val sessionResetStatus = "session_reset_status"
val publicChat = "public_chat"
@JvmStatic val createFriendRequestTableCommand = "CREATE TABLE $friendRequestTableName ($threadID INTEGER PRIMARY KEY, $friendRequestStatus INTEGER DEFAULT 0);"
@JvmStatic val createSessionResetTableCommand = "CREATE TABLE $sessionResetTableName ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);"
@JvmStatic val createPublicChatTableCommand = "CREATE TABLE $publicChatTableName ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);"
@JvmStatic val createSessionResetTableCommand = "CREATE TABLE $sessionResetTable ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);"
@JvmStatic val createPublicChatTableCommand = "CREATE TABLE $publicChatTable ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);"
}
override fun getThreadID(hexEncodedPublicKey: String): Long {
@ -44,74 +41,40 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
return DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageID)
}
fun getFriendRequestStatus(threadID: Long): LokiThreadFriendRequestStatus {
if (threadID < 0) { return LokiThreadFriendRequestStatus.NONE }
val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadID)
if (recipient != null && recipient.isGroupRecipient) { return LokiThreadFriendRequestStatus.FRIENDS; }
val database = databaseHelper.readableDatabase
val result = database.get(friendRequestTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor ->
cursor.getInt(friendRequestStatus)
}
return if (result != null) {
LokiThreadFriendRequestStatus.values().first { it.rawValue == result }
} else {
LokiThreadFriendRequestStatus.NONE
}
}
override fun setFriendRequestStatus(threadID: Long, friendRequestStatus: LokiThreadFriendRequestStatus) {
if (threadID < 0) { return }
Log.d("Loki", "Setting FR status for thread with ID $threadID to $friendRequestStatus.")
val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2)
contentValues.put(Companion.threadID, threadID)
contentValues.put(Companion.friendRequestStatus, friendRequestStatus.rawValue)
database.insertOrUpdate(friendRequestTableName, contentValues, "${Companion.threadID} = ?", arrayOf( threadID.toString() ))
notifyConversationListListeners()
notifyConversationListeners(threadID)
delegate?.handleThreadFriendRequestStatusChanged(threadID)
}
fun hasPendingFriendRequest(threadID: Long): Boolean {
val friendRequestStatus = getFriendRequestStatus(threadID)
return friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_SENDING || friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_SENT
|| friendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_RECEIVED
}
fun getSessionResetStatus(hexEncodedPublicKey: String): LokiSessionResetStatus {
fun getSessionResetStatus(hexEncodedPublicKey: String): SessionResetStatus {
val threadID = getThreadID(hexEncodedPublicKey)
val database = databaseHelper.readableDatabase
val result = database.get(sessionResetTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor ->
val result = database.get(sessionResetTable, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor ->
cursor.getInt(sessionResetStatus)
}
return if (result != null) {
LokiSessionResetStatus.values().first { it.rawValue == result }
SessionResetStatus.values().first { it.rawValue == result }
} else {
LokiSessionResetStatus.NONE
SessionResetStatus.NONE
}
}
fun setSessionResetStatus(hexEncodedPublicKey: String, sessionResetStatus: LokiSessionResetStatus) {
fun setSessionResetStatus(hexEncodedPublicKey: String, sessionResetStatus: SessionResetStatus) {
val threadID = getThreadID(hexEncodedPublicKey)
val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2)
contentValues.put(Companion.threadID, threadID)
contentValues.put(Companion.sessionResetStatus, sessionResetStatus.rawValue)
database.insertOrUpdate(sessionResetTableName, contentValues, "${Companion.threadID} = ?", arrayOf( threadID.toString() ))
database.insertOrUpdate(sessionResetTable, contentValues, "${Companion.threadID} = ?", arrayOf( threadID.toString() ))
notifyConversationListListeners()
notifyConversationListeners(threadID)
}
fun getAllPublicChats(): Map<Long, LokiPublicChat> {
fun getAllPublicChats(): Map<Long, PublicChat> {
val database = databaseHelper.readableDatabase
var cursor: Cursor? = null
val result = mutableMapOf<Long, LokiPublicChat>()
val result = mutableMapOf<Long, PublicChat>()
try {
cursor = database.rawQuery("select * from $publicChatTableName", null)
cursor = database.rawQuery("select * from $publicChatTable", null)
while (cursor != null && cursor.moveToNext()) {
val threadID = cursor.getLong(threadID)
val string = cursor.getString(publicChat)
val publicChat = LokiPublicChat.fromJSON(string)
val publicChat = PublicChat.fromJSON(string)
if (publicChat != null) { result[threadID] = publicChat }
}
} catch (e: Exception) {
@ -126,31 +89,31 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
return getAllPublicChats().values.fold(setOf()) { set, chat -> set.plus(chat.server) }
}
override fun getPublicChat(threadID: Long): LokiPublicChat? {
override fun getPublicChat(threadID: Long): PublicChat? {
if (threadID < 0) { return null }
val database = databaseHelper.readableDatabase
return database.get(publicChatTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor ->
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor ->
val publicChatAsJSON = cursor.getString(publicChat)
LokiPublicChat.fromJSON(publicChatAsJSON)
PublicChat.fromJSON(publicChatAsJSON)
}
}
override fun setPublicChat(publicChat: LokiPublicChat, threadID: Long) {
override fun setPublicChat(publicChat: PublicChat, threadID: Long) {
if (threadID < 0) { return }
val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2)
contentValues.put(Companion.threadID, threadID)
contentValues.put(Companion.publicChat, JsonUtil.toJson(publicChat.toJSON()))
database.insertOrUpdate(publicChatTableName, contentValues, "${Companion.threadID} = ?", arrayOf( threadID.toString() ))
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf( threadID.toString() ))
}
override fun removePublicChat(threadID: Long) {
databaseHelper.writableDatabase.delete(publicChatTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() ))
databaseHelper.writableDatabase.delete(publicChatTable, "${Companion.threadID} = ?", arrayOf( threadID.toString() ))
}
fun addSessionRestoreDevice(threadID: Long, hexEncodedPublicKey: String) {
fun addSessionRestoreDevice(threadID: Long, publicKey: String) {
val devices = getSessionRestoreDevices(threadID).toMutableSet()
if (devices.add(hexEncodedPublicKey)) {
if (devices.add(publicKey)) {
TextSecurePreferences.setStringPreference(context, "session_restore_devices_$threadID", devices.joinToString(","))
delegate?.handleSessionRestoreDevicesChanged(threadID)
}

View File

@ -2,6 +2,5 @@ package org.thoughtcrime.securesms.loki.database
interface LokiThreadDatabaseDelegate {
fun handleThreadFriendRequestStatusChanged(threadID: Long)
fun handleSessionRestoreDevicesChanged(threadID: Long)
}

View File

@ -20,60 +20,60 @@ class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database
private val displayName = "display_name"
// Display name cache
private val displayNameTable = "loki_user_display_name_database"
private val hexEncodedPublicKey = "hex_encoded_public_key"
@JvmStatic val createDisplayNameTableCommand = "CREATE TABLE $displayNameTable ($hexEncodedPublicKey TEXT PRIMARY KEY, $displayName TEXT);"
private val publicKey = "hex_encoded_public_key"
@JvmStatic val createDisplayNameTableCommand = "CREATE TABLE $displayNameTable ($publicKey TEXT PRIMARY KEY, $displayName TEXT);"
// Server display name cache
private val serverDisplayNameTable = "loki_user_server_display_name_database"
private val serverID = "server_id"
@JvmStatic val createServerDisplayNameTableCommand = "CREATE TABLE $serverDisplayNameTable ($hexEncodedPublicKey TEXT, $serverID TEXT, $displayName TEXT, PRIMARY KEY ($hexEncodedPublicKey, $serverID));"
@JvmStatic val createServerDisplayNameTableCommand = "CREATE TABLE $serverDisplayNameTable ($publicKey TEXT, $serverID TEXT, $displayName TEXT, PRIMARY KEY ($publicKey, $serverID));"
}
override fun getDisplayName(hexEncodedPublicKey: String): String? {
if (hexEncodedPublicKey == TextSecurePreferences.getLocalNumber(context)) {
override fun getDisplayName(publicKey: String): String? {
if (publicKey == TextSecurePreferences.getLocalNumber(context)) {
return TextSecurePreferences.getProfileName(context)
} else {
val database = databaseHelper.readableDatabase
return database.get(displayNameTable, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey )) { cursor ->
return database.get(displayNameTable, "${Companion.publicKey} = ?", arrayOf( publicKey )) { cursor ->
cursor.getString(cursor.getColumnIndexOrThrow(displayName))
}
}
}
fun setDisplayName(hexEncodedPublicKey: String, displayName: String) {
fun setDisplayName(publicKey: String, displayName: String) {
val database = databaseHelper.writableDatabase
val row = ContentValues(2)
row.put(Companion.hexEncodedPublicKey, hexEncodedPublicKey)
row.put(Companion.publicKey, publicKey)
row.put(Companion.displayName, displayName)
database.insertOrUpdate(displayNameTable, row, "${Companion.hexEncodedPublicKey} = ?", arrayOf( hexEncodedPublicKey ))
Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false).notifyListeners()
database.insertOrUpdate(displayNameTable, row, "${Companion.publicKey} = ?", arrayOf( publicKey ))
Recipient.from(context, Address.fromSerialized(publicKey), false).notifyListeners()
}
override fun getServerDisplayName(serverID: String, hexEncodedPublicKey: String): String? {
override fun getServerDisplayName(serverID: String, publicKey: String): String? {
val database = databaseHelper.readableDatabase
return database.get(serverDisplayNameTable, "${Companion.hexEncodedPublicKey} = ? AND ${Companion.serverID} = ?", arrayOf( hexEncodedPublicKey, serverID )) { cursor ->
return database.get(serverDisplayNameTable, "${Companion.publicKey} = ? AND ${Companion.serverID} = ?", arrayOf( publicKey, serverID )) { cursor ->
cursor.getString(cursor.getColumnIndexOrThrow(displayName))
}
}
fun setServerDisplayName(serverID: String, hexEncodedPublicKey: String, displayName: String) {
fun setServerDisplayName(serverID: String, publicKey: String, displayName: String) {
val database = databaseHelper.writableDatabase
val values = ContentValues(3)
values.put(Companion.serverID, serverID)
values.put(Companion.hexEncodedPublicKey, hexEncodedPublicKey)
values.put(Companion.publicKey, publicKey)
values.put(Companion.displayName, displayName)
try {
database.insertWithOnConflict(serverDisplayNameTable, null, values, SQLiteDatabase.CONFLICT_REPLACE)
Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false).notifyListeners()
Recipient.from(context, Address.fromSerialized(publicKey), false).notifyListeners()
} catch (e: Exception) {
Log.d("Loki", "Couldn't save server display name due to exception: $e.")
}
}
override fun getProfilePictureURL(hexEncodedPublicKey: String): String? {
return if (hexEncodedPublicKey == TextSecurePreferences.getLocalNumber(context)) {
override fun getProfilePictureURL(publicKey: String): String? {
return if (publicKey == TextSecurePreferences.getLocalNumber(context)) {
TextSecurePreferences.getProfilePictureURL(context)
} else {
Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false).resolve().profileAvatar
Recipient.from(context, Address.fromSerialized(publicKey), false).resolve().profileAvatar
}
}
}

View File

@ -0,0 +1,36 @@
package org.thoughtcrime.securesms.loki.dialogs
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_conversation_bottom_sheet.*
import kotlinx.android.synthetic.main.fragment_device_list_bottom_sheet.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.recipients.Recipient
public class ConversationOptionsBottomSheet : BottomSheetDialogFragment() {
lateinit var recipient: Recipient
var onBlockOrUnblockTapped: (() -> Unit)? = null
var onDeleteTapped: (() -> Unit)? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_conversation_bottom_sheet, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (!recipient.isGroupRecipient && !recipient.isLocalNumber) {
val textID = if (recipient.isBlocked) R.string.RecipientPreferenceActivity_unblock else R.string.RecipientPreferenceActivity_block
blockOrUnblockTextView.setText(textID)
val iconID = if (recipient.isBlocked) R.drawable.ic_check_white_24dp else R.drawable.ic_block_white_24dp
val icon = context!!.resources.getDrawable(iconID, context!!.theme)
blockOrUnblockTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
blockOrUnblockTextView.setOnClickListener { onBlockOrUnblockTapped?.invoke() }
} else {
blockOrUnblockTextView.visibility = View.GONE
}
deleteTextView.setOnClickListener { onDeleteTapped?.invoke() }
}
}

View File

@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.loki.api.SnodeAPI
import org.whispersystems.signalservice.loki.api.fileserver.LokiFileServerAPI
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLink
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLinkingSession
@ -52,7 +52,7 @@ class LinkDeviceMasterModeDialog : DialogFragment(), DeviceLinkingSessionListene
}
override fun requestUserAuthorization(deviceLink: DeviceLink) {
if (deviceLink.type != DeviceLink.Type.REQUEST || deviceLink.masterHexEncodedPublicKey != TextSecurePreferences.getLocalNumber(context!!) || this.deviceLink != null) { return }
if (deviceLink.type != DeviceLink.Type.REQUEST || deviceLink.masterPublicKey != TextSecurePreferences.getLocalNumber(context!!) || this.deviceLink != null) { return }
Util.runOnMain {
this.deviceLink = deviceLink
contentView.qrCodeImageView.visibility = View.GONE
@ -62,7 +62,7 @@ class LinkDeviceMasterModeDialog : DialogFragment(), DeviceLinkingSessionListene
contentView.titleTextView.text = resources.getString(R.string.dialog_link_device_master_mode_title_2)
contentView.explanationTextView.text = resources.getString(R.string.dialog_link_device_master_mode_explanation_2)
contentView.mnemonicTextView.visibility = View.VISIBLE
contentView.mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), deviceLink.slaveHexEncodedPublicKey)
contentView.mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), deviceLink.slavePublicKey)
contentView.authorizeButton.visibility = View.VISIBLE
}
}
@ -84,7 +84,7 @@ class LinkDeviceMasterModeDialog : DialogFragment(), DeviceLinkingSessionListene
contentView.cancelButton.visibility = View.GONE
contentView.authorizeButton.visibility = View.GONE
}
LokiFileServerAPI.shared.addDeviceLink(deviceLink).bind(SnodeAPI.sharedContext) {
FileServerAPI.shared.addDeviceLink(deviceLink).bind(SnodeAPI.sharedContext) {
MultiDeviceProtocol.signAndSendDeviceLinkMessage(context!!, deviceLink)
}.success {
TextSecurePreferences.setMultiDevice(context!!, true)
@ -92,8 +92,8 @@ class LinkDeviceMasterModeDialog : DialogFragment(), DeviceLinkingSessionListene
delegate?.onDeviceLinkRequestAuthorized()
dismiss()
}.fail {
LokiFileServerAPI.shared.removeDeviceLink(deviceLink) // If this fails we have a problem
DatabaseFactory.getLokiPreKeyBundleDatabase(context!!).removePreKeyBundle(deviceLink.slaveHexEncodedPublicKey)
FileServerAPI.shared.removeDeviceLink(deviceLink) // If this fails we have a problem
DatabaseFactory.getLokiPreKeyBundleDatabase(context!!).removePreKeyBundle(deviceLink.slavePublicKey)
}.failUi {
delegate?.onDeviceLinkAuthorizationFailed()
dismiss()
@ -104,7 +104,7 @@ class LinkDeviceMasterModeDialog : DialogFragment(), DeviceLinkingSessionListene
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
DeviceLinkingSession.shared.removeListener(this)
if (deviceLink != null) {
DatabaseFactory.getLokiPreKeyBundleDatabase(context).removePreKeyBundle(deviceLink!!.slaveHexEncodedPublicKey)
DatabaseFactory.getLokiPreKeyBundleDatabase(context).removePreKeyBundle(deviceLink!!.slavePublicKey)
}
dismiss()
delegate?.onDeviceLinkCanceled()

View File

@ -41,7 +41,7 @@ class LinkDeviceSlaveModeDialog : DialogFragment(), DeviceLinkingSessionListener
}
override fun onDeviceLinkRequestAuthorized(deviceLink: DeviceLink) {
if (deviceLink.type != DeviceLink.Type.AUTHORIZATION || deviceLink.slaveHexEncodedPublicKey != TextSecurePreferences.getLocalNumber(context!!) || this.deviceLink != null) { return }
if (deviceLink.type != DeviceLink.Type.AUTHORIZATION || deviceLink.slavePublicKey != TextSecurePreferences.getLocalNumber(context!!) || this.deviceLink != null) { return }
Util.runOnMain {
this.deviceLink = deviceLink
DeviceLinkingSession.shared.stopListeningForLinkingRequests()

View File

@ -15,10 +15,10 @@ sealed class ContactSelectionListItem {
class ContactSelectionListLoader(context: Context, val mode: Int, val filter: String?) : AsyncLoader<List<ContactSelectionListItem>>(context) {
object DisplayMode {
const val FLAG_FRIENDS = 1
const val FLAG_CONTACTS = 1
const val FLAG_CLOSED_GROUPS = 1 shl 1
const val FLAG_OPEN_GROUPS = 1 shl 2
const val FLAG_ALL = FLAG_FRIENDS or FLAG_CLOSED_GROUPS or FLAG_OPEN_GROUPS
const val FLAG_ALL = FLAG_CONTACTS or FLAG_CLOSED_GROUPS or FLAG_OPEN_GROUPS
}
private fun isFlagSet(flag: Int): Boolean {
@ -39,15 +39,15 @@ class ContactSelectionListLoader(context: Context, val mode: Int, val filter: St
if (isFlagSet(DisplayMode.FLAG_OPEN_GROUPS)) {
list.addAll(getOpenGroups(contacts))
}
if (isFlagSet(DisplayMode.FLAG_FRIENDS)) {
list.addAll(getFriends(contacts))
if (isFlagSet(DisplayMode.FLAG_CONTACTS)) {
list.addAll(getContacts(contacts))
}
return list
}
private fun getFriends(contacts: List<Contact>): List<ContactSelectionListItem> {
private fun getContacts(contacts: List<Contact>): List<ContactSelectionListItem> {
return getItems(contacts, context.getString(R.string.fragment_contact_selection_contacts_title)) {
!it.recipient.isGroupRecipient && it.isFriend && !it.isOurDevice && !it.isSlave
!it.recipient.isGroupRecipient && !it.isOurDevice && !it.isSlave
}
}

View File

@ -18,7 +18,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.messages.SignalServiceGroup
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.loki.api.SnodeAPI
import org.whispersystems.signalservice.loki.api.fileserver.LokiFileServerAPI
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
import java.util.*
@ -32,7 +32,7 @@ object ClosedGroupsProtocol {
if (!conversation.address.isClosedGroup || groupID == null) { return false }
// A closed group's members should never include slave devices
val senderPublicKey = content.sender
LokiFileServerAPI.shared.getDeviceLinks(senderPublicKey).timeout(6000).get()
FileServerAPI.shared.getDeviceLinks(senderPublicKey).timeout(6000).get()
val senderMasterPublicKey = MultiDeviceProtocol.shared.getMasterDevice(senderPublicKey)
val publicKeyToCheckFor = senderMasterPublicKey ?: senderPublicKey
val members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, true)
@ -57,7 +57,7 @@ object ClosedGroupsProtocol {
} else {
// A closed group's members should never include slave devices
val members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false)
return LokiFileServerAPI.shared.getDeviceLinks(members.map { it.address.serialize() }.toSet()).map {
return FileServerAPI.shared.getDeviceLinks(members.map { it.address.serialize() }.toSet()).map {
val result = members.flatMap { member ->
MultiDeviceProtocol.shared.getAllLinkedDevices(member.address.serialize()).map { Address.fromSerialized(it) }
}.toMutableSet()
@ -106,7 +106,7 @@ object ClosedGroupsProtocol {
allDevices.remove(userPublicKey)
}
for (device in allDevices) {
ApplicationContext.getInstance(context).sendSessionRequest(device)
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(device)
}
}
}

View File

@ -1,34 +0,0 @@
package org.thoughtcrime.securesms.loki.protocol
import org.whispersystems.signalservice.internal.util.JsonUtil
class EphemeralMessage private constructor(val data: Map<*, *>) {
companion object {
@JvmStatic
fun create(publicKey: String) = EphemeralMessage(mapOf( "recipient" to publicKey ))
@JvmStatic
fun createUnlinkingRequest(publicKey: String) = EphemeralMessage(mapOf( "recipient" to publicKey, "unpairingRequest" to true ))
@JvmStatic
fun createSessionRestorationRequest(publicKey: String) = EphemeralMessage(mapOf( "recipient" to publicKey, "friendRequest" to true, "sessionRestore" to true ))
@JvmStatic
fun createSessionRequest(publicKey: String) = EphemeralMessage(mapOf( "recipient" to publicKey, "friendRequest" to true, "sessionRequest" to true ))
internal fun parse(serialized: String): EphemeralMessage {
val data = JsonUtil.fromJson(serialized, Map::class.java) ?: throw IllegalArgumentException("Couldn't parse string to JSON")
return EphemeralMessage(data)
}
}
fun <T> get(key: String, defaultValue: T): T {
return data[key] as? T ?: defaultValue
}
fun serialize(): String {
return JsonUtil.toJson(data)
}
}

View File

@ -1,318 +0,0 @@
package org.thoughtcrime.securesms.loki.protocol
import android.content.Context
import android.util.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.recipient
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage
import org.thoughtcrime.securesms.sms.OutgoingTextMessage
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
import org.whispersystems.signalservice.loki.protocol.todo.LokiMessageFriendRequestStatus
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
object FriendRequestProtocol {
@JvmStatic
fun acceptFriendRequest(context: Context, recipient: Recipient) {
if (recipient.isGroupRecipient) { return; }
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val allUserDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey)
// Accept all outstanding friend requests associated with this user and try to establish sessions with the
// subset of their devices that haven't sent a friend request.
val allContactDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(recipient.address.serialize())
val threadDB = DatabaseFactory.getThreadDatabase(context)
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
for (device in allContactDevices) {
val deviceAsRecipient = recipient(context, device)
val deviceThreadID = threadDB.getThreadIdFor(deviceAsRecipient)
val deviceFRStatus = lokiThreadDB.getFriendRequestStatus(deviceThreadID)
if (deviceFRStatus == LokiThreadFriendRequestStatus.REQUEST_RECEIVED) {
lokiThreadDB.setFriendRequestStatus(deviceThreadID, LokiThreadFriendRequestStatus.FRIENDS)
val lastMessageID = getLastMessageID(context, deviceThreadID)
if (lastMessageID != null) {
DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(lastMessageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED)
}
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient(context, device), true)
val ephemeralMessage = EphemeralMessage.create(device)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage))
// Sync contact if needed
if (allUserDevices.contains(device)) { return }
val deviceToSync = MultiDeviceProtocol.shared.getMasterDevice(device) ?: device
SyncMessagesProtocol.syncContact(context, Address.fromSerialized(deviceToSync))
} else if (deviceFRStatus == LokiThreadFriendRequestStatus.REQUEST_SENT) {
// Do nothing
} else if (!allUserDevices.contains(device)
&& (deviceFRStatus == LokiThreadFriendRequestStatus.NONE || deviceFRStatus == LokiThreadFriendRequestStatus.REQUEST_EXPIRED)) {
sendAutoGeneratedFriendRequest(context, device)
}
}
}
@JvmStatic
fun rejectFriendRequest(context: Context, recipient: Recipient) {
if (recipient.isGroupRecipient) { return; }
val linkedDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(recipient.address.serialize())
val threadDB = DatabaseFactory.getThreadDatabase(context)
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
for (device in linkedDevices) {
val deviceAsRecipient = recipient(context, device)
val deviceThreadID = threadDB.getThreadIdFor(deviceAsRecipient)
val deviceFRStatus = lokiThreadDB.getFriendRequestStatus(deviceThreadID)
// We only want to decline incoming requests
if (deviceFRStatus == LokiThreadFriendRequestStatus.REQUEST_RECEIVED) {
// Delete the pre key bundle for the given contact. This ensures that if we send a
// new message after this, it restarts the friend request process from scratch.
DatabaseFactory.getLokiPreKeyBundleDatabase(context).removePreKeyBundle(device)
lokiThreadDB.setFriendRequestStatus(deviceThreadID, LokiThreadFriendRequestStatus.NONE)
val lastMessageID = getLastMessageID(context, deviceThreadID)
if (lastMessageID != null) {
DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(lastMessageID, LokiMessageFriendRequestStatus.REQUEST_REJECTED)
}
}
}
}
@JvmStatic
fun shouldInputPanelBeEnabled(context: Context, recipient: Recipient): Boolean {
// Friend requests have nothing to do with groups, so if this is a group thread the input panel should be enabled
if (recipient.isGroupRecipient) { return true }
// If this is a note to self the input panel should be enabled
if (SessionMetaProtocol.shared.isNoteToSelf(recipient.address.serialize())) { return true }
// Gather friend request statuses
val linkedDeviceFRStatuses = mutableSetOf<LokiThreadFriendRequestStatus>()
val linkedDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(recipient.address.serialize())
val threadDB = DatabaseFactory.getThreadDatabase(context)
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
for (device in linkedDevices) {
val deviceAsRecipient = recipient(context, device)
val deviceThreadID = threadDB.getThreadIdFor(deviceAsRecipient)
val deviceFRStatus = lokiThreadDB.getFriendRequestStatus(deviceThreadID)
linkedDeviceFRStatuses.add(deviceFRStatus)
}
// If the user is friends with any of the other user's devices the input panel should be enabled
if (linkedDeviceFRStatuses.contains(LokiThreadFriendRequestStatus.FRIENDS)) { return true }
// If no friend request has been sent the input panel should be enabled
if (linkedDeviceFRStatuses.all { it == LokiThreadFriendRequestStatus.NONE || it == LokiThreadFriendRequestStatus.REQUEST_EXPIRED }) { return true }
// There must be a pending friend request
return false
}
@JvmStatic
fun shouldAttachmentButtonBeEnabled(context: Context, recipient: Recipient): Boolean {
// Friend requests have nothing to do with groups, so if this is a group thread the attachment button should be enabled
if (recipient.isGroupRecipient) { return true }
// If this is a note to self the attachment button should be enabled
if (SessionMetaProtocol.shared.isNoteToSelf(recipient.address.serialize())) { return true }
// Gather friend request statuses
val linkedDeviceFRStatuses = mutableSetOf<LokiThreadFriendRequestStatus>()
val linkedDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(recipient.address.serialize())
val threadDB = DatabaseFactory.getThreadDatabase(context)
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
for (device in linkedDevices) {
val deviceAsRecipient = recipient(context, device)
val deviceThreadID = threadDB.getThreadIdFor(deviceAsRecipient)
val deviceFRStatus = lokiThreadDB.getFriendRequestStatus(deviceThreadID)
linkedDeviceFRStatuses.add(deviceFRStatus)
}
// If the user is friends with any of the other user's devices the attachment button should be enabled
if (linkedDeviceFRStatuses.contains(LokiThreadFriendRequestStatus.FRIENDS)) { return true }
// Otherwise don't allow attachments
return false
}
@JvmStatic
fun getLastMessageID(context: Context, threadID: Long): Long? {
val db = DatabaseFactory.getSmsDatabase(context)
val messageCount = db.getMessageCountForThread(threadID)
if (messageCount == 0) { return null }
return db.getIDForMessageAtIndex(threadID, messageCount - 1)
}
@JvmStatic
fun handleFriendRequestAcceptanceIfNeeded(context: Context, publicKey: String, content: SignalServiceContent) {
// If we get an envelope that isn't a friend request, then we can infer that we had to use
// Signal cipher decryption and thus that we have a session with the other person.
if (content.isFriendRequest) { return }
val recipient = recipient(context, publicKey)
// Friend requests don't apply to groups
if (recipient.isGroupRecipient) { return }
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
val threadFRStatus = lokiThreadDB.getFriendRequestStatus(threadID)
// Guard against invalid state transitions
if (threadFRStatus != LokiThreadFriendRequestStatus.REQUEST_SENDING && threadFRStatus != LokiThreadFriendRequestStatus.REQUEST_SENT
&& threadFRStatus != LokiThreadFriendRequestStatus.REQUEST_RECEIVED) { return }
Log.d("Loki", "Received a friend request accepted message from $publicKey.")
lokiThreadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS)
val lastMessageID = getLastMessageID(context, threadID)
if (lastMessageID != null) {
DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(lastMessageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED)
}
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true)
// Send a contact sync message if needed
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val allUserDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey)
if (allUserDevices.contains(publicKey)) { return }
val deviceToSync = MultiDeviceProtocol.shared.getMasterDevice(publicKey) ?: publicKey
SyncMessagesProtocol.syncContact(context, Address.fromSerialized(deviceToSync))
}
private fun canFriendRequestBeAutoAccepted(context: Context, publicKey: String): Boolean {
val recipient = recipient(context, publicKey)
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
val threadFRStatus = lokiThreadDB.getFriendRequestStatus(threadID)
if (threadFRStatus == LokiThreadFriendRequestStatus.REQUEST_SENT) {
// This can happen if Alice sent Bob a friend request, Bob declined, but then Bob changed his
// mind and sent a friend request to Alice. In this case we want Alice to auto-accept the request
// and send a friend request accepted message back to Bob. We don't check that sending the
// friend request accepted message succeeds. Even if it doesn't, the thread's current friend
// request status will be set to FRIENDS for Alice making it possible for Alice to send messages
// to Bob. When Bob receives a message, his thread's friend request status will then be set to
// FRIENDS. If we do check for a successful send before updating Alice's thread's friend request
// status to FRIENDS, we can end up in a deadlock where both users' threads' friend request statuses
// are SENT.
return true
}
// Auto-accept any friend requests from the user's own linked devices
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val allUserDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey)
if (allUserDevices.contains(publicKey)) { return true }
// Auto-accept if the user is friends with any of the sender's linked devices.
val allSenderDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(publicKey)
if (allSenderDevices.any { device ->
val deviceAsRecipient = recipient(context, publicKey)
val deviceThreadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(deviceAsRecipient)
lokiThreadDB.getFriendRequestStatus(deviceThreadID) == LokiThreadFriendRequestStatus.FRIENDS
}) {
return true
}
return false
}
@JvmStatic
fun handleFriendRequestMessageIfNeeded(context: Context, publicKey: String, content: SignalServiceContent) {
if (!content.isFriendRequest) { return }
val recipient = recipient(context, publicKey)
// Friend requests don't apply to groups
if (recipient.isGroupRecipient) { return }
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
val threadFRStatus = lokiThreadDB.getFriendRequestStatus(threadID)
if (canFriendRequestBeAutoAccepted(context, publicKey)) {
Log.d("Loki", "Auto-accepting friend request from $publicKey.")
lokiThreadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS)
val lastMessageID = getLastMessageID(context, threadID)
if (lastMessageID != null) {
DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(lastMessageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED)
}
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true)
val ephemeralMessage = EphemeralMessage.create(publicKey)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage))
} else if (threadFRStatus != LokiThreadFriendRequestStatus.FRIENDS) {
Log.d("Loki", "Handling friend request from $publicKey.")
// Checking that the sender of the message isn't already a friend is necessary because otherwise
// the following situation can occur: Alice and Bob are friends. Bob loses his database and his
// friend request status is reset to NONE. Bob now sends Alice a friend request. Alice's thread's
// friend request status is reset to RECEIVED
lokiThreadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_RECEIVED)
val masterPublicKey = MultiDeviceProtocol.shared.getMasterDevice(publicKey) ?: publicKey
val masterThreadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient(context, masterPublicKey))
val masterThreadLastMessageID = getLastMessageID(context, masterThreadID) // Messages get routed into the master thread
if (masterThreadLastMessageID != null) {
DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(masterThreadLastMessageID, LokiMessageFriendRequestStatus.REQUEST_PENDING)
} else {
// Device link fetching could fail, in which case the message could get routed into the slave thread
val slaveThreadLastMessageID = getLastMessageID(context, threadID)
if (slaveThreadLastMessageID != null) {
DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(slaveThreadLastMessageID, LokiMessageFriendRequestStatus.REQUEST_PENDING)
}
}
}
}
@JvmStatic
fun isFriendRequestFromBeforeRestoration(context: Context, content: SignalServiceContent): Boolean {
return content.isFriendRequest && content.timestamp < TextSecurePreferences.getRestorationTime(context)
}
@JvmStatic
fun shouldUpdateFriendRequestStatusFromOutgoingTextMessage(context: Context, message: OutgoingTextMessage): Boolean {
// The order of these checks matters
if (message.recipient.isGroupRecipient) { return false }
if (message.recipient.address.serialize() == TextSecurePreferences.getLocalNumber(context)) { return false }
// TODO: Return true if the message is a device linking request
// TODO: Return false if the message is a session request
return message.isFriendRequest
}
@JvmStatic
fun shouldUpdateFriendRequestStatusFromOutgoingMediaMessage(context: Context, message: OutgoingMediaMessage): Boolean {
// The order of these checks matters
if (message.recipient.isGroupRecipient) { return false }
if (message.recipient.address.serialize() == TextSecurePreferences.getLocalNumber(context)) { return false }
// TODO: Return true if the message is a device linking request
// TODO: Return false if the message is a session request
return message.isFriendRequest
}
@JvmStatic
fun setFriendRequestStatusToSendingIfNeeded(context: Context, messageID: Long, threadID: Long) {
val messageDB = DatabaseFactory.getLokiMessageDatabase(context)
val messageFRStatus = messageDB.getFriendRequestStatus(messageID)
if (messageFRStatus == LokiMessageFriendRequestStatus.NONE || messageFRStatus == LokiMessageFriendRequestStatus.REQUEST_EXPIRED) {
messageDB.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_SENDING)
}
val threadDB = DatabaseFactory.getLokiThreadDatabase(context)
val threadFRStatus = threadDB.getFriendRequestStatus(threadID)
if (threadFRStatus == LokiThreadFriendRequestStatus.NONE || threadFRStatus == LokiThreadFriendRequestStatus.REQUEST_EXPIRED) {
threadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_SENDING)
}
}
@JvmStatic
fun setFriendRequestStatusToSentIfNeeded(context: Context, messageID: Long, threadID: Long) {
val messageDB = DatabaseFactory.getLokiMessageDatabase(context)
val messageFRStatus = messageDB.getFriendRequestStatus(messageID)
if (messageFRStatus == LokiMessageFriendRequestStatus.NONE || messageFRStatus == LokiMessageFriendRequestStatus.REQUEST_EXPIRED
|| messageFRStatus == LokiMessageFriendRequestStatus.REQUEST_SENDING) {
messageDB.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_PENDING)
}
val threadDB = DatabaseFactory.getLokiThreadDatabase(context)
val threadFRStatus = threadDB.getFriendRequestStatus(threadID)
if (threadFRStatus == LokiThreadFriendRequestStatus.NONE || threadFRStatus == LokiThreadFriendRequestStatus.REQUEST_EXPIRED
|| threadFRStatus == LokiThreadFriendRequestStatus.REQUEST_SENDING) {
threadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_SENT)
}
}
@JvmStatic
fun setFriendRequestStatusToFailedIfNeeded(context: Context, messageID: Long, threadID: Long) {
val messageDB = DatabaseFactory.getLokiMessageDatabase(context)
val messageFRStatus = messageDB.getFriendRequestStatus(messageID)
if (messageFRStatus == LokiMessageFriendRequestStatus.REQUEST_SENDING) {
messageDB.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_FAILED)
}
val threadDB = DatabaseFactory.getLokiThreadDatabase(context)
val threadFRStatus = threadDB.getFriendRequestStatus(threadID)
if (threadFRStatus == LokiThreadFriendRequestStatus.REQUEST_SENDING) {
threadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.NONE)
}
}
fun sendAutoGeneratedFriendRequest(context: Context, publicKey: String) {
val recipient = recipient(context, publicKey)
val message = OutgoingEncryptedMessage(recipient, "Please accept to enable messages to be synced across devices", 0)
message.isFriendRequest = true
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
MessageSender.send(context, message, threadID, false, null)
}
}

View File

@ -1,33 +0,0 @@
package org.thoughtcrime.securesms.loki.protocol
import android.content.Context
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.whispersystems.libsignal.loki.LokiSessionResetProtocol
import org.whispersystems.libsignal.loki.LokiSessionResetStatus
import org.whispersystems.libsignal.protocol.PreKeySignalMessage
class LokiSessionResetImplementation(private val context: Context) : LokiSessionResetProtocol {
override fun getSessionResetStatus(hexEncodedPublicKey: String): LokiSessionResetStatus {
return DatabaseFactory.getLokiThreadDatabase(context).getSessionResetStatus(hexEncodedPublicKey)
}
override fun setSessionResetStatus(hexEncodedPublicKey: String, sessionResetStatus: LokiSessionResetStatus) {
return DatabaseFactory.getLokiThreadDatabase(context).setSessionResetStatus(hexEncodedPublicKey, sessionResetStatus)
}
override fun onNewSessionAdopted(hexEncodedPublicKey: String, oldSessionResetStatus: LokiSessionResetStatus) {
if (oldSessionResetStatus == LokiSessionResetStatus.IN_PROGRESS) {
val ephemeralMessage = EphemeralMessage.create(hexEncodedPublicKey)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage))
}
// TODO: Show session reset succeed message
}
override fun validatePreKeySignalMessage(sender: String, message: PreKeySignalMessage) {
val preKeyRecord = DatabaseFactory.getLokiPreKeyRecordDatabase(context).getPreKeyRecord(sender) ?: return
// TODO: Checking that the pre key record isn't null is causing issues when it shouldn't
check(preKeyRecord.id == (message.preKeyId ?: -1)) { "Received a background message from an unknown source." }
}
}

View File

@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.logging.Log
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.SignalServiceMessageSender
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChat
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -27,7 +27,7 @@ class MultiDeviceOpenGroupUpdateJob private constructor(parameters: Parameters)
constructor() : this(Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setQueue("MultiDeviceOpenGroupUpdateJob")
.setQueue(KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build())
@ -43,7 +43,7 @@ class MultiDeviceOpenGroupUpdateJob private constructor(parameters: Parameters)
return
}
// Gather open groups
val openGroups = mutableListOf<LokiPublicChat>()
val openGroups = mutableListOf<PublicChat>()
DatabaseFactory.getGroupDatabase(context).groups.use { reader ->
while (true) {
val record = reader.next ?: return@use

View File

@ -18,12 +18,11 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.loki.api.fileserver.LokiFileServerAPI
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI
import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLink
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLinkingSession
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
object MultiDeviceProtocol {
@ -31,46 +30,16 @@ object MultiDeviceProtocol {
enum class MessageType { Text, Media }
@JvmStatic
fun sendTextPush(context: Context, recipient: Recipient, messageID: Long, isEndSession: Boolean) {
sendMessagePush(context, recipient, messageID, MessageType.Text, isEndSession)
fun sendTextPush(context: Context, recipient: Recipient, messageID: Long) {
sendMessagePush(context, recipient, messageID, MessageType.Text)
}
@JvmStatic
fun sendMediaPush(context: Context, recipient: Recipient, messageID: Long) {
sendMessagePush(context, recipient, messageID, MessageType.Media, false)
sendMessagePush(context, recipient, messageID, MessageType.Media)
}
private fun sendMessagePushToDevice(context: Context, recipient: Recipient, messageID: Long, messageType: MessageType, isEndSession: Boolean): PushSendJob {
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
val threadFRStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID)
val isNoteToSelf = SessionMetaProtocol.shared.isNoteToSelf(recipient.address.serialize())
val isContactFriend = (threadFRStatus == LokiThreadFriendRequestStatus.FRIENDS || isNoteToSelf) // In the note to self case the device linking request was the FR
val isFRMessage = !isContactFriend
val hasVisibleContent = when (messageType) {
MessageType.Text -> DatabaseFactory.getSmsDatabase(context).getMessage(messageID).body.isNotBlank()
MessageType.Media -> {
val outgoingMediaMessage = DatabaseFactory.getMmsDatabase(context).getOutgoingMessage(messageID)
outgoingMediaMessage.body.isNotBlank() || outgoingMediaMessage.attachments.isNotEmpty()
}
}
val shouldSendAutoGeneratedFR = !isContactFriend && !isFRMessage
&& !isNoteToSelf && !recipient.address.isGroup // Group threads work through session requests
&& hasVisibleContent && !isEndSession
if (!shouldSendAutoGeneratedFR) {
when (messageType) {
MessageType.Text -> return PushTextSendJob(messageID, messageID, recipient.address, isFRMessage, null)
MessageType.Media -> return PushMediaSendJob(messageID, messageID, recipient.address, isFRMessage, null)
}
} else {
val autoGeneratedFRMessage = "Please accept to enable messages to be synced across devices"
when (messageType) {
MessageType.Text -> return PushTextSendJob(messageID, messageID, recipient.address, true, autoGeneratedFRMessage)
MessageType.Media -> return PushMediaSendJob(messageID, messageID, recipient.address, true, autoGeneratedFRMessage)
}
}
}
private fun sendMessagePush(context: Context, recipient: Recipient, messageID: Long, messageType: MessageType, isEndSession: Boolean) {
private fun sendMessagePush(context: Context, recipient: Recipient, messageID: Long, messageType: MessageType) {
val jobManager = ApplicationContext.getInstance(context).jobManager
val isMultiDeviceRequired = !recipient.address.isOpenGroup
if (!isMultiDeviceRequired) {
@ -80,9 +49,14 @@ object MultiDeviceProtocol {
}
}
val publicKey = recipient.address.serialize()
LokiFileServerAPI.shared.getDeviceLinks(publicKey).success {
FileServerAPI.shared.getDeviceLinks(publicKey).success {
val devices = MultiDeviceProtocol.shared.getAllLinkedDevices(publicKey)
val jobs = devices.map { sendMessagePushToDevice(context, recipient(context, it), messageID, messageType, isEndSession) }
val jobs = devices.map {
when (messageType) {
MessageType.Text -> PushTextSendJob(messageID, messageID, recipient(context, it).address) as PushSendJob
MessageType.Media -> PushMediaSendJob(messageID, messageID, recipient(context, it).address) as PushSendJob
}
}
@Suppress("UNCHECKED_CAST")
when (messageType) {
MessageType.Text -> jobManager.startChain(jobs).enqueue()
@ -105,7 +79,7 @@ object MultiDeviceProtocol {
// A request should include a pre key bundle. An authorization should be a normal message.
if (deviceLink.type == DeviceLink.Type.REQUEST) {
val preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(address.number)
message.asFriendRequest(true).withPreKeyBundle(preKeyBundle)
message.withPreKeyBundle(preKeyBundle)
} else {
// Include the user's profile key so that the slave device can get the user's profile picture
message.withProfileKey(ProfileKeyUtil.getProfileKey(context))
@ -135,7 +109,7 @@ object MultiDeviceProtocol {
return Promise.ofFail(Exception("Failed to sign device link."))
}
return retryIfNeeded(8) {
sendDeviceLinkMessage(context, deviceLink.slaveHexEncodedPublicKey, signedDeviceLink)
sendDeviceLinkMessage(context, deviceLink.slavePublicKey, signedDeviceLink)
}
}
@ -144,7 +118,7 @@ object MultiDeviceProtocol {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
if (deviceLink.type == DeviceLink.Type.REQUEST) {
handleDeviceLinkRequestMessage(context, deviceLink, content)
} else if (deviceLink.slaveHexEncodedPublicKey == userPublicKey) {
} else if (deviceLink.slavePublicKey == userPublicKey) {
handleDeviceLinkAuthorizedMessage(context, deviceLink, content)
}
}
@ -158,10 +132,10 @@ object MultiDeviceProtocol {
} else if (isRequest && TextSecurePreferences.getMasterHexEncodedPublicKey(context) != null) {
Log.d("Loki", "Ignoring unexpected device link message (the device is a slave device).")
return false
} else if (isRequest && deviceLink.masterHexEncodedPublicKey != userPublicKey) {
} else if (isRequest && deviceLink.masterPublicKey != userPublicKey) {
Log.d("Loki", "Ignoring device linking message addressed to another user.")
return false
} else if (isRequest && deviceLink.slaveHexEncodedPublicKey == userPublicKey) {
} else if (isRequest && deviceLink.slavePublicKey == userPublicKey) {
Log.d("Loki", "Ignoring device linking request message from self.")
return false
}
@ -188,16 +162,14 @@ object MultiDeviceProtocol {
}
val isValid = isValidDeviceLinkMessage(context, deviceLink)
if (!isValid) { return }
SessionManagementProtocol.handlePreKeyBundleMessageIfNeeded(context, content)
linkingSession.processLinkingAuthorization(deviceLink)
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
DatabaseFactory.getLokiAPIDatabase(context).clearDeviceLinks(userPublicKey)
DatabaseFactory.getLokiAPIDatabase(context).addDeviceLink(deviceLink)
TextSecurePreferences.setMasterHexEncodedPublicKey(context, deviceLink.masterHexEncodedPublicKey)
TextSecurePreferences.setMasterHexEncodedPublicKey(context, deviceLink.masterPublicKey)
TextSecurePreferences.setMultiDevice(context, true)
LokiFileServerAPI.shared.addDeviceLink(deviceLink)
org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content)
org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol.duplicate_handleProfileKey(context, content)
FileServerAPI.shared.addDeviceLink(deviceLink)
org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol.handleProfileKeyUpdate(context, content)
}
@JvmStatic
@ -210,19 +182,19 @@ object MultiDeviceProtocol {
// Ignore the request if we don't know about the device link in question
val masterDeviceLinks = DatabaseFactory.getLokiAPIDatabase(context).getDeviceLinks(masterDevicePublicKey)
if (masterDeviceLinks.none {
it.masterHexEncodedPublicKey == masterDevicePublicKey && it.slaveHexEncodedPublicKey == userPublicKey
it.masterPublicKey == masterDevicePublicKey && it.slavePublicKey == userPublicKey
}) {
return
}
LokiFileServerAPI.shared.getDeviceLinks(userPublicKey, true).success { slaveDeviceLinks ->
FileServerAPI.shared.getDeviceLinks(userPublicKey, true).success { slaveDeviceLinks ->
// Check that the device link IS present on the file server.
// Note that the device link as seen from the master device's perspective has been deleted at this point, but the
// device link as seen from the slave perspective hasn't.
if (slaveDeviceLinks.any {
it.masterHexEncodedPublicKey == masterDevicePublicKey && it.slaveHexEncodedPublicKey == userPublicKey
it.masterPublicKey == masterDevicePublicKey && it.slavePublicKey == userPublicKey
}) {
for (slaveDeviceLink in slaveDeviceLinks) { // In theory there should only be one
LokiFileServerAPI.shared.removeDeviceLink(slaveDeviceLink) // Attempt to clean up on the file server
FileServerAPI.shared.removeDeviceLink(slaveDeviceLink) // Attempt to clean up on the file server
}
TextSecurePreferences.setWasUnlinked(context, true)
ApplicationContext.getInstance(context).clearData()

View File

@ -1,85 +0,0 @@
package org.thoughtcrime.securesms.loki.protocol
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.jobmanager.Data
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.BaseJob
import org.thoughtcrime.securesms.logging.Log
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import java.io.IOException
import java.util.concurrent.TimeUnit
class PushEphemeralMessageSendJob private constructor(parameters: Parameters, private val message: EphemeralMessage) : BaseJob(parameters) {
companion object {
private const val KEY_MESSAGE = "message"
const val KEY = "PushBackgroundMessageSendJob"
}
constructor(message: EphemeralMessage) : this(Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setQueue(KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(1)
.build(),
message)
override fun serialize(): Data {
return Data.Builder()
.putString(KEY_MESSAGE, message.serialize())
.build()
}
override fun getFactoryKey(): String { return KEY }
public override fun onRun() {
val recipient = message.get<String?>("recipient", null) ?: throw IllegalStateException()
val dataMessage = SignalServiceDataMessage.newBuilder().withTimestamp(System.currentTimeMillis())
// Attach a pre key bundle if needed
if (message.get("friendRequest", false)) {
val bundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(recipient)
dataMessage.withPreKeyBundle(bundle).asFriendRequest(true)
}
// Set flags if needed (these are mutually exclusive)
when {
message.get("unpairingRequest", false) -> dataMessage.asUnlinkingRequest(true)
message.get("sessionRestore", false) -> dataMessage.asSessionRestorationRequest(true)
message.get("sessionRequest", false) -> dataMessage.asSessionRequest(true)
}
// Send the message
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(recipient)
try {
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(recipient), false))
messageSender.sendMessage(0, address, udAccess, dataMessage.build()) // The message ID doesn't matter
} catch (e: Exception) {
Log.d("Loki", "Failed to send background message to: $recipient due to error: $e.")
throw e
}
}
public override fun onShouldRetry(e: Exception): Boolean {
// Disable since we have our own retrying
return false
}
override fun onCanceled() { }
class Factory : Job.Factory<PushEphemeralMessageSendJob> {
override fun create(parameters: Parameters, data: Data): PushEphemeralMessageSendJob {
try {
val messageJSON = data.getString(KEY_MESSAGE)
return PushEphemeralMessageSendJob(parameters, EphemeralMessage.parse(messageJSON))
} catch (e: IOException) {
throw AssertionError(e)
}
}
}
}

View File

@ -56,7 +56,7 @@ class PushNullMessageSendJob private constructor(parameters: Parameters, private
try {
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
Date().time, serializedContentMessage, false, ttl, false,
false, false, false, false)
false, false, false)
} catch (e: Exception) {
Log.d("Loki", "Failed to send null message to: $publicKey due to error: $e.")
throw e

View File

@ -0,0 +1,107 @@
package org.thoughtcrime.securesms.loki.protocol
import com.google.protobuf.ByteString
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.jobmanager.Data
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.BaseJob
import org.thoughtcrime.securesms.logging.Log
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.loki.protocol.meta.TTLUtilities
import java.io.IOException
import java.security.SecureRandom
import java.util.*
import java.util.concurrent.TimeUnit
class PushSessionRequestMessageSendJob private constructor(parameters: Parameters, private val publicKey: String, private val timestamp: Long) : BaseJob(parameters) {
companion object {
const val KEY = "PushSessionRequestMessageSendJob"
}
constructor(publicKey: String, timestamp: Long) : this(Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setQueue(KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(1)
.build(),
publicKey,
timestamp)
override fun serialize(): Data {
return Data.Builder().putString("publicKey", publicKey).putLong("timestamp", timestamp).build()
}
override fun getFactoryKey(): String { return KEY }
public override fun onRun() {
// Prepare
val contentMessage = SignalServiceProtos.Content.newBuilder()
// Attach the pre key bundle message
val preKeyBundleMessage = SignalServiceProtos.PreKeyBundleMessage.newBuilder()
val preKeyBundle = DatabaseFactory.getLokiPreKeyBundleDatabase(context).generatePreKeyBundle(publicKey) ?: return
preKeyBundleMessage.identityKey = ByteString.copyFrom(preKeyBundle.identityKey.serialize())
preKeyBundleMessage.deviceId = preKeyBundle.deviceId
preKeyBundleMessage.preKeyId = preKeyBundle.preKeyId
preKeyBundleMessage.signedKeyId = preKeyBundle.signedPreKeyId
preKeyBundleMessage.preKey = ByteString.copyFrom(preKeyBundle.preKey.serialize())
preKeyBundleMessage.signedKey = ByteString.copyFrom(preKeyBundle.signedPreKey.serialize())
preKeyBundleMessage.signature = ByteString.copyFrom(preKeyBundle.signedPreKeySignature)
contentMessage.preKeyBundleMessage = preKeyBundleMessage.build()
// Attach the null message
val nullMessage = SignalServiceProtos.NullMessage.newBuilder()
val sr = SecureRandom()
val paddingSize = sr.nextInt(512)
val padding = ByteArray(paddingSize)
sr.nextBytes(padding)
nullMessage.padding = ByteString.copyFrom(padding)
contentMessage.nullMessage = nullMessage.build()
// Send the result
val serializedContentMessage = contentMessage.build().toByteArray()
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(publicKey)
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
val ttl = TTLUtilities.getTTL(TTLUtilities.MessageType.SessionRequest)
try {
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
Date().time, serializedContentMessage, false, ttl, false,
true, false, false)
} catch (e: Exception) {
Log.d("Loki", "Failed to send session request to: $publicKey due to error: $e.")
throw e
}
}
public override fun onShouldRetry(e: Exception): Boolean {
// Disable since we have our own retrying
return false
}
override fun onCanceled() {
// Update the DB on fail if this is still the most recently sent session request (should always be true)
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
if (apiDB.getSessionRequestSentTimestamp(publicKey) == timestamp) {
apiDB.setSessionRequestSentTimestamp(publicKey, 0)
}
}
class Factory : Job.Factory<PushSessionRequestMessageSendJob> {
override fun create(parameters: Parameters, data: Data): PushSessionRequestMessageSendJob {
try {
val publicKey = data.getString("publicKey")
val timestamp = data.getLong("timestamp")
return PushSessionRequestMessageSendJob(parameters, publicKey, timestamp)
} catch (e: IOException) {
throw AssertionError(e)
}
}
}
}

View File

@ -11,9 +11,11 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.jobs.CleanPreKeysJob
import org.thoughtcrime.securesms.loki.utilities.recipient
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage
import org.thoughtcrime.securesms.sms.OutgoingTextMessage
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.libsignal.loki.LokiSessionResetStatus
import org.whispersystems.libsignal.loki.SessionResetStatus
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
import java.util.*
@ -27,8 +29,8 @@ object SessionManagementProtocol {
val smsDB = DatabaseFactory.getSmsDatabase(context)
val devices = lokiThreadDB.getSessionRestoreDevices(threadID)
for (device in devices) {
val sessionRestorationRequest = EphemeralMessage.createSessionRestorationRequest(recipient.address.serialize())
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(sessionRestorationRequest))
val endSessionMessage = OutgoingEndSessionMessage(OutgoingTextMessage(recipient, "TERMINATE", 0, -1))
MessageSender.send(context, endSessionMessage, threadID, false, null)
}
val infoMessage = OutgoingTextMessage(recipient, "", 0, 0)
val infoMessageID = smsDB.insertMessageOutbox(threadID, infoMessage, false, System.currentTimeMillis(), null)
@ -52,38 +54,30 @@ object SessionManagementProtocol {
}
@JvmStatic
fun handlePreKeyBundleMessageIfNeeded(context: Context, content: SignalServiceContent) {
val recipient = recipient(context, content.sender)
if (recipient.isGroupRecipient) { return }
val preKeyBundleMessage = content.lokiServiceMessage.orNull()?.preKeyBundleMessage ?: return
val registrationID = TextSecurePreferences.getLocalRegistrationId(context) // TODO: It seems wrong to use the local registration ID for this?
val lokiPreKeyBundleDatabase = DatabaseFactory.getLokiPreKeyBundleDatabase(context)
Log.d("Loki", "Received a pre key bundle from: " + content.sender.toString() + ".")
if (content.dataMessage.isPresent && content.dataMessage.get().isSessionRequest) {
val sessionRequestTimestamp = DatabaseFactory.getLokiAPIDatabase(context).getSessionRequestTimestamp(content.sender)
if (sessionRequestTimestamp != null && content.timestamp < sessionRequestTimestamp) {
// We sent or processed a session request after this one was sent
Log.d("Loki", "Ignoring session request from: ${content.sender}.")
return
}
}
val preKeyBundle = preKeyBundleMessage.getPreKeyBundle(registrationID)
lokiPreKeyBundleDatabase.setPreKeyBundle(content.sender, preKeyBundle)
fun shouldProcessSessionRequest(context: Context, publicKey: String, timestamp: Long): Boolean {
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val sentTimestamp = apiDB.getSessionRequestSentTimestamp(publicKey) ?: 0
val processedTimestamp = apiDB.getSessionRequestProcessedTimestamp(publicKey) ?: 0
return timestamp > sentTimestamp && timestamp > processedTimestamp
}
@JvmStatic
fun handleSessionRequestIfNeeded(context: Context, content: SignalServiceContent): Boolean {
if (!content.dataMessage.isPresent || !content.dataMessage.get().isSessionRequest) { return false }
val sessionRequestTimestamp = DatabaseFactory.getLokiAPIDatabase(context).getSessionRequestTimestamp(content.sender)
if (sessionRequestTimestamp != null && content.timestamp < sessionRequestTimestamp) {
// We sent or processed a session request after this one was sent
Log.d("Loki", "Ignoring session request from: ${content.sender}.")
return false
fun handlePreKeyBundleMessageIfNeeded(context: Context, content: SignalServiceContent) {
val preKeyBundleMessage = content.preKeyBundleMessage.orNull() ?: return
val publicKey = content.sender
if (recipient(context, publicKey).isGroupRecipient) { return } // Should never occur
Log.d("Loki", "Received a pre key bundle from: $publicKey.")
if (!shouldProcessSessionRequest(context, publicKey, content.timestamp)) {
Log.d("Loki", "Ignoring session request from: $publicKey.")
return
}
DatabaseFactory.getLokiAPIDatabase(context).setSessionRequestTimestamp(content.sender, Date().time)
val ephemeralMessage = EphemeralMessage.create(content.sender)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage))
return true
val registrationID = TextSecurePreferences.getLocalRegistrationId(context)
val lokiPreKeyBundleDatabase = DatabaseFactory.getLokiPreKeyBundleDatabase(context)
val preKeyBundle = preKeyBundleMessage.getPreKeyBundle(registrationID)
lokiPreKeyBundleDatabase.setPreKeyBundle(publicKey, preKeyBundle)
DatabaseFactory.getLokiAPIDatabase(context).setSessionRequestProcessedTimestamp(publicKey, Date().time)
val job = PushNullMessageSendJob(publicKey)
ApplicationContext.getInstance(context).jobManager.add(job)
}
@JvmStatic
@ -93,10 +87,10 @@ object SessionManagementProtocol {
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
Log.d("Loki", "Received a session reset request from: ${content.sender}; archiving the session.")
sessionStore.archiveAllSessions(content.sender)
lokiThreadDB.setSessionResetStatus(content.sender, LokiSessionResetStatus.REQUEST_RECEIVED)
lokiThreadDB.setSessionResetStatus(content.sender, SessionResetStatus.REQUEST_RECEIVED)
Log.d("Loki", "Sending an ephemeral message back to: ${content.sender}.")
val ephemeralMessage = EphemeralMessage.create(content.sender)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage))
val job = PushNullMessageSendJob(content.sender)
ApplicationContext.getInstance(context).jobManager.add(job)
SecurityEvent.broadcastSecurityUpdateEvent(context)
}

View File

@ -11,7 +11,6 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
import java.security.MessageDigest
object SessionMetaProtocol {
@ -48,9 +47,8 @@ object SessionMetaProtocol {
}
}
// FIXME: Basically a duplicate of PushDecryptJob's handleProfileKey
@JvmStatic
fun duplicate_handleProfileKey(context: Context, content: SignalServiceContent) {
fun handleProfileKeyUpdate(context: Context, content: SignalServiceContent) {
val message = content.dataMessage.get()
if (!message.profileKey.isPresent) { return }
val database = DatabaseFactory.getRecipientDatabase(context)
@ -60,46 +58,34 @@ object SessionMetaProtocol {
database.setUnidentifiedAccessMode(recipient, RecipientDatabase.UnidentifiedAccessMode.UNKNOWN)
val url = content.senderProfilePictureURL.or("")
ApplicationContext.getInstance(context).jobManager.add(RetrieveProfileAvatarJob(recipient, url))
handleProfileKeyUpdateIfNeeded(context, content)
val userMasterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
if (userMasterPublicKey == content.sender) {
ApplicationContext.getInstance(context).updateOpenGroupProfilePicturesIfNeeded()
}
}
}
/**
* Should be invoked for the recipient's master device.
*/
@JvmStatic
fun handleProfileKeyUpdateIfNeeded(context: Context, content: SignalServiceContent) {
val userMasterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
if (userMasterPublicKey != content.sender) { return }
ApplicationContext.getInstance(context).updateOpenGroupProfilePicturesIfNeeded()
fun canUserReplyToNotification(recipient: Recipient): Boolean {
return !recipient.address.isRSSFeed
}
/**
* Should be invoked for the recipient's master device.
*/
@JvmStatic
fun canUserReplyToNotification(recipient: Recipient, context: Context): Boolean {
val isGroup = recipient.isGroupRecipient
if (isGroup) { return !recipient.address.isRSSFeed }
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
return DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS
fun shouldSendReadReceipt(address: Address): Boolean {
return !address.isGroup
}
/**
* Should be invoked for the recipient's master device.
*/
@JvmStatic
fun shouldSendReadReceipt(address: Address, context: Context): Boolean {
if (address.isGroup) { return false }
val recipient = Recipient.from(context, address,false)
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
return DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS
}
/**
* Should be invoked for the recipient's master device.
*/
@JvmStatic
fun shouldSendTypingIndicator(recipient: Recipient?, context: Context): Boolean {
if (recipient == null || recipient.isGroupRecipient) { return false }
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
return DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS
fun shouldSendTypingIndicator(address: Address): Boolean {
return !address.isGroup
}
}

View File

@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.loki.protocol
import android.content.Context
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.whispersystems.libsignal.loki.SessionResetProtocol
import org.whispersystems.libsignal.loki.SessionResetStatus
import org.whispersystems.libsignal.protocol.PreKeySignalMessage
class SessionResetImplementation(private val context: Context) : SessionResetProtocol {
override fun getSessionResetStatus(publicKey: String): SessionResetStatus {
return DatabaseFactory.getLokiThreadDatabase(context).getSessionResetStatus(publicKey)
}
override fun setSessionResetStatus(publicKey: String, sessionResetStatus: SessionResetStatus) {
return DatabaseFactory.getLokiThreadDatabase(context).setSessionResetStatus(publicKey, sessionResetStatus)
}
override fun onNewSessionAdopted(publicKey: String, oldSessionResetStatus: SessionResetStatus) {
if (oldSessionResetStatus == SessionResetStatus.IN_PROGRESS) {
val job = PushNullMessageSendJob(publicKey)
ApplicationContext.getInstance(context).jobManager.add(job)
}
// TODO: Show session reset succeed message
}
override fun validatePreKeySignalMessage(publicKey: String, message: PreKeySignalMessage) {
val preKeyRecord = DatabaseFactory.getLokiPreKeyRecordDatabase(context).getPreKeyRecord(publicKey) ?: return
// TODO: Checking that the pre key record isn't null is causing issues when it shouldn't
check(preKeyRecord.id == (message.preKeyId ?: -1)) { "Received a background message from an unknown source." }
}
}

View File

@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData
import org.thoughtcrime.securesms.contacts.ContactAccessor.NumberData
import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.groups.GroupMessageProcessor
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob
@ -19,13 +20,12 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.messages.SignalServiceGroup
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream
import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChat
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
import org.whispersystems.signalservice.loki.protocol.todo.LokiMessageFriendRequestStatus
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
import java.util.*
@ -52,8 +52,9 @@ object SyncMessagesProtocol {
val allAddresses = ArrayList(DatabaseFactory.getRecipientDatabase(context).allAddresses)
val result = mutableSetOf<ContactData>()
for (address in allAddresses) {
if (!shouldSyncContact(context, address)) { continue }
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, address, false))
if (!shouldSyncContact(context, address.serialize())) { continue }
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(Recipient.from(context, address, false))
if (threadID < 0) { continue }
val displayName = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(address.serialize())
val contactData = ContactData(threadID, displayName)
contactData.numbers.add(NumberData("TextSecure", address.serialize()))
@ -63,13 +64,12 @@ object SyncMessagesProtocol {
}
@JvmStatic
fun shouldSyncContact(context: Context, address: Address): Boolean {
if (!PublicKeyValidation.isValid(address.serialize())) { return false }
if (address.serialize() == TextSecurePreferences.getMasterHexEncodedPublicKey(context)) { return false }
if (address.serialize() == TextSecurePreferences.getLocalNumber(context)) { return false }
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, address, false))
val isFriend = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS
return isFriend
fun shouldSyncContact(context: Context, publicKey: String): Boolean {
if (!PublicKeyValidation.isValid(publicKey)) { return false }
if (publicKey == TextSecurePreferences.getMasterHexEncodedPublicKey(context)) { return false }
if (publicKey == TextSecurePreferences.getLocalNumber(context)) { return false }
if (MultiDeviceProtocol.shared.getSlaveDevices(publicKey).contains(publicKey)) { return false }
return true
}
@JvmStatic
@ -95,33 +95,13 @@ object SyncMessagesProtocol {
if (!allUserDevices.contains(content.sender)) { return }
Log.d("Loki", "Received a contact sync message.")
val contactsInputStream = DeviceContactsInputStream(message.contactsStream.asStream().inputStream)
val contactPublicKeys = contactsInputStream.readAll().map { it.number }
for (contactPublicKey in contactPublicKeys) {
val contacts = contactsInputStream.readAll()
for (contact in contacts) {
val contactPublicKey = contact.number
if (contactPublicKey == userPublicKey || !PublicKeyValidation.isValid(contactPublicKey)) { return }
val recipient = recipient(context, contactPublicKey)
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context)
val threadFRStatus = lokiThreadDB.getFriendRequestStatus(threadID)
when (threadFRStatus) {
LokiThreadFriendRequestStatus.NONE, LokiThreadFriendRequestStatus.REQUEST_EXPIRED -> {
val contactLinkedDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(contactPublicKey)
for (device in contactLinkedDevices) {
FriendRequestProtocol.sendAutoGeneratedFriendRequest(context, device)
}
}
LokiThreadFriendRequestStatus.REQUEST_RECEIVED -> {
FriendRequestProtocol.acceptFriendRequest(context, recipient(context, contactPublicKey)) // Takes into account multi device internally
lokiThreadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS)
val lastMessageID = FriendRequestProtocol.getLastMessageID(context, threadID)
if (lastMessageID != null) {
DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(lastMessageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED)
}
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient(context, contactPublicKey), true)
}
else -> {
// Do nothing
}
}
val applicationContext = context.applicationContext as ApplicationContext
applicationContext.sendSessionRequestIfNeeded(contactPublicKey)
DatabaseFactory.getRecipientDatabase(context).setBlocked(recipient(context, contactPublicKey), contact.isBlocked)
}
}
@ -136,22 +116,22 @@ object SyncMessagesProtocol {
val closedGroups = closedGroupsInputStream.readAll()
for (closedGroup in closedGroups) {
val signalServiceGroup = SignalServiceGroup(
SignalServiceGroup.Type.UPDATE,
closedGroup.id,
SignalServiceGroup.GroupType.SIGNAL,
closedGroup.name.orNull(),
closedGroup.members,
closedGroup.avatar.orNull(),
closedGroup.admins
SignalServiceGroup.Type.UPDATE,
closedGroup.id,
SignalServiceGroup.GroupType.SIGNAL,
closedGroup.name.orNull(),
closedGroup.members,
closedGroup.avatar.orNull(),
closedGroup.admins
)
val signalServiceDataMessage = SignalServiceDataMessage(content.timestamp, signalServiceGroup, null, null)
val dataMessage = SignalServiceDataMessage(content.timestamp, signalServiceGroup, null, null)
// This establishes sessions internally
GroupMessageProcessor.process(context, content, signalServiceDataMessage, false)
GroupMessageProcessor.process(context, content, dataMessage, false)
}
}
@JvmStatic
fun handleOpenGroupSyncMessage(context: Context, content: SignalServiceContent, openGroups: List<LokiPublicChat>) {
fun handleOpenGroupSyncMessage(context: Context, content: SignalServiceContent, openGroups: List<PublicChat>) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val allUserDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey)
if (!allUserDevices.contains(content.sender)) { return }
@ -164,4 +144,27 @@ object SyncMessagesProtocol {
OpenGroupUtilities.addGroup(context, url, channel)
}
}
@JvmStatic
fun handleBlockedContactsSyncMessage(context: Context, content: SignalServiceContent, blockedContacts: BlockedListMessage) {
val recipientDB = DatabaseFactory.getRecipientDatabase(context)
val cursor = recipientDB.blocked
val blockedPublicKeys = blockedContacts.numbers.toSet()
val publicKeysToUnblock = mutableSetOf<String>()
fun addToUnblockListIfNeeded() {
val publicKey = cursor.getString(cursor.getColumnIndex(RecipientDatabase.ADDRESS)) ?: return
if (blockedPublicKeys.contains(publicKey)) { return }
publicKeysToUnblock.add(publicKey)
}
while (cursor.moveToNext()) {
addToUnblockListIfNeeded()
}
publicKeysToUnblock.forEach {
recipientDB.setBlocked(recipient(context, it), false)
}
blockedPublicKeys.forEach {
recipientDB.setBlocked(recipient(context, it), true)
}
ApplicationContext.getInstance(context).broadcaster.broadcast("blockedContactsChanged")
}
}

View File

@ -66,7 +66,7 @@ class LokiRSSFeedPoller(private val context: Context, private val feed: LokiRSSF
val id = feed.id.toByteArray()
val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.RSS_FEED, null, null, null, null)
val x2 = SignalServiceDataMessage(timestamp, x1, null, body)
val x3 = SignalServiceContent(x2, "Loki", SignalServiceAddress.DEFAULT_DEVICE_ID, timestamp, false, false, false, false)
val x3 = SignalServiceContent(x2, "Loki", SignalServiceAddress.DEFAULT_DEVICE_ID, timestamp, false, false)
PushDecryptJob(context).handleTextMessage(x3, x2, Optional.absent(), Optional.absent())
}
}.fail { exception ->

View File

@ -5,11 +5,9 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
data class Contact(
val recipient: Recipient,
val isFriend: Boolean,
val isSlave: Boolean,
val isOurDevice: Boolean
) {
@ -31,10 +29,9 @@ object ContactUtilities {
@JvmStatic
fun getAllContacts(context: Context): Set<Contact> {
val threadDatabase = DatabaseFactory.getThreadDatabase(context)
val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context)
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(context)
val userDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userHexEncodedPublicKey)
val userDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey)
val cursor = threadDatabase.conversationList
val result = mutableSetOf<Contact>()
threadDatabase.readerFor(cursor).use { reader ->
@ -43,13 +40,12 @@ object ContactUtilities {
val recipient = thread.recipient
val publicKey = recipient.address.serialize()
val isUserDevice = userDevices.contains(publicKey)
val isFriend = lokiThreadDatabase.getFriendRequestStatus(thread.threadId) == LokiThreadFriendRequestStatus.FRIENDS
var isSlave = false
if (!recipient.isGroupRecipient) {
val deviceLinks = lokiAPIDatabase.getDeviceLinks(publicKey)
isSlave = deviceLinks.find { it.slaveHexEncodedPublicKey == publicKey } != null
isSlave = deviceLinks.find { it.slavePublicKey == publicKey } != null
}
result.add(Contact(recipient, isFriend, isSlave, isUserDevice))
result.add(Contact(recipient, isSlave, isUserDevice))
}
}
return result

View File

@ -28,22 +28,22 @@ object MentionUtilities {
val mentions = mutableListOf<Tuple2<Range<Int>, String>>()
var startIndex = 0
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
if (matcher.find(startIndex)) {
while (true) {
val hexEncodedPublicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
val userDisplayName: String? = if (hexEncodedPublicKey.toLowerCase() == userHexEncodedPublicKey.toLowerCase()) {
val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
val userDisplayName: String? = if (publicKey.toLowerCase() == userPublicKey.toLowerCase()) {
TextSecurePreferences.getProfileName(context)
} else if (publicChat != null) {
DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, hexEncodedPublicKey)
DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, publicKey)
} else {
DatabaseFactory.getLokiUserDatabase(context).getDisplayName(hexEncodedPublicKey)
DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
}
if (userDisplayName != null) {
text = text.subSequence(0, matcher.start()).toString() + "@" + userDisplayName + text.subSequence(matcher.end(), text.length)
val endIndex = matcher.start() + 1 + userDisplayName.length
startIndex = endIndex
mentions.add(Tuple2(Range.create(matcher.start(), endIndex), hexEncodedPublicKey))
mentions.add(Tuple2(Range.create(matcher.start(), endIndex), publicKey))
} else {
startIndex = matcher.end()
}

View File

@ -8,11 +8,11 @@ import org.thoughtcrime.securesms.recipients.Recipient
fun getOpenGroupDisplayName(recipient: Recipient, threadRecipient: Recipient, context: Context): String {
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(threadRecipient)
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
val hexEncodedPublicKey = recipient.address.toString()
val publicKey = recipient.address.toString()
val displayName = if (publicChat != null) {
DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, hexEncodedPublicKey)
DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, publicKey)
} else {
DatabaseFactory.getLokiUserDatabase(context).getDisplayName(hexEncodedPublicKey)
DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey)
}
return displayName ?: hexEncodedPublicKey
return displayName ?: publicKey
}

View File

@ -8,21 +8,21 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChat
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat
object OpenGroupUtilities {
@JvmStatic fun addGroup(context: Context, url: String, channel: Long): Promise<LokiPublicChat, Exception> {
@JvmStatic fun addGroup(context: Context, url: String, channel: Long): Promise<PublicChat, Exception> {
// Check for an existing group
val groupID = LokiPublicChat.getId(channel, url)
val groupID = PublicChat.getId(channel, url)
val threadID = GroupManager.getOpenGroupThreadID(groupID, context)
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
if (openGroup != null) { return Promise.of(openGroup) }
// Add the new group
val application = ApplicationContext.getInstance(context)
val displayName = TextSecurePreferences.getProfileName(context)
val lokiPublicChatAPI = application.lokiPublicChatAPI ?: throw Error("LokiPublicChatAPI is not initialized.")
return application.lokiPublicChatManager.addChat(url, channel).then { group ->
val lokiPublicChatAPI = application.publicChatAPI ?: throw Error("LokiPublicChatAPI is not initialized.")
return application.publicChatManager.addChat(url, channel).then { group ->
DatabaseFactory.getLokiAPIDatabase(context).removeLastMessageServerID(channel, url)
DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(channel, url)
lokiPublicChatAPI.getMessages(channel, url)

View File

@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities.populat
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities.highlightMentions
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager
import java.util.*
@ -46,22 +47,34 @@ class ConversationView : LinearLayout {
// region Updating
fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) {
this.thread = thread
populateUserPublicKeyCacheIfNeeded(thread.threadId, context) // FIXME: This is a terrible place to do this
unreadMessagesIndicatorView.visibility = if (thread.unreadCount > 0) View.VISIBLE else View.INVISIBLE
populateUserPublicKeyCacheIfNeeded(thread.threadId, context) // FIXME: This is a bad place to do this
if (thread.recipient.isBlocked) {
accentView.setBackgroundResource(R.color.destructive)
accentView.visibility = View.VISIBLE
} else {
accentView.setBackgroundResource(R.color.accent)
accentView.visibility = if (thread.unreadCount > 0) View.VISIBLE else View.INVISIBLE
}
if (thread.recipient.isGroupRecipient) {
if ("Session Public Chat" == thread.recipient.name) {
profilePictureView.hexEncodedPublicKey = ""
profilePictureView.publicKey = ""
profilePictureView.additionalPublicKey = null
profilePictureView.isRSSFeed = true
} else {
val users = MentionsManager.shared.userPublicKeyCache[thread.threadId]?.toList() ?: listOf()
val users = MentionsManager.shared.userPublicKeyCache[thread.threadId]?.toMutableList() ?: mutableListOf()
users.remove(TextSecurePreferences.getLocalNumber(context))
val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
if (masterPublicKey != null) {
users.remove(masterPublicKey)
}
val randomUsers = users.sorted() // Sort to provide a level of stability
profilePictureView.hexEncodedPublicKey = randomUsers.getOrNull(0) ?: ""
profilePictureView.additionalHexEncodedPublicKey = randomUsers.getOrNull(1) ?: ""
profilePictureView.publicKey = randomUsers.getOrNull(0) ?: ""
profilePictureView.additionalPublicKey = randomUsers.getOrNull(1) ?: ""
profilePictureView.isRSSFeed = thread.recipient.name == "Loki News" || thread.recipient.name == "Session Updates"
}
} else {
profilePictureView.hexEncodedPublicKey = thread.recipient.address.toString()
profilePictureView.additionalHexEncodedPublicKey = null
profilePictureView.publicKey = thread.recipient.address.toString()
profilePictureView.additionalPublicKey = null
profilePictureView.isRSSFeed = false
}
profilePictureView.glide = glide

View File

@ -1,190 +0,0 @@
package org.thoughtcrime.securesms.loki.views
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.widget.Button
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import com.github.ybq.android.spinkit.style.DoubleBounce
import network.loki.messenger.R
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.whispersystems.signalservice.loki.protocol.todo.LokiMessageFriendRequestStatus
class FriendRequestView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
private var isUISetUp = false
private var message: MessageRecord? = null
var delegate: FriendRequestViewDelegate? = null
// region Components
private val topSpacer by lazy {
val result = View(context)
result.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, toPx(12, resources))
result
}
private val label by lazy {
val result = TextView(context)
result.setTextColor(resources.getColorWithID(R.color.text, context.theme))
result.textAlignment = TextView.TEXT_ALIGNMENT_CENTER
result.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.small_font_size))
result
}
private val buttonLinearLayout by lazy {
val result = LinearLayout(context)
result.orientation = HORIZONTAL
result.setPadding(0, resources.getDimension(R.dimen.medium_spacing).toInt(), 0, 0)
result
}
private val loaderContainer by lazy {
val result = LinearLayout(context)
val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, toPx(50, resources))
result.layoutParams = layoutParams
result.gravity = Gravity.CENTER
result
}
// endregion
// region Initialization
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context) : this(context, null)
// endregion
// region Updating
fun update(message: MessageRecord) {
this.message = message
setUpUIIfNeeded()
updateUI()
}
private fun setUpUIIfNeeded() {
if (isUISetUp) { return }
isUISetUp = true
orientation = VERTICAL
setPadding(toPx(48, resources), 0, toPx(48, resources), 0)
addView(topSpacer)
addView(label)
if (!message!!.isOutgoing) {
val loader = ProgressBar(context)
loader.isIndeterminate = true
loader.indeterminateDrawable = DoubleBounce()
val loaderLayoutParams = LayoutParams(LayoutParams.MATCH_PARENT, toPx(24, resources))
loader.layoutParams = loaderLayoutParams
loaderContainer.addView(loader)
addView(loaderContainer)
fun button(): Button {
val result = Button(context)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
result.elevation = 0f
result.stateListAnimator = null
}
result.setTextColor(resources.getColorWithID(R.color.text, context.theme))
result.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.small_font_size))
result.isAllCaps = false
result.setPadding(0, 0, 0, 0)
val buttonLayoutParams = LayoutParams(0, resources.getDimension(R.dimen.small_button_height).toInt())
buttonLayoutParams.weight = 1f
result.layoutParams = buttonLayoutParams
return result
}
val rejectButton = button()
rejectButton.text = resources.getString(R.string.view_friend_request_reject_button_title)
rejectButton.setBackgroundResource(R.drawable.unimportant_dialog_button_background)
rejectButton.setOnClickListener { reject() }
buttonLinearLayout.addView(rejectButton)
val acceptButton = button()
acceptButton.text = resources.getString(R.string.view_friend_request_accept_button_title)
acceptButton.setBackgroundResource(R.drawable.prominent_dialog_button_background)
val acceptButtonLayoutParams = acceptButton.layoutParams as LayoutParams
acceptButtonLayoutParams.setMargins(resources.getDimension(R.dimen.medium_spacing).toInt(), 0, 0, 0)
acceptButton.layoutParams = acceptButtonLayoutParams
acceptButton.setOnClickListener { accept() }
buttonLinearLayout.addView(acceptButton)
buttonLinearLayout.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, toPx(50, resources))
addView(buttonLinearLayout)
}
}
private fun updateUI() {
val message = message
val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
val contactID = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(message!!.threadId)!!.address.toString()
val contactDisplayName = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(contactID) ?: contactID
val friendRequestStatus = lokiMessageDatabase.getFriendRequestStatus(message.id)
if (message is MediaMmsMessageRecord) {
visibility = View.GONE
return
}
if (!message.isOutgoing) {
visibility = if (friendRequestStatus == LokiMessageFriendRequestStatus.NONE) View.GONE else View.VISIBLE
buttonLinearLayout.visibility = if (friendRequestStatus != LokiMessageFriendRequestStatus.REQUEST_PENDING) View.GONE else View.VISIBLE
loaderContainer.visibility = if (friendRequestStatus == LokiMessageFriendRequestStatus.REQUEST_SENDING) View.VISIBLE else View.GONE
val formatID = when (friendRequestStatus) {
LokiMessageFriendRequestStatus.NONE, LokiMessageFriendRequestStatus.REQUEST_SENDING, LokiMessageFriendRequestStatus.REQUEST_FAILED -> return
LokiMessageFriendRequestStatus.REQUEST_PENDING -> R.string.view_friend_request_incoming_pending_message
LokiMessageFriendRequestStatus.REQUEST_ACCEPTED -> R.string.view_friend_request_incoming_accepted_message
LokiMessageFriendRequestStatus.REQUEST_REJECTED -> R.string.view_friend_request_incoming_declined_message
LokiMessageFriendRequestStatus.REQUEST_EXPIRED -> R.string.view_friend_request_incoming_expired_message
}
label.text = resources.getString(formatID, contactDisplayName)
} else {
visibility = if (friendRequestStatus == LokiMessageFriendRequestStatus.NONE) View.GONE else View.VISIBLE
buttonLinearLayout.visibility = View.GONE
loaderContainer.visibility = View.GONE
val formatID = when (friendRequestStatus) {
LokiMessageFriendRequestStatus.NONE -> return
LokiMessageFriendRequestStatus.REQUEST_SENDING, LokiMessageFriendRequestStatus.REQUEST_FAILED -> null
LokiMessageFriendRequestStatus.REQUEST_PENDING, LokiMessageFriendRequestStatus.REQUEST_REJECTED -> R.string.view_friend_request_outgoing_pending_message
LokiMessageFriendRequestStatus.REQUEST_ACCEPTED -> R.string.view_friend_request_outgoing_accepted_message
LokiMessageFriendRequestStatus.REQUEST_EXPIRED -> R.string.view_friend_request_outgoing_expired_message
}
if (formatID != null) {
label.text = resources.getString(formatID, contactDisplayName)
}
label.visibility = if (formatID != null) View.VISIBLE else View.GONE
topSpacer.visibility = label.visibility
}
}
// endregion
// region Interaction
private fun accept() {
val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
lokiMessageDatabase.setFriendRequestStatus(message!!.id, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED)
updateUI()
delegate?.acceptFriendRequest(message!!)
}
private fun reject() {
val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
lokiMessageDatabase.setFriendRequestStatus(message!!.id, LokiMessageFriendRequestStatus.REQUEST_REJECTED)
updateUI()
delegate?.rejectFriendRequest(message!!)
}
// endregion
}
// region Delegate
interface FriendRequestViewDelegate {
/**
* Implementations of this method should update the thread's friend request status
* and send a friend request accepted message.
*/
fun acceptFriendRequest(friendRequest: MessageRecord)
/**
* Implementations of this method should update the thread's friend request status
* and remove the pre keys associated with the contact.
*/
fun rejectFriendRequest(friendRequest: MessageRecord)
}
// endregion

View File

@ -9,7 +9,7 @@ import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_mention_candidate.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.mms.GlideRequests
import org.whispersystems.signalservice.loki.api.opengroups.LokiPublicChatAPI
import org.whispersystems.signalservice.loki.api.opengroups.PublicChatAPI
import org.whispersystems.signalservice.loki.protocol.mentions.Mention
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
@ -31,13 +31,13 @@ class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr:
private fun update() {
displayNameTextView.text = mentionCandidate.displayName
profilePictureView.hexEncodedPublicKey = mentionCandidate.hexEncodedPublicKey
profilePictureView.additionalHexEncodedPublicKey = null
profilePictureView.publicKey = mentionCandidate.publicKey
profilePictureView.additionalPublicKey = null
profilePictureView.isRSSFeed = false
profilePictureView.glide = glide!!
profilePictureView.update()
if (publicChatServer != null && publicChatChannel != null) {
val isUserModerator = LokiPublicChatAPI.isUserModerator(mentionCandidate.hexEncodedPublicKey, publicChatChannel!!, publicChatServer!!)
val isUserModerator = PublicChatAPI.isUserModerator(mentionCandidate.publicKey, publicChatChannel!!, publicChatServer!!)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
} else {
moderatorIconImageView.visibility = View.GONE

View File

@ -16,6 +16,7 @@ import android.os.Vibrator
import android.support.annotation.ColorRes
import android.support.annotation.DrawableRes
import android.util.AttributeSet
import android.view.Gravity
import android.view.MotionEvent
import android.widget.ImageView
import android.widget.RelativeLayout
@ -91,6 +92,7 @@ class NewConversationButtonSetView : RelativeLayout {
addView(imageView)
imageView.x = collapsedImageViewPosition.x
imageView.y = collapsedImageViewPosition.y
gravity = Gravity.TOP or Gravity.LEFT // Intentionally not Gravity.START
}
fun expand() {

View File

@ -21,8 +21,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
class ProfilePictureView : RelativeLayout {
lateinit var glide: GlideRequests
var hexEncodedPublicKey: String? = null
var additionalHexEncodedPublicKey: String? = null
var publicKey: String? = null
var additionalPublicKey: String? = null
var isRSSFeed = false
var isLarge = false
@ -52,11 +52,11 @@ class ProfilePictureView : RelativeLayout {
// region Updating
fun update() {
val hexEncodedPublicKey = hexEncodedPublicKey ?: return
val additionalHexEncodedPublicKey = additionalHexEncodedPublicKey
doubleModeImageViewContainer.visibility = if (additionalHexEncodedPublicKey != null && !isRSSFeed) View.VISIBLE else View.INVISIBLE
singleModeImageViewContainer.visibility = if (additionalHexEncodedPublicKey == null && !isRSSFeed && !isLarge) View.VISIBLE else View.INVISIBLE
largeSingleModeImageViewContainer.visibility = if (additionalHexEncodedPublicKey == null && !isRSSFeed && isLarge) View.VISIBLE else View.INVISIBLE
val publicKey = publicKey ?: return
val additionalPublicKey = additionalPublicKey
doubleModeImageViewContainer.visibility = if (additionalPublicKey != null && !isRSSFeed) View.VISIBLE else View.INVISIBLE
singleModeImageViewContainer.visibility = if (additionalPublicKey == null && !isRSSFeed && !isLarge) View.VISIBLE else View.INVISIBLE
largeSingleModeImageViewContainer.visibility = if (additionalPublicKey == null && !isRSSFeed && isLarge) View.VISIBLE else View.INVISIBLE
rssImageView.visibility = if (isRSSFeed) View.VISIBLE else View.INVISIBLE
fun setProfilePictureIfNeeded(imageView: ImageView, hexEncodedPublicKey: String, @DimenRes sizeID: Int) {
glide.clear(imageView)
@ -76,10 +76,10 @@ class ProfilePictureView : RelativeLayout {
imageView.setImageDrawable(null)
}
}
setProfilePictureIfNeeded(doubleModeImageView1, hexEncodedPublicKey, R.dimen.small_profile_picture_size)
setProfilePictureIfNeeded(doubleModeImageView2, additionalHexEncodedPublicKey ?: "", R.dimen.small_profile_picture_size)
setProfilePictureIfNeeded(singleModeImageView, hexEncodedPublicKey, R.dimen.medium_profile_picture_size)
setProfilePictureIfNeeded(largeSingleModeImageView, hexEncodedPublicKey, R.dimen.large_profile_picture_size)
setProfilePictureIfNeeded(doubleModeImageView1, publicKey, R.dimen.small_profile_picture_size)
setProfilePictureIfNeeded(doubleModeImageView2, additionalPublicKey ?: "", R.dimen.small_profile_picture_size)
setProfilePictureIfNeeded(singleModeImageView, publicKey, R.dimen.medium_profile_picture_size)
setProfilePictureIfNeeded(largeSingleModeImageView, publicKey, R.dimen.large_profile_picture_size)
}
// endregion
}

View File

@ -48,20 +48,20 @@ class UserView : LinearLayout {
val address = user.address.serialize()
if (user.isGroupRecipient) {
if ("Session Public Chat" == user.name || user.address.isRSSFeed) {
profilePictureView.hexEncodedPublicKey = ""
profilePictureView.additionalHexEncodedPublicKey = null
profilePictureView.publicKey = ""
profilePictureView.additionalPublicKey = null
profilePictureView.isRSSFeed = true
} else {
val threadID = GroupManager.getThreadIDFromGroupID(address, context)
val users = MentionsManager.shared.userPublicKeyCache[threadID]?.toList() ?: listOf()
val randomUsers = users.sorted() // Sort to provide a level of stability
profilePictureView.hexEncodedPublicKey = randomUsers.getOrNull(0) ?: ""
profilePictureView.additionalHexEncodedPublicKey = randomUsers.getOrNull(1) ?: ""
profilePictureView.publicKey = randomUsers.getOrNull(0) ?: ""
profilePictureView.additionalPublicKey = randomUsers.getOrNull(1) ?: ""
profilePictureView.isRSSFeed = false
}
} else {
profilePictureView.hexEncodedPublicKey = address
profilePictureView.additionalHexEncodedPublicKey = null
profilePictureView.publicKey = address
profilePictureView.additionalPublicKey = null
profilePictureView.isRSSFeed = false
}
profilePictureView.glide = glide

View File

@ -23,7 +23,6 @@ public class OutgoingMediaMessage {
private final int distributionType;
private final int subscriptionId;
private final long expiresIn;
public boolean isFriendRequest = false;
private final QuoteModel outgoingQuote;
private final List<NetworkFailure> networkFailures = new LinkedList<>();

View File

@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.loki.api.fileserver.LokiFileServerAPI;
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI;
public class PushMediaConstraints extends MediaConstraints {
@ -22,26 +22,26 @@ public class PushMediaConstraints extends MediaConstraints {
@Override
public int getImageMaxSize(Context context) {
return LokiFileServerAPI.Companion.getMaxFileSize();
return FileServerAPI.Companion.getMaxFileSize();
}
@Override
public int getGifMaxSize(Context context) {
return LokiFileServerAPI.Companion.getMaxFileSize();
return FileServerAPI.Companion.getMaxFileSize();
}
@Override
public int getVideoMaxSize(Context context) {
return LokiFileServerAPI.Companion.getMaxFileSize();
return FileServerAPI.Companion.getMaxFileSize();
}
@Override
public int getAudioMaxSize(Context context) {
return LokiFileServerAPI.Companion.getMaxFileSize();
return FileServerAPI.Companion.getMaxFileSize();
}
@Override
public int getDocumentMaxSize(Context context) {
return LokiFileServerAPI.Companion.getMaxFileSize();
return FileServerAPI.Companion.getMaxFileSize();
}
}

View File

@ -325,7 +325,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
ReplyMethod replyMethod = ReplyMethod.forRecipient(context, recipient);
boolean canReply = SessionMetaProtocol.canUserReplyToNotification(recipient, context);
boolean canReply = SessionMetaProtocol.canUserReplyToNotification(recipient);
PendingIntent quickReplyIntent = canReply ? notificationState.getQuickReplyIntent(context, recipient) : null;
PendingIntent remoteReplyIntent = canReply ? notificationState.getRemoteReplyIntent(context, recipient, replyMethod) : null;

View File

@ -93,7 +93,7 @@ public class MarkReadReceiver extends BroadcastReceiver {
for (Address address : addressMap.keySet()) {
List<Long> timestamps = Stream.of(addressMap.get(address)).map(SyncMessageId::getTimetamp).toList();
// Loki - Check whether we want to send a read receipt to this user
if (!SessionMetaProtocol.shouldSendReadReceipt(address, context)) { continue; }
if (!SessionMetaProtocol.shouldSendReadReceipt(address)) { continue; }
// Loki - Take into account multi device
Set<String> linkedDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(address.serialize());
for (String device : linkedDevices) {

View File

@ -7,7 +7,7 @@ import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.loki.api.LokiPublicChatManager;
import org.thoughtcrime.securesms.loki.api.PublicChatManager;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Debouncer;
import org.whispersystems.signalservice.loki.api.Poller;
@ -40,15 +40,15 @@ public class OptimizedMessageNotifier implements MessageNotifier {
@Override
public void updateNotification(@NonNull Context context) {
Poller lokiPoller = ApplicationContext.getInstance(context).lokiPoller;
LokiPublicChatManager lokiPublicChatManager = ApplicationContext.getInstance(context).lokiPublicChatManager;
Poller lokiPoller = ApplicationContext.getInstance(context).poller;
PublicChatManager publicChatManager = ApplicationContext.getInstance(context).publicChatManager;
boolean isCaughtUp = true;
if (lokiPoller != null) {
isCaughtUp = isCaughtUp && lokiPoller.isCaughtUp();
}
if (lokiPublicChatManager != null) {
isCaughtUp = isCaughtUp && lokiPublicChatManager.areAllCaughtUp();
if (publicChatManager != null) {
isCaughtUp = isCaughtUp && publicChatManager.areAllCaughtUp();
}
if (isCaughtUp) {
@ -60,15 +60,15 @@ public class OptimizedMessageNotifier implements MessageNotifier {
@Override
public void updateNotification(@NonNull Context context, long threadId) {
Poller lokiPoller = ApplicationContext.getInstance(context).lokiPoller;
LokiPublicChatManager lokiPublicChatManager = ApplicationContext.getInstance(context).lokiPublicChatManager;
Poller lokiPoller = ApplicationContext.getInstance(context).poller;
PublicChatManager publicChatManager = ApplicationContext.getInstance(context).publicChatManager;
boolean isCaughtUp = true;
if (lokiPoller != null) {
isCaughtUp = isCaughtUp && lokiPoller.isCaughtUp();
}
if (lokiPublicChatManager != null) {
isCaughtUp = isCaughtUp && lokiPublicChatManager.areAllCaughtUp();
if (publicChatManager != null) {
isCaughtUp = isCaughtUp && publicChatManager.areAllCaughtUp();
}
if (isCaughtUp) {
@ -80,15 +80,15 @@ public class OptimizedMessageNotifier implements MessageNotifier {
@Override
public void updateNotification(@NonNull Context context, long threadId, boolean signal) {
Poller lokiPoller = ApplicationContext.getInstance(context).lokiPoller;
LokiPublicChatManager lokiPublicChatManager = ApplicationContext.getInstance(context).lokiPublicChatManager;
Poller lokiPoller = ApplicationContext.getInstance(context).poller;
PublicChatManager publicChatManager = ApplicationContext.getInstance(context).publicChatManager;
boolean isCaughtUp = true;
if (lokiPoller != null) {
isCaughtUp = isCaughtUp && lokiPoller.isCaughtUp();
}
if (lokiPublicChatManager != null) {
isCaughtUp = isCaughtUp && lokiPublicChatManager.areAllCaughtUp();
if (publicChatManager != null) {
isCaughtUp = isCaughtUp && publicChatManager.areAllCaughtUp();
}
if (isCaughtUp) {
@ -100,15 +100,15 @@ public class OptimizedMessageNotifier implements MessageNotifier {
@Override
public void updateNotification(@android.support.annotation.NonNull Context context, boolean signal, int reminderCount) {
Poller lokiPoller = ApplicationContext.getInstance(context).lokiPoller;
LokiPublicChatManager lokiPublicChatManager = ApplicationContext.getInstance(context).lokiPublicChatManager;
Poller lokiPoller = ApplicationContext.getInstance(context).poller;
PublicChatManager publicChatManager = ApplicationContext.getInstance(context).publicChatManager;
boolean isCaughtUp = true;
if (lokiPoller != null) {
isCaughtUp = isCaughtUp && lokiPoller.isCaughtUp();
}
if (lokiPublicChatManager != null) {
isCaughtUp = isCaughtUp && lokiPublicChatManager.areAllCaughtUp();
if (publicChatManager != null) {
isCaughtUp = isCaughtUp && publicChatManager.areAllCaughtUp();
}
if (isCaughtUp) {

View File

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.push;
import android.content.Context;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.loki.protocol.FriendRequestProtocol;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@ -18,19 +17,4 @@ public class MessageSenderEventListener implements SignalServiceMessageSender.Ev
public void onSecurityEvent(SignalServiceAddress textSecureAddress) {
SecurityEvent.broadcastSecurityUpdateEvent(context);
}
@Override
public void onFriendRequestSending(long messageID, long threadID) {
FriendRequestProtocol.setFriendRequestStatusToSendingIfNeeded(context, messageID, threadID);
}
@Override
public void onFriendRequestSent(long messageID, long threadID) {
FriendRequestProtocol.setFriendRequestStatusToSentIfNeeded(context, messageID, threadID);
}
@Override
public void onFriendRequestSendingFailed(long messageID, long threadID) {
FriendRequestProtocol.setFriendRequestStatusToFailedIfNeeded(context, messageID, threadID);
}
}

View File

@ -38,7 +38,6 @@ import org.thoughtcrime.securesms.jobs.MmsSendJob;
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.protocol.FriendRequestProtocol;
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
@ -76,12 +75,7 @@ public class MessageSender {
long messageId = database.insertMessageOutbox(allocatedThreadId, message, forceSms, System.currentTimeMillis(), insertListener);
// Loki - Set the message's friend request status as soon as it hits the database
if (FriendRequestProtocol.shouldUpdateFriendRequestStatusFromOutgoingTextMessage(context, message)) {
FriendRequestProtocol.setFriendRequestStatusToSendingIfNeeded(context, messageId, allocatedThreadId);
}
sendTextMessage(context, recipient, forceSms, keyExchange, messageId, message.isEndSession());
sendTextMessage(context, recipient, forceSms, keyExchange, messageId);
return allocatedThreadId;
}
@ -107,11 +101,6 @@ public class MessageSender {
Recipient recipient = message.getRecipient();
long messageId = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
// Loki - Set the message's friend request status as soon as it hits the database
if (FriendRequestProtocol.shouldUpdateFriendRequestStatusFromOutgoingMediaMessage(context, message)) {
FriendRequestProtocol.setFriendRequestStatusToSendingIfNeeded(context, messageId, allocatedThreadId);
}
sendMediaMessage(context, recipient, forceSms, messageId, message.getExpiresIn());
return allocatedThreadId;
} catch (MmsException e) {
@ -135,7 +124,7 @@ public class MessageSender {
if (messageRecord.isMms()) {
sendMediaMessage(context, recipient, forceSms, messageId, expiresIn);
} else {
sendTextMessage(context, recipient, forceSms, keyExchange, messageId, messageRecord.isEndSession());
sendTextMessage(context, recipient, forceSms, keyExchange, messageId);
}
}
@ -152,17 +141,17 @@ public class MessageSender {
private static void sendTextMessage(Context context, Recipient recipient,
boolean forceSms, boolean keyExchange,
long messageId, boolean isEndSession)
long messageId)
{
if (isLocalSelfSend(context, recipient, forceSms)) {
sendLocalTextSelf(context, messageId);
} else {
sendTextPush(context, recipient, messageId, isEndSession);
sendTextPush(context, recipient, messageId);
}
}
private static void sendTextPush(Context context, Recipient recipient, long messageId, boolean isEndSession) {
MultiDeviceProtocol.sendTextPush(context, recipient, messageId, isEndSession);
private static void sendTextPush(Context context, Recipient recipient, long messageId) {
MultiDeviceProtocol.sendTextPush(context, recipient, messageId);
}
private static void sendMediaPush(Context context, Recipient recipient, long messageId) {

View File

@ -9,7 +9,6 @@ public class OutgoingTextMessage {
private final String message;
private final int subscriptionId;
private final long expiresIn;
public boolean isFriendRequest = false;
public OutgoingTextMessage(Recipient recipient, String message, int subscriptionId) {
this(recipient, message, 0, subscriptionId);