mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-24 00:37:47 +00:00
Merge branch 'dev' into editgroup
This commit is contained in:
commit
f7923cd8a4
@ -12,27 +12,15 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<EditText
|
||||
style="@style/SmallSessionEditText"
|
||||
style="@style/SessionEditText"
|
||||
android:id="@+id/nameEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:hint="@string/activity_create_closed_group_edit_text_hint" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:layout_marginBottom="@dimen/medium_spacing"
|
||||
android:textSize="@dimen/small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:alpha="0.6"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/activity_create_closed_group_explanation" />
|
||||
android:hint="@string/activity_create_closed_group_edit_text_hint" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
|
@ -1274,50 +1274,50 @@
|
||||
<!-- Session -->
|
||||
<string name="continue_2">继续</string>
|
||||
<string name="copy">复制</string>
|
||||
<string name="invalid_url">无效的网址</string>
|
||||
<string name="invalid_url">无效的链接</string>
|
||||
<string name="copied_to_clipboard">复制到剪贴板</string>
|
||||
<string name="device_linking_failed">无法链接设备。</string>
|
||||
<string name="next">下一步</string>
|
||||
<string name="share">分享</string>
|
||||
<string name="share">共享</string>
|
||||
<string name="invalid_session_id">无效的Session ID</string>
|
||||
<string name="cancel">取消</string>
|
||||
<string name="your_session_id">您的Session ID</string>
|
||||
|
||||
<string name="activity_landing_title_2">您的Session从这里开始...</string>
|
||||
<string name="activity_landing_register_button_title">注册Session ID</string>
|
||||
<string name="activity_landing_register_button_title">创建Session ID</string>
|
||||
<string name="activity_landing_restore_button_title">继续使用您的Session ID</string>
|
||||
<string name="activity_landing_link_button_title">链接到现有帐号</string>
|
||||
<string name="activity_landing_device_unlinked_dialog_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">什么是Session?</string>
|
||||
<string name="view_fake_chat_bubble_2">Session是一个去中心化的加密消息应用。</string>
|
||||
<string name="view_fake_chat_bubble_3">所以Session不会收集我的个人信息或对话原始数据?怎么做到的?。</string>
|
||||
<string name="view_fake_chat_bubble_3">所以Session不会收集我的个人信息或者对话元数据?怎么做到的?</string>
|
||||
<string name="view_fake_chat_bubble_4">通过结合高效的匿名路由和端到端的加密技术。</string>
|
||||
<string name="view_fake_chat_bubble_5">好朋友就要与朋友使用能够保证信息安全的聊天工具,不用谢啦</string>
|
||||
<string name="view_fake_chat_bubble_5">好朋友之间就要使用能够保证信息安全的聊天工具,不用谢啦!</string>
|
||||
|
||||
<string name="activity_register_title">向您的新Session ID打个招呼吧</string>
|
||||
<string name="activity_register_explanation">Session ID是其他用户需要与您聊天时使用的独一无二的地址。与您的真实身份无关,Session ID的设计是完全是匿名和私有的。</string>
|
||||
<string name="activity_register_title">向您的Session ID打个招呼吧</string>
|
||||
<string name="activity_register_explanation">您的Session ID是其他用户在与您聊天时使用的独一无二的地址。Session ID与您的真实身份无关,它在设计上完全是匿名且私密的。</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_title">关联设备</string>
|
||||
<string name="activity_link_device_enter_session_id_tab_title">输入Session ID</string>
|
||||
<string name="activity_link_device_scan_qr_code_tab_title">扫描二维码</string>
|
||||
<string name="activity_link_device_scan_qr_code_explanation">在您的设备上导航到“设置”>“设备”>“链接设备”,然后扫描出现的二维码以开始链接过程。</string>
|
||||
<string name="activity_link_device_scan_qr_code_explanation">在您的设备上导航到“设置”>“设备”>“链接设备”,然后扫描出现的二维码以开始关联。</string>
|
||||
|
||||
<string name="fragment_enter_session_id_title">链接您的设备</string>
|
||||
<string name="fragment_enter_session_id_explanation">在您的另一个设备上导航到“设置”>“设备” >“链接设备”,然后在此处输入Session ID以开始链接过程。</string>
|
||||
<string name="fragment_enter_session_id_title">关联您的设备</string>
|
||||
<string name="fragment_enter_session_id_explanation">在您的另一个设备上导航到“设置”>“设备” >“链接设备”,然后在此处输入Session ID以开始关联。</string>
|
||||
<string name="fragment_enter_session_id_edit_text_hint">输入Session ID</string>
|
||||
|
||||
<string name="activity_display_name_title_2">选择您的显示名称</string>
|
||||
<string name="activity_display_name_explanation">使用Session时,这就是您的名字。它可以是您的真实姓名,别名或您喜欢的其他任何名称。</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_display_name_title_2">选择您想显示的名称</string>
|
||||
<string name="activity_display_name_explanation">这就是您在使用Session时的名字。它可以是您的真实姓名,别名或您喜欢的其他任何名称。</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">请设定一个仅包含 a-z,A-Z,0-9 和 _ 的名称</string>
|
||||
<string name="activity_display_name_display_name_too_long_error">请设定一个较短的名称</string>
|
||||
|
||||
<string name="activity_pn_mode_recommended_option_tag">推荐的选项</string>
|
||||
<string name="activity_pn_mode_no_option_picked_dialog_title">请选择一个选项</string>
|
||||
@ -1331,15 +1331,15 @@
|
||||
|
||||
<string name="activity_seed_title">您的恢复口令</string>
|
||||
<string name="activity_seed_title_2">这里是您的恢复口令</string>
|
||||
<string name="activity_seed_explanation">您的恢复口令是Session ID的主密钥 - 如果您无法访问您的现有设备,则可以使用它在其他设备上恢复Session ID。将您的恢复口令存储在安全的地方,不要将其提供给任何人。</string>
|
||||
<string name="activity_seed_explanation">您的恢复口令是Session ID的主密钥 - 如果您无法访问您的现有设备,则可以使用它在其他设备上恢复您的Session ID。请将您的恢复口令存储在安全的地方,不要将其提供给任何人。</string>
|
||||
<string name="activity_seed_reveal_button_title">长按显示内容</string>
|
||||
|
||||
<string name="view_seed_reminder_subtitle_1">保存恢复短语以保护您的帐号安全</string>
|
||||
<string name="view_seed_reminder_subtitle_2">点击并按住遮盖住的单词以显示您的恢复短语,然后安全地存储它以保护Session ID。</string>
|
||||
<string name="view_seed_reminder_subtitle_3">确保将恢复短语存储在安全的地方</string>
|
||||
<string name="view_seed_reminder_subtitle_1">保存恢复口令以保护您的帐号安全</string>
|
||||
<string name="view_seed_reminder_subtitle_2">点击并按住遮盖住的单词以显示您的恢复口令,请将它安全地存储以保护您的Session ID。</string>
|
||||
<string name="view_seed_reminder_subtitle_3">请确保将恢复口令存储在安全的地方</string>
|
||||
|
||||
<string name="activity_path_title">路径</string>
|
||||
<string name="activity_path_explanation">Session会通过Session的分散网络中的多个服务节点跳转消息以隐藏IP。以下是国家您目前的消息连接跳转服务节点所在地:</string>
|
||||
<string name="activity_path_explanation">Session会通过其去中心化网络中的多个服务节点跳转消息以隐藏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>
|
||||
@ -1349,38 +1349,38 @@
|
||||
<string name="activity_create_private_chat_title">新建私人聊天</string>
|
||||
<string name="activity_create_private_chat_enter_session_id_tab_title">输入Session ID</string>
|
||||
<string name="activity_create_private_chat_scan_qr_code_tab_title">扫描二维码</string>
|
||||
<string name="activity_create_private_chat_scan_qr_code_explanation">扫描另一用户的二维码以开始使用Session。您可以在帐号设置中点击二维码图标找到二维码。</string>
|
||||
<string name="activity_create_private_chat_scan_qr_code_explanation">扫描其他用户的二维码来发起对话。您可以在帐号设置中点击二维码图标找到二维码。</string>
|
||||
|
||||
<string name="fragment_enter_public_key_edit_text_hint">输入对方的Session ID</string>
|
||||
<string name="fragment_enter_public_key_explanation">用户可以通过进入帐号设置并点击共享Session ID来分享自己的Session ID,或通过共享其二维码来分享其Session ID。</string>
|
||||
<string name="fragment_enter_public_key_explanation">用户可以通过进入帐号设置并点击“共享Session ID”来分享自己的Session ID,或通过共享其二维码来分享其Session ID。</string>
|
||||
|
||||
<string name="fragment_scan_qr_code_camera_access_explanation">Session需要摄像头访问权限才能扫描二维码</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_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_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">您群组中的一位成员的Session ID无效</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_enter_group_url_tab_title">公开群组链接</string>
|
||||
<string name="activity_join_public_chat_scan_qr_code_tab_title">扫描二维码</string>
|
||||
<string name="activity_join_public_chat_scan_qr_code_explanation">扫描您想加入的公开群组的二维码</string>
|
||||
|
||||
<string name="fragment_enter_chat_url_edit_text_hint">输入一个公开群组网址</string>
|
||||
<string name="fragment_enter_chat_url_edit_text_hint">输入公开群组链接</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_display_name_edit_text_hint">输入您想显示的名称</string>
|
||||
<string name="activity_settings_display_name_missing_error">请设定一个名称</string>
|
||||
<string name="activity_settings_invalid_display_name_error">请设定一个仅包含 a-z,A-Z,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>
|
||||
@ -1389,44 +1389,44 @@
|
||||
<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_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_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="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="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_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_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">在其他设备上下载Session,然后点击登陆页面屏幕底部的“链接到现有帐号”。如果您的其他设备上已有一个帐号,则必须先删除已有帐号。</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">在其他设备上下载Session,然后点击登陆页面屏幕底部的“关联到现有帐号”。如果您的其他设备上已有一个帐号,则必须先删除已有帐号。</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_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="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_edit_device_name_edit_text_hint">输入名称</string>
|
||||
|
||||
<string name="dialog_seed_title">您的恢复口令</string>
|
||||
<string name="dialog_seed_explanation">这是您的恢复口令。有了它,您可以将Session ID还原或迁移到新设备上。</string>
|
||||
<string name="dialog_seed_explanation">这是您的恢复口令。您可以通过该口令将Session ID还原或迁移到新设备上。</string>
|
||||
|
||||
<string name="dialog_clear_all_data_title">清除所有数据</string>
|
||||
<string name="dialog_clear_all_data_explanation">这将永久删除您的消息、对话和联系人。</string>
|
||||
@ -1434,13 +1434,13 @@
|
||||
<string name="activity_qr_code_title">二维码</string>
|
||||
<string name="activity_qr_code_view_my_qr_code_tab_title">查看我的二维码</string>
|
||||
<string name="activity_qr_code_view_scan_qr_code_tab_title">扫描二维码</string>
|
||||
<string name="activity_qr_code_view_scan_qr_code_explanation">扫描对方的二维码,与他们开始对话</string>
|
||||
<string name="activity_qr_code_view_scan_qr_code_explanation">扫描对方的二维码以发起对话</string>
|
||||
|
||||
<string name="fragment_view_my_qr_code_explanation">这是您的二维码。其他用户可以对其进行扫描以开始对话。</string>
|
||||
<string name="fragment_view_my_qr_code_explanation">这是您的二维码。其他用户可以对其进行扫描以发起与您的对话。</string>
|
||||
<string name="fragment_view_my_qr_code_share_title">分享二维码</string>
|
||||
|
||||
<string name="session_reset_banner_message">您要恢复与%s的对话吗?</string>
|
||||
<string name="session_reset_banner_dismiss_button_title">解散</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>
|
||||
|
@ -61,12 +61,15 @@ 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.ClosedGroupPoller;
|
||||
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.PushSessionRequestMessageSendJob;
|
||||
import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation;
|
||||
import org.thoughtcrime.securesms.loki.utilities.Broadcaster;
|
||||
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
|
||||
@ -106,6 +109,8 @@ 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.closedgroups.SharedSenderKeysImplementation;
|
||||
import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementationDelegate;
|
||||
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager;
|
||||
import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol;
|
||||
import org.whispersystems.signalservice.loki.protocol.meta.TTLUtilities;
|
||||
@ -138,7 +143,8 @@ import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public class ApplicationContext extends MultiDexApplication implements DependencyInjector, DefaultLifecycleObserver, LokiP2PAPIDelegate, SessionManagementProtocolDelegate {
|
||||
public class ApplicationContext extends MultiDexApplication implements DependencyInjector, DefaultLifecycleObserver, LokiP2PAPIDelegate,
|
||||
SessionManagementProtocolDelegate, SharedSenderKeysImplementationDelegate {
|
||||
|
||||
private static final String TAG = ApplicationContext.class.getSimpleName();
|
||||
private final static int OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
@ -154,6 +160,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
// Loki
|
||||
public MessageNotifier messageNotifier = null;
|
||||
public Poller poller = null;
|
||||
public ClosedGroupPoller closedGroupPoller = null;
|
||||
public PublicChatManager publicChatManager = null;
|
||||
private PublicChatAPI publicChatAPI = null;
|
||||
public Broadcaster broadcaster = null;
|
||||
@ -183,8 +190,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this);
|
||||
LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(this);
|
||||
LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this);
|
||||
SharedSenderKeysDatabase sskDatabase = DatabaseFactory.getSSKDatabase(this);
|
||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
||||
SessionResetImplementation sessionResetImpl = new SessionResetImplementation(this);
|
||||
SharedSenderKeysImplementation.Companion.configureIfNeeded(sskDatabase, this);
|
||||
if (userPublicKey != null) {
|
||||
SwarmAPI.Companion.configureIfNeeded(apiDB);
|
||||
SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster);
|
||||
@ -193,7 +202,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
SyncMessagesProtocol.Companion.configureIfNeeded(apiDB, userPublicKey);
|
||||
}
|
||||
MultiDeviceProtocol.Companion.configureIfNeeded(apiDB);
|
||||
SessionManagementProtocol.Companion.configureIfNeeded(sessionResetImpl, threadDB, this);
|
||||
SessionManagementProtocol.Companion.configureIfNeeded(sessionResetImpl, sskDatabase, this);
|
||||
setUpP2PAPIIfNeeded();
|
||||
PushNotificationAcknowledgement.Companion.configureIfNeeded(BuildConfig.DEBUG);
|
||||
if (setUpStorageAPIIfNeeded()) {
|
||||
@ -507,16 +516,20 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
SharedSenderKeysDatabase sskDatabase = DatabaseFactory.getSSKDatabase(this);
|
||||
ClosedGroupPoller.Companion.configureIfNeeded(this, sskDatabase);
|
||||
closedGroupPoller = ClosedGroupPoller.Companion.getShared();
|
||||
}
|
||||
|
||||
public void startPollingIfNeeded() {
|
||||
setUpPollingIfNeeded();
|
||||
if (poller != null) { poller.startIfNeeded(); }
|
||||
if (closedGroupPoller != null) { closedGroupPoller.startIfNeeded(); }
|
||||
}
|
||||
|
||||
public void stopPolling() {
|
||||
if (poller == null) { return; }
|
||||
poller.stopIfNeeded();
|
||||
if (poller != null) { poller.stopIfNeeded(); }
|
||||
if (closedGroupPoller != null) { closedGroupPoller.stopIfNeeded(); }
|
||||
}
|
||||
|
||||
private void resubmitProfilePictureIfNeeded() {
|
||||
@ -621,8 +634,13 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
// Send the session request
|
||||
long timestamp = new Date().getTime();
|
||||
apiDB.setSessionRequestSentTimestamp(publicKey, timestamp);
|
||||
PushSessionRequestMessageSendJob job = new PushSessionRequestMessageSendJob(publicKey, timestamp);
|
||||
SessionRequestMessageSendJob job = new SessionRequestMessageSendJob(publicKey, timestamp);
|
||||
jobManager.add(job);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestSenderKey(@NotNull String groupPublicKey, @NotNull String senderPublicKey) {
|
||||
ClosedGroupsProtocol.requestSenderKey(this, groupPublicKey, senderPublicKey);
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
@ -211,6 +211,7 @@ import org.thoughtcrime.securesms.util.Dialogs;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
@ -229,6 +230,7 @@ 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.shelved.multidevice.MultiDeviceProtocol;
|
||||
import org.whispersystems.signalservice.loki.utilities.HexEncodingKt;
|
||||
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -1167,10 +1169,26 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
builder.setCancelable(true);
|
||||
builder.setMessage(getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group));
|
||||
builder.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
Recipient groupRecipient = getRecipient();
|
||||
if (ClosedGroupsProtocol.leaveGroup(this, groupRecipient)) {
|
||||
initializeEnabledCheck();
|
||||
} else {
|
||||
Recipient groupRecipient = getRecipient();
|
||||
String groupPublicKey;
|
||||
boolean isSSKBasedClosedGroup;
|
||||
try {
|
||||
groupPublicKey = HexEncodingKt.toHexString(GroupUtil.getDecodedId(groupRecipient.getAddress().toString()));
|
||||
isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey);
|
||||
} catch (IOException e) {
|
||||
groupPublicKey = null;
|
||||
isSSKBasedClosedGroup = false;
|
||||
}
|
||||
try {
|
||||
if (isSSKBasedClosedGroup) {
|
||||
ClosedGroupsProtocol.leave(this, groupPublicKey);
|
||||
initializeEnabledCheck();
|
||||
} else if (ClosedGroupsProtocol.leaveLegacyGroup(this, groupRecipient)) {
|
||||
initializeEnabledCheck();
|
||||
} else {
|
||||
Toast.makeText(this, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(this, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
@ -2229,13 +2247,20 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void markThreadAsRead() {
|
||||
Recipient recipient = this.recipient;
|
||||
new AsyncTask<Long, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Long... params) {
|
||||
Context context = ConversationActivity.this;
|
||||
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(params[0], false);
|
||||
|
||||
MarkReadReceiver.process(context, messageIds);
|
||||
if (!org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol.shouldSendReadReceipt(recipient.getAddress())) {
|
||||
for (MarkedMessageInfo messageInfo : messageIds) {
|
||||
MarkReadReceiver.scheduleDeletion(context, messageInfo.getExpirationInfo());
|
||||
}
|
||||
} else {
|
||||
MarkReadReceiver.process(context, messageIds);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -2389,10 +2414,6 @@ 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) {
|
||||
@ -2400,7 +2421,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
|
||||
}
|
||||
|
||||
return MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
|
||||
long result = MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
|
||||
|
||||
if (!recipient.isGroupRecipient()) {
|
||||
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(recipient.getAddress().serialize());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -2436,10 +2463,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
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) {
|
||||
@ -2447,7 +2470,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
|
||||
}
|
||||
|
||||
return MessageSender.send(context, messages[0], threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
|
||||
long result = MessageSender.send(context, messages[0], threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
|
||||
|
||||
if (!recipient.isGroupRecipient()) {
|
||||
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(recipient.getAddress().serialize());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.loki.database.LokiPreKeyBundleDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiPreKeyRecordDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class DatabaseFactory {
|
||||
@ -73,6 +74,7 @@ public class DatabaseFactory {
|
||||
private final LokiMessageDatabase lokiMessageDatabase;
|
||||
private final LokiThreadDatabase lokiThreadDatabase;
|
||||
private final LokiUserDatabase lokiUserDatabase;
|
||||
private final SharedSenderKeysDatabase sskDatabase;
|
||||
|
||||
public static DatabaseFactory getInstance(Context context) {
|
||||
synchronized (lock) {
|
||||
@ -187,6 +189,10 @@ public class DatabaseFactory {
|
||||
public static LokiUserDatabase getLokiUserDatabase(Context context) {
|
||||
return getInstance(context).lokiUserDatabase;
|
||||
}
|
||||
|
||||
public static SharedSenderKeysDatabase getSSKDatabase(Context context) {
|
||||
return getInstance(context).sskDatabase;
|
||||
}
|
||||
// endregion
|
||||
|
||||
public static void upgradeRestored(Context context, SQLiteDatabase database){
|
||||
@ -200,32 +206,33 @@ public class DatabaseFactory {
|
||||
DatabaseSecret databaseSecret = new DatabaseSecretProvider(context).getOrCreateDatabaseSecret();
|
||||
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
|
||||
|
||||
this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret);
|
||||
this.sms = new SmsDatabase(context, databaseHelper);
|
||||
this.mms = new MmsDatabase(context, databaseHelper);
|
||||
this.attachments = new AttachmentDatabase(context, databaseHelper, attachmentSecret);
|
||||
this.media = new MediaDatabase(context, databaseHelper);
|
||||
this.thread = new ThreadDatabase(context, databaseHelper);
|
||||
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
|
||||
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
|
||||
this.draftDatabase = new DraftDatabase(context, databaseHelper);
|
||||
this.pushDatabase = new PushDatabase(context, databaseHelper);
|
||||
this.groupDatabase = new GroupDatabase(context, databaseHelper);
|
||||
this.recipientDatabase = new RecipientDatabase(context, databaseHelper);
|
||||
this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper);
|
||||
this.contactsDatabase = new ContactsDatabase(context);
|
||||
this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper);
|
||||
this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper);
|
||||
this.sessionDatabase = new SessionDatabase(context, databaseHelper);
|
||||
this.searchDatabase = new SearchDatabase(context, databaseHelper);
|
||||
this.jobDatabase = new JobDatabase(context, databaseHelper);
|
||||
this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret);
|
||||
this.lokiAPIDatabase = new LokiAPIDatabase(context, databaseHelper);
|
||||
this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret);
|
||||
this.sms = new SmsDatabase(context, databaseHelper);
|
||||
this.mms = new MmsDatabase(context, databaseHelper);
|
||||
this.attachments = new AttachmentDatabase(context, databaseHelper, attachmentSecret);
|
||||
this.media = new MediaDatabase(context, databaseHelper);
|
||||
this.thread = new ThreadDatabase(context, databaseHelper);
|
||||
this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper);
|
||||
this.identityDatabase = new IdentityDatabase(context, databaseHelper);
|
||||
this.draftDatabase = new DraftDatabase(context, databaseHelper);
|
||||
this.pushDatabase = new PushDatabase(context, databaseHelper);
|
||||
this.groupDatabase = new GroupDatabase(context, databaseHelper);
|
||||
this.recipientDatabase = new RecipientDatabase(context, databaseHelper);
|
||||
this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper);
|
||||
this.contactsDatabase = new ContactsDatabase(context);
|
||||
this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper);
|
||||
this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper);
|
||||
this.sessionDatabase = new SessionDatabase(context, databaseHelper);
|
||||
this.searchDatabase = new SearchDatabase(context, databaseHelper);
|
||||
this.jobDatabase = new JobDatabase(context, databaseHelper);
|
||||
this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret);
|
||||
this.lokiAPIDatabase = new LokiAPIDatabase(context, databaseHelper);
|
||||
this.lokiContactPreKeyDatabase = new LokiPreKeyRecordDatabase(context, databaseHelper);
|
||||
this.lokiPreKeyBundleDatabase = new LokiPreKeyBundleDatabase(context, databaseHelper);
|
||||
this.lokiMessageDatabase = new LokiMessageDatabase(context, databaseHelper);
|
||||
this.lokiThreadDatabase = new LokiThreadDatabase(context, databaseHelper);
|
||||
this.lokiUserDatabase = new LokiUserDatabase(context, databaseHelper);
|
||||
this.lokiPreKeyBundleDatabase = new LokiPreKeyBundleDatabase(context, databaseHelper);
|
||||
this.lokiMessageDatabase = new LokiMessageDatabase(context, databaseHelper);
|
||||
this.lokiThreadDatabase = new LokiThreadDatabase(context, databaseHelper);
|
||||
this.lokiUserDatabase = new LokiUserDatabase(context, databaseHelper);
|
||||
this.sskDatabase = new SharedSenderKeysDatabase(context, databaseHelper);
|
||||
}
|
||||
|
||||
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,
|
||||
|
@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.loki.database.LokiPreKeyBundleDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiPreKeyRecordDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
|
||||
import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
@ -85,8 +86,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int lokiV9 = 30;
|
||||
private static final int lokiV10 = 31;
|
||||
private static final int lokiV11 = 32;
|
||||
private static final int lokiV12 = 33;
|
||||
|
||||
private static final int DATABASE_VERSION = lokiV11; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||
private static final int DATABASE_VERSION = lokiV12; // 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;
|
||||
@ -134,20 +136,20 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
}
|
||||
db.execSQL(StickerDatabase.CREATE_TABLE);
|
||||
|
||||
db.execSQL(LokiAPIDatabase.getCreateSnodePoolCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateSwarmCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateReceivedMessageHashValuesCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateOpenGroupAuthTokenCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateSnodePoolTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateSwarmTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueTable2Command());
|
||||
db.execSQL(LokiAPIDatabase.getCreateReceivedMessageHashValuesTable2Command());
|
||||
db.execSQL(LokiAPIDatabase.getCreateOpenGroupAuthTokenTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateDeviceLinkCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateUserCountCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateUserCountTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateSessionRequestTimestampCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyDBCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyTableCommand());
|
||||
db.execSQL(LokiPreKeyBundleDatabase.getCreateTableCommand());
|
||||
db.execSQL(LokiPreKeyRecordDatabase.getCreateTableCommand());
|
||||
db.execSQL(LokiMessageDatabase.getCreateMessageIDTableCommand());
|
||||
@ -157,6 +159,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand());
|
||||
db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand());
|
||||
db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand());
|
||||
db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupRatchetTableCommand());
|
||||
db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupPrivateKeyTableCommand());
|
||||
|
||||
executeStatements(db, SmsDatabase.CREATE_INDEXS);
|
||||
executeStatements(db, MmsDatabase.CREATE_INDEXS);
|
||||
@ -519,9 +523,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV1) {
|
||||
db.execSQL(LokiAPIDatabase.getCreateOpenGroupAuthTokenCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateOpenGroupAuthTokenTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDTableCommand());
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV2) {
|
||||
@ -541,7 +545,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV5) {
|
||||
db.execSQL(LokiAPIDatabase.getCreateUserCountCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateUserCountTableCommand());
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV6) {
|
||||
@ -590,17 +594,24 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV9) {
|
||||
db.execSQL(LokiAPIDatabase.getCreateSnodePoolCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateSnodePoolTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathTableCommand());
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV10) {
|
||||
db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampCacheCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampTableCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampTableCommand());
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV11) {
|
||||
db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyDBCommand());
|
||||
db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyTableCommand());
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV12) {
|
||||
db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueTable2Command());
|
||||
db.execSQL(LokiAPIDatabase.getCreateReceivedMessageHashValuesTable2Command());
|
||||
db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupRatchetTableCommand());
|
||||
db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupPrivateKeyTableCommand());
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
|
@ -46,7 +46,7 @@ import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation;
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceOpenGroupUpdateJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceOpenGroupUpdateJob;
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.push.MessageSenderEventListener;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
@ -153,6 +153,7 @@ public class SignalCommunicationModule {
|
||||
Optional.of(new MessageSenderEventListener(context)),
|
||||
TextSecurePreferences.getLocalNumber(context),
|
||||
DatabaseFactory.getLokiAPIDatabase(context),
|
||||
DatabaseFactory.getSSKDatabase(context),
|
||||
DatabaseFactory.getLokiThreadDatabase(context),
|
||||
DatabaseFactory.getLokiMessageDatabase(context),
|
||||
DatabaseFactory.getLokiPreKeyBundleDatabase(context),
|
||||
|
@ -97,11 +97,6 @@ public class GroupMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
// Loki - Ignore message if needed
|
||||
if (ClosedGroupsProtocol.shouldIgnoreGroupCreatedMessage(context, group)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Loki - Parse admins
|
||||
if (group.getAdmins().isPresent()) {
|
||||
for (String admin : group.getAdmins().get()) {
|
||||
|
@ -44,7 +44,8 @@ import org.thoughtcrime.securesms.jobs.SmsSentJob;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
import org.thoughtcrime.securesms.jobs.TypingSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.UpdateApkJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.PushNullMessageSendJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@ -75,7 +76,8 @@ public class WorkManagerFactoryMappings {
|
||||
put(PushMediaSendJob.class.getName(), PushMediaSendJob.KEY);
|
||||
put(PushNotificationReceiveJob.class.getName(), PushNotificationReceiveJob.KEY);
|
||||
put(PushTextSendJob.class.getName(), PushTextSendJob.KEY);
|
||||
put(PushNullMessageSendJob.class.getName(), PushNullMessageSendJob.KEY);
|
||||
put(NullMessageSendJob.class.getName(), NullMessageSendJob.KEY);
|
||||
put(ClosedGroupUpdateMessageSendJob.class.getName(), ClosedGroupUpdateMessageSendJob.KEY);
|
||||
put(RefreshAttributesJob.class.getName(), RefreshAttributesJob.KEY);
|
||||
put(RefreshPreKeysJob.class.getName(), RefreshPreKeysJob.KEY);
|
||||
put(RefreshUnidentifiedDeliveryAbilityJob.class.getName(), RefreshUnidentifiedDeliveryAbilityJob.KEY);
|
||||
|
@ -13,9 +13,10 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver;
|
||||
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.PushNullMessageSendJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.PushSessionRequestMessageSendJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceOpenGroupUpdateJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@ -51,7 +52,8 @@ public final class JobManagerFactories {
|
||||
put(PushMediaSendJob.KEY, new PushMediaSendJob.Factory());
|
||||
put(PushNotificationReceiveJob.KEY, new PushNotificationReceiveJob.Factory());
|
||||
put(PushTextSendJob.KEY, new PushTextSendJob.Factory());
|
||||
put(PushNullMessageSendJob.KEY, new PushNullMessageSendJob.Factory());
|
||||
put(NullMessageSendJob.KEY, new NullMessageSendJob.Factory());
|
||||
put(ClosedGroupUpdateMessageSendJob.KEY, new ClosedGroupUpdateMessageSendJob.Factory());
|
||||
put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory());
|
||||
put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory());
|
||||
put(RefreshUnidentifiedDeliveryAbilityJob.KEY, new RefreshUnidentifiedDeliveryAbilityJob.Factory());
|
||||
@ -72,7 +74,7 @@ public final class JobManagerFactories {
|
||||
put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
|
||||
put(TypingSendJob.KEY, new TypingSendJob.Factory());
|
||||
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
|
||||
put(PushSessionRequestMessageSendJob.KEY, new PushSessionRequestMessageSendJob.Factory());
|
||||
put(SessionRequestMessageSendJob.KEY, new SessionRequestMessageSendJob.Factory());
|
||||
put(MultiDeviceOpenGroupUpdateJob.KEY, new MultiDeviceOpenGroupUpdateJob.Factory());
|
||||
}};
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
@ -66,11 +66,11 @@ 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.protocol.ClosedGroupsProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceProtocol;
|
||||
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.protocol.shelved.SyncMessagesProtocol;
|
||||
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities;
|
||||
import org.thoughtcrime.securesms.loki.utilities.PromiseUtilities;
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||
@ -127,6 +127,7 @@ import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher;
|
||||
import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager;
|
||||
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@ -255,7 +256,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
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());
|
||||
LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, DatabaseFactory.getSSKDatabase(context), sessionResetProtocol, UnidentifiedAccessUtil.getCertificateValidator());
|
||||
|
||||
SignalServiceContent content = cipher.decrypt(envelope);
|
||||
|
||||
@ -278,6 +279,10 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
MultiDeviceProtocol.handleUnlinkingRequestIfNeeded(context, content);
|
||||
} else {
|
||||
|
||||
if (message.getClosedGroupUpdate().isPresent()) {
|
||||
ClosedGroupsProtocol.handleSharedSenderKeysUpdate(context, message.getClosedGroupUpdate().get(), content.getSender());
|
||||
}
|
||||
|
||||
if (message.isEndSession()) {
|
||||
handleEndSessionMessage(content, smsMessageId);
|
||||
} else if (message.isGroupUpdate()) {
|
||||
@ -298,7 +303,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
SessionMetaProtocol.handleProfileKeyUpdate(context, content);
|
||||
}
|
||||
|
||||
if (content.isNeedsReceipt()) {
|
||||
if (content.isNeedsReceipt() && SessionMetaProtocol.shouldSendDeliveryReceipt(Address.fromSerialized(content.getSender()))) {
|
||||
handleNeedsDeliveryReceipt(content, message);
|
||||
}
|
||||
}
|
||||
@ -369,6 +374,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
Log.w(TAG, e);
|
||||
} catch (SelfSendException e) {
|
||||
Log.i(TAG, "Dropping UD message from self.");
|
||||
} catch (IOException e) {
|
||||
Log.i(TAG, "IOException during message decryption.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1454,7 +1461,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get());
|
||||
boolean isLeaveMessage = message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() == SignalServiceGroup.Type.QUIT;
|
||||
|
||||
boolean shouldIgnoreContentMessage = ClosedGroupsProtocol.shouldIgnoreContentMessage(context, conversation, groupId.orNull(), content);
|
||||
boolean shouldIgnoreContentMessage = ClosedGroupsProtocol.shouldIgnoreContentMessage(context, conversation.getAddress(), groupId.orNull(), content.getSender());
|
||||
return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage) || (isContentMessage && shouldIgnoreContentMessage);
|
||||
} else {
|
||||
return sender.isBlocked();
|
||||
|
@ -154,7 +154,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
|
||||
if (filterAddress != null) targets = Collections.singletonList(Address.fromSerialized(filterAddress));
|
||||
else if (!existingNetworkFailures.isEmpty()) targets = Stream.of(existingNetworkFailures).map(NetworkFailure::getAddress).toList();
|
||||
else targets = ClosedGroupsProtocol.getDestinations(message.getRecipient().getAddress().toGroupString(), context).get();
|
||||
else targets = ClosedGroupsProtocol.getMessageDestinations(context, message.getRecipient().getAddress().toGroupString());
|
||||
|
||||
List<SendMessageResult> results = deliver(message, targets);
|
||||
List<NetworkFailure> networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Address.fromSerialized(result.getAddress().getNumber()))).toList();
|
||||
|
@ -36,7 +36,8 @@ public abstract class PushReceivedJob extends BaseJob {
|
||||
|
||||
if (envelope.isReceipt()) {
|
||||
handleReceipt(envelope);
|
||||
} else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender() || envelope.isFallbackMessage()) {
|
||||
} else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage()
|
||||
|| envelope.isUnidentifiedSender() || envelope.isFallbackMessage() || envelope.isClosedGroupCiphertext()) {
|
||||
handleMessage(envelope, isPushNotification);
|
||||
} else {
|
||||
Log.w(TAG, "Received envelope of unknown type: " + envelope.getType());
|
||||
|
@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
@ -83,6 +84,8 @@ public class SendDeliveryReceiptJob extends BaseJob implements InjectableType {
|
||||
Collections.singletonList(messageId),
|
||||
timestamp);
|
||||
|
||||
if (!SessionMetaProtocol.shouldSendDeliveryReceipt(Address.fromSerialized(address))) { return; }
|
||||
|
||||
messageSender.sendReceipt(remoteAddress,
|
||||
UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(address), false)),
|
||||
receiptMessage);
|
||||
|
@ -18,8 +18,10 @@ import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
@ -93,6 +95,35 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
|
||||
}
|
||||
|
||||
private fun createClosedGroup() {
|
||||
if (ClosedGroupsProtocol.isSharedSenderKeysEnabled) {
|
||||
createSSKBasedClosedGroup()
|
||||
} else {
|
||||
createLegacyClosedGroup()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSSKBasedClosedGroup() {
|
||||
val name = nameEditText.text.trim()
|
||||
if (name.isEmpty()) {
|
||||
return Toast.makeText(this, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
if (name.length >= 64) {
|
||||
return Toast.makeText(this, R.string.activity_create_closed_group_group_name_too_long_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
val selectedMembers = this.selectContactsAdapter.selectedMembers
|
||||
if (selectedMembers.count() < 2) {
|
||||
return Toast.makeText(this, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
if (selectedMembers.count() > 49) { // Minus one because we're going to include self later
|
||||
return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
val groupID = ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey ))
|
||||
val threadID = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false))
|
||||
openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false))
|
||||
}
|
||||
|
||||
private fun createLegacyClosedGroup() {
|
||||
val name = nameEditText.text.trim()
|
||||
if (name.isEmpty()) {
|
||||
return Toast.makeText(this, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show()
|
||||
|
@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.loki.views.NewConversationButtonSetViewDelegat
|
||||
import org.thoughtcrime.securesms.loki.views.SeedReminderViewDelegate
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.GroupUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI
|
||||
@ -49,6 +50,7 @@ import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol
|
||||
import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol
|
||||
import org.whispersystems.signalservice.loki.protocol.sessionmanagement.SessionManagementProtocol
|
||||
import org.whispersystems.signalservice.loki.protocol.shelved.syncmessages.SyncMessagesProtocol
|
||||
import org.whispersystems.signalservice.loki.utilities.toHexString
|
||||
|
||||
class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate {
|
||||
private lateinit var glide: GlideRequests
|
||||
@ -154,6 +156,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(this)
|
||||
val threadDB = DatabaseFactory.getLokiThreadDatabase(this)
|
||||
val userDB = DatabaseFactory.getLokiUserDatabase(this)
|
||||
val sskDatabase = DatabaseFactory.getSSKDatabase(this)
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
val sessionResetImpl = SessionResetImplementation(this)
|
||||
if (userPublicKey != null) {
|
||||
@ -162,7 +165,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
SyncMessagesProtocol.configureIfNeeded(apiDB, userPublicKey)
|
||||
application.publicChatManager.startPollersIfNeeded()
|
||||
}
|
||||
SessionManagementProtocol.configureIfNeeded(sessionResetImpl, threadDB, application)
|
||||
SessionManagementProtocol.configureIfNeeded(sessionResetImpl, sskDatabase, application)
|
||||
MultiDeviceProtocol.configureIfNeeded(apiDB)
|
||||
IP2Country.configureIfNeeded(this)
|
||||
// Preload device links to make message sending quicker
|
||||
@ -332,7 +335,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
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)) {
|
||||
val groupPublicKey = GroupUtil.getDecodedId(recipient.address.toString()).toHexString()
|
||||
val isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey)
|
||||
if (isSSKBasedClosedGroup) {
|
||||
ClosedGroupsProtocol.leave(this, groupPublicKey)
|
||||
} else if (!ClosedGroupsProtocol.leaveLegacyGroup(this, recipient)) {
|
||||
Toast.makeText(this, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()
|
||||
return@setPositiveButton
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol
|
||||
import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
|
||||
|
||||
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
|
@ -16,7 +16,7 @@ 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.SessionResetImplementation
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo
|
||||
import org.thoughtcrime.securesms.loki.utilities.show
|
||||
@ -108,12 +108,13 @@ class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelega
|
||||
val apiDB = DatabaseFactory.getLokiAPIDatabase(this)
|
||||
val threadDB = DatabaseFactory.getLokiThreadDatabase(this)
|
||||
val userDB = DatabaseFactory.getLokiUserDatabase(this)
|
||||
val sskDatabase = DatabaseFactory.getSSKDatabase(this)
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
val sessionResetImpl = SessionResetImplementation(this)
|
||||
MentionsManager.configureIfNeeded(userPublicKey, threadDB, userDB)
|
||||
SessionMetaProtocol.configureIfNeeded(apiDB, userPublicKey)
|
||||
org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol.configureIfNeeded(apiDB)
|
||||
SessionManagementProtocol.configureIfNeeded(sessionResetImpl, threadDB, application)
|
||||
SessionManagementProtocol.configureIfNeeded(sessionResetImpl, sskDatabase, application)
|
||||
SyncMessagesProtocol.configureIfNeeded(apiDB, userPublicKey)
|
||||
application.setUpP2PAPIIfNeeded()
|
||||
application.setUpStorageAPIIfNeeded()
|
||||
|
@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.devicelist.Device
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.loki.dialogs.*
|
||||
import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol
|
||||
import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.recipient
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
|
||||
|
@ -36,7 +36,7 @@ class BackgroundPollWorker : PersistentAlarmManagerListener() {
|
||||
val applicationContext = context.applicationContext as ApplicationContext
|
||||
val broadcaster = applicationContext.broadcaster
|
||||
SnodeAPI.configureIfNeeded(userPublicKey, lokiAPIDatabase, broadcaster)
|
||||
SnodeAPI.shared.getMessages().map { messages ->
|
||||
SnodeAPI.shared.getMessages(userPublicKey).map { messages ->
|
||||
messages.forEach {
|
||||
PushContentReceiveJob(context).processEnvelope(SignalServiceEnvelope(it), false)
|
||||
}
|
||||
|
81
src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt
Normal file
81
src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt
Normal file
@ -0,0 +1,81 @@
|
||||
package org.thoughtcrime.securesms.loki.api
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import org.thoughtcrime.securesms.jobs.PushContentReceiveJob
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase
|
||||
import org.thoughtcrime.securesms.loki.utilities.successBackground
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope
|
||||
import org.whispersystems.signalservice.loki.api.SnodeAPI
|
||||
import org.whispersystems.signalservice.loki.api.SwarmAPI
|
||||
import org.whispersystems.signalservice.loki.utilities.getRandomElementOrNull
|
||||
|
||||
class ClosedGroupPoller private constructor(private val context: Context, private val database: SharedSenderKeysDatabase) {
|
||||
private var isPolling = false
|
||||
private val handler = Handler()
|
||||
|
||||
private val task = object : Runnable {
|
||||
|
||||
override fun run() {
|
||||
poll()
|
||||
handler.postDelayed(this, ClosedGroupPoller.pollInterval)
|
||||
}
|
||||
}
|
||||
|
||||
// region Settings
|
||||
companion object {
|
||||
private val pollInterval: Long = 2 * 1000
|
||||
|
||||
public lateinit var shared: ClosedGroupPoller
|
||||
|
||||
public fun configureIfNeeded(context: Context, sskDatabase: SharedSenderKeysDatabase) {
|
||||
if (::shared.isInitialized) { return; }
|
||||
shared = ClosedGroupPoller(context, sskDatabase)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Error
|
||||
public class InsufficientSnodesException() : Exception("No snodes left to poll.")
|
||||
public class PollingCanceledException() : Exception("Polling canceled.")
|
||||
// endregion
|
||||
|
||||
// region Public API
|
||||
public fun startIfNeeded() {
|
||||
if (isPolling) { return }
|
||||
isPolling = true
|
||||
task.run()
|
||||
}
|
||||
|
||||
public fun stopIfNeeded() {
|
||||
isPolling = false
|
||||
handler.removeCallbacks(task)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Private API
|
||||
private fun poll() {
|
||||
if (!isPolling) { return }
|
||||
val publicKeys = database.getAllClosedGroupPublicKeys()
|
||||
publicKeys.forEach { publicKey ->
|
||||
SwarmAPI.shared.getSwarm(publicKey).bind { swarm ->
|
||||
val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure
|
||||
if (!isPolling) { throw PollingCanceledException() }
|
||||
SnodeAPI.shared.getRawMessages(snode, publicKey).map {SnodeAPI.shared.parseRawMessagesResponse(it, snode, publicKey) }
|
||||
}.successBackground { messages ->
|
||||
if (messages.isNotEmpty()) {
|
||||
Log.d("Loki", "Received ${messages.count()} new message(s) in closed group with public key: $publicKey.")
|
||||
}
|
||||
messages.forEach {
|
||||
PushContentReceiveJob(context).processEnvelope(SignalServiceEnvelope(it), false)
|
||||
}
|
||||
}.fail {
|
||||
Log.d("Loki", "Polling failed for closed group with public key: $publicKey due to error: $it.")
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -6,7 +6,6 @@ import android.util.Log
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.loki.utilities.*
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.api.Snode
|
||||
import org.whispersystems.signalservice.loki.database.LokiAPIDatabaseProtocol
|
||||
@ -14,78 +13,73 @@ import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.Device
|
||||
|
||||
class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiAPIDatabaseProtocol {
|
||||
|
||||
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 snode = "snode"
|
||||
// Snode pool
|
||||
private val snodePoolTable = "loki_snode_pool_cache"
|
||||
private val dummyKey = "dummy_key"
|
||||
private val snodePool = "snode_pool_key"
|
||||
@JvmStatic val createSnodePoolCacheCommand = "CREATE TABLE $snodePoolCache ($dummyKey TEXT PRIMARY KEY, $snodePool TEXT);"
|
||||
// Onion request path cache
|
||||
private val onionRequestPathCache = "loki_path_cache"
|
||||
@JvmStatic val createSnodePoolTableCommand = "CREATE TABLE $snodePoolTable ($dummyKey TEXT PRIMARY KEY, $snodePool TEXT);"
|
||||
// Onion request paths
|
||||
private val onionRequestPathTable = "loki_path_cache"
|
||||
private val indexPath = "index_path"
|
||||
private val snode = "snode"
|
||||
@JvmStatic val createOnionRequestPathCacheCommand = "CREATE TABLE $onionRequestPathCache ($indexPath TEXT PRIMARY KEY, $snode TEXT);"
|
||||
// Swarm cache
|
||||
private val swarmCache = "loki_api_swarm_cache"
|
||||
@JvmStatic val createOnionRequestPathTableCommand = "CREATE TABLE $onionRequestPathTable ($indexPath TEXT PRIMARY KEY, $snode TEXT);"
|
||||
// Swarms
|
||||
private val swarmTable = "loki_api_swarm_cache"
|
||||
private val swarmPublicKey = "hex_encoded_public_key"
|
||||
private val swarm = "swarm"
|
||||
@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"
|
||||
@JvmStatic val createSwarmTableCommand = "CREATE TABLE $swarmTable ($swarmPublicKey TEXT PRIMARY KEY, $swarm TEXT);"
|
||||
// Last message hash values
|
||||
private val lastMessageHashValueTable2 = "last_message_hash_value_table"
|
||||
private val lastMessageHashValue = "last_message_hash_value"
|
||||
@JvmStatic val createLastMessageHashValueCacheCommand = "CREATE TABLE $lastMessageHashValueCache ($target TEXT PRIMARY KEY, $lastMessageHashValue TEXT);"
|
||||
// Received message hash values cache
|
||||
private val receivedMessageHashValuesCache = "loki_api_received_message_hash_values_cache"
|
||||
private val userID = "user_id"
|
||||
@JvmStatic val createLastMessageHashValueTable2Command
|
||||
= "CREATE TABLE $lastMessageHashValueTable2 ($snode TEXT, $publicKey TEXT, $lastMessageHashValue TEXT, PRIMARY KEY ($snode, $publicKey));"
|
||||
// Received message hash values
|
||||
private val receivedMessageHashValuesTable2 = "received_message_hash_values_table"
|
||||
private val receivedMessageHashValues = "received_message_hash_values"
|
||||
@JvmStatic val createReceivedMessageHashValuesCacheCommand = "CREATE TABLE $receivedMessageHashValuesCache ($userID TEXT PRIMARY KEY, $receivedMessageHashValues TEXT);"
|
||||
// Open group auth token cache
|
||||
private val openGroupAuthTokenCache = "loki_api_group_chat_auth_token_database"
|
||||
@JvmStatic val createReceivedMessageHashValuesTable2Command
|
||||
= "CREATE TABLE $receivedMessageHashValuesTable2 ($snode STRING, $publicKey STRING, $receivedMessageHashValues TEXT, PRIMARY KEY ($snode, $publicKey));"
|
||||
// Open group auth tokens
|
||||
private val openGroupAuthTokenTable = "loki_api_group_chat_auth_token_database"
|
||||
private val server = "server"
|
||||
private val token = "token"
|
||||
@JvmStatic val createOpenGroupAuthTokenCacheCommand = "CREATE TABLE $openGroupAuthTokenCache ($server TEXT PRIMARY KEY, $token TEXT);"
|
||||
// Last message server ID cache
|
||||
private val lastMessageServerIDCache = "loki_api_last_message_server_id_cache"
|
||||
private val lastMessageServerIDCacheIndex = "loki_api_last_message_server_id_cache_index"
|
||||
@JvmStatic val createOpenGroupAuthTokenTableCommand = "CREATE TABLE $openGroupAuthTokenTable ($server TEXT PRIMARY KEY, $token TEXT);"
|
||||
// Last message server IDs
|
||||
private val lastMessageServerIDTable = "loki_api_last_message_server_id_cache"
|
||||
private val lastMessageServerIDTableIndex = "loki_api_last_message_server_id_cache_index"
|
||||
private val lastMessageServerID = "last_message_server_id"
|
||||
@JvmStatic val createLastMessageServerIDCacheCommand = "CREATE TABLE $lastMessageServerIDCache ($lastMessageServerIDCacheIndex STRING PRIMARY KEY, $lastMessageServerID INTEGER DEFAULT 0);"
|
||||
// Last deletion server ID cache
|
||||
private val lastDeletionServerIDCache = "loki_api_last_deletion_server_id_cache"
|
||||
private val lastDeletionServerIDCacheIndex = "loki_api_last_deletion_server_id_cache_index"
|
||||
@JvmStatic val createLastMessageServerIDTableCommand = "CREATE TABLE $lastMessageServerIDTable ($lastMessageServerIDTableIndex STRING PRIMARY KEY, $lastMessageServerID INTEGER DEFAULT 0);"
|
||||
// Last deletion server IDs
|
||||
private val lastDeletionServerIDTable = "loki_api_last_deletion_server_id_cache"
|
||||
private val lastDeletionServerIDTableIndex = "loki_api_last_deletion_server_id_cache_index"
|
||||
private val lastDeletionServerID = "last_deletion_server_id"
|
||||
@JvmStatic val createLastDeletionServerIDCacheCommand = "CREATE TABLE $lastDeletionServerIDCache ($lastDeletionServerIDCacheIndex STRING PRIMARY KEY, $lastDeletionServerID INTEGER DEFAULT 0);"
|
||||
// Device link cache
|
||||
@JvmStatic val createLastDeletionServerIDTableCommand = "CREATE TABLE $lastDeletionServerIDTable ($lastDeletionServerIDTableIndex STRING PRIMARY KEY, $lastDeletionServerID INTEGER DEFAULT 0);"
|
||||
// User counts
|
||||
private val userCountTable = "loki_user_count_cache"
|
||||
private val publicChatID = "public_chat_id"
|
||||
private val userCount = "user_count"
|
||||
@JvmStatic val createUserCountTableCommand = "CREATE TABLE $userCountTable ($publicChatID STRING PRIMARY KEY, $userCount INTEGER DEFAULT 0);"
|
||||
// Session request sent timestamps
|
||||
private val sessionRequestSentTimestampTable = "session_request_sent_timestamp_cache"
|
||||
@JvmStatic val createSessionRequestSentTimestampTableCommand = "CREATE TABLE $sessionRequestSentTimestampTable ($publicKey STRING PRIMARY KEY, $timestamp INTEGER DEFAULT 0);"
|
||||
// Session request processed timestamp cache
|
||||
private val sessionRequestProcessedTimestampTable = "session_request_processed_timestamp_cache"
|
||||
@JvmStatic val createSessionRequestProcessedTimestampTableCommand = "CREATE TABLE $sessionRequestProcessedTimestampTable ($publicKey STRING PRIMARY KEY, $timestamp INTEGER DEFAULT 0);"
|
||||
// Open group public keys
|
||||
private val openGroupPublicKeyTable = "open_group_public_keys"
|
||||
@JvmStatic val createOpenGroupPublicKeyTableCommand = "CREATE TABLE $openGroupPublicKeyTable ($server STRING PRIMARY KEY, $publicKey INTEGER DEFAULT 0);"
|
||||
|
||||
// region Deprecated
|
||||
private val deviceLinkCache = "loki_pairing_authorisation_cache"
|
||||
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 ($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 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);"
|
||||
// Open group public keys
|
||||
private val openGroupPublicKeyDB = "open_group_public_keys"
|
||||
@JvmStatic val createOpenGroupPublicKeyDBCommand = "CREATE TABLE $openGroupPublicKeyDB ($server STRING PRIMARY KEY, $publicKey INTEGER DEFAULT 0);"
|
||||
|
||||
|
||||
|
||||
// region Deprecated
|
||||
@JvmStatic val createDeviceLinkCacheCommand = "CREATE TABLE $deviceLinkCache ($masterPublicKey STRING, $slavePublicKey STRING, " +
|
||||
"$requestSignature STRING NULLABLE DEFAULT NULL, $authorizationSignature STRING NULLABLE DEFAULT NULL, PRIMARY KEY ($masterPublicKey, $slavePublicKey));"
|
||||
private val sessionRequestTimestampCache = "session_request_timestamp_cache"
|
||||
@JvmStatic val createSessionRequestTimestampCacheCommand = "CREATE TABLE $sessionRequestTimestampCache ($publicKey STRING PRIMARY KEY, $timestamp STRING);"
|
||||
// endregion
|
||||
@ -93,7 +87,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
|
||||
override fun getSnodePool(): Set<Snode> {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(snodePoolCache, "${Companion.dummyKey} = ?", wrap("dummy_key")) { cursor ->
|
||||
return database.get(snodePoolTable, "${Companion.dummyKey} = ?", wrap("dummy_key")) { cursor ->
|
||||
val snodePoolAsString = cursor.getString(cursor.getColumnIndexOrThrow(snodePool))
|
||||
snodePoolAsString.split(", ").mapNotNull { snodeAsString ->
|
||||
val components = snodeAsString.split("-")
|
||||
@ -116,14 +110,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
}
|
||||
string
|
||||
}
|
||||
val row = wrap(mapOf(Companion.dummyKey to "dummy_key", snodePool to snodePoolAsString))
|
||||
database.insertOrUpdate(snodePoolCache, row, "${Companion.dummyKey} = ?", wrap("dummy_key"))
|
||||
val row = wrap(mapOf( Companion.dummyKey to "dummy_key", snodePool to snodePoolAsString ))
|
||||
database.insertOrUpdate(snodePoolTable, row, "${Companion.dummyKey} = ?", wrap("dummy_key"))
|
||||
}
|
||||
|
||||
override fun getOnionRequestPaths(): List<List<Snode>> {
|
||||
val database = databaseHelper.readableDatabase
|
||||
fun get(indexPath: String): Snode? {
|
||||
return database.get(onionRequestPathCache, "${Companion.indexPath} = ?", wrap(indexPath)) { cursor ->
|
||||
return database.get(onionRequestPathTable, "${Companion.indexPath} = ?", wrap(indexPath)) { cursor ->
|
||||
val snodeAsString = cursor.getString(cursor.getColumnIndexOrThrow(snode))
|
||||
val components = snodeAsString.split("-")
|
||||
val address = components[0]
|
||||
@ -146,7 +140,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
fun clearOnionRequestPaths() {
|
||||
val database = databaseHelper.writableDatabase
|
||||
fun delete(indexPath: String) {
|
||||
database.delete(onionRequestPathCache, "${Companion.indexPath} = ?", wrap(indexPath))
|
||||
database.delete(onionRequestPathTable, "${Companion.indexPath} = ?", wrap(indexPath))
|
||||
}
|
||||
delete("0-0"); delete("0-1")
|
||||
delete("0-2"); delete("1-0")
|
||||
@ -154,21 +148,21 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
}
|
||||
|
||||
override fun setOnionRequestPaths(newValue: List<List<Snode>>) {
|
||||
// FIXME: This is a bit of a dirty approach that assumes 2 paths of length 3 each. We should do better than this.
|
||||
// TODO: Make this work with arbitrary paths
|
||||
if (newValue.count() != 2) { return }
|
||||
val path0 = newValue[0]
|
||||
val path1 = newValue[1]
|
||||
if (path0.count() != 3 || path1.count() != 3) { return }
|
||||
Log.d("Loki", "Persisting onion request paths to database.")
|
||||
val database = databaseHelper.writableDatabase
|
||||
fun set(indexPath: String ,snode: Snode) {
|
||||
fun set(indexPath: String, snode: Snode) {
|
||||
var snodeAsString = "${snode.address}-${snode.port}"
|
||||
val keySet = snode.publicKeySet
|
||||
if (keySet != null) {
|
||||
snodeAsString += "-${keySet.ed25519Key}-${keySet.x25519Key}"
|
||||
}
|
||||
val row = wrap(mapOf(Companion.indexPath to indexPath, Companion.snode to snodeAsString))
|
||||
database.insertOrUpdate(onionRequestPathCache, row, "${Companion.indexPath} = ?", wrap(indexPath))
|
||||
val row = wrap(mapOf( Companion.indexPath to indexPath, Companion.snode to snodeAsString ))
|
||||
database.insertOrUpdate(onionRequestPathTable, row, "${Companion.indexPath} = ?", wrap(indexPath))
|
||||
}
|
||||
set("0-0", path0[0]); set("0-1", path0[1])
|
||||
set("0-2", path0[2]); set("1-0", path1[0])
|
||||
@ -177,7 +171,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
|
||||
override fun getSwarm(publicKey: String): Set<Snode>? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(swarmCache, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) { cursor ->
|
||||
return database.get(swarmTable, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) { cursor ->
|
||||
val swarmAsString = cursor.getString(cursor.getColumnIndexOrThrow(swarm))
|
||||
swarmAsString.split(", ").mapNotNull { targetAsString ->
|
||||
val components = targetAsString.split("-")
|
||||
@ -200,41 +194,45 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
}
|
||||
string
|
||||
}
|
||||
val row = wrap(mapOf(Companion.swarmPublicKey to publicKey, swarm to swarmAsString))
|
||||
database.insertOrUpdate(swarmCache, row, "${Companion.swarmPublicKey} = ?", wrap(publicKey))
|
||||
val row = wrap(mapOf( Companion.swarmPublicKey to publicKey, swarm to swarmAsString ))
|
||||
database.insertOrUpdate(swarmTable, row, "${Companion.swarmPublicKey} = ?", wrap(publicKey))
|
||||
}
|
||||
|
||||
override fun getLastMessageHashValue(snode: Snode): String? {
|
||||
override fun getLastMessageHashValue(snode: Snode, publicKey: String): String? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(lastMessageHashValueCache, "${Companion.target} = ?", wrap(snode.address)) { cursor ->
|
||||
val query = "${Companion.snode} = ? AND ${Companion.publicKey} = ?"
|
||||
return database.get(lastMessageHashValueTable2, query, arrayOf( snode.toString(), publicKey )) { cursor ->
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(lastMessageHashValue))
|
||||
}
|
||||
}
|
||||
|
||||
override fun setLastMessageHashValue(snode: Snode, newValue: String) {
|
||||
override fun setLastMessageHashValue(snode: Snode, publicKey: String, newValue: String) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val row = wrap(mapOf(Companion.target to snode.address, lastMessageHashValue to newValue))
|
||||
database.insertOrUpdate(lastMessageHashValueCache, row, "${Companion.target} = ?", wrap(snode.address))
|
||||
val row = wrap(mapOf( Companion.snode to snode.toString(), Companion.publicKey to publicKey, lastMessageHashValue to newValue ))
|
||||
val query = "${Companion.snode} = ? AND ${Companion.publicKey} = ?"
|
||||
database.insertOrUpdate(lastMessageHashValueTable2, row, query, arrayOf( snode.toString(), publicKey ))
|
||||
}
|
||||
|
||||
override fun getReceivedMessageHashValues(): Set<String>? {
|
||||
override fun getReceivedMessageHashValues(publicKey: String): Set<String>? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(receivedMessageHashValuesCache, "$userID = ?", wrap(userPublicKey)) { cursor ->
|
||||
val query = "$Companion.publicKey = ?"
|
||||
return database.get(receivedMessageHashValuesTable2, query, arrayOf( publicKey )) { cursor ->
|
||||
val receivedMessageHashValuesAsString = cursor.getString(cursor.getColumnIndexOrThrow(receivedMessageHashValues))
|
||||
receivedMessageHashValuesAsString.split(", ").toSet()
|
||||
receivedMessageHashValuesAsString.split("-").toSet()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setReceivedMessageHashValues(newValue: Set<String>) {
|
||||
override fun setReceivedMessageHashValues(publicKey: String, newValue: Set<String>) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val receivedMessageHashValuesAsString = newValue.joinToString(", ")
|
||||
val row = wrap(mapOf(userID to userPublicKey, receivedMessageHashValues to receivedMessageHashValuesAsString))
|
||||
database.insertOrUpdate(receivedMessageHashValuesCache, row, "$userID = ?", wrap(userPublicKey))
|
||||
val receivedMessageHashValuesAsString = newValue.joinToString("-")
|
||||
val row = wrap(mapOf( Companion.publicKey to publicKey, receivedMessageHashValues to receivedMessageHashValuesAsString ))
|
||||
val query = "$Companion.publicKey = ?"
|
||||
database.insertOrUpdate(receivedMessageHashValuesTable2, row, query, arrayOf( publicKey ))
|
||||
}
|
||||
|
||||
override fun getAuthToken(server: String): String? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(openGroupAuthTokenCache, "${Companion.server} = ?", wrap(server)) { cursor ->
|
||||
return database.get(openGroupAuthTokenTable, "${Companion.server} = ?", wrap(server)) { cursor ->
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(token))
|
||||
}
|
||||
}
|
||||
@ -242,17 +240,17 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
override fun setAuthToken(server: String, newValue: String?) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
if (newValue != null) {
|
||||
val row = wrap(mapOf(Companion.server to server, token to newValue))
|
||||
database.insertOrUpdate(openGroupAuthTokenCache, row, "${Companion.server} = ?", wrap(server))
|
||||
val row = wrap(mapOf( Companion.server to server, token to newValue ))
|
||||
database.insertOrUpdate(openGroupAuthTokenTable, row, "${Companion.server} = ?", wrap(server))
|
||||
} else {
|
||||
database.delete(openGroupAuthTokenCache, "${Companion.server} = ?", wrap(server))
|
||||
database.delete(openGroupAuthTokenTable, "${Companion.server} = ?", wrap(server))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLastMessageServerID(group: Long, server: String): Long? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val index = "$server.$group"
|
||||
return database.get(lastMessageServerIDCache, "$lastMessageServerIDCacheIndex = ?", wrap(index)) { cursor ->
|
||||
return database.get(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index)) { cursor ->
|
||||
cursor.getInt(lastMessageServerID)
|
||||
}?.toLong()
|
||||
}
|
||||
@ -260,20 +258,20 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
override fun setLastMessageServerID(group: Long, server: String, newValue: Long) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val index = "$server.$group"
|
||||
val row = wrap(mapOf(lastMessageServerIDCacheIndex to index, lastMessageServerID to newValue.toString()))
|
||||
database.insertOrUpdate(lastMessageServerIDCache, row, "$lastMessageServerIDCacheIndex = ?", wrap(index))
|
||||
val row = wrap(mapOf( lastMessageServerIDTableIndex to index, lastMessageServerID to newValue.toString() ))
|
||||
database.insertOrUpdate(lastMessageServerIDTable, row, "$lastMessageServerIDTableIndex = ?", wrap(index))
|
||||
}
|
||||
|
||||
fun removeLastMessageServerID(group: Long, server: String) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val index = "$server.$group"
|
||||
database.delete(lastMessageServerIDCache,"$lastMessageServerIDCacheIndex = ?", wrap(index))
|
||||
database.delete(lastMessageServerIDTable,"$lastMessageServerIDTableIndex = ?", wrap(index))
|
||||
}
|
||||
|
||||
override fun getLastDeletionServerID(group: Long, server: String): Long? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val index = "$server.$group"
|
||||
return database.get(lastDeletionServerIDCache, "$lastDeletionServerIDCacheIndex = ?", wrap(index)) { cursor ->
|
||||
return database.get(lastDeletionServerIDTable, "$lastDeletionServerIDTableIndex = ?", wrap(index)) { cursor ->
|
||||
cursor.getInt(lastDeletionServerID)
|
||||
}?.toLong()
|
||||
}
|
||||
@ -281,16 +279,71 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
override fun setLastDeletionServerID(group: Long, server: String, newValue: Long) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val index = "$server.$group"
|
||||
val row = wrap(mapOf(lastDeletionServerIDCacheIndex to index, lastDeletionServerID to newValue.toString()))
|
||||
database.insertOrUpdate(lastDeletionServerIDCache, row, "$lastDeletionServerIDCacheIndex = ?", wrap(index))
|
||||
val row = wrap(mapOf( lastDeletionServerIDTableIndex to index, lastDeletionServerID to newValue.toString() ))
|
||||
database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index))
|
||||
}
|
||||
|
||||
fun removeLastDeletionServerID(group: Long, server: String) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val index = "$server.$group"
|
||||
database.delete(lastDeletionServerIDCache,"$lastDeletionServerIDCacheIndex = ?", wrap(index))
|
||||
database.delete(lastDeletionServerIDTable,"$lastDeletionServerIDTableIndex = ?", wrap(index))
|
||||
}
|
||||
|
||||
fun getUserCount(group: Long, server: String): Int? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val index = "$server.$group"
|
||||
return database.get(userCountTable, "$publicChatID = ?", wrap(index)) { cursor ->
|
||||
cursor.getInt(userCount)
|
||||
}?.toInt()
|
||||
}
|
||||
|
||||
override fun setUserCount(group: Long, server: String, newValue: Int) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val index = "$server.$group"
|
||||
val row = wrap(mapOf( publicChatID to index, Companion.userCount to newValue.toString() ))
|
||||
database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index))
|
||||
}
|
||||
|
||||
override fun getSessionRequestSentTimestamp(publicKey: String): Long? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(sessionRequestSentTimestampTable, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor ->
|
||||
cursor.getInt(LokiAPIDatabase.timestamp)
|
||||
}?.toLong()
|
||||
}
|
||||
|
||||
override fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val row = wrap(mapOf( LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to newValue.toString() ))
|
||||
database.insertOrUpdate(sessionRequestSentTimestampTable, row, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey))
|
||||
}
|
||||
|
||||
override fun getSessionRequestProcessedTimestamp(publicKey: String): Long? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(sessionRequestProcessedTimestampTable, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor ->
|
||||
cursor.getInt(LokiAPIDatabase.timestamp)
|
||||
}?.toLong()
|
||||
}
|
||||
|
||||
override fun setSessionRequestProcessedTimestamp(publicKey: String, newValue: Long) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val row = wrap(mapOf(LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to newValue.toString()))
|
||||
database.insertOrUpdate(sessionRequestProcessedTimestampTable, row, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey))
|
||||
}
|
||||
|
||||
override fun getOpenGroupPublicKey(server: String): String? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(openGroupPublicKeyTable, "${LokiAPIDatabase.server} = ?", wrap(server)) { cursor ->
|
||||
cursor.getString(LokiAPIDatabase.publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setOpenGroupPublicKey(server: String, newValue: String) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val row = wrap(mapOf( LokiAPIDatabase.server to server, LokiAPIDatabase.publicKey to newValue ))
|
||||
database.insertOrUpdate(openGroupPublicKeyTable, row, "${LokiAPIDatabase.server} = ?", wrap(server))
|
||||
}
|
||||
|
||||
// region Deprecated
|
||||
override fun getDeviceLinks(publicKey: String): Set<DeviceLink> {
|
||||
return setOf()
|
||||
/*
|
||||
@ -330,60 +383,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
database.delete(deviceLinkCache, "$masterPublicKey = ? OR $slavePublicKey = ?", arrayOf( deviceLink.masterPublicKey, deviceLink.slavePublicKey ))
|
||||
*/
|
||||
}
|
||||
|
||||
fun getUserCount(group: Long, server: String): Int? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val index = "$server.$group"
|
||||
return database.get(userCountCache, "$publicChatID = ?", wrap(index)) { cursor ->
|
||||
cursor.getInt(userCount)
|
||||
}?.toInt()
|
||||
}
|
||||
|
||||
override fun setUserCount(group: Long, server: String, newValue: Int) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val index = "$server.$group"
|
||||
val row = wrap(mapOf(publicChatID to index, Companion.userCount to newValue.toString()))
|
||||
database.insertOrUpdate(userCountCache, row, "$publicChatID = ?", wrap(index))
|
||||
}
|
||||
|
||||
override fun getSessionRequestSentTimestamp(publicKey: String): Long? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(sessionRequestSentTimestampCache, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor ->
|
||||
cursor.getInt(LokiAPIDatabase.timestamp)
|
||||
}?.toLong()
|
||||
}
|
||||
|
||||
override fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val row = wrap(mapOf(LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to newValue.toString()))
|
||||
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, newValue: Long) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val row = wrap(mapOf(LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to newValue.toString()))
|
||||
database.insertOrUpdate(sessionRequestProcessedTimestampCache, row, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey))
|
||||
}
|
||||
|
||||
override fun getOpenGroupPublicKey(server: String): String? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(openGroupPublicKeyDB, "${LokiAPIDatabase.server} = ?", wrap(server)) { cursor ->
|
||||
cursor.getString(LokiAPIDatabase.publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setOpenGroupPublicKey(server: String, newValue: String) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val row = wrap(mapOf(LokiAPIDatabase.server to server, LokiAPIDatabase.publicKey to newValue))
|
||||
database.insertOrUpdate(openGroupPublicKeyDB, row, "${LokiAPIDatabase.server} = ?", wrap(server))
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Convenience
|
||||
|
@ -0,0 +1,115 @@
|
||||
package org.thoughtcrime.securesms.loki.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.loki.utilities.*
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchet
|
||||
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey
|
||||
import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol
|
||||
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
|
||||
|
||||
class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), SharedSenderKeysDatabaseProtocol {
|
||||
|
||||
companion object {
|
||||
// Shared
|
||||
private val closedGroupPublicKey = "closed_group_public_key"
|
||||
// Ratchets
|
||||
private val closedGroupRatchetTable = "closed_group_ratchet_table"
|
||||
private val senderPublicKey = "sender_public_key"
|
||||
private val chainKey = "chain_key"
|
||||
private val keyIndex = "key_index"
|
||||
private val messageKeys = "message_keys"
|
||||
@JvmStatic val createClosedGroupRatchetTableCommand
|
||||
= "CREATE TABLE $closedGroupRatchetTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " +
|
||||
"$keyIndex INTEGER DEFAULT 0, $messageKeys TEXT, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));"
|
||||
// Private keys
|
||||
private val closedGroupPrivateKeyTable = "closed_group_private_key_table"
|
||||
private val closedGroupPrivateKey = "closed_group_private_key"
|
||||
@JvmStatic val createClosedGroupPrivateKeyTableCommand
|
||||
= "CREATE TABLE $closedGroupPrivateKeyTable ($closedGroupPublicKey STRING PRIMARY KEY, $closedGroupPrivateKey STRING);"
|
||||
}
|
||||
|
||||
// region Ratchets & Sender Keys
|
||||
override fun getClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String): ClosedGroupRatchet? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?"
|
||||
return database.get(closedGroupRatchetTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor ->
|
||||
val chainKey = cursor.getString(Companion.chainKey)
|
||||
val keyIndex = cursor.getInt(Companion.keyIndex)
|
||||
val messageKeys = cursor.getString(Companion.messageKeys).split("-")
|
||||
ClosedGroupRatchet(chainKey, keyIndex, messageKeys)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val values = ContentValues()
|
||||
values.put(Companion.closedGroupPublicKey, groupPublicKey)
|
||||
values.put(Companion.senderPublicKey, senderPublicKey)
|
||||
values.put(Companion.chainKey, ratchet.chainKey)
|
||||
values.put(Companion.keyIndex, ratchet.keyIndex)
|
||||
values.put(Companion.messageKeys, ratchet.messageKeys.joinToString("-"))
|
||||
val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?"
|
||||
database.insertOrUpdate(closedGroupRatchetTable, values, query, arrayOf( groupPublicKey, senderPublicKey ))
|
||||
}
|
||||
|
||||
override fun removeAllClosedGroupRatchets(groupPublicKey: String) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
database.delete(closedGroupRatchetTable, null, null)
|
||||
}
|
||||
|
||||
override fun getAllClosedGroupSenderKeys(groupPublicKey: String): Set<ClosedGroupSenderKey> {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?"
|
||||
return database.getAll(closedGroupRatchetTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor ->
|
||||
val chainKey = cursor.getString(Companion.chainKey)
|
||||
val keyIndex = cursor.getInt(Companion.keyIndex)
|
||||
val senderPublicKey = cursor.getString(Companion.senderPublicKey)
|
||||
ClosedGroupSenderKey(Hex.fromStringCondensed(chainKey), keyIndex, Hex.fromStringCondensed(senderPublicKey))
|
||||
}.toSet()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Public & Private Keys
|
||||
override fun getClosedGroupPrivateKey(groupPublicKey: String): String? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val query = "${Companion.closedGroupPublicKey} = ?"
|
||||
return database.get(closedGroupPrivateKeyTable, query, arrayOf( groupPublicKey )) { cursor ->
|
||||
cursor.getString(Companion.closedGroupPrivateKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setClosedGroupPrivateKey(groupPublicKey: String, groupPrivateKey: String) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val values = ContentValues()
|
||||
values.put(Companion.closedGroupPublicKey, groupPublicKey)
|
||||
values.put(Companion.closedGroupPrivateKey, groupPrivateKey)
|
||||
val query = "${Companion.closedGroupPublicKey} = ?"
|
||||
database.insertOrUpdate(closedGroupPrivateKeyTable, values, query, arrayOf( groupPublicKey ))
|
||||
}
|
||||
|
||||
override fun removeClosedGroupPrivateKey(groupPublicKey: String) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val query = "${Companion.closedGroupPublicKey} = ?"
|
||||
database.delete(closedGroupPrivateKeyTable, query, arrayOf( groupPublicKey ))
|
||||
}
|
||||
|
||||
override fun getAllClosedGroupPublicKeys(): Set<String> {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.getAll(closedGroupPrivateKeyTable, null, null) { cursor ->
|
||||
cursor.getString(Companion.closedGroupPublicKey)
|
||||
}.filter {
|
||||
PublicKeyValidation.isValid(it)
|
||||
}.toSet()
|
||||
}
|
||||
// endregion
|
||||
|
||||
override fun isSSKBasedClosedGroup(groupPublicKey: String): Boolean {
|
||||
if (!PublicKeyValidation.isValid(groupPublicKey)) { return false }
|
||||
return getAllClosedGroupPublicKeys().contains(groupPublicKey)
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -15,7 +15,7 @@ import nl.komponents.kovenant.functional.bind
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
|
||||
import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.QRCodeUtilities
|
||||
import org.thoughtcrime.securesms.loki.utilities.toPx
|
||||
|
@ -0,0 +1,181 @@
|
||||
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.crypto.storage.SignalProtocolStoreImpl
|
||||
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.loki.utilities.recipient
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey
|
||||
import org.whispersystems.signalservice.loki.protocol.meta.TTLUtilities
|
||||
import org.whispersystems.signalservice.loki.utilities.toHexString
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ClosedGroupUpdateMessageSendJob private constructor(parameters: Parameters, private val destination: String, private val kind: Kind) : BaseJob(parameters) {
|
||||
|
||||
sealed class Kind {
|
||||
class New(val groupPublicKey: ByteArray, val name: String, val groupPrivateKey: ByteArray, val senderKeys: Collection<ClosedGroupSenderKey>, val members: Collection<ByteArray>, val admins: Collection<ByteArray>) : Kind()
|
||||
class Info(val groupPublicKey: ByteArray, val name: String, val senderKeys: Collection<ClosedGroupSenderKey>, val members: Collection<ByteArray>, val admins: Collection<ByteArray>) : Kind()
|
||||
class SenderKeyRequest(val groupPublicKey: ByteArray) : Kind()
|
||||
class SenderKey(val groupPublicKey: ByteArray, val senderKey: ClosedGroupSenderKey) : Kind()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY = "ClosedGroupUpdateMessageSendJob"
|
||||
}
|
||||
|
||||
constructor(destination: String, kind: Kind) : this(Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(1)
|
||||
.build(),
|
||||
destination,
|
||||
kind)
|
||||
|
||||
override fun getFactoryKey(): String { return KEY }
|
||||
|
||||
override fun serialize(): Data {
|
||||
val builder = Data.Builder()
|
||||
builder.putString("destination", destination)
|
||||
when (kind) {
|
||||
is Kind.New -> {
|
||||
builder.putString("kind", "New")
|
||||
builder.putByteArray("groupPublicKey", kind.groupPublicKey)
|
||||
builder.putString("name", kind.name)
|
||||
builder.putByteArray("groupPrivateKey", kind.groupPrivateKey)
|
||||
val senderKeys = kind.senderKeys.joinToString(" - ") { it.toJSON() }
|
||||
builder.putString("senderKeys", senderKeys)
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
val admins = kind.admins.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("admins", admins)
|
||||
}
|
||||
is Kind.Info -> {
|
||||
builder.putString("kind", "Info")
|
||||
builder.putByteArray("groupPublicKey", kind.groupPublicKey)
|
||||
builder.putString("name", kind.name)
|
||||
val senderKeys = kind.senderKeys.joinToString(" - ") { it.toJSON() }
|
||||
builder.putString("senderKeys", senderKeys)
|
||||
val members = kind.members.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("members", members)
|
||||
val admins = kind.admins.joinToString(" - ") { it.toHexString() }
|
||||
builder.putString("admins", admins)
|
||||
}
|
||||
is Kind.SenderKeyRequest -> {
|
||||
builder.putString("kind", "SenderKeyRequest")
|
||||
builder.putByteArray("groupPublicKey", kind.groupPublicKey)
|
||||
}
|
||||
is Kind.SenderKey -> {
|
||||
builder.putString("kind", "SenderKey")
|
||||
builder.putByteArray("groupPublicKey", kind.groupPublicKey)
|
||||
builder.putString("senderKey", kind.senderKey.toJSON())
|
||||
}
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
public override fun onRun() {
|
||||
val contentMessage = SignalServiceProtos.Content.newBuilder()
|
||||
val dataMessage = SignalServiceProtos.DataMessage.newBuilder()
|
||||
val closedGroupUpdate = SignalServiceProtos.ClosedGroupUpdate.newBuilder()
|
||||
when (kind) {
|
||||
is Kind.New -> {
|
||||
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.NEW
|
||||
closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey)
|
||||
closedGroupUpdate.name = kind.name
|
||||
closedGroupUpdate.groupPrivateKey = ByteString.copyFrom(kind.groupPrivateKey)
|
||||
closedGroupUpdate.addAllSenderKeys(kind.senderKeys.map { it.toProto() })
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.Info -> {
|
||||
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.INFO
|
||||
closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey)
|
||||
closedGroupUpdate.name = kind.name
|
||||
closedGroupUpdate.addAllSenderKeys(kind.senderKeys.map { it.toProto() })
|
||||
closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) })
|
||||
closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) })
|
||||
}
|
||||
is Kind.SenderKeyRequest -> {
|
||||
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST
|
||||
closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey)
|
||||
}
|
||||
is Kind.SenderKey -> {
|
||||
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY
|
||||
closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey)
|
||||
closedGroupUpdate.addAllSenderKeys(listOf( kind.senderKey.toProto() ))
|
||||
}
|
||||
}
|
||||
dataMessage.closedGroupUpdate = closedGroupUpdate.build()
|
||||
contentMessage.dataMessage = dataMessage.build()
|
||||
val serializedContentMessage = contentMessage.build().toByteArray()
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(destination)
|
||||
val recipient = recipient(context, destination)
|
||||
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
|
||||
val ttl = TTLUtilities.getTTL(TTLUtilities.MessageType.ClosedGroupUpdate)
|
||||
val useFallbackEncryption = SignalProtocolStoreImpl(context).containsSession(SignalProtocolAddress(destination, 1))
|
||||
try {
|
||||
// isClosedGroup can always be false as it's only used in the context of legacy closed groups
|
||||
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
|
||||
Date().time, serializedContentMessage, false, ttl, false,
|
||||
useFallbackEncryption, false, false)
|
||||
} catch (e: Exception) {
|
||||
Log.d("Loki", "Failed to send closed group update message to: $destination 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<ClosedGroupUpdateMessageSendJob> {
|
||||
|
||||
override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJob {
|
||||
val destination = data.getString("destination")
|
||||
val rawKind = data.getString("kind")
|
||||
val groupPublicKey = data.getByteArray("groupPublicKey")
|
||||
val kind: Kind
|
||||
when (rawKind) {
|
||||
"New" -> {
|
||||
val name = data.getString("name")
|
||||
val groupPrivateKey = data.getByteArray("groupPrivateKey")
|
||||
val senderKeys = data.getString("senderKeys").split(" - ").map { ClosedGroupSenderKey.fromJSON(it)!! }
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.New(groupPublicKey, name, groupPrivateKey, senderKeys, members, admins)
|
||||
}
|
||||
"Info" -> {
|
||||
val name = data.getString("name")
|
||||
val senderKeys = data.getStringOrDefault("senderKeys", "").split(" - ").mapNotNull { ClosedGroupSenderKey.fromJSON(it) } // Can be empty
|
||||
val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) }
|
||||
kind = Kind.Info(groupPublicKey, name, senderKeys, members, admins)
|
||||
}
|
||||
"SenderKeyRequest" -> {
|
||||
kind = Kind.SenderKeyRequest(groupPublicKey)
|
||||
}
|
||||
"SenderKey" -> {
|
||||
val senderKey = ClosedGroupSenderKey.fromJSON(data.getString("senderKey"))!!
|
||||
kind = Kind.SenderKey(groupPublicKey, senderKey)
|
||||
}
|
||||
else -> throw Exception("Invalid closed group update message kind: $rawKind.")
|
||||
}
|
||||
return ClosedGroupUpdateMessageSendJob(parameters, destination, kind)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,62 +1,402 @@
|
||||
package org.thoughtcrime.securesms.loki.protocol
|
||||
|
||||
import android.content.Context
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import android.util.Log
|
||||
import com.google.protobuf.ByteString
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.utilities.recipient
|
||||
import org.thoughtcrime.securesms.loki.utilities.timeout
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.sms.IncomingGroupMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.util.GroupUtil
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent
|
||||
import org.whispersystems.libsignal.ecc.Curve
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
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.FileServerAPI
|
||||
import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup.GroupType
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext
|
||||
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchet
|
||||
import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey
|
||||
import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementation
|
||||
import org.whispersystems.signalservice.loki.utilities.hexEncodedPrivateKey
|
||||
import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey
|
||||
import org.whispersystems.signalservice.loki.utilities.toHexString
|
||||
import java.util.*
|
||||
|
||||
object ClosedGroupsProtocol {
|
||||
val isSharedSenderKeysEnabled = false
|
||||
|
||||
public fun createClosedGroup(context: Context, name: String, members: Collection<String>): String {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
// Generate a key pair for the group
|
||||
val groupKeyPair = Curve.generateKeyPair()
|
||||
val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix
|
||||
val membersAsData = members.map { Hex.fromStringCondensed(it) }
|
||||
// Create ratchets for all members
|
||||
val senderKeys: List<ClosedGroupSenderKey> = members.map { publicKey ->
|
||||
val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey)
|
||||
ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey))
|
||||
}
|
||||
// Create the group
|
||||
val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false)
|
||||
val admins = setOf( userPublicKey )
|
||||
DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList<Address>(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList<Address>(admins.map { Address.fromSerialized(it) }))
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
|
||||
// Establish sessions if needed
|
||||
establishSessionsWithMembersIfNeeded(context, members)
|
||||
// Send a closed group update message to all members using established channels
|
||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(),
|
||||
senderKeys, membersAsData, adminsAsData)
|
||||
for (member in members) {
|
||||
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
}
|
||||
// TODO: Wait for the messages to finish sending
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
DatabaseFactory.getSSKDatabase(context).setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey)
|
||||
// Notify the user
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
|
||||
// Return
|
||||
return groupID
|
||||
}
|
||||
|
||||
public fun addMembers(context: Context, newMembers: Collection<String>, groupPublicKey: String) {
|
||||
// Prepare
|
||||
val sskDatabase = DatabaseFactory.getSSKDatabase(context)
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't add users to nonexistent closed group.")
|
||||
return
|
||||
}
|
||||
val name = group.title
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||
val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey)
|
||||
if (groupPrivateKey == null) {
|
||||
Log.d("Loki", "Couldn't get private key for closed group.")
|
||||
return
|
||||
}
|
||||
// Add the members to the member list
|
||||
val members = group.members.map { it.serialize() }.toMutableSet()
|
||||
members.addAll(newMembers)
|
||||
val membersAsData = members.map { Hex.fromStringCondensed(it) }
|
||||
// Generate ratchets for the new members
|
||||
val senderKeys: List<ClosedGroupSenderKey> = newMembers.map { publicKey ->
|
||||
val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey)
|
||||
ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey))
|
||||
}
|
||||
// Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group)
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name,
|
||||
senderKeys, membersAsData, adminsAsData)
|
||||
val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
// Establish sessions if needed
|
||||
establishSessionsWithMembersIfNeeded(context, newMembers)
|
||||
// Send closed group update messages to the new members using established channels
|
||||
val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + senderKeys
|
||||
for (member in members) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name,
|
||||
Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
}
|
||||
// Update the group
|
||||
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
// Notify the user
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks the calling thread.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun shouldIgnoreContentMessage(context: Context, conversation: Recipient, groupID: String?, content: SignalServiceContent): Boolean {
|
||||
if (!conversation.address.isClosedGroup || groupID == null) { return false }
|
||||
// A closed group's members should never include slave devices
|
||||
val senderPublicKey = content.sender
|
||||
public fun leave(context: Context, groupPublicKey: String) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
removeMembers(context, setOf( userPublicKey ), groupPublicKey)
|
||||
}
|
||||
|
||||
public fun removeMembers(context: Context, membersToRemove: Collection<String>, groupPublicKey: String) {
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val sskDatabase = DatabaseFactory.getSSKDatabase(context)
|
||||
val isUserLeaving = membersToRemove.contains(userPublicKey)
|
||||
if (isUserLeaving && membersToRemove.count() != 1) {
|
||||
Log.d("Loki", "Can't remove self and others simultaneously.")
|
||||
return
|
||||
}
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Can't add users to nonexistent closed group.")
|
||||
return
|
||||
}
|
||||
val name = group.title
|
||||
val admins = group.admins.map { it.serialize() }
|
||||
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
|
||||
// Remove the members from the member list
|
||||
val members = group.members.map { it.serialize() }.toSet().minus(membersToRemove)
|
||||
val membersAsData = members.map { Hex.fromStringCondensed(it) }
|
||||
// Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually)
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey),
|
||||
name, setOf(), membersAsData, adminsAsData)
|
||||
val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind)
|
||||
job.setContext(context)
|
||||
job.onRun() // Run the job immediately
|
||||
// Delete all ratchets (it's important that this happens after sending out the update)
|
||||
sskDatabase.removeAllClosedGroupRatchets(groupPublicKey)
|
||||
// Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and
|
||||
// send it out to all members (minus the removed ones) using established channels.
|
||||
if (isUserLeaving) {
|
||||
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
|
||||
groupDB.setActive(groupID, false)
|
||||
} else {
|
||||
// Establish sessions if needed
|
||||
establishSessionsWithMembersIfNeeded(context, members)
|
||||
// Send out the user's new ratchet to all members (minus the removed ones) using established channels
|
||||
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
|
||||
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
|
||||
for (member in members) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
}
|
||||
}
|
||||
// Update the group
|
||||
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
// Notify the user
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun requestSenderKey(context: Context, groupPublicKey: String, senderPublicKey: String) {
|
||||
// Establish session if needed
|
||||
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey)
|
||||
// Send the request
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKeyRequest(Hex.fromStringCondensed(groupPublicKey))
|
||||
val job = ClosedGroupUpdateMessageSendJob(senderPublicKey, closedGroupUpdateKind)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
public fun handleSharedSenderKeysUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) {
|
||||
if (!isValid(closedGroupUpdate)) { return; }
|
||||
when (closedGroupUpdate.type) {
|
||||
SignalServiceProtos.ClosedGroupUpdate.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate)
|
||||
SignalServiceProtos.ClosedGroupUpdate.Type.INFO -> handleClosedGroupUpdate(context, closedGroupUpdate, senderPublicKey)
|
||||
SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST -> handleSenderKeyRequest(context, closedGroupUpdate, senderPublicKey)
|
||||
SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY -> handleSenderKey(context, closedGroupUpdate, senderPublicKey)
|
||||
else -> {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValid(closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate): Boolean {
|
||||
if (closedGroupUpdate.groupPublicKey.isEmpty) { return false }
|
||||
when (closedGroupUpdate.type) {
|
||||
SignalServiceProtos.ClosedGroupUpdate.Type.NEW -> {
|
||||
return !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.groupPrivateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty
|
||||
&& closedGroupUpdate.senderKeysCount > 0 && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0
|
||||
}
|
||||
SignalServiceProtos.ClosedGroupUpdate.Type.INFO -> {
|
||||
return !closedGroupUpdate.name.isNullOrEmpty() && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0 // senderKeys may be empty
|
||||
}
|
||||
SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY -> return true
|
||||
SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST -> return closedGroupUpdate.senderKeysCount > 0
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
public fun handleNewClosedGroup(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate) {
|
||||
// Prepare
|
||||
val sskDatabase = DatabaseFactory.getSSKDatabase(context)
|
||||
// Unwrap the message
|
||||
val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString()
|
||||
val name = closedGroupUpdate.name
|
||||
val groupPrivateKey = closedGroupUpdate.groupPrivateKey.toByteArray()
|
||||
val senderKeys = closedGroupUpdate.senderKeysList.map {
|
||||
ClosedGroupSenderKey(it.chainKey.toByteArray(), it.keyIndex, it.publicKey.toByteArray())
|
||||
}
|
||||
val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
|
||||
val admins = closedGroupUpdate.adminsList.map { it.toByteArray().toHexString() }
|
||||
// Persist the ratchets
|
||||
senderKeys.forEach { senderKey ->
|
||||
if (!members.contains(senderKey.publicKey.toHexString())) { return@forEach }
|
||||
val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf())
|
||||
sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet)
|
||||
}
|
||||
// Create the group
|
||||
val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false)
|
||||
DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList<Address>(members.map { Address.fromSerialized(it) }),
|
||||
null, null, LinkedList<Address>(admins.map { Address.fromSerialized(it) }))
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString())
|
||||
// Notify the user
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertIncomingInfoMessage(context, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID)
|
||||
// Establish sessions if needed
|
||||
establishSessionsWithMembersIfNeeded(context, members)
|
||||
}
|
||||
|
||||
public fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val sskDatabase = DatabaseFactory.getSSKDatabase(context)
|
||||
// Unwrap the message
|
||||
val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString()
|
||||
val name = closedGroupUpdate.name
|
||||
val senderKeys = closedGroupUpdate.senderKeysList.map {
|
||||
ClosedGroupSenderKey(it.chainKey.toByteArray(), it.keyIndex, it.publicKey.toByteArray())
|
||||
}
|
||||
val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() }
|
||||
val admins = closedGroupUpdate.adminsList.map { it.toByteArray().toHexString() }
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
|
||||
return
|
||||
}
|
||||
val oldMembers = group.members.map { it.serialize() }
|
||||
// Check that the sender is a member of the group (before the update)
|
||||
if (!oldMembers.contains(senderPublicKey)) {
|
||||
Log.d("Loki", "Ignoring closed group info message from non-member.")
|
||||
return
|
||||
}
|
||||
// Store the ratchets for any new members (it's important that this happens before the code below)
|
||||
senderKeys.forEach { senderKey ->
|
||||
if (!members.contains(senderKey.publicKey.toHexString())) { return@forEach }
|
||||
val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf())
|
||||
sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet)
|
||||
}
|
||||
// Delete all ratchets and either:
|
||||
// • Send out the user's new ratchet using established channels if other members of the group left or were removed
|
||||
// • Remove the group from the user's set of public keys to poll for if the current user was among the members that were removed
|
||||
val wasCurrentUserRemoved = !members.contains(userPublicKey)
|
||||
val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet()
|
||||
if (wasAnyUserRemoved) {
|
||||
sskDatabase.removeAllClosedGroupRatchets(groupPublicKey)
|
||||
if (wasCurrentUserRemoved) {
|
||||
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
|
||||
groupDB.setActive(groupID, false)
|
||||
} else {
|
||||
establishSessionsWithMembersIfNeeded(context, members)
|
||||
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
|
||||
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
|
||||
for (member in members) {
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
|
||||
val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update the group
|
||||
groupDB.updateTitle(groupID, name)
|
||||
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
|
||||
// Notify the user
|
||||
val type0 = if (wasAnyUserRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
|
||||
val type1 = if (wasAnyUserRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
|
||||
insertIncomingInfoMessage(context, groupID, type0, type1, name, members, admins, threadID)
|
||||
}
|
||||
|
||||
public fun handleSenderKeyRequest(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) {
|
||||
// Prepare
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString()
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group sender key request for nonexistent group.")
|
||||
return
|
||||
}
|
||||
// Check that the requesting user is a member of the group
|
||||
if (!group.members.map { it.serialize() }.contains(senderPublicKey)) {
|
||||
Log.d("Loki", "Ignoring closed group sender key request from non-member.")
|
||||
return
|
||||
}
|
||||
// Respond to the request
|
||||
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey)
|
||||
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
|
||||
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
|
||||
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
|
||||
val job = ClosedGroupUpdateMessageSendJob(senderPublicKey, closedGroupUpdateKind)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
}
|
||||
|
||||
public fun handleSenderKey(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) {
|
||||
// Prepare
|
||||
val sskDatabase = DatabaseFactory.getSSKDatabase(context)
|
||||
val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString()
|
||||
val groupDB = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false)
|
||||
val group = groupDB.getGroup(groupID).orNull()
|
||||
if (group == null) {
|
||||
Log.d("Loki", "Ignoring closed group sender key for nonexistent group.")
|
||||
return
|
||||
}
|
||||
val senderKeyProto = closedGroupUpdate.senderKeysList.firstOrNull()
|
||||
if (senderKeyProto == null) {
|
||||
Log.d("Loki", "Ignoring invalid closed group sender key.")
|
||||
return
|
||||
}
|
||||
val senderKey = ClosedGroupSenderKey(senderKeyProto.chainKey.toByteArray(), senderKeyProto.keyIndex, senderKeyProto.publicKey.toByteArray())
|
||||
// Check that the sending user is a member of the group
|
||||
if (!group.members.map { it.serialize() }.contains(senderPublicKey)) {
|
||||
Log.d("Loki", "Ignoring closed group sender key from non-member.")
|
||||
return
|
||||
}
|
||||
if (senderKeyProto.publicKey.toByteArray().toHexString() != senderPublicKey) {
|
||||
Log.d("Loki", "Ignoring invalid closed group sender key.")
|
||||
return
|
||||
}
|
||||
// Store the sender key
|
||||
val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf())
|
||||
sskDatabase.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun shouldIgnoreContentMessage(context: Context, address: Address, groupID: String?, senderPublicKey: String): Boolean {
|
||||
if (!address.isClosedGroup || groupID == null) { return false }
|
||||
/*
|
||||
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)
|
||||
return !members.contains(recipient(context, publicKeyToCheckFor))
|
||||
return !members.contains(recipient(context, senderPublicKey))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun shouldIgnoreGroupCreatedMessage(context: Context, group: SignalServiceGroup): Boolean {
|
||||
val members = group.members
|
||||
val masterPublicKeyOrNull = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
|
||||
val masterPublicKey = masterPublicKeyOrNull ?: TextSecurePreferences.getLocalNumber(context)
|
||||
return !members.isPresent || !members.get().contains(masterPublicKey)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDestinations(groupID: String, context: Context): Promise<List<Address>, Exception> {
|
||||
if (GroupUtil.isRSSFeed(groupID)) { return Promise.of(listOf()) }
|
||||
fun getMessageDestinations(context: Context, groupID: String): List<Address> {
|
||||
if (GroupUtil.isRSSFeed(groupID)) { return listOf() }
|
||||
if (GroupUtil.isOpenGroup(groupID)) {
|
||||
val result = mutableListOf<Address>()
|
||||
result.add(Address.fromSerialized(groupID))
|
||||
return Promise.of(result)
|
||||
return listOf( Address.fromSerialized(groupID) )
|
||||
} else {
|
||||
// A closed group's members should never include slave devices
|
||||
val members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false)
|
||||
val groupPublicKey = GroupUtil.getDecodedId(groupID).toHexString()
|
||||
if (DatabaseFactory.getSSKDatabase(context).isSSKBasedClosedGroup(groupPublicKey)) {
|
||||
return listOf( Address.fromSerialized(groupPublicKey) )
|
||||
} else {
|
||||
return DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false).map { it.address }
|
||||
}
|
||||
/*
|
||||
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) }
|
||||
@ -71,29 +411,33 @@ object ClosedGroupsProtocol {
|
||||
}
|
||||
result.toList()
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun leaveGroup(context: Context, recipient: Recipient): Boolean {
|
||||
fun leaveLegacyGroup(context: Context, recipient: Recipient): Boolean {
|
||||
if (!recipient.address.isClosedGroup) { return true }
|
||||
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
|
||||
val message = GroupUtil.createGroupLeaveMessage(context, recipient)
|
||||
if (threadID < 0 || !message.isPresent) { return false }
|
||||
MessageSender.send(context, message.get(), threadID, false, null)
|
||||
// Remove the master device from the group (a closed group's members should never include slave devices)
|
||||
val message = GroupUtil.createGroupLeaveMessage(context, recipient).orNull()
|
||||
if (threadID < 0 || message == null) { return false }
|
||||
MessageSender.send(context, message, threadID, false, null)
|
||||
/*
|
||||
val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
|
||||
val publicKeyToRemove = masterPublicKey ?: TextSecurePreferences.getLocalNumber(context)
|
||||
*/
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val groupDatabase = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupID = recipient.address.toGroupString()
|
||||
groupDatabase.setActive(groupID, false)
|
||||
groupDatabase.remove(groupID, Address.fromSerialized(publicKeyToRemove))
|
||||
groupDatabase.remove(groupID, Address.fromSerialized(userPublicKey))
|
||||
return true
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun establishSessionsWithMembersIfNeeded(context: Context, members: List<String>) {
|
||||
// A closed group's members should never include slave devices
|
||||
fun establishSessionsWithMembersIfNeeded(context: Context, members: Collection<String>) {
|
||||
@Suppress("NAME_SHADOWING") val members = members.toMutableSet()
|
||||
/*
|
||||
val allDevices = members.flatMap { member ->
|
||||
MultiDeviceProtocol.shared.getAllLinkedDevices(member)
|
||||
}.toMutableSet()
|
||||
@ -101,12 +445,43 @@ object ClosedGroupsProtocol {
|
||||
if (userMasterPublicKey != null && allDevices.contains(userMasterPublicKey)) {
|
||||
allDevices.remove(userMasterPublicKey)
|
||||
}
|
||||
*/
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
if (userPublicKey != null && allDevices.contains(userPublicKey)) {
|
||||
allDevices.remove(userPublicKey)
|
||||
if (userPublicKey != null && members.contains(userPublicKey)) {
|
||||
members.remove(userPublicKey)
|
||||
}
|
||||
for (device in allDevices) {
|
||||
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(device)
|
||||
for (member in members) {
|
||||
ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(member)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertIncomingInfoMessage(context: Context, groupID: String, type0: GroupContext.Type, type1: SignalServiceGroup.Type, name: String,
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long) {
|
||||
val groupContextBuilder = GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupID)))
|
||||
.setType(type0)
|
||||
.setName(name)
|
||||
.addAllMembers(members)
|
||||
.addAllAdmins(admins)
|
||||
val group = SignalServiceGroup(type1, GroupUtil.getDecodedId(groupID), GroupType.SIGNAL, name, members.toList(), null, admins.toList())
|
||||
val m = IncomingTextMessage(Address.fromSerialized(groupID), 1, System.currentTimeMillis(), "", Optional.of(group), 0, true)
|
||||
val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "")
|
||||
val smsDB = DatabaseFactory.getSmsDatabase(context)
|
||||
smsDB.insertMessageInbox(infoMessage)
|
||||
}
|
||||
|
||||
private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String,
|
||||
members: Collection<String>, admins: Collection<String>, threadID: Long) {
|
||||
val recipient = Recipient.from(context, Address.fromSerialized(groupID), false)
|
||||
val groupContextBuilder = GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupID)))
|
||||
.setType(type)
|
||||
.setName(name)
|
||||
.addAllMembers(members)
|
||||
.addAllAdmins(admins)
|
||||
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf())
|
||||
val mmsDB = DatabaseFactory.getMmsDatabase(context)
|
||||
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
|
||||
mmsDB.markAsSent(infoMessageID, true)
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class PushNullMessageSendJob private constructor(parameters: Parameters, private val publicKey: String) : BaseJob(parameters) {
|
||||
class NullMessageSendJob private constructor(parameters: Parameters, private val publicKey: String) : BaseJob(parameters) {
|
||||
|
||||
companion object {
|
||||
const val KEY = "PushNullMessageSendJob"
|
||||
@ -70,12 +70,12 @@ class PushNullMessageSendJob private constructor(parameters: Parameters, private
|
||||
|
||||
override fun onCanceled() { }
|
||||
|
||||
class Factory : Job.Factory<PushNullMessageSendJob> {
|
||||
class Factory : Job.Factory<NullMessageSendJob> {
|
||||
|
||||
override fun create(parameters: Parameters, data: Data): PushNullMessageSendJob {
|
||||
override fun create(parameters: Parameters, data: Data): NullMessageSendJob {
|
||||
try {
|
||||
val publicKey = data.getString("publicKey")
|
||||
return PushNullMessageSendJob(parameters, publicKey)
|
||||
return NullMessageSendJob(parameters, publicKey)
|
||||
} catch (e: IOException) {
|
||||
throw AssertionError(e)
|
||||
}
|
@ -76,7 +76,7 @@ object SessionManagementProtocol {
|
||||
val preKeyBundle = preKeyBundleMessage.getPreKeyBundle(registrationID)
|
||||
lokiPreKeyBundleDatabase.setPreKeyBundle(publicKey, preKeyBundle)
|
||||
DatabaseFactory.getLokiAPIDatabase(context).setSessionRequestProcessedTimestamp(publicKey, Date().time)
|
||||
val job = PushNullMessageSendJob(publicKey)
|
||||
val job = NullMessageSendJob(publicKey)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ object SessionManagementProtocol {
|
||||
sessionStore.archiveAllSessions(content.sender)
|
||||
lokiThreadDB.setSessionResetStatus(content.sender, SessionResetStatus.REQUEST_RECEIVED)
|
||||
Log.d("Loki", "Sending an ephemeral message back to: ${content.sender}.")
|
||||
val job = PushNullMessageSendJob(content.sender)
|
||||
val job = NullMessageSendJob(content.sender)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
SecurityEvent.broadcastSecurityUpdateEvent(context)
|
||||
}
|
||||
|
@ -78,6 +78,11 @@ object SessionMetaProtocol {
|
||||
return !recipient.address.isRSSFeed
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun shouldSendDeliveryReceipt(address: Address): Boolean {
|
||||
return !address.isGroup
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be invoked for the recipient's master device.
|
||||
*/
|
||||
|
@ -19,20 +19,20 @@ 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) {
|
||||
class SessionRequestMessageSendJob 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)
|
||||
.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()
|
||||
@ -92,13 +92,13 @@ class PushSessionRequestMessageSendJob private constructor(parameters: Parameter
|
||||
}
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<PushSessionRequestMessageSendJob> {
|
||||
class Factory : Job.Factory<SessionRequestMessageSendJob> {
|
||||
|
||||
override fun create(parameters: Parameters, data: Data): PushSessionRequestMessageSendJob {
|
||||
override fun create(parameters: Parameters, data: Data): SessionRequestMessageSendJob {
|
||||
try {
|
||||
val publicKey = data.getString("publicKey")
|
||||
val timestamp = data.getLong("timestamp")
|
||||
return PushSessionRequestMessageSendJob(parameters, publicKey, timestamp)
|
||||
return SessionRequestMessageSendJob(parameters, publicKey, timestamp)
|
||||
} catch (e: IOException) {
|
||||
throw AssertionError(e)
|
||||
}
|
@ -19,7 +19,7 @@ class SessionResetImplementation(private val context: Context) : SessionResetPro
|
||||
|
||||
override fun onNewSessionAdopted(publicKey: String, oldSessionResetStatus: SessionResetStatus) {
|
||||
if (oldSessionResetStatus == SessionResetStatus.IN_PROGRESS) {
|
||||
val job = PushNullMessageSendJob(publicKey)
|
||||
val job = NullMessageSendJob(publicKey)
|
||||
ApplicationContext.getInstance(context).jobManager.add(job)
|
||||
}
|
||||
// TODO: Show session reset succeed message
|
||||
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.protocol
|
||||
package org.thoughtcrime.securesms.loki.protocol.shelved
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.protocol
|
||||
package org.thoughtcrime.securesms.loki.protocol.shelved
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.jobs.PushMediaSendJob
|
||||
import org.thoughtcrime.securesms.jobs.PushSendJob
|
||||
import org.thoughtcrime.securesms.jobs.PushTextSendJob
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol
|
||||
import org.thoughtcrime.securesms.loki.utilities.Broadcaster
|
||||
import org.thoughtcrime.securesms.loki.utilities.recipient
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@ -19,7 +20,6 @@ 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.FileServerAPI
|
||||
import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol
|
||||
import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.DeviceLink
|
||||
import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.DeviceLinkingSession
|
||||
import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki.protocol
|
||||
package org.thoughtcrime.securesms.loki.protocol.shelved
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
@ -28,7 +28,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInp
|
||||
import org.whispersystems.signalservice.loki.api.opengroups.PublicChat
|
||||
import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol
|
||||
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
|
||||
import java.util.*
|
||||
|
||||
object SyncMessagesProtocol {
|
||||
|
@ -5,7 +5,7 @@ import net.sqlcipher.Cursor
|
||||
import net.sqlcipher.database.SQLiteDatabase
|
||||
import org.whispersystems.signalservice.internal.util.Base64
|
||||
|
||||
fun <T> SQLiteDatabase.get(table: String, query: String, arguments: Array<String>, get: (Cursor) -> T): T? {
|
||||
fun <T> SQLiteDatabase.get(table: String, query: String?, arguments: Array<String>?, get: (Cursor) -> T): T? {
|
||||
var cursor: Cursor? = null
|
||||
try {
|
||||
cursor = query(table, null, query, arguments, null, null, null)
|
||||
@ -18,7 +18,7 @@ fun <T> SQLiteDatabase.get(table: String, query: String, arguments: Array<String
|
||||
return null
|
||||
}
|
||||
|
||||
fun <T> SQLiteDatabase.getAll(table: String, query: String, arguments: Array<String>, get: (Cursor) -> T): List<T> {
|
||||
fun <T> SQLiteDatabase.getAll(table: String, query: String?, arguments: Array<String>?, get: (Cursor) -> T): List<T> {
|
||||
val result = mutableListOf<T>()
|
||||
var cursor: Cursor? = null
|
||||
try {
|
||||
|
@ -213,7 +213,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateNotification(@NonNull Context context, long threadId, boolean signal)
|
||||
public void updateNotification(@NonNull Context context, long threadId, boolean signal)
|
||||
{
|
||||
boolean isVisible = visibleThread == threadId;
|
||||
|
||||
@ -221,7 +221,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
||||
Recipient recipients = DatabaseFactory.getThreadDatabase(context)
|
||||
.getRecipientForThreadId(threadId);
|
||||
|
||||
if (isVisible) {
|
||||
if (isVisible && recipients != null && SessionMetaProtocol.shouldSendReadReceipt(recipients.getAddress())) {
|
||||
List<MarkedMessageInfo> messageIds = threads.setRead(threadId, false);
|
||||
MarkReadReceiver.process(context, messageIds);
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol;
|
||||
import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol;
|
||||
|
||||
@ -105,7 +105,7 @@ public class MarkReadReceiver extends BroadcastReceiver {
|
||||
}
|
||||
}
|
||||
|
||||
private static void scheduleDeletion(Context context, ExpirationInfo expirationInfo) {
|
||||
public static void scheduleDeletion(Context context, ExpirationInfo expirationInfo) {
|
||||
if (expirationInfo.getExpiresIn() > 0 && expirationInfo.getExpireStarted() <= 0) {
|
||||
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user