Merge branch 'develop' into feature/bca/rust_flavor

This commit is contained in:
valere 2022-12-08 13:37:43 +01:00
commit 14cee226c5
161 changed files with 3588 additions and 811 deletions

View file

@ -1,3 +1,11 @@
Changes in Element 1.5.11 (2022-12-07)
======================================
Bugfixes 🐛
----------
- Fix crash when the network is not available. ([#7725](https://github.com/vector-im/element-android/issues/7725))
Changes in Element v1.5.10 (2022-11-30) Changes in Element v1.5.10 (2022-11-30)
======================================= =======================================

View file

@ -45,10 +45,10 @@ plugins {
// Detekt // Detekt
id "io.gitlab.arturbosch.detekt" version "1.22.0" id "io.gitlab.arturbosch.detekt" version "1.22.0"
// Ksp // Ksp
id "com.google.devtools.ksp" version "1.7.21-1.0.8" id "com.google.devtools.ksp" version "1.7.22-1.0.8"
// Dependency Analysis // Dependency Analysis
id 'com.autonomousapps.dependency-analysis' version "1.16.0" id 'com.autonomousapps.dependency-analysis' version "1.17.0"
// Gradle doctor // Gradle doctor
id "com.osacky.doctor" version "0.8.1" id "com.osacky.doctor" version "0.8.1"
} }

1
changelog.d/7274.bugfix Normal file
View file

@ -0,0 +1 @@
Fix bad pills color background. For light and dark theme the color is now 61708B (iso EleWeb)

1
changelog.d/7596.feature Normal file
View file

@ -0,0 +1 @@
Save m.local_notification_settings.<device-id> event in account_data

1
changelog.d/7632.feature Normal file
View file

@ -0,0 +1 @@
Update notifications setting when m.local_notification_settings.<device-id> event changes for current device

1
changelog.d/7653.bugfix Normal file
View file

@ -0,0 +1 @@
ANR when asking to select the notification method

1
changelog.d/7693.feature Normal file
View file

@ -0,0 +1 @@
[Session manager] Add action to signout all the other session

1
changelog.d/7695.bugfix Normal file
View file

@ -0,0 +1 @@
[Rich text editor] Add error tracking for rich text editor

1
changelog.d/7697.feature Normal file
View file

@ -0,0 +1 @@
[Session manager] Add actions to rename and signout current session

1
changelog.d/7708.misc Normal file
View file

@ -0,0 +1 @@
Add tracing Id for to device messages

1
changelog.d/7710.bugfix Normal file
View file

@ -0,0 +1 @@
Fix usage of unknown shield in room summary

1
changelog.d/7723.misc Normal file
View file

@ -0,0 +1 @@
Disable nightly popup and add an entry point in the advanced settings instead.

1
changelog.d/7725.bugfix Normal file
View file

@ -0,0 +1 @@
Fix crash when the network is not available.

View file

@ -8,7 +8,7 @@ ext.versions = [
def gradle = "7.3.1" def gradle = "7.3.1"
// Ref: https://kotlinlang.org/releases.html // Ref: https://kotlinlang.org/releases.html
def kotlin = "1.7.21" def kotlin = "1.7.22"
def kotlinCoroutines = "1.6.4" def kotlinCoroutines = "1.6.4"
def dagger = "2.44.2" def dagger = "2.44.2"
def appDistribution = "16.0.0-beta05" def appDistribution = "16.0.0-beta05"
@ -98,7 +98,7 @@ ext.libs = [
], ],
element : [ element : [
'opusencoder' : "io.element.android:opusencoder:1.1.0", 'opusencoder' : "io.element.android:opusencoder:1.1.0",
'wysiwyg' : "io.element.android:wysiwyg:0.7.0.1" 'wysiwyg' : "io.element.android:wysiwyg:0.9.0"
], ],
squareup : [ squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi", 'moshi' : "com.squareup.moshi:moshi:$moshi",

View file

@ -0,0 +1,2 @@
Hlavní změny v této verzi: Nová implementace celoobrazovkového režimu pro editor formátovaného textu a opravy chyb.
Úplný seznam změn: https://github.com/vector-im/element-android/releases

View file

@ -0,0 +1,2 @@
Die wichtigsten Änderungen in dieser Version: Der Vollbildmodus des Textverarbeitungseditors wurde neu umgesetzt und es wurden diverse Fehler behoben.
Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases

View file

@ -0,0 +1,2 @@
Main changes in this version: New implementation of the full screen mode for the Rich Text Editor and bugfixes.
Full changelog: https://github.com/vector-im/element-android/releases

View file

@ -0,0 +1,2 @@
Põhilised muutused selles versioonis: tekstitoimeti täisekraanivaade ja erinevate vigade parandused.
Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases

View file

@ -0,0 +1,2 @@
Perubahan utama dalam versi ini: Penerapan baru mode layar penuh untuk Penyunting Teks Kaya dan perbaikan kutu.
Catatan perubahan lanjutan: https://github.com/vector-im/element-android/releases

View file

@ -0,0 +1,2 @@
Hlavné zmeny v tejto verzii: Nová implementácia celo-obrazovkového režimu pre Rozšírený textový editor a opravy chýb.
Úplný zoznam zmien: https://github.com/vector-im/element-android/releases

View file

@ -0,0 +1,2 @@
Ndryshimet kryesore në këtë version: ndreqje të metash dhe përmirësime.
Regjistër i plotë ndryshimesh: https://github.com/vector-im/element-android/releases

View file

@ -0,0 +1,2 @@
Huvudsakliga ändringar i den här versionen: buggfixar och förbättringar.
Full ändringslogg: https://github.com/vector-im/element-android/releases

View file

@ -0,0 +1,2 @@
Основні зміни в цій версії: Нова реалізація повноекранного режиму для редактора розширеного тексту та виправлення помилок.
Перелік усіх змін: https://github.com/vector-im/element-android/releases

View file

@ -0,0 +1,2 @@
此版本中的主要變動:格式化文字編輯器的全螢幕模式新實作與臭蟲修復。
完整的變更紀錄https://github.com/vector-im/element-android/releases

View file

@ -2789,7 +2789,7 @@
<string name="key_authenticity_not_guaranteed">Pravost této šifrované zprávy nelze v tomto zařízení zaručit.</string> <string name="key_authenticity_not_guaranteed">Pravost této šifrované zprávy nelze v tomto zařízení zaručit.</string>
<string name="settings_security_incognito_keyboard_summary">Požadujte, aby klávesnice neaktualizovala žádné personalizované údaje, jako je historie psaní a slovník, na základě toho, co jste napsali v konverzacích. Upozorňujeme, že některé klávesnice nemusí toto nastavení respektovat.</string> <string name="settings_security_incognito_keyboard_summary">Požadujte, aby klávesnice neaktualizovala žádné personalizované údaje, jako je historie psaní a slovník, na základě toho, co jste napsali v konverzacích. Upozorňujeme, že některé klávesnice nemusí toto nastavení respektovat.</string>
<string name="settings_security_incognito_keyboard_title">Inkognito klávesnice</string> <string name="settings_security_incognito_keyboard_title">Inkognito klávesnice</string>
<string name="command_description_table_flip">Přidá znaky (╯°□°)╯︵ ┻━┻ před zprávy ve formátu obyčejného textu</string> <string name="command_description_table_flip">Přidá znaky (╯°□°)╯︵ ┻━┻ před zprávy ve formátu prostého textu</string>
<string name="attachment_type_voice_broadcast">Hlasové vysílání</string> <string name="attachment_type_voice_broadcast">Hlasové vysílání</string>
<string name="command_description_devtools">Otevřít nástroje pro vývojáře</string> <string name="command_description_devtools">Otevřít nástroje pro vývojáře</string>
<string name="room_settings_global_block_unverified_info_text">🔒 V nastavení zabezpečení jste povolili šifrování pouze do ověřených relací pro všechny místnosti.</string> <string name="room_settings_global_block_unverified_info_text">🔒 V nastavení zabezpečení jste povolili šifrování pouze do ověřených relací pro všechny místnosti.</string>
@ -2824,8 +2824,8 @@
<string name="permissions_rationale_msg_notification">${app_name} potřebuje oprávnění k zobrazování oznámení. Oznámení mohou zobrazovat vaše zprávy, pozvánky atd. <string name="permissions_rationale_msg_notification">${app_name} potřebuje oprávnění k zobrazování oznámení. Oznámení mohou zobrazovat vaše zprávy, pozvánky atd.
\n \n
\nPro zobrazování oznámení povolte přístup na dalších vyskakovacích oknech.</string> \nPro zobrazování oznámení povolte přístup na dalších vyskakovacích oknech.</string>
<string name="labs_enable_rich_text_editor_summary">Vyzkoušejte rozšířený textový editor (textový režim již brzy)</string> <string name="labs_enable_rich_text_editor_summary">Vyzkoušejte editor formátovaného textu (režim prostého textu již brzy)</string>
<string name="labs_enable_rich_text_editor_title">Povolit rozšířený textový editor</string> <string name="labs_enable_rich_text_editor_title">Povolit editor formátovaného textu</string>
<string name="qr_code_login_confirm_security_code_description">Ujistěte se, že znáte původ tohoto kódu. Propojením zařízení poskytnete někomu plný přístup ke svému účtu.</string> <string name="qr_code_login_confirm_security_code_description">Ujistěte se, že znáte původ tohoto kódu. Propojením zařízení poskytnete někomu plný přístup ke svému účtu.</string>
<string name="qr_code_login_confirm_security_code">Potvrdit</string> <string name="qr_code_login_confirm_security_code">Potvrdit</string>
<string name="qr_code_login_try_again">Zkuste to znovu</string> <string name="qr_code_login_try_again">Zkuste to znovu</string>
@ -2868,7 +2868,7 @@
<string name="qr_code_login_header_failed_other_device_already_signed_in_description">Druhé zařízení je již přihlášeno.</string> <string name="qr_code_login_header_failed_other_device_already_signed_in_description">Druhé zařízení je již přihlášeno.</string>
<string name="qr_code_login_header_failed_e2ee_security_issue_description">Při nastavování zabezpečeného zasílání zpráv se vyskytl problém se zabezpečením. Může být napadena jedna z následujících věcí: váš domovský server; vaše internetové připojení; vaše zařízení;</string> <string name="qr_code_login_header_failed_e2ee_security_issue_description">Při nastavování zabezpečeného zasílání zpráv se vyskytl problém se zabezpečením. Může být napadena jedna z následujících věcí: váš domovský server; vaše internetové připojení; vaše zařízení;</string>
<string name="qr_code_login_header_failed_other_description">Žádost se nezdařila.</string> <string name="qr_code_login_header_failed_other_description">Žádost se nezdařila.</string>
<string name="a11y_voice_broadcast_buffering">Ukládání do vyrovnávací paměti</string> <string name="a11y_voice_broadcast_buffering">Ukládání do vyrovnávací paměti</string>
<string name="a11y_pause_voice_broadcast">Pozastavit hlasové vysílání</string> <string name="a11y_pause_voice_broadcast">Pozastavit hlasové vysílání</string>
<string name="a11y_play_voice_broadcast">Přehrát nebo obnovit hlasové vysílání</string> <string name="a11y_play_voice_broadcast">Přehrát nebo obnovit hlasové vysílání</string>
<string name="a11y_stop_voice_broadcast_record">Ukončit záznam hlasového vysílání</string> <string name="a11y_stop_voice_broadcast_record">Ukončit záznam hlasového vysílání</string>
@ -2922,4 +2922,6 @@
<string name="quoting">Citace</string> <string name="quoting">Citace</string>
<string name="replying_to">Odpovídám na %s</string> <string name="replying_to">Odpovídám na %s</string>
<string name="editing">Úpravy</string> <string name="editing">Úpravy</string>
<string name="settings_enable_direct_share_summary">Zobrazit poslední chaty v nabídce sdílení systému</string>
<string name="settings_enable_direct_share_title">Povolit přímé sdílení</string>
</resources> </resources>

View file

@ -2815,7 +2815,7 @@
<string name="qr_code_login_header_failed_other_description">Die Anfrage ist fehlgeschlagen.</string> <string name="qr_code_login_header_failed_other_description">Die Anfrage ist fehlgeschlagen.</string>
<string name="a11y_play_voice_broadcast">Abspielen oder fortsetzen der Sprachübertragung</string> <string name="a11y_play_voice_broadcast">Abspielen oder fortsetzen der Sprachübertragung</string>
<string name="a11y_resume_voice_broadcast_record">Fortsetzen der Sprachübertragung</string> <string name="a11y_resume_voice_broadcast_record">Fortsetzen der Sprachübertragung</string>
<string name="a11y_voice_broadcast_buffering">Puffere</string> <string name="a11y_voice_broadcast_buffering">Puffere</string>
<string name="a11y_pause_voice_broadcast">Pausiere Sprachübertragung</string> <string name="a11y_pause_voice_broadcast">Pausiere Sprachübertragung</string>
<string name="a11y_stop_voice_broadcast_record">Stoppe Aufzeichnung der Sprachübertragung</string> <string name="a11y_stop_voice_broadcast_record">Stoppe Aufzeichnung der Sprachübertragung</string>
<string name="a11y_pause_voice_broadcast_record">Pausiere Aufzeichnung der Sprachübertragung</string> <string name="a11y_pause_voice_broadcast_record">Pausiere Aufzeichnung der Sprachübertragung</string>
@ -2865,4 +2865,6 @@
<string name="replying_to">%s antworten</string> <string name="replying_to">%s antworten</string>
<string name="device_manager_other_sessions_hide_ip_address">IP-Adresse ausblenden</string> <string name="device_manager_other_sessions_hide_ip_address">IP-Adresse ausblenden</string>
<string name="device_manager_other_sessions_show_ip_address">IP-Adresse anzeigen</string> <string name="device_manager_other_sessions_show_ip_address">IP-Adresse anzeigen</string>
<string name="settings_enable_direct_share_summary">Kürzliche Unterhaltungen im Teilen-Menü des Systems anzeigen</string>
<string name="settings_enable_direct_share_title">Direktes Teilen aktivieren</string>
</resources> </resources>

View file

@ -2805,7 +2805,7 @@
<string name="qr_code_login_header_failed_other_device_already_signed_in_description">Teine seade on juba võrku loginud.</string> <string name="qr_code_login_header_failed_other_device_already_signed_in_description">Teine seade on juba võrku loginud.</string>
<string name="qr_code_login_header_failed_e2ee_security_issue_description">Turvalise sõnumivahetuse ülesseadmisel tekkis turvaviga. Üks kolmest võib olla sattunud vale osapoole kontrolli alla: sinu koduserver, sinu internetiühendus või sinu seade;</string> <string name="qr_code_login_header_failed_e2ee_security_issue_description">Turvalise sõnumivahetuse ülesseadmisel tekkis turvaviga. Üks kolmest võib olla sattunud vale osapoole kontrolli alla: sinu koduserver, sinu internetiühendus või sinu seade;</string>
<string name="qr_code_login_header_failed_other_description">Päring ei õnnestunud.</string> <string name="qr_code_login_header_failed_other_description">Päring ei õnnestunud.</string>
<string name="a11y_voice_broadcast_buffering">Andmed on puhverdamisel</string> <string name="a11y_voice_broadcast_buffering">Andmed on puhverdamisel</string>
<string name="a11y_play_voice_broadcast">Alusta või jätka ringhäälingukõne esitamist</string> <string name="a11y_play_voice_broadcast">Alusta või jätka ringhäälingukõne esitamist</string>
<string name="a11y_stop_voice_broadcast_record">Lõpeta ringhäälingukõne salvestamine</string> <string name="a11y_stop_voice_broadcast_record">Lõpeta ringhäälingukõne salvestamine</string>
<string name="a11y_pause_voice_broadcast_record">Peata ringhäälingukõne salvestamine</string> <string name="a11y_pause_voice_broadcast_record">Peata ringhäälingukõne salvestamine</string>
@ -2857,4 +2857,6 @@
<string name="message_reply_to_sender_sent_video">saatis video.</string> <string name="message_reply_to_sender_sent_video">saatis video.</string>
<string name="message_reply_to_sender_sent_sticker">saatis kleepsu.</string> <string name="message_reply_to_sender_sent_sticker">saatis kleepsu.</string>
<string name="message_reply_to_sender_created_poll">koostas küsitluse.</string> <string name="message_reply_to_sender_created_poll">koostas küsitluse.</string>
<string name="settings_enable_direct_share_title">Kasuta otsejagamist</string>
<string name="settings_enable_direct_share_summary">Näita viimaseid vestlusi süsteemses jagamisvaates</string>
</resources> </resources>

View file

@ -943,7 +943,7 @@
\n \n
\nپیامهایتان با قفل‌هایی امن شده‌اند و فقط شما و گیرندگان دیگر، کلیدهای یکتا را برای قفل‌گشاییشان دارید.</string> \nپیامهایتان با قفل‌هایی امن شده‌اند و فقط شما و گیرندگان دیگر، کلیدهای یکتا را برای قفل‌گشاییشان دارید.</string>
<string name="room_profile_section_security">امنیت</string> <string name="room_profile_section_security">امنیت</string>
<string name="room_profile_section_security_learn_more">بثیش‌تر بدانید</string> <string name="room_profile_section_security_learn_more">بیش‌تر بدانید</string>
<string name="room_profile_section_more">بیش‌تر</string> <string name="room_profile_section_more">بیش‌تر</string>
<string name="room_profile_section_admin">کنش‌های مدیر</string> <string name="room_profile_section_admin">کنش‌های مدیر</string>
<string name="room_profile_section_more_settings">تنظمیات اتاق</string> <string name="room_profile_section_more_settings">تنظمیات اتاق</string>
@ -2783,7 +2783,7 @@
<string name="attachment_type_selector_poll">نظرسنجی‌ها</string> <string name="attachment_type_selector_poll">نظرسنجی‌ها</string>
<string name="attachment_type_selector_file">پیوست‌ها</string> <string name="attachment_type_selector_file">پیوست‌ها</string>
<string name="attachment_type_selector_sticker">برچسب‌ها</string> <string name="attachment_type_selector_sticker">برچسب‌ها</string>
<string name="a11y_voice_broadcast_buffering">میانگیری</string> <string name="a11y_voice_broadcast_buffering">میانگیری</string>
<string name="voice_broadcast_live">زنده</string> <string name="voice_broadcast_live">زنده</string>
<string name="qr_code_login_confirm_security_code">تأیید</string> <string name="qr_code_login_confirm_security_code">تأیید</string>
<string name="three">۳</string> <string name="three">۳</string>
@ -2844,4 +2844,9 @@
<string name="quoting">نقل کردن</string> <string name="quoting">نقل کردن</string>
<string name="replying_to">پاسخ دادن به %s</string> <string name="replying_to">پاسخ دادن به %s</string>
<string name="editing">ویرایش کردن</string> <string name="editing">ویرایش کردن</string>
<string name="device_manager_sessions_sign_in_with_qr_code_description">می‌توانید با یک رمز QR از این افزاره برای ورود به افزاره‌ای همراه یا روی وب استفاده کنید. دو راه برای این کار وجود دارد:</string>
<string name="qr_code_login_header_failed_e2ee_security_issue_description">مشکلی امنیتی در برپایی پیام‌رسانی امن وجود داشت. ممکن است یکی از موارد زیر دستکاری شده باشند: کارساز خانیگیتان؛ اتّصال اینترنتیتان؛ افزاره(های)تان؛</string>
<string name="qr_code_login_confirm_security_code_description">لطفاً مطمئن شوید که مبدأ این کد را می‌دانید. با پیوند دادن افزاره‌ها، دسترسی کامل را به حسابتان می‌دهید.</string>
<string name="settings_enable_direct_share_summary">نمایش گپ‌های اخیر در فهرست هم رسانی سامانه</string>
<string name="settings_enable_direct_share_title">به کار انداختن هم‌رسانی مستقیم</string>
</resources> </resources>

View file

@ -2814,7 +2814,7 @@
<string name="device_manager_sessions_sign_in_with_qr_code_description">Vous pouvez utiliser cet appareil pour connecter un appareil mobile ou un client web avec un QR code. Il y a deux façons de le faire :</string> <string name="device_manager_sessions_sign_in_with_qr_code_description">Vous pouvez utiliser cet appareil pour connecter un appareil mobile ou un client web avec un QR code. Il y a deux façons de le faire :</string>
<string name="device_manager_sessions_sign_in_with_qr_code_title">Se connecter avec un QR code</string> <string name="device_manager_sessions_sign_in_with_qr_code_title">Se connecter avec un QR code</string>
<string name="login_scan_qr_code">Scanner le QR code</string> <string name="login_scan_qr_code">Scanner le QR code</string>
<string name="a11y_voice_broadcast_buffering">Mise en mémoire tampon</string> <string name="a11y_voice_broadcast_buffering">Mise en mémoire tampon</string>
<string name="a11y_pause_voice_broadcast">Mettre en pause la diffusion audio</string> <string name="a11y_pause_voice_broadcast">Mettre en pause la diffusion audio</string>
<string name="a11y_play_voice_broadcast">Lire ou continuer la diffusion audio</string> <string name="a11y_play_voice_broadcast">Lire ou continuer la diffusion audio</string>
<string name="a11y_stop_voice_broadcast_record">Arrêter lenregistrement de la diffusion audio</string> <string name="a11y_stop_voice_broadcast_record">Arrêter lenregistrement de la diffusion audio</string>
@ -2866,4 +2866,6 @@
<string name="quoting">Citation de</string> <string name="quoting">Citation de</string>
<string name="replying_to">Réponse à %s</string> <string name="replying_to">Réponse à %s</string>
<string name="editing">Modification</string> <string name="editing">Modification</string>
<string name="settings_enable_direct_share_summary">Affiche les conversations récentes dans le menu de partage du système</string>
<string name="settings_enable_direct_share_title">Activer le partage direct</string>
</resources> </resources>

View file

@ -2762,7 +2762,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.</string>
<string name="qr_code_login_header_failed_other_description">Permintaan gagal.</string> <string name="qr_code_login_header_failed_other_description">Permintaan gagal.</string>
<string name="labs_enable_voice_broadcast_summary">Memungkinkan untuk merekam dan mengirim siaran suara dalam linimasa ruangan.</string> <string name="labs_enable_voice_broadcast_summary">Memungkinkan untuk merekam dan mengirim siaran suara dalam linimasa ruangan.</string>
<string name="labs_enable_voice_broadcast_title">Aktifkan siaran suara (dalam pengembangan aktif)</string> <string name="labs_enable_voice_broadcast_title">Aktifkan siaran suara (dalam pengembangan aktif)</string>
<string name="a11y_voice_broadcast_buffering">Memuat</string> <string name="a11y_voice_broadcast_buffering">Memuat</string>
<string name="a11y_pause_voice_broadcast">Jeda siaran suara</string> <string name="a11y_pause_voice_broadcast">Jeda siaran suara</string>
<string name="a11y_play_voice_broadcast">Mainkan atau lanjutkan siaran suara</string> <string name="a11y_play_voice_broadcast">Mainkan atau lanjutkan siaran suara</string>
<string name="a11y_stop_voice_broadcast_record">Hentikan rekaman siaran suara</string> <string name="a11y_stop_voice_broadcast_record">Hentikan rekaman siaran suara</string>
@ -2812,4 +2812,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.</string>
<string name="editing">Mengedit</string> <string name="editing">Mengedit</string>
<string name="device_manager_other_sessions_show_ip_address">Tampilkan alamat IP</string> <string name="device_manager_other_sessions_show_ip_address">Tampilkan alamat IP</string>
<string name="replying_to">Membalas ke %s</string> <string name="replying_to">Membalas ke %s</string>
<string name="settings_enable_direct_share_summary">Tampilkan obrolan terkini dalam menu pembagian sistem</string>
<string name="settings_enable_direct_share_title">Aktifkan pembagian langsung</string>
</resources> </resources>

View file

@ -2805,7 +2805,7 @@
<string name="qr_code_login_header_failed_other_device_already_signed_in_description">L\'altro dispositivo ha già fatto l\'accesso.</string> <string name="qr_code_login_header_failed_other_device_already_signed_in_description">L\'altro dispositivo ha già fatto l\'accesso.</string>
<string name="qr_code_login_header_failed_e2ee_security_issue_description">Si è verificato un problema di sicurezza configurando i messaggi sicuri. Una delle seguenti cose potrebbe essere compromessa: il tuo homeserver; la/e connessione/i internet; il/i dispositivo/i;</string> <string name="qr_code_login_header_failed_e2ee_security_issue_description">Si è verificato un problema di sicurezza configurando i messaggi sicuri. Una delle seguenti cose potrebbe essere compromessa: il tuo homeserver; la/e connessione/i internet; il/i dispositivo/i;</string>
<string name="qr_code_login_header_failed_other_description">La richiesta è fallita.</string> <string name="qr_code_login_header_failed_other_description">La richiesta è fallita.</string>
<string name="a11y_voice_broadcast_buffering">Buffering</string> <string name="a11y_voice_broadcast_buffering">Buffer</string>
<string name="a11y_pause_voice_broadcast">Sospendi trasmissione vocale</string> <string name="a11y_pause_voice_broadcast">Sospendi trasmissione vocale</string>
<string name="a11y_play_voice_broadcast">Avvia o riprendi trasmissione vocale</string> <string name="a11y_play_voice_broadcast">Avvia o riprendi trasmissione vocale</string>
<string name="a11y_stop_voice_broadcast_record">Ferma registrazione trasmissione vocale</string> <string name="a11y_stop_voice_broadcast_record">Ferma registrazione trasmissione vocale</string>
@ -2857,4 +2857,6 @@
<string name="quoting">Citazione</string> <string name="quoting">Citazione</string>
<string name="replying_to">Risposta a %s</string> <string name="replying_to">Risposta a %s</string>
<string name="editing">Modifica</string> <string name="editing">Modifica</string>
<string name="settings_enable_direct_share_summary">Mostra chat recenti nel menu di condivisione di sistema</string>
<string name="settings_enable_direct_share_title">Attiva condivisione diretta</string>
</resources> </resources>

View file

@ -2723,7 +2723,7 @@
<string name="device_manager_learn_more_sessions_unverified_title">Sessões não-verificadas</string> <string name="device_manager_learn_more_sessions_unverified_title">Sessões não-verificadas</string>
<string name="device_manager_learn_more_sessions_inactive">Sessões inativas são sessões que você não tem usado em algum tempo, mas elas continuam a receber chaves de encriptação. <string name="device_manager_learn_more_sessions_inactive">Sessões inativas são sessões que você não tem usado em algum tempo, mas elas continuam a receber chaves de encriptação.
\n \n
\nRemover sessões inativas melhora segurança e performance, e torna-o mais fácil para você identificar se uma nova sessão é suspeita.</string> \nRemover sessões inativas melhora segurança e performance, e torna mais fácil para você identificar se uma nova sessão é suspeita.</string>
<string name="device_manager_learn_more_sessions_inactive_title">Sessões inativas</string> <string name="device_manager_learn_more_sessions_inactive_title">Sessões inativas</string>
<string name="device_manager_session_rename_warning">Por favor esteja ciente que nomes de sessões também são visíveis a pessoas com quem você se comunica.</string> <string name="device_manager_session_rename_warning">Por favor esteja ciente que nomes de sessões também são visíveis a pessoas com quem você se comunica.</string>
<string name="device_manager_session_rename_description">Nomes de sessões personalizadas podem ajudar você a reconhecer seus dispositivos mais facilmente.</string> <string name="device_manager_session_rename_description">Nomes de sessões personalizadas podem ajudar você a reconhecer seus dispositivos mais facilmente.</string>
@ -2844,9 +2844,9 @@
<string name="error_voice_broadcast_unauthorized_title">Não dá pra começar um novo broadcast de voz</string> <string name="error_voice_broadcast_unauthorized_title">Não dá pra começar um novo broadcast de voz</string>
<string name="a11y_voice_broadcast_fast_forward">Avançar rápido 30 segundos</string> <string name="a11y_voice_broadcast_fast_forward">Avançar rápido 30 segundos</string>
<string name="a11y_voice_broadcast_fast_backward">Retroceder 30 segundos</string> <string name="a11y_voice_broadcast_fast_backward">Retroceder 30 segundos</string>
<string name="device_manager_learn_more_sessions_verified_description">Sessões verificadas são onde quer que você está usando esta conta depois de entrar sua frasepasse ou confirmar sua identidade com uma outra sessão verificada. <string name="device_manager_learn_more_sessions_verified_description">Sessões verificadas são onde quer que você esteja usando esta conta depois de entrar sua frasepasse ou confirmar sua identidade com uma outra sessão verificada.
\n \n
\nIsto significa que você tem todas as chaves necessitadas para destrancar suas mensagens encriptadas e confirmar a outras(os) usuárias(os) que você confia nesta sessão.</string> \nIsto significa que você tem todas as chaves necessárias para destrancar suas mensagens encriptadas e confirmar a outras(os) usuárias(os) que você confia nesta sessão.</string>
<plurals name="device_manager_other_sessions_multi_signout_all"> <plurals name="device_manager_other_sessions_multi_signout_all">
<item quantity="one">Fazer signout de %1$d sessão</item> <item quantity="one">Fazer signout de %1$d sessão</item>
<item quantity="other">Fazer signout de %1$d sessões</item> <item quantity="other">Fazer signout de %1$d sessões</item>

View file

@ -2868,7 +2868,7 @@
<string name="qr_code_login_header_failed_other_description">Žiadosť zlyhala.</string> <string name="qr_code_login_header_failed_other_description">Žiadosť zlyhala.</string>
<string name="labs_enable_voice_broadcast_summary">Možnosť nahrávania a odosielania hlasového vysielania v časovej osi miestnosti.</string> <string name="labs_enable_voice_broadcast_summary">Možnosť nahrávania a odosielania hlasového vysielania v časovej osi miestnosti.</string>
<string name="labs_enable_voice_broadcast_title">Zapnúť hlasové vysielanie (v štádiu aktívneho vývoja)</string> <string name="labs_enable_voice_broadcast_title">Zapnúť hlasové vysielanie (v štádiu aktívneho vývoja)</string>
<string name="a11y_voice_broadcast_buffering">Načítavanie do vyrovnávacej pamäte</string> <string name="a11y_voice_broadcast_buffering">Načítavanie do vyrovnávacej pamäte</string>
<string name="a11y_pause_voice_broadcast">Pozastaviť hlasové vysielanie</string> <string name="a11y_pause_voice_broadcast">Pozastaviť hlasové vysielanie</string>
<string name="a11y_play_voice_broadcast">Prehrať alebo pokračovať v nahrávaní hlasového vysielania</string> <string name="a11y_play_voice_broadcast">Prehrať alebo pokračovať v nahrávaní hlasového vysielania</string>
<string name="a11y_stop_voice_broadcast_record">Zastaviť nahrávanie hlasového vysielania</string> <string name="a11y_stop_voice_broadcast_record">Zastaviť nahrávanie hlasového vysielania</string>
@ -2922,4 +2922,6 @@
<string name="device_manager_other_sessions_show_ip_address">Zobraziť IP adresu</string> <string name="device_manager_other_sessions_show_ip_address">Zobraziť IP adresu</string>
<string name="replying_to">Odpoveď na %s</string> <string name="replying_to">Odpoveď na %s</string>
<string name="editing">Úprava</string> <string name="editing">Úprava</string>
<string name="settings_enable_direct_share_summary">Zobraziť posledné konverzácie v systémovej ponuke zdieľania</string>
<string name="settings_enable_direct_share_title">Povoliť priame zdieľanie</string>
</resources> </resources>

View file

@ -2659,7 +2659,7 @@
\nKy shërbyes Home mund të mos jetë formësuar të shfaqë harta.</string> \nKy shërbyes Home mund të mos jetë formësuar të shfaqë harta.</string>
<string name="poll_undisclosed_not_ended">Përfundimet do të jenë të dukshme pasi të ketë përfunduar pyetësori</string> <string name="poll_undisclosed_not_ended">Përfundimet do të jenë të dukshme pasi të ketë përfunduar pyetësori</string>
<string name="labs_enable_msc3061_share_history_desc">Kur bëhet ftesë në një dhomë të fshehtëzuar që ka historik ndarjesh me të tjerët, historiku i fshehtëzuar do të jetë i dukshëm.</string> <string name="labs_enable_msc3061_share_history_desc">Kur bëhet ftesë në një dhomë të fshehtëzuar që ka historik ndarjesh me të tjerët, historiku i fshehtëzuar do të jetë i dukshëm.</string>
<string name="a11y_voice_broadcast_buffering">Përdo</string> <string name="a11y_voice_broadcast_buffering"></string>
<string name="a11y_pause_voice_broadcast">Ndal transmetim zanor</string> <string name="a11y_pause_voice_broadcast">Ndal transmetim zanor</string>
<string name="a11y_play_voice_broadcast">Luani ose vazhdoni luajtje transmetimi zanor</string> <string name="a11y_play_voice_broadcast">Luani ose vazhdoni luajtje transmetimi zanor</string>
<string name="a11y_stop_voice_broadcast_record">Ndal incizim transmetimi zanor</string> <string name="a11y_stop_voice_broadcast_record">Ndal incizim transmetimi zanor</string>
@ -2851,4 +2851,6 @@
<string name="a11y_voice_broadcast_fast_backward">Kthim prapa 30 sekonda</string> <string name="a11y_voice_broadcast_fast_backward">Kthim prapa 30 sekonda</string>
<string name="replying_to">Si përgjigje për %s</string> <string name="replying_to">Si përgjigje për %s</string>
<string name="labs_enable_deferred_dm_title">Aktivizo MD të lënë për më vonë</string> <string name="labs_enable_deferred_dm_title">Aktivizo MD të lënë për më vonë</string>
<string name="a11y_collapse_space_children">Tkurr pjella të %s</string>
<string name="a11y_expand_space_children">Zgjero pjella të %s</string>
</resources> </resources>

View file

@ -2852,4 +2852,18 @@
<string name="error_voice_broadcast_unauthorized_title">Kan inte starta en ny röstsändning</string> <string name="error_voice_broadcast_unauthorized_title">Kan inte starta en ny röstsändning</string>
<string name="a11y_voice_broadcast_fast_forward">Spola framåt 30 sekunder</string> <string name="a11y_voice_broadcast_fast_forward">Spola framåt 30 sekunder</string>
<string name="a11y_voice_broadcast_fast_backward">Spola tillbaka 30 sekunder</string> <string name="a11y_voice_broadcast_fast_backward">Spola tillbaka 30 sekunder</string>
<string name="message_reply_to_sender_created_poll">skickade en omröstning.</string>
<string name="message_reply_to_sender_sent_sticker">skickade en dekal.</string>
<string name="message_reply_to_sender_sent_video">skickade en video.</string>
<string name="message_reply_to_sender_sent_image">skickade en bild.</string>
<string name="message_reply_to_sender_sent_voice_message">skickade ett röstmeddelande.</string>
<string name="message_reply_to_sender_sent_audio_file">skickade en ljudfil.</string>
<string name="message_reply_to_sender_sent_file">skickade en fil.</string>
<string name="message_reply_to_prefix">Svar på</string>
<string name="device_manager_other_sessions_hide_ip_address">Dölj IP-adress</string>
<string name="device_manager_other_sessions_show_ip_address">Visa IP-adress</string>
<string name="voice_broadcast_recording_time_left">%1$s kvar</string>
<string name="quoting">Citerar</string>
<string name="replying_to">Besvarar %s</string>
<string name="editing">Redigerar</string>
</resources> </resources>

View file

@ -2922,7 +2922,7 @@
<string name="qr_code_login_header_failed_other_description">Запит не виконаний.</string> <string name="qr_code_login_header_failed_other_description">Запит не виконаний.</string>
<string name="labs_enable_voice_broadcast_summary">Можливість записувати та надсилати голосові трансляції до стрічки кімнати.</string> <string name="labs_enable_voice_broadcast_summary">Можливість записувати та надсилати голосові трансляції до стрічки кімнати.</string>
<string name="labs_enable_voice_broadcast_title">Увімкнути голосові трансляції (в активній розробці)</string> <string name="labs_enable_voice_broadcast_title">Увімкнути голосові трансляції (в активній розробці)</string>
<string name="a11y_voice_broadcast_buffering">Буферизація</string> <string name="a11y_voice_broadcast_buffering">Буферизація</string>
<string name="a11y_pause_voice_broadcast">Призупинити голосову трансляцію</string> <string name="a11y_pause_voice_broadcast">Призупинити голосову трансляцію</string>
<string name="a11y_play_voice_broadcast">Відтворити або поновити відтворення голосової трансляції</string> <string name="a11y_play_voice_broadcast">Відтворити або поновити відтворення голосової трансляції</string>
<string name="a11y_stop_voice_broadcast_record">Припинити запис голосової трансляції</string> <string name="a11y_stop_voice_broadcast_record">Припинити запис голосової трансляції</string>
@ -2966,16 +2966,18 @@
<string name="device_manager_other_sessions_multi_signout_selection">Вийти</string> <string name="device_manager_other_sessions_multi_signout_selection">Вийти</string>
<string name="voice_broadcast_recording_time_left">Залишилося %1$s</string> <string name="voice_broadcast_recording_time_left">Залишилося %1$s</string>
<string name="message_reply_to_sender_sent_audio_file">надсилає аудіофайл.</string> <string name="message_reply_to_sender_sent_audio_file">надсилає аудіофайл.</string>
<string name="message_reply_to_sender_sent_file">відправив файл.</string> <string name="message_reply_to_sender_sent_file">надсилає файл.</string>
<string name="message_reply_to_prefix">У відповідь на</string> <string name="message_reply_to_prefix">У відповідь на</string>
<string name="device_manager_other_sessions_hide_ip_address">Сховати IP-адресу</string> <string name="device_manager_other_sessions_hide_ip_address">Сховати IP-адресу</string>
<string name="message_reply_to_sender_created_poll">створив голосування.</string> <string name="message_reply_to_sender_created_poll">створює опитування.</string>
<string name="message_reply_to_sender_sent_sticker">відправив наліпку.</string> <string name="message_reply_to_sender_sent_sticker">надсилає наліпку.</string>
<string name="message_reply_to_sender_sent_video">відправив відео.</string> <string name="message_reply_to_sender_sent_video">надсилає відео.</string>
<string name="message_reply_to_sender_sent_image">відправив зображення.</string> <string name="message_reply_to_sender_sent_image">надсилає зображення.</string>
<string name="message_reply_to_sender_sent_voice_message">відправив голосове повідомлення.</string> <string name="message_reply_to_sender_sent_voice_message">надсилає голосове повідомлення.</string>
<string name="device_manager_other_sessions_show_ip_address">Показати IP-адресу</string> <string name="device_manager_other_sessions_show_ip_address">Показати IP-адресу</string>
<string name="quoting">Цитуючи</string> <string name="quoting">Цитуючи</string>
<string name="replying_to">У відповідь на %s</string> <string name="replying_to">У відповідь %s</string>
<string name="editing">Редагування</string> <string name="editing">Редагування</string>
<string name="settings_enable_direct_share_summary">Показувати останні бесіди в системному меню загального доступу</string>
<string name="settings_enable_direct_share_title">Увімкнути пряме поширення</string>
</resources> </resources>

View file

@ -1007,7 +1007,7 @@
<string name="settings_discovery_disconnect_with_bound_pid">您当前在身份服务器 %1$s 上共享电子邮件地址或电话号码。您需要重新连接到 %2$s 才能停止共享它们。</string> <string name="settings_discovery_disconnect_with_bound_pid">您当前在身份服务器 %1$s 上共享电子邮件地址或电话号码。您需要重新连接到 %2$s 才能停止共享它们。</string>
<string name="settings_agree_to_terms">同意身份服务器 (%s) 服务条款使你可以通过电子邮件地址或电话号码被发现。</string> <string name="settings_agree_to_terms">同意身份服务器 (%s) 服务条款使你可以通过电子邮件地址或电话号码被发现。</string>
<string name="labs_allow_extended_logging">启用详细日志。</string> <string name="labs_allow_extended_logging">启用详细日志。</string>
<string name="labs_allow_extended_logging_summary">详细日志将通过在您发送 RageShake 时提供更多日志来帮助开发人员。即使启用,应用程序也不会记录消息内容或任何其他私人数据。</string> <string name="labs_allow_extended_logging_summary">详细日志将通过在您发送愤怒摇动RageShake时提供更多日志来帮助开发人员。即使启用,应用程序也不会记录消息内容或任何其他私人数据。</string>
<string name="error_terms_not_accepted">接收你的主服务器条款和条件后请重试。</string> <string name="error_terms_not_accepted">接收你的主服务器条款和条件后请重试。</string>
<string name="error_network_timeout">服务器似乎响应时间太长,这可能是由于连接不良或服务器错误引起的。请稍后再试。</string> <string name="error_network_timeout">服务器似乎响应时间太长,这可能是由于连接不良或服务器错误引起的。请稍后再试。</string>
<string name="send_attachment">发送附件</string> <string name="send_attachment">发送附件</string>
@ -1205,7 +1205,7 @@
<string name="settings_advanced_settings">高级设置</string> <string name="settings_advanced_settings">高级设置</string>
<string name="settings_developer_mode">开发者模式</string> <string name="settings_developer_mode">开发者模式</string>
<string name="settings_developer_mode_summary">开发者模式激活隐藏的功能,也可能使应用不稳定。仅供开发者使用!</string> <string name="settings_developer_mode_summary">开发者模式激活隐藏的功能,也可能使应用不稳定。仅供开发者使用!</string>
<string name="settings_rageshake">摇一摇</string> <string name="settings_rageshake">愤怒摇动Rageshake</string>
<string name="settings_rageshake_detection_threshold">检测阈值</string> <string name="settings_rageshake_detection_threshold">检测阈值</string>
<string name="settings_rageshake_detection_threshold_summary">摇动手机以测试检测阈值</string> <string name="settings_rageshake_detection_threshold_summary">摇动手机以测试检测阈值</string>
<string name="rageshake_detected">检测到摇动!</string> <string name="rageshake_detected">检测到摇动!</string>
@ -1213,7 +1213,7 @@
<string name="devices_current_device">当前会话</string> <string name="devices_current_device">当前会话</string>
<string name="devices_other_devices">其它会话</string> <string name="devices_other_devices">其它会话</string>
<string name="autocomplete_limited_results">仅显示第一个结果,请输入更多字符…</string> <string name="autocomplete_limited_results">仅显示第一个结果,请输入更多字符…</string>
<string name="settings_developer_mode_fail_fast_title">快速失败</string> <string name="settings_developer_mode_fail_fast_title">快速失败Fail-fast</string>
<string name="settings_developer_mode_fail_fast_summary">发生意外错误时,${app_name} 可能更经常崩溃</string> <string name="settings_developer_mode_fail_fast_summary">发生意外错误时,${app_name} 可能更经常崩溃</string>
<string name="command_description_shrug">在明文消息前添加 ¯\\_(ツ)_/¯</string> <string name="command_description_shrug">在明文消息前添加 ¯\\_(ツ)_/¯</string>
<string name="create_room_encryption_title">启用加密</string> <string name="create_room_encryption_title">启用加密</string>
@ -2694,7 +2694,7 @@
<string name="device_manager_verification_status_detail_other_session_unknown">验证您当前的会话以显示此会话的验证状态。</string> <string name="device_manager_verification_status_detail_other_session_unknown">验证您当前的会话以显示此会话的验证状态。</string>
<string name="device_manager_verification_status_unknown">未知的验证状态</string> <string name="device_manager_verification_status_unknown">未知的验证状态</string>
<string name="tooltip_attachment_voice_broadcast">开始语音广播</string> <string name="tooltip_attachment_voice_broadcast">开始语音广播</string>
<string name="a11y_voice_broadcast_buffering">缓冲</string> <string name="a11y_voice_broadcast_buffering">正在缓冲……</string>
<string name="a11y_pause_voice_broadcast">暂停语音广播</string> <string name="a11y_pause_voice_broadcast">暂停语音广播</string>
<string name="voice_broadcast_live">实时</string> <string name="voice_broadcast_live">实时</string>
<string name="action_got_it">知道了</string> <string name="action_got_it">知道了</string>
@ -2789,4 +2789,19 @@
<plurals name="x_selected"> <plurals name="x_selected">
<item quantity="other">已选择 %1$d</item> <item quantity="other">已选择 %1$d</item>
</plurals> </plurals>
<string name="message_reply_to_sender_created_poll">已创建投票。</string>
<string name="message_reply_to_sender_sent_sticker">已发送贴纸。</string>
<string name="message_reply_to_sender_sent_video">已发送视频。</string>
<string name="message_reply_to_sender_sent_image">已发送图片。</string>
<string name="message_reply_to_sender_sent_voice_message">已发送语音消息。</string>
<string name="message_reply_to_sender_sent_audio_file">已发送音频文件。</string>
<string name="message_reply_to_sender_sent_file">已发送文件。</string>
<string name="device_manager_learn_more_sessions_verified_description">已验证的会话是在输入你的口令词组或用另一个已验证的会话确认你的身份之后你使用此账户的任何地方。
\n
\n这意味着你拥有解锁你的已加密消息和向其他用户证明你信任此会话所需的全部密钥。</string>
<plurals name="device_manager_other_sessions_multi_signout_all">
<item quantity="other">登出%1$d个会话</item>
</plurals>
<string name="device_manager_other_sessions_multi_signout_selection">登出</string>
<string name="voice_broadcast_recording_time_left">剩余%1$s</string>
</resources> </resources>

View file

@ -2760,7 +2760,7 @@
<string name="qr_code_login_header_failed_other_description">請求失敗。</string> <string name="qr_code_login_header_failed_other_description">請求失敗。</string>
<string name="labs_enable_voice_broadcast_summary">可以在聊天室時間軸中錄製並傳送語音廣播。</string> <string name="labs_enable_voice_broadcast_summary">可以在聊天室時間軸中錄製並傳送語音廣播。</string>
<string name="labs_enable_voice_broadcast_title">啟用語音廣播(正在積極開發中)</string> <string name="labs_enable_voice_broadcast_title">啟用語音廣播(正在積極開發中)</string>
<string name="a11y_voice_broadcast_buffering">正在緩衝</string> <string name="a11y_voice_broadcast_buffering">正在緩衝……</string>
<string name="a11y_pause_voice_broadcast">暫停語音廣播</string> <string name="a11y_pause_voice_broadcast">暫停語音廣播</string>
<string name="a11y_play_voice_broadcast">播放或繼續語音廣播</string> <string name="a11y_play_voice_broadcast">播放或繼續語音廣播</string>
<string name="a11y_stop_voice_broadcast_record">停止語音廣播錄製</string> <string name="a11y_stop_voice_broadcast_record">停止語音廣播錄製</string>
@ -2810,4 +2810,6 @@
<string name="quoting">引用</string> <string name="quoting">引用</string>
<string name="replying_to">回覆給 %s</string> <string name="replying_to">回覆給 %s</string>
<string name="editing">正在編輯</string> <string name="editing">正在編輯</string>
<string name="settings_enable_direct_share_summary">在系統分享選單中顯示最近聊天</string>
<string name="settings_enable_direct_share_title">啟用直接分享</string>
</resources> </resources>

View file

@ -2491,6 +2491,9 @@
<string name="settings_key_requests">Key Requests</string> <string name="settings_key_requests">Key Requests</string>
<string name="settings_export_trail">Export Audit</string> <string name="settings_export_trail">Export Audit</string>
<string name="settings_nightly_build">Nightly build</string>
<string name="settings_nightly_build_update">Get the latest build (note: you may have trouble to sign in)</string>
<string name="e2e_use_keybackup">Unlock encrypted messages history</string> <string name="e2e_use_keybackup">Unlock encrypted messages history</string>
<string name="refresh">Refresh</string> <string name="refresh">Refresh</string>
@ -3370,6 +3373,7 @@
<item quantity="one">Sign out of %1$d session</item> <item quantity="one">Sign out of %1$d session</item>
<item quantity="other">Sign out of %1$d sessions</item> <item quantity="other">Sign out of %1$d sessions</item>
</plurals> </plurals>
<string name="device_manager_signout_all_other_sessions">Sign out of all other sessions</string>
<string name="device_manager_other_sessions_show_ip_address">Show IP address</string> <string name="device_manager_other_sessions_show_ip_address">Show IP address</string>
<string name="device_manager_other_sessions_hide_ip_address">Hide IP address</string> <string name="device_manager_other_sessions_hide_ip_address">Hide IP address</string>
<string name="device_manager_session_overview_signout">Sign out of this session</string> <string name="device_manager_session_overview_signout">Sign out of this session</string>

View file

@ -53,7 +53,7 @@
<item name="vctr_list_separator">?vctr_content_quinary</item> <item name="vctr_list_separator">?vctr_content_quinary</item>
<item name="vctr_list_separator_system">?vctr_system</item> <item name="vctr_list_separator_system">?vctr_system</item>
<item name="vctr_list_separator_on_surface">?vctr_system</item> <item name="vctr_list_separator_on_surface">?vctr_system</item>
<item name="vctr_unread_background">?vctr_content_tertiary</item> <item name="vctr_unread_background">?vctr_notice_secondary</item>
<!-- Material color --> <!-- Material color -->
<item name="colorPrimary">@color/element_accent_dark</item> <item name="colorPrimary">@color/element_accent_dark</item>

View file

@ -53,7 +53,7 @@
<item name="vctr_list_separator">?vctr_content_quinary</item> <item name="vctr_list_separator">?vctr_content_quinary</item>
<item name="vctr_list_separator_system">?vctr_system</item> <item name="vctr_list_separator_system">?vctr_system</item>
<item name="vctr_list_separator_on_surface">?vctr_system</item> <item name="vctr_list_separator_on_surface">?vctr_system</item>
<item name="vctr_unread_background">?vctr_content_tertiary</item> <item name="vctr_unread_background">?vctr_notice_secondary</item>
<!-- Material color --> <!-- Material color -->
<item name="colorPrimary">@color/element_accent_light</item> <item name="colorPrimary">@color/element_accent_light</item>

View file

@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
import org.matrix.android.sdk.internal.crypto.tasks.toDeviceTracingId
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
import org.matrix.android.sdk.internal.session.sync.ProgressReporter import org.matrix.android.sdk.internal.session.sync.ProgressReporter
import timber.log.Timber import timber.log.Timber
@ -46,12 +47,14 @@ internal class CryptoSyncHandler @Inject constructor(
.forEachIndexed { index, event -> .forEachIndexed { index, event ->
progressReporter?.reportProgress(index * 100F / total) progressReporter?.reportProgress(index * 100F / total)
// Decrypt event if necessary // Decrypt event if necessary
Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}") Timber.tag(loggerTag.value).d("To device event msgid:${event.toDeviceTracingId()}")
decryptToDeviceEvent(event, null) decryptToDeviceEvent(event, null)
if (event.getClearType() == EventType.MESSAGE && if (event.getClearType() == EventType.MESSAGE &&
event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") { event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") {
Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
} else { } else {
Timber.tag(loggerTag.value).d("received to-device ${event.getClearType()} from:${event.senderId} msgid:${event.toDeviceTracingId()}")
verificationService.onToDeviceEvent(event) verificationService.onToDeviceEvent(event)
cryptoService.get().onToDeviceEvent(event) cryptoService.get().onToDeviceEvent(event)
} }

View file

@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LocalNotificationSettingsContent( data class LocalNotificationSettingsContent(
@Json(name = "is_silenced") val isSilenced: Boolean = false @Json(name = "is_silenced")
val isSilenced: Boolean?
) )

View file

@ -16,6 +16,8 @@
package org.matrix.android.sdk.api.session.events.model package org.matrix.android.sdk.api.session.events.model
import org.matrix.android.sdk.api.session.room.model.message.MessageType.MSGTYPE_VERIFICATION_REQUEST
/** /**
* Constants defining known event types from Matrix specifications. * Constants defining known event types from Matrix specifications.
*/ */
@ -127,6 +129,7 @@ object EventType {
fun isVerificationEvent(type: String): Boolean { fun isVerificationEvent(type: String): Boolean {
return when (type) { return when (type) {
MSGTYPE_VERIFICATION_REQUEST,
KEY_VERIFICATION_START, KEY_VERIFICATION_START,
KEY_VERIFICATION_ACCEPT, KEY_VERIFICATION_ACCEPT,
KEY_VERIFICATION_KEY, KEY_VERIFICATION_KEY,

View file

@ -17,15 +17,23 @@
package org.matrix.android.sdk.internal.crypto.tasks package org.matrix.android.sdk.internal.crypto.tasks
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.api.CryptoApi
import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody
import org.matrix.android.sdk.internal.network.DEFAULT_REQUEST_RETRY_COUNT import org.matrix.android.sdk.internal.network.DEFAULT_REQUEST_RETRY_COUNT
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import timber.log.Timber
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
const val TO_DEVICE_TRACING_ID_KEY = "org.matrix.msgid"
fun Event.toDeviceTracingId(): String? = content?.get(TO_DEVICE_TRACING_ID_KEY) as? String
internal interface SendToDeviceTask : Task<SendToDeviceTask.Params, Unit> { internal interface SendToDeviceTask : Task<SendToDeviceTask.Params, Unit> {
data class Params( data class Params(
// the type of event to send // the type of event to send
@ -35,7 +43,9 @@ internal interface SendToDeviceTask : Task<SendToDeviceTask.Params, Unit> {
// the transactionId. If not provided, a transactionId will be created by the task // the transactionId. If not provided, a transactionId will be created by the task
val transactionId: String? = null, val transactionId: String? = null,
// Number of retry before failing // Number of retry before failing
val retryCount: Int = DEFAULT_REQUEST_RETRY_COUNT val retryCount: Int = DEFAULT_REQUEST_RETRY_COUNT,
// add tracing id, notice that to device events that do signature on content might be broken by it
val addTracingIds: Boolean = !EventType.isVerificationEvent(eventType),
) )
} }
@ -45,15 +55,22 @@ internal class DefaultSendToDeviceTask @Inject constructor(
) : SendToDeviceTask { ) : SendToDeviceTask {
override suspend fun execute(params: SendToDeviceTask.Params) { override suspend fun execute(params: SendToDeviceTask.Params) {
val sendToDeviceBody = SendToDeviceBody(
messages = params.contentMap.map
)
// If params.transactionId is not provided, we create a unique txnId. // If params.transactionId is not provided, we create a unique txnId.
// It's important to do that outside the requestBlock parameter of executeRequest() // It's important to do that outside the requestBlock parameter of executeRequest()
// to use the same value if the request is retried // to use the same value if the request is retried
val txnId = params.transactionId ?: createUniqueTxnId() val txnId = params.transactionId ?: createUniqueTxnId()
// add id tracing to debug
val decorated = if (params.addTracingIds) {
decorateWithToDeviceTracingIds(params)
} else {
params.contentMap.map to emptyList()
}
val sendToDeviceBody = SendToDeviceBody(
messages = decorated.first
)
return executeRequest( return executeRequest(
globalErrorReceiver, globalErrorReceiver,
canRetry = true, canRetry = true,
@ -64,8 +81,35 @@ internal class DefaultSendToDeviceTask @Inject constructor(
transactionId = txnId, transactionId = txnId,
body = sendToDeviceBody body = sendToDeviceBody
) )
Timber.i("Sent to device type=${params.eventType} txnid=$txnId [${decorated.second.joinToString(",")}]")
} }
} }
/**
* To make it easier to track down where to-device messages are getting lost,
* add a custom property to each one, and that will be logged after sent and on reception. Synapse will also log
* this property.
* @return A pair, first is the decorated content, and second info to log out after sending
*/
private fun decorateWithToDeviceTracingIds(params: SendToDeviceTask.Params): Pair<Map<String, Map<String, Any>>, List<String>> {
val tracingInfo = mutableListOf<String>()
val decoratedContent = params.contentMap.map.map { userToDeviceMap ->
val userId = userToDeviceMap.key
userId to userToDeviceMap.value.map {
val deviceId = it.key
deviceId to it.value.toContent().toMutableMap().apply {
put(
TO_DEVICE_TRACING_ID_KEY,
UUID.randomUUID().toString().also {
tracingInfo.add("$userId/$deviceId (msgid $it)")
}
)
}
}.toMap()
}.toMap()
return decoratedContent to tracingInfo
}
} }
internal fun createUniqueTxnId() = UUID.randomUUID().toString() internal fun createUniqueTxnId() = UUID.randomUUID().toString()

View file

@ -42,7 +42,7 @@ internal class DefaultGetCurrentFilterTask @Inject constructor(
return when (storedFilterBody) { return when (storedFilterBody) {
currentFilterBody -> storedFilterId ?: storedFilterBody currentFilterBody -> storedFilterId ?: storedFilterBody
else -> saveFilter(currentFilter) else -> saveFilter(currentFilter) ?: currentFilterBody
} }
} }

View file

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.filter package org.matrix.android.sdk.internal.session.filter
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
@ -24,8 +25,9 @@ import javax.inject.Inject
/** /**
* Save a filter, in db and if any changes, upload to the server. * Save a filter, in db and if any changes, upload to the server.
* Return the filterId if uploading to the server is successful, else return null.
*/ */
internal interface SaveFilterTask : Task<SaveFilterTask.Params, String> { internal interface SaveFilterTask : Task<SaveFilterTask.Params, String?> {
data class Params( data class Params(
val filter: Filter val filter: Filter
@ -39,18 +41,20 @@ internal class DefaultSaveFilterTask @Inject constructor(
private val globalErrorReceiver: GlobalErrorReceiver, private val globalErrorReceiver: GlobalErrorReceiver,
) : SaveFilterTask { ) : SaveFilterTask {
override suspend fun execute(params: SaveFilterTask.Params): String { override suspend fun execute(params: SaveFilterTask.Params): String? {
val filter = params.filter val filter = params.filter
val filterResponse = executeRequest(globalErrorReceiver) { val filterResponse = tryOrNull {
// TODO auto retry executeRequest(globalErrorReceiver) {
filterAPI.uploadFilter(userId, filter) filterAPI.uploadFilter(userId, filter)
}
} }
val filterId = filterResponse?.filterId
filterRepository.storeSyncFilter( filterRepository.storeSyncFilter(
filter = filter, filter = filter,
filterId = filterResponse.filterId, filterId = filterId.orEmpty(),
roomEventFilter = FilterFactory.createDefaultRoomFilter() roomEventFilter = FilterFactory.createDefaultRoomFilter()
) )
return filterResponse.filterId return filterId
} }
} }

View file

@ -0,0 +1,255 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.amshove.kluent.internal.assertEquals
import org.junit.Assert
import org.junit.Test
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.internal.crypto.api.CryptoApi
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams
import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse
import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody
import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryBody
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadBody
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse
import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody
import org.matrix.android.sdk.internal.crypto.model.rest.SignatureUploadResponse
import org.matrix.android.sdk.internal.crypto.model.rest.UpdateDeviceInfoBody
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody
import org.matrix.android.sdk.internal.crypto.tasks.DefaultSendToDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
class DefaultSendToDeviceTaskTest {
private val users = listOf(
"@alice:example.com" to listOf("D0", "D1"),
"bob@example.com" to listOf("D2", "D3")
)
private val fakeEncryptedContent = mapOf(
"algorithm" to "m.olm.v1.curve25519-aes-sha2",
"sender_key" to "gMObR+/4dqL5T4DisRRRYBJpn+OjzFnkyCFOktP6Eyw",
"ciphertext" to mapOf(
"tdwXf7006FDgzmufMCVI4rDdVPO51ecRTTT6HkRxUwE" to mapOf(
"type" to 0,
"body" to "AwogCA1ULEc0abGIFxMDIC9iv7ul3jqJSnapTHQ+8JJx"
)
)
)
private val fakeStartVerificationContent = mapOf(
"method" to "m.sas.v1",
"from_device" to "MNQHVEISFQ",
"key_agreement_protocols" to listOf(
"curve25519-hkdf-sha256",
"curve25519"
),
"hashes" to listOf("sha256"),
"message_authentication_codes" to listOf(
"org.matrix.msc3783.hkdf-hmac-sha256",
"hkdf-hmac-sha256",
"hmac-sha256"
),
"short_authentication_string" to listOf(
"decimal",
"emoji"
),
"transaction_id" to "4wNOpkHGwGZPXjkZToooCDWfb8hsf7vW"
)
@Test
fun `tracing id should be added to to_device contents`() {
val fakeCryptoAPi = FakeCryptoApi()
val sendToDeviceTask = DefaultSendToDeviceTask(
cryptoApi = fakeCryptoAPi,
globalErrorReceiver = mockk(relaxed = true)
)
val contentMap = MXUsersDevicesMap<Any>()
users.forEach { pairOfUserDevices ->
val userId = pairOfUserDevices.first
pairOfUserDevices.second.forEach {
contentMap.setObject(userId, it, fakeEncryptedContent)
}
}
val params = SendToDeviceTask.Params(
eventType = EventType.ENCRYPTED,
contentMap = contentMap
)
runBlocking {
sendToDeviceTask.execute(params)
}
val generatedIds = mutableListOf<String>()
users.forEach { pairOfUserDevices ->
val userId = pairOfUserDevices.first
pairOfUserDevices.second.forEach {
val modifiedContent = fakeCryptoAPi.body!!.messages!![userId]!![it] as Map<*, *>
Assert.assertNotNull("Tracing id should have been added", modifiedContent["org.matrix.msgid"])
generatedIds.add(modifiedContent["org.matrix.msgid"] as String)
assertEquals(
"The rest of the content should be the same",
fakeEncryptedContent.keys,
modifiedContent.toMutableMap().apply { remove("org.matrix.msgid") }.keys
)
}
}
assertEquals("Id should be unique per content", generatedIds.size, generatedIds.toSet().size)
println("modified content ${fakeCryptoAPi.body}")
}
@Test
fun `tracing id should not be added to verification start to_device contents`() {
val fakeCryptoAPi = FakeCryptoApi()
val sendToDeviceTask = DefaultSendToDeviceTask(
cryptoApi = fakeCryptoAPi,
globalErrorReceiver = mockk(relaxed = true)
)
val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject("@alice:example.com", "MNQHVEISFQ", fakeStartVerificationContent)
val params = SendToDeviceTask.Params(
eventType = EventType.KEY_VERIFICATION_START,
contentMap = contentMap
)
runBlocking {
sendToDeviceTask.execute(params)
}
val modifiedContent = fakeCryptoAPi.body!!.messages!!["@alice:example.com"]!!["MNQHVEISFQ"] as Map<*, *>
Assert.assertNull("Tracing id should not have been added", modifiedContent["org.matrix.msgid"])
// try to force
runBlocking {
sendToDeviceTask.execute(
SendToDeviceTask.Params(
eventType = EventType.KEY_VERIFICATION_START,
contentMap = contentMap,
addTracingIds = true
)
)
}
val modifiedContentForced = fakeCryptoAPi.body!!.messages!!["@alice:example.com"]!!["MNQHVEISFQ"] as Map<*, *>
Assert.assertNotNull("Tracing id should have been added", modifiedContentForced["org.matrix.msgid"])
}
@Test
fun `tracing id should not be added to all verification to_device contents`() {
val fakeCryptoAPi = FakeCryptoApi()
val sendToDeviceTask = DefaultSendToDeviceTask(
cryptoApi = fakeCryptoAPi,
globalErrorReceiver = mockk(relaxed = true)
)
val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject("@alice:example.com", "MNQHVEISFQ", emptyMap<String, Any>())
val verificationEvents = listOf(
MessageType.MSGTYPE_VERIFICATION_REQUEST,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_READY
)
for (type in verificationEvents) {
val params = SendToDeviceTask.Params(
eventType = type,
contentMap = contentMap
)
runBlocking {
sendToDeviceTask.execute(params)
}
val modifiedContent = fakeCryptoAPi.body!!.messages!!["@alice:example.com"]!!["MNQHVEISFQ"] as Map<*, *>
Assert.assertNull("Tracing id should not have been added", modifiedContent["org.matrix.msgid"])
}
}
internal class FakeCryptoApi : CryptoApi {
override suspend fun getDevices(): DevicesListResponse {
throw java.lang.AssertionError("Should not be called")
}
override suspend fun getDeviceInfo(deviceId: String): DeviceInfo {
throw java.lang.AssertionError("Should not be called")
}
override suspend fun uploadKeys(body: KeysUploadBody): KeysUploadResponse {
throw java.lang.AssertionError("Should not be called")
}
override suspend fun downloadKeysForUsers(params: KeysQueryBody): KeysQueryResponse {
throw java.lang.AssertionError("Should not be called")
}
override suspend fun uploadSigningKeys(params: UploadSigningKeysBody): KeysQueryResponse {
throw java.lang.AssertionError("Should not be called")
}
override suspend fun uploadSignatures(params: Map<String, Any>?): SignatureUploadResponse {
throw java.lang.AssertionError("Should not be called")
}
override suspend fun claimOneTimeKeysForUsersDevices(body: KeysClaimBody): KeysClaimResponse {
throw java.lang.AssertionError("Should not be called")
}
var body: SendToDeviceBody? = null
override suspend fun sendToDevice(eventType: String, transactionId: String, body: SendToDeviceBody) {
this.body = body
}
override suspend fun deleteDevice(deviceId: String, params: DeleteDeviceParams) {
throw java.lang.AssertionError("Should not be called")
}
override suspend fun deleteDevices(params: DeleteDevicesParams) {
throw java.lang.AssertionError("Should not be called")
}
override suspend fun updateDeviceInfo(deviceId: String, params: UpdateDeviceInfoBody) {
throw java.lang.AssertionError("Should not be called")
}
override suspend fun getKeyChanges(oldToken: String, newToken: String): KeyChangesResponse {
throw java.lang.AssertionError("Should not be called")
}
}
}

View file

@ -345,7 +345,8 @@ ${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86-release-signe
printf "File vector-gplay-x86_64-release-signed.apk:\n" printf "File vector-gplay-x86_64-release-signed.apk:\n"
${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86_64-release-signed.apk | grep package ${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86_64-release-signed.apk | grep package
read -p "\nDoes it look correct? Press enter when it's done." printf "\n"
read -p "Does it look correct? Press enter when it's done."
printf "\n================================================================================\n" printf "\n================================================================================\n"
read -p "Installing apk on a real device, press enter when a real device is connected. " read -p "Installing apk on a real device, press enter when a real device is connected. "
@ -356,7 +357,7 @@ read -p "Please run the APK on your phone to check that the upgrade went well (n
# TODO Get the block to copy from towncrier earlier (be may be edited by the release manager)? # TODO Get the block to copy from towncrier earlier (be may be edited by the release manager)?
read -p "Create the release on gitHub from the tag https://github.com/vector-im/element-android/tags, copy paste the block from the file CHANGES.md. Press enter when it's done." read -p "Create the release on gitHub from the tag https://github.com/vector-im/element-android/tags, copy paste the block from the file CHANGES.md. Press enter when it's done."
read -p "Add the 4 signed APKs to the GitHub release. Press enter when it's done." read -p "Add the 4 signed APKs to the GitHub release. They are located at ${targetPath}. Press enter when it's done."
printf "\n================================================================================\n" printf "\n================================================================================\n"
printf "Message for the Android internal room:\n\n" printf "Message for the Android internal room:\n\n"

View file

@ -403,7 +403,7 @@ dependencies {
debugImplementation(libs.flipper.flipperNetworkPlugin) { debugImplementation(libs.flipper.flipperNetworkPlugin) {
exclude group: 'com.facebook.fbjni', module: 'fbjni' exclude group: 'com.facebook.fbjni', module: 'fbjni'
} }
debugImplementation 'com.facebook.soloader:soloader:0.10.4' debugImplementation 'com.facebook.soloader:soloader:0.10.5'
debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0" debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0"
gplayImplementation "com.google.android.gms:play-services-location:21.0.1" gplayImplementation "com.google.android.gms:play-services-location:21.0.1"
@ -418,7 +418,7 @@ dependencies {
// API-only library // API-only library
gplayImplementation libs.google.appdistributionApi gplayImplementation libs.google.appdistributionApi
// Full SDK implementation // Full SDK implementation
gplayImplementation libs.google.appdistribution nightlyImplementation libs.google.appdistribution
// OSS License, gplay flavor only // OSS License, gplay flavor only
gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
@ -448,5 +448,5 @@ dependencies {
androidTestImplementation libs.androidx.fragmentTesting androidTestImplementation libs.androidx.fragmentTesting
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22" androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22"
debugImplementation libs.androidx.fragmentTesting debugImplementation libs.androidx.fragmentTesting
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
} }

View file

@ -46,9 +46,9 @@ abstract class FlavorModule {
@Provides @Provides
fun provideNightlyProxy() = object : NightlyProxy { fun provideNightlyProxy() = object : NightlyProxy {
override fun onHomeResumed() { override fun canDisplayPopup() = false
// no op override fun isNightlyBuild() = false
} override fun updateApplication() = Unit
} }
@Provides @Provides

View file

@ -17,7 +17,6 @@
package im.vector.app.push.fcm package im.vector.app.push.fcm
import android.app.Activity
import android.content.Context import android.content.Context
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.pushers.FcmHelper import im.vector.app.core.pushers.FcmHelper
@ -44,7 +43,7 @@ class FdroidFcmHelper @Inject constructor(
// No op // No op
} }
override fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) { override fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) {
// No op // No op
} }

View file

@ -34,8 +34,11 @@ class FirebaseNightlyProxy @Inject constructor(
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
) : NightlyProxy { ) : NightlyProxy {
override fun onHomeResumed() { override fun isNightlyBuild(): Boolean {
if (!canDisplayPopup()) return return buildMeta.applicationId in nightlyPackages
}
override fun updateApplication() {
val firebaseAppDistribution = FirebaseAppDistribution.getInstance() val firebaseAppDistribution = FirebaseAppDistribution.getInstance()
firebaseAppDistribution.updateIfNewReleaseAvailable() firebaseAppDistribution.updateIfNewReleaseAvailable()
.addOnProgressListener { up -> .addOnProgressListener { up ->
@ -46,6 +49,7 @@ class FirebaseNightlyProxy @Inject constructor(
when (e.errorCode) { when (e.errorCode) {
FirebaseAppDistributionException.Status.NOT_IMPLEMENTED -> { FirebaseAppDistributionException.Status.NOT_IMPLEMENTED -> {
// SDK did nothing. This is expected when building for Play. // SDK did nothing. This is expected when building for Play.
Timber.d("FirebaseAppDistribution NOT_IMPLEMENTED error")
} }
else -> { else -> {
// Handle other errors. // Handle other errors.
@ -56,10 +60,14 @@ class FirebaseNightlyProxy @Inject constructor(
Timber.e(e, "FirebaseAppDistribution - other error") Timber.e(e, "FirebaseAppDistribution - other error")
} }
} }
.addOnSuccessListener {
Timber.d("FirebaseAppDistribution Success!")
}
} }
private fun canDisplayPopup(): Boolean { override fun canDisplayPopup(): Boolean {
if (buildMeta.applicationId != "im.vector.app.nightly") return false if (!POPUP_IS_ENABLED) return false
if (!isNightlyBuild()) return false
val today = clock.epochMillis() / A_DAY_IN_MILLIS val today = clock.epochMillis() / A_DAY_IN_MILLIS
val lastDisplayPopupDay = sharedPreferences.getLong(SHARED_PREF_KEY, 0) val lastDisplayPopupDay = sharedPreferences.getLong(SHARED_PREF_KEY, 0)
return (today > lastDisplayPopupDay) return (today > lastDisplayPopupDay)
@ -73,7 +81,12 @@ class FirebaseNightlyProxy @Inject constructor(
} }
companion object { companion object {
private const val POPUP_IS_ENABLED = false
private const val A_DAY_IN_MILLIS = 8_600_000L private const val A_DAY_IN_MILLIS = 8_600_000L
private const val SHARED_PREF_KEY = "LAST_NIGHTLY_POPUP_DAY" private const val SHARED_PREF_KEY = "LAST_NIGHTLY_POPUP_DAY"
private val nightlyPackages = listOf(
"im.vector.app.nightly"
)
} }
} }

View file

@ -15,7 +15,6 @@
*/ */
package im.vector.app.push.fcm package im.vector.app.push.fcm
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.widget.Toast import android.widget.Toast
@ -23,6 +22,7 @@ import androidx.core.content.edit
import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.common.GoogleApiAvailability
import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessaging
import dagger.hilt.android.qualifiers.ApplicationContext
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.DefaultPreferences import im.vector.app.core.di.DefaultPreferences
@ -36,8 +36,8 @@ import javax.inject.Inject
* It has an alter ego in the fdroid variant. * It has an alter ego in the fdroid variant.
*/ */
class GoogleFcmHelper @Inject constructor( class GoogleFcmHelper @Inject constructor(
@DefaultPreferences @ApplicationContext private val context: Context,
private val sharedPrefs: SharedPreferences, @DefaultPreferences private val sharedPrefs: SharedPreferences,
) : FcmHelper { ) : FcmHelper {
companion object { companion object {
private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN"
@ -56,10 +56,9 @@ class GoogleFcmHelper @Inject constructor(
} }
} }
override fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) { override fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) {
// if (TextUtils.isEmpty(getFcmToken(activity))) {
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features' // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
if (checkPlayServices(activity)) { if (checkPlayServices(context)) {
try { try {
FirebaseMessaging.getInstance().token FirebaseMessaging.getInstance().token
.addOnSuccessListener { token -> .addOnSuccessListener { token ->
@ -75,7 +74,7 @@ class GoogleFcmHelper @Inject constructor(
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")
} }
} else { } else {
Toast.makeText(activity, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show()
Timber.e("No valid Google Play Services found. Cannot use FCM.") Timber.e("No valid Google Play Services found. Cannot use FCM.")
} }
} }

View file

@ -46,6 +46,7 @@ import im.vector.app.core.utils.AndroidSystemSettingsProvider
import im.vector.app.core.utils.SystemSettingsProvider import im.vector.app.core.utils.SystemSettingsProvider
import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.analytics.errors.ErrorTracker
import im.vector.app.features.analytics.impl.DefaultVectorAnalytics import im.vector.app.features.analytics.impl.DefaultVectorAnalytics
import im.vector.app.features.analytics.metrics.VectorPlugins import im.vector.app.features.analytics.metrics.VectorPlugins
import im.vector.app.features.invite.AutoAcceptInvites import im.vector.app.features.invite.AutoAcceptInvites
@ -84,6 +85,9 @@ import javax.inject.Singleton
@Binds @Binds
abstract fun bindVectorAnalytics(analytics: DefaultVectorAnalytics): VectorAnalytics abstract fun bindVectorAnalytics(analytics: DefaultVectorAnalytics): VectorAnalytics
@Binds
abstract fun bindErrorTracker(analytics: DefaultVectorAnalytics): ErrorTracker
@Binds @Binds
abstract fun bindAnalyticsTracker(analytics: DefaultVectorAnalytics): AnalyticsTracker abstract fun bindAnalyticsTracker(analytics: DefaultVectorAnalytics): AnalyticsTracker

View file

@ -72,7 +72,9 @@
<application android:supportsRtl="true"> <application android:supportsRtl="true">
<!-- Sentry auto-initialization disable --> <!-- Sentry auto-initialization disable -->
<meta-data android:name="io.sentry.auto-init" android:value="false" /> <meta-data
android:name="io.sentry.auto-init"
android:value="false" />
<!-- No limit for screen ratio: avoid black strips --> <!-- No limit for screen ratio: avoid black strips -->
<meta-data <meta-data
@ -330,6 +332,7 @@
<service <service
android:name=".core.services.CallAndroidService" android:name=".core.services.CallAndroidService"
android:foregroundServiceType="phoneCall"
android:exported="false"> android:exported="false">
<!-- in order to get headset button events --> <!-- in order to get headset button events -->
<intent-filter> <intent-filter>
@ -341,6 +344,7 @@
<service <service
android:name=".core.services.VectorSyncAndroidService" android:name=".core.services.VectorSyncAndroidService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync"
tools:ignore="Instantiatable" /> tools:ignore="Instantiatable" />
<service <service

View file

@ -19,7 +19,7 @@ package im.vector.app.core.di
import android.content.Context import android.content.Context
import im.vector.app.ActiveSessionDataSource import im.vector.app.ActiveSessionDataSource
import im.vector.app.core.extensions.startSyncing import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
import im.vector.app.core.services.GuardServiceStarter import im.vector.app.core.services.GuardServiceStarter
import im.vector.app.core.session.ConfigureAndStartSessionUseCase import im.vector.app.core.session.ConfigureAndStartSessionUseCase
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
@ -46,12 +46,12 @@ class ActiveSessionHolder @Inject constructor(
private val pushRuleTriggerListener: PushRuleTriggerListener, private val pushRuleTriggerListener: PushRuleTriggerListener,
private val sessionListener: SessionListener, private val sessionListener: SessionListener,
private val imageManager: ImageManager, private val imageManager: ImageManager,
private val unifiedPushHelper: UnifiedPushHelper,
private val guardServiceStarter: GuardServiceStarter, private val guardServiceStarter: GuardServiceStarter,
private val sessionInitializer: SessionInitializer, private val sessionInitializer: SessionInitializer,
private val applicationContext: Context, private val applicationContext: Context,
private val authenticationService: AuthenticationService, private val authenticationService: AuthenticationService,
private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase, private val configureAndStartSessionUseCase: ConfigureAndStartSessionUseCase,
private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
) { ) {
private var activeSessionReference: AtomicReference<Session?> = AtomicReference() private var activeSessionReference: AtomicReference<Session?> = AtomicReference()
@ -85,7 +85,7 @@ class ActiveSessionHolder @Inject constructor(
incomingVerificationRequestHandler.stop() incomingVerificationRequestHandler.stop()
pushRuleTriggerListener.stop() pushRuleTriggerListener.stop()
// No need to unregister the pusher, the sign out will (should?) do it server side. // No need to unregister the pusher, the sign out will (should?) do it server side.
unifiedPushHelper.unregister(pushersManager = null) unregisterUnifiedPushUseCase.execute(pushersManager = null)
guardServiceStarter.stop() guardServiceStarter.stop()
} }

View file

@ -104,6 +104,7 @@ import im.vector.app.features.settings.ignored.IgnoredUsersViewModel
import im.vector.app.features.settings.labs.VectorSettingsLabsViewModel import im.vector.app.features.settings.labs.VectorSettingsLabsViewModel
import im.vector.app.features.settings.legals.LegalsViewModel import im.vector.app.features.settings.legals.LegalsViewModel
import im.vector.app.features.settings.locale.LocalePickerViewModel import im.vector.app.features.settings.locale.LocalePickerViewModel
import im.vector.app.features.settings.notifications.VectorSettingsNotificationPreferenceViewModel
import im.vector.app.features.settings.push.PushGatewaysViewModel import im.vector.app.features.settings.push.PushGatewaysViewModel
import im.vector.app.features.settings.threepids.ThreePidsSettingsViewModel import im.vector.app.features.settings.threepids.ThreePidsSettingsViewModel
import im.vector.app.features.share.IncomingShareViewModel import im.vector.app.features.share.IncomingShareViewModel
@ -687,4 +688,11 @@ interface MavericksViewModelModule {
@IntoMap @IntoMap
@MavericksViewModelKey(AttachmentTypeSelectorViewModel::class) @MavericksViewModelKey(AttachmentTypeSelectorViewModel::class)
fun attachmentTypeSelectorViewModelFactory(factory: AttachmentTypeSelectorViewModel.Factory): MavericksAssistedViewModelFactory<*, *> fun attachmentTypeSelectorViewModelFactory(factory: AttachmentTypeSelectorViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(VectorSettingsNotificationPreferenceViewModel::class)
fun vectorSettingsNotificationPreferenceViewModelFactory(
factory: VectorSettingsNotificationPreferenceViewModel.Factory
): MavericksAssistedViewModelFactory<*, *>
} }

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.extensions
import android.app.Notification
import android.app.Service
import android.content.pm.ServiceInfo
import android.os.Build
fun Service.startForegroundCompat(
id: Int,
notification: Notification,
provideForegroundServiceType: (() -> Int)? = null
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
id,
notification,
provideForegroundServiceType?.invoke() ?: ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
)
} else {
startForeground(id, notification)
}
}

View file

@ -23,14 +23,21 @@ import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
/**
* Listen changes in Pusher or Account Data to update the local setting for notification toggle.
*/
@Singleton @Singleton
class EnableNotificationsSettingUpdater @Inject constructor( class NotificationsSettingUpdater @Inject constructor(
private val updateEnableNotificationsSettingOnChangeUseCase: UpdateEnableNotificationsSettingOnChangeUseCase, private val updateEnableNotificationsSettingOnChangeUseCase: UpdateEnableNotificationsSettingOnChangeUseCase,
) { ) {
private var job: Job? = null private var job: Job? = null
fun onSessionsStarted(session: Session) { fun onSessionStarted(session: Session) {
updateEnableNotificationsSettingOnChange(session)
}
private fun updateEnableNotificationsSettingOnChange(session: Session) {
job?.cancel() job?.cancel()
job = session.coroutineScope.launch { job = session.coroutineScope.launch {
updateEnableNotificationsSettingOnChangeUseCase.execute(session) updateEnableNotificationsSettingOnChangeUseCase.execute(session)

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.pushers
import im.vector.app.core.di.ActiveSessionHolder
import javax.inject.Inject
class EnsureFcmTokenIsRetrievedUseCase @Inject constructor(
private val unifiedPushHelper: UnifiedPushHelper,
private val fcmHelper: FcmHelper,
private val activeSessionHolder: ActiveSessionHolder,
) {
fun execute(pushersManager: PushersManager, registerPusher: Boolean) {
if (unifiedPushHelper.isEmbeddedDistributor()) {
fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher))
}
}
private fun shouldAddHttpPusher(registerPusher: Boolean) = if (registerPusher) {
val currentSession = activeSessionHolder.getActiveSession()
val currentPushers = currentSession.pushersService().getPushers()
currentPushers.none { it.deviceId == currentSession.sessionParams.deviceId }
} else {
false
}
}

View file

@ -16,7 +16,6 @@
package im.vector.app.core.pushers package im.vector.app.core.pushers
import android.app.Activity
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
interface FcmHelper { interface FcmHelper {
@ -39,11 +38,10 @@ interface FcmHelper {
/** /**
* onNewToken may not be called on application upgrade, so ensure my shared pref is set. * onNewToken may not be called on application upgrade, so ensure my shared pref is set.
* *
* @param activity the first launch Activity.
* @param pushersManager the instance to register the pusher on. * @param pushersManager the instance to register the pusher on.
* @param registerPusher whether the pusher should be registered. * @param registerPusher whether the pusher should be registered.
*/ */
fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean)
fun onEnterForeground(activeSessionHolder: ActiveSessionHolder) fun onEnterForeground(activeSessionHolder: ActiveSessionHolder)

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.pushers
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import im.vector.app.features.VectorFeatures
import org.unifiedpush.android.connector.UnifiedPush
import javax.inject.Inject
class RegisterUnifiedPushUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val vectorFeatures: VectorFeatures,
) {
sealed interface RegisterUnifiedPushResult {
object Success : RegisterUnifiedPushResult
object NeedToAskUserForDistributor : RegisterUnifiedPushResult
}
fun execute(distributor: String = ""): RegisterUnifiedPushResult {
if (distributor.isNotEmpty()) {
saveAndRegisterApp(distributor)
return RegisterUnifiedPushResult.Success
}
if (!vectorFeatures.allowExternalUnifiedPushDistributors()) {
saveAndRegisterApp(context.packageName)
return RegisterUnifiedPushResult.Success
}
if (UnifiedPush.getDistributor(context).isNotEmpty()) {
registerApp()
return RegisterUnifiedPushResult.Success
}
val distributors = UnifiedPush.getDistributors(context)
return if (distributors.size == 1) {
saveAndRegisterApp(distributors.first())
RegisterUnifiedPushResult.Success
} else {
RegisterUnifiedPushResult.NeedToAskUserForDistributor
}
}
private fun saveAndRegisterApp(distributor: String) {
UnifiedPush.saveDistributor(context, distributor)
registerApp()
}
private fun registerApp() {
UnifiedPush.registerApp(context)
}
}

View file

@ -17,18 +17,13 @@
package im.vector.app.core.pushers package im.vector.app.core.pushers
import android.content.Context import android.content.Context
import androidx.fragment.app.FragmentActivity import androidx.annotation.MainThread
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.getApplicationLabel import im.vector.app.core.utils.getApplicationLabel
import im.vector.app.features.VectorFeatures
import im.vector.app.features.settings.BackgroundSyncMode
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.api.cache.CacheStrategy
import org.matrix.android.sdk.api.util.MatrixJsonParser import org.matrix.android.sdk.api.util.MatrixJsonParser
@ -41,90 +36,14 @@ class UnifiedPushHelper @Inject constructor(
private val context: Context, private val context: Context,
private val unifiedPushStore: UnifiedPushStore, private val unifiedPushStore: UnifiedPushStore,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences,
private val matrix: Matrix, private val matrix: Matrix,
private val vectorFeatures: VectorFeatures,
private val fcmHelper: FcmHelper, private val fcmHelper: FcmHelper,
) { ) {
// Called when the home activity starts @MainThread
// or when notifications are enabled fun showSelectDistributorDialog(
fun register( context: Context,
activity: FragmentActivity, onDistributorSelected: (String) -> Unit,
onDoneRunnable: Runnable? = null,
) {
registerInternal(
activity,
onDoneRunnable = onDoneRunnable
)
}
// If registration is forced:
// * the current distributor (if any) is removed
// * The dialog is opened
//
// The registration is forced in 2 cases :
// * in the settings
// * in the troubleshoot list (doFix)
fun forceRegister(
activity: FragmentActivity,
pushersManager: PushersManager,
onDoneRunnable: Runnable? = null
) {
registerInternal(
activity,
force = true,
pushersManager = pushersManager,
onDoneRunnable = onDoneRunnable
)
}
private fun registerInternal(
activity: FragmentActivity,
force: Boolean = false,
pushersManager: PushersManager? = null,
onDoneRunnable: Runnable? = null
) {
activity.lifecycleScope.launch {
if (!vectorFeatures.allowExternalUnifiedPushDistributors()) {
UnifiedPush.saveDistributor(context, context.packageName)
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
return@launch
}
if (force) {
// Un-register first
unregister(pushersManager)
}
// the !force should not be needed
if (!force && UnifiedPush.getDistributor(context).isNotEmpty()) {
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
return@launch
}
val distributors = UnifiedPush.getDistributors(context)
if (!force && distributors.size == 1) {
UnifiedPush.saveDistributor(context, distributors.first())
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
} else {
openDistributorDialogInternal(
activity = activity,
onDoneRunnable = onDoneRunnable,
distributors = distributors
)
}
}
}
// There is no case where this function is called
// with a saved distributor and/or a pusher
private fun openDistributorDialogInternal(
activity: FragmentActivity,
onDoneRunnable: Runnable?,
distributors: List<String>
) { ) {
val internalDistributorName = stringProvider.getString( val internalDistributorName = stringProvider.getString(
if (fcmHelper.isFirebaseAvailable()) { if (fcmHelper.isFirebaseAvailable()) {
@ -134,6 +53,7 @@ class UnifiedPushHelper @Inject constructor(
} }
) )
val distributors = UnifiedPush.getDistributors(context)
val distributorsName = distributors.map { val distributorsName = distributors.map {
if (it == context.packageName) { if (it == context.packageName) {
internalDistributorName internalDistributorName
@ -142,44 +62,23 @@ class UnifiedPushHelper @Inject constructor(
} }
} }
MaterialAlertDialogBuilder(activity) MaterialAlertDialogBuilder(context)
.setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title)) .setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title))
.setItems(distributorsName.toTypedArray()) { _, which -> .setItems(distributorsName.toTypedArray()) { _, which ->
val distributor = distributors[which] val distributor = distributors[which]
onDistributorSelected(distributor)
activity.lifecycleScope.launch {
UnifiedPush.saveDistributor(context, distributor)
Timber.i("Saving distributor: $distributor")
UnifiedPush.registerApp(context)
onDoneRunnable?.run()
}
} }
.setOnCancelListener { .setOnCancelListener {
// By default, use internal solution (fcm/background sync) // we do not want to change the distributor on behalf of the user
UnifiedPush.saveDistributor(context, context.packageName) if (UnifiedPush.getDistributor(context).isEmpty()) {
UnifiedPush.registerApp(context) // By default, use internal solution (fcm/background sync)
onDoneRunnable?.run() onDistributorSelected(context.packageName)
}
} }
.setCancelable(true) .setCancelable(true)
.show() .show()
} }
suspend fun unregister(pushersManager: PushersManager? = null) {
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
vectorPreferences.setFdroidSyncBackgroundMode(mode)
try {
getEndpointOrToken()?.let {
Timber.d("Removing $it")
pushersManager?.unregisterPusher(it)
}
} catch (e: Exception) {
Timber.d(e, "Probably unregistering a non existing pusher")
}
unifiedPushStore.storeUpEndpoint(null)
unifiedPushStore.storePushGateway(null)
UnifiedPush.unregisterApp(context)
}
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class DiscoveryResponse( internal data class DiscoveryResponse(
@Json(name = "unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush() @Json(name = "unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush()

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.pushers
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import im.vector.app.features.settings.BackgroundSyncMode
import im.vector.app.features.settings.VectorPreferences
import org.unifiedpush.android.connector.UnifiedPush
import timber.log.Timber
import javax.inject.Inject
class UnregisterUnifiedPushUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val vectorPreferences: VectorPreferences,
private val unifiedPushStore: UnifiedPushStore,
private val unifiedPushHelper: UnifiedPushHelper,
) {
suspend fun execute(pushersManager: PushersManager?) {
val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME
vectorPreferences.setFdroidSyncBackgroundMode(mode)
try {
unifiedPushHelper.getEndpointOrToken()?.let {
Timber.d("Removing $it")
pushersManager?.unregisterPusher(it)
}
} catch (e: Exception) {
Timber.d(e, "Probably unregistering a non existing pusher")
}
unifiedPushStore.storeUpEndpoint(null)
unifiedPushStore.storePushGateway(null)
UnifiedPush.unregisterApp(context)
}
}

View file

@ -28,6 +28,7 @@ import androidx.media.session.MediaButtonReceiver
import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Mavericks
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.startForegroundCompat
import im.vector.app.features.call.CallArgs import im.vector.app.features.call.CallArgs
import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.telecom.CallConnection import im.vector.app.features.call.telecom.CallConnection
@ -181,7 +182,7 @@ class CallAndroidService : VectorAndroidService() {
fromBg = fromBg fromBg = fromBg
) )
if (knownCalls.isEmpty()) { if (knownCalls.isEmpty()) {
startForeground(callId.hashCode(), notification) startForegroundCompat(callId.hashCode(), notification)
} else { } else {
notificationManager.notify(callId.hashCode(), notification) notificationManager.notify(callId.hashCode(), notification)
} }
@ -201,7 +202,7 @@ class CallAndroidService : VectorAndroidService() {
} }
val notification = notificationUtils.buildCallEndedNotification(false) val notification = notificationUtils.buildCallEndedNotification(false)
val notificationId = callId.hashCode() val notificationId = callId.hashCode()
startForeground(notificationId, notification) startForegroundCompat(notificationId, notification)
if (knownCalls.isEmpty()) { if (knownCalls.isEmpty()) {
Timber.tag(loggerTag.value).v("No more call, stop the service") Timber.tag(loggerTag.value).v("No more call, stop the service")
stopForegroundCompat() stopForegroundCompat()
@ -236,7 +237,7 @@ class CallAndroidService : VectorAndroidService() {
title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId
) )
if (knownCalls.isEmpty()) { if (knownCalls.isEmpty()) {
startForeground(callId.hashCode(), notification) startForegroundCompat(callId.hashCode(), notification)
} else { } else {
notificationManager.notify(callId.hashCode(), notification) notificationManager.notify(callId.hashCode(), notification)
} }
@ -260,7 +261,7 @@ class CallAndroidService : VectorAndroidService() {
title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId
) )
if (knownCalls.isEmpty()) { if (knownCalls.isEmpty()) {
startForeground(callId.hashCode(), notification) startForegroundCompat(callId.hashCode(), notification)
} else { } else {
notificationManager.notify(callId.hashCode(), notification) notificationManager.notify(callId.hashCode(), notification)
} }
@ -273,9 +274,9 @@ class CallAndroidService : VectorAndroidService() {
callRingPlayerOutgoing?.stop() callRingPlayerOutgoing?.stop()
val notification = notificationUtils.buildCallEndedNotification(false) val notification = notificationUtils.buildCallEndedNotification(false)
if (callId != null) { if (callId != null) {
startForeground(callId.hashCode(), notification) startForegroundCompat(callId.hashCode(), notification)
} else { } else {
startForeground(DEFAULT_NOTIFICATION_ID, notification) startForegroundCompat(DEFAULT_NOTIFICATION_ID, notification)
} }
if (knownCalls.isEmpty()) { if (knownCalls.isEmpty()) {
mediaSession?.isActive = false mediaSession?.isActive = false

View file

@ -32,6 +32,7 @@ import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.startForegroundCompat
import im.vector.app.core.platform.PendingIntentCompat import im.vector.app.core.platform.PendingIntentCompat
import im.vector.app.core.time.Clock import im.vector.app.core.time.Clock
import im.vector.app.core.time.DefaultClock import im.vector.app.core.time.DefaultClock
@ -98,7 +99,7 @@ class VectorSyncAndroidService : SyncAndroidService() {
R.string.notification_listening_for_notifications R.string.notification_listening_for_notifications
} }
val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false) val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false)
startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification) startForegroundCompat(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
} }
override fun onRescheduleAsked( override fun onRescheduleAsked(

View file

@ -19,11 +19,12 @@ package im.vector.app.core.session
import android.content.Context import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import im.vector.app.core.extensions.startSyncing import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.notification.EnableNotificationsSettingUpdater import im.vector.app.core.notification.NotificationsSettingUpdater
import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase
import im.vector.app.features.sync.SyncUtils import im.vector.app.features.sync.SyncUtils
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -35,7 +36,8 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
private val webRtcCallManager: WebRtcCallManager, private val webRtcCallManager: WebRtcCallManager,
private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase, private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val enableNotificationsSettingUpdater: EnableNotificationsSettingUpdater, private val notificationsSettingUpdater: NotificationsSettingUpdater,
private val updateNotificationSettingsAccountDataUseCase: UpdateNotificationSettingsAccountDataUseCase,
) { ) {
fun execute(session: Session, startSyncing: Boolean = true) { fun execute(session: Session, startSyncing: Boolean = true) {
@ -49,11 +51,22 @@ class ConfigureAndStartSessionUseCase @Inject constructor(
} }
session.pushersService().refreshPushers() session.pushersService().refreshPushers()
webRtcCallManager.checkForProtocolsSupportIfNeeded() webRtcCallManager.checkForProtocolsSupportIfNeeded()
updateMatrixClientInfoIfNeeded(session)
createNotificationSettingsAccountDataIfNeeded(session)
notificationsSettingUpdater.onSessionStarted(session)
}
private fun updateMatrixClientInfoIfNeeded(session: Session) {
session.coroutineScope.launch { session.coroutineScope.launch {
if (vectorPreferences.isClientInfoRecordingEnabled()) { if (vectorPreferences.isClientInfoRecordingEnabled()) {
updateMatrixClientInfoUseCase.execute(session) updateMatrixClientInfoUseCase.execute(session)
} }
} }
enableNotificationsSettingUpdater.onSessionsStarted(session) }
private fun createNotificationSettingsAccountDataIfNeeded(session: Session) {
session.coroutineScope.launch {
updateNotificationSettingsAccountDataUseCase.execute(session)
}
} }
} }

View file

@ -38,6 +38,25 @@ class ShieldImageView @JvmOverloads constructor(
} }
} }
/**
* Renders device shield with the support of unknown shields instead of black shields which is used for rooms.
* @param roomEncryptionTrustLevel trust level that is usally calculated with [im.vector.app.features.settings.devices.TrustUtils.shieldForTrust]
* @param borderLess if true then the shield icon with border around is used
*/
fun renderDeviceShield(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, borderLess: Boolean = false) {
isVisible = roomEncryptionTrustLevel != null
if (roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Default) {
contentDescription = context.getString(R.string.a11y_trust_level_default)
setImageResource(
if (borderLess) R.drawable.ic_shield_unknown_no_border
else R.drawable.ic_shield_unknown
)
} else {
render(roomEncryptionTrustLevel, borderLess)
}
}
fun render(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, borderLess: Boolean = false) { fun render(roomEncryptionTrustLevel: RoomEncryptionTrustLevel?, borderLess: Boolean = false) {
isVisible = roomEncryptionTrustLevel != null isVisible = roomEncryptionTrustLevel != null
@ -45,8 +64,8 @@ class ShieldImageView @JvmOverloads constructor(
RoomEncryptionTrustLevel.Default -> { RoomEncryptionTrustLevel.Default -> {
contentDescription = context.getString(R.string.a11y_trust_level_default) contentDescription = context.getString(R.string.a11y_trust_level_default)
setImageResource( setImageResource(
if (borderLess) R.drawable.ic_shield_unknown_no_border if (borderLess) R.drawable.ic_shield_black_no_border
else R.drawable.ic_shield_unknown else R.drawable.ic_shield_black
) )
} }
RoomEncryptionTrustLevel.Warning -> { RoomEncryptionTrustLevel.Warning -> {
@ -137,7 +156,7 @@ class ShieldImageView @JvmOverloads constructor(
@DrawableRes @DrawableRes
fun RoomEncryptionTrustLevel.toDrawableRes(): Int { fun RoomEncryptionTrustLevel.toDrawableRes(): Int {
return when (this) { return when (this) {
RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_unknown RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_black
RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning
RoomEncryptionTrustLevel.Trusted -> R.drawable.ic_shield_trusted RoomEncryptionTrustLevel.Trusted -> R.drawable.ic_shield_trusted
RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm -> R.drawable.ic_warning_badge RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm -> R.drawable.ic_warning_badge

View file

@ -16,9 +16,10 @@
package im.vector.app.features.analytics package im.vector.app.features.analytics
import im.vector.app.features.analytics.errors.ErrorTracker
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface VectorAnalytics : AnalyticsTracker { interface VectorAnalytics : AnalyticsTracker, ErrorTracker {
/** /**
* Return a Flow of Boolean, true if the user has given their consent. * Return a Flow of Boolean, true if the user has given their consent.
*/ */

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.analytics.errors
interface ErrorTracker {
fun trackError(throwable: Throwable)
}

View file

@ -41,7 +41,7 @@ private val IGNORED_OPTIONS: Options? = null
@Singleton @Singleton
class DefaultVectorAnalytics @Inject constructor( class DefaultVectorAnalytics @Inject constructor(
postHogFactory: PostHogFactory, postHogFactory: PostHogFactory,
private val sentryFactory: SentryFactory, private val sentryAnalytics: SentryAnalytics,
analyticsConfig: AnalyticsConfig, analyticsConfig: AnalyticsConfig,
private val analyticsStore: AnalyticsStore, private val analyticsStore: AnalyticsStore,
private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory, private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory,
@ -97,7 +97,7 @@ class DefaultVectorAnalytics @Inject constructor(
setAnalyticsId("") setAnalyticsId("")
// Close Sentry SDK. // Close Sentry SDK.
sentryFactory.stopSentry() sentryAnalytics.stopSentry()
} }
private fun observeAnalyticsId() { private fun observeAnalyticsId() {
@ -135,8 +135,8 @@ class DefaultVectorAnalytics @Inject constructor(
private fun initOrStopSentry() { private fun initOrStopSentry() {
userConsent?.let { userConsent?.let {
when (it) { when (it) {
true -> sentryFactory.initSentry() true -> sentryAnalytics.initSentry()
false -> sentryFactory.stopSentry() false -> sentryAnalytics.stopSentry()
} }
} }
} }
@ -180,4 +180,10 @@ class DefaultVectorAnalytics @Inject constructor(
putAll(this@toPostHogUserProperties.filter { it.value != null }) putAll(this@toPostHogUserProperties.filter { it.value != null })
} }
} }
override fun trackError(throwable: Throwable) {
sentryAnalytics
.takeIf { userConsent == true }
?.trackError(throwable)
}
} }

View file

@ -18,6 +18,7 @@ package im.vector.app.features.analytics.impl
import android.content.Context import android.content.Context
import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.analytics.errors.ErrorTracker
import im.vector.app.features.analytics.log.analyticsTag import im.vector.app.features.analytics.log.analyticsTag
import io.sentry.Sentry import io.sentry.Sentry
import io.sentry.SentryOptions import io.sentry.SentryOptions
@ -25,10 +26,10 @@ import io.sentry.android.core.SentryAndroid
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class SentryFactory @Inject constructor( class SentryAnalytics @Inject constructor(
private val context: Context, private val context: Context,
private val analyticsConfig: AnalyticsConfig, private val analyticsConfig: AnalyticsConfig,
) { ) : ErrorTracker {
fun initSentry() { fun initSentry() {
Timber.tag(analyticsTag.value).d("Initializing Sentry") Timber.tag(analyticsTag.value).d("Initializing Sentry")
@ -47,4 +48,8 @@ class SentryFactory @Inject constructor(
Timber.tag(analyticsTag.value).d("Stopping Sentry") Timber.tag(analyticsTag.value).d("Stopping Sentry")
Sentry.close() Sentry.close()
} }
override fun trackError(throwable: Throwable) {
Sentry.captureException(throwable)
}
} }

View file

@ -20,6 +20,7 @@ import android.content.Intent
import android.os.Binder import android.os.Binder
import android.os.IBinder import android.os.IBinder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.startForegroundCompat
import im.vector.app.core.services.VectorAndroidService import im.vector.app.core.services.VectorAndroidService
import im.vector.app.core.time.Clock import im.vector.app.core.time.Clock
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
@ -41,7 +42,7 @@ class ScreenCaptureAndroidService : VectorAndroidService() {
private fun showStickyNotification() { private fun showStickyNotification() {
val notificationId = clock.epochMillis().toInt() val notificationId = clock.epochMillis().toInt()
val notification = notificationUtils.buildScreenSharingNotification() val notification = notificationUtils.buildScreenSharingNotification()
startForeground(notificationId, notification) startForegroundCompat(notificationId, notification)
} }
override fun onBind(intent: Intent?): IBinder { override fun onBind(intent: Intent?): IBinder {

View file

@ -44,8 +44,6 @@ import im.vector.app.core.extensions.restart
import im.vector.app.core.extensions.validateBackPressed import im.vector.app.core.extensions.validateBackPressed
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.platform.VectorMenuProvider
import im.vector.app.core.pushers.FcmHelper
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.UnifiedPushHelper import im.vector.app.core.pushers.UnifiedPushHelper
import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.core.utils.startSharePlainTextIntent
@ -131,7 +129,6 @@ class HomeActivity :
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel()
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var pushersManager: PushersManager
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var shortcutsHandler: ShortcutsHandler @Inject lateinit var shortcutsHandler: ShortcutsHandler
@ -140,7 +137,6 @@ class HomeActivity :
@Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter @Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter
@Inject lateinit var spaceStateHandler: SpaceStateHandler @Inject lateinit var spaceStateHandler: SpaceStateHandler
@Inject lateinit var unifiedPushHelper: UnifiedPushHelper @Inject lateinit var unifiedPushHelper: UnifiedPushHelper
@Inject lateinit var fcmHelper: FcmHelper
@Inject lateinit var nightlyProxy: NightlyProxy @Inject lateinit var nightlyProxy: NightlyProxy
@Inject lateinit var disclaimerDialog: DisclaimerDialog @Inject lateinit var disclaimerDialog: DisclaimerDialog
@Inject lateinit var notificationPermissionManager: NotificationPermissionManager @Inject lateinit var notificationPermissionManager: NotificationPermissionManager
@ -212,16 +208,6 @@ class HomeActivity :
isNewAppLayoutEnabled = vectorPreferences.isNewAppLayoutEnabled() isNewAppLayoutEnabled = vectorPreferences.isNewAppLayoutEnabled()
analyticsScreenName = MobileScreen.ScreenName.Home analyticsScreenName = MobileScreen.ScreenName.Home
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false)
unifiedPushHelper.register(this) {
if (unifiedPushHelper.isEmbeddedDistributor()) {
fcmHelper.ensureFcmTokenIsRetrieved(
this,
pushersManager,
homeActivityViewModel.shouldAddHttpPusher()
)
}
}
sharedActionViewModel = viewModelProvider[HomeSharedActionViewModel::class.java] sharedActionViewModel = viewModelProvider[HomeSharedActionViewModel::class.java]
roomListSharedActionViewModel = viewModelProvider[RoomListSharedActionViewModel::class.java] roomListSharedActionViewModel = viewModelProvider[RoomListSharedActionViewModel::class.java]
views.drawerLayout.addDrawerListener(drawerListener) views.drawerLayout.addDrawerListener(drawerListener)
@ -284,6 +270,7 @@ class HomeActivity :
HomeActivityViewEvents.ShowReleaseNotes -> handleShowReleaseNotes() HomeActivityViewEvents.ShowReleaseNotes -> handleShowReleaseNotes()
HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration() HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration()
is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession) is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession)
is HomeActivityViewEvents.AskUserForPushDistributor -> askUserToSelectPushDistributor()
} }
} }
homeActivityViewModel.onEach { renderState(it) } homeActivityViewModel.onEach { renderState(it) }
@ -296,6 +283,12 @@ class HomeActivity :
homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted) homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted)
} }
private fun askUserToSelectPushDistributor() {
unifiedPushHelper.showSelectDistributorDialog(this) { selection ->
homeActivityViewModel.handle(HomeActivityViewActions.RegisterPushDistributor(selection))
}
}
private fun handleShowNotificationDialog() { private fun handleShowNotificationDialog() {
notificationPermissionManager.eventuallyRequestPermission(this, postPermissionLauncher) notificationPermissionManager.eventuallyRequestPermission(this, postPermissionLauncher)
} }
@ -419,14 +412,6 @@ class HomeActivity :
} }
private fun renderState(state: HomeActivityViewState) { private fun renderState(state: HomeActivityViewState) {
lifecycleScope.launch {
if (state.areNotificationsSilenced) {
unifiedPushHelper.unregister(pushersManager)
} else {
unifiedPushHelper.register(this@HomeActivity)
}
}
when (val status = state.syncRequestState) { when (val status = state.syncRequestState) {
is SyncRequestState.InitialSyncProgressing -> { is SyncRequestState.InitialSyncProgressing -> {
val initSyncStepStr = initSyncStepFormatter.format(status.initialSyncStep) val initSyncStepStr = initSyncStepFormatter.format(status.initialSyncStep)
@ -609,7 +594,9 @@ class HomeActivity :
serverBackupStatusViewModel.refreshRemoteStateIfNeeded() serverBackupStatusViewModel.refreshRemoteStateIfNeeded()
// Check nightly // Check nightly
nightlyProxy.onHomeResumed() if (nightlyProxy.canDisplayPopup()) {
nightlyProxy.updateApplication()
}
checkNewAppLayoutFlagChange() checkNewAppLayoutFlagChange()
} }

View file

@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed interface HomeActivityViewActions : VectorViewModelAction { sealed interface HomeActivityViewActions : VectorViewModelAction {
object ViewStarted : HomeActivityViewActions object ViewStarted : HomeActivityViewActions
object PushPromptHasBeenReviewed : HomeActivityViewActions object PushPromptHasBeenReviewed : HomeActivityViewActions
data class RegisterPushDistributor(val distributor: String) : HomeActivityViewActions
} }

View file

@ -25,9 +25,11 @@ sealed interface HomeActivityViewEvents : VectorViewEvents {
val userItem: MatrixItem.UserItem, val userItem: MatrixItem.UserItem,
// val waitForIncomingRequest: Boolean = true, // val waitForIncomingRequest: Boolean = true,
) : HomeActivityViewEvents ) : HomeActivityViewEvents
data class CurrentSessionCannotBeVerified( data class CurrentSessionCannotBeVerified(
val userItem: MatrixItem.UserItem, val userItem: MatrixItem.UserItem,
) : HomeActivityViewEvents ) : HomeActivityViewEvents
data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents
object PromptToEnableSessionPush : HomeActivityViewEvents object PromptToEnableSessionPush : HomeActivityViewEvents
object ShowAnalyticsOptIn : HomeActivityViewEvents object ShowAnalyticsOptIn : HomeActivityViewEvents
@ -37,4 +39,5 @@ sealed interface HomeActivityViewEvents : VectorViewEvents {
data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents
object StartRecoverySetupFlow : HomeActivityViewEvents object StartRecoverySetupFlow : HomeActivityViewEvents
data class ForceVerification(val sendRequest: Boolean) : HomeActivityViewEvents data class ForceVerification(val sendRequest: Boolean) : HomeActivityViewEvents
object AskUserForPushDistributor : HomeActivityViewEvents
} }

View file

@ -16,7 +16,6 @@
package im.vector.app.features.home package im.vector.app.features.home
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
@ -27,7 +26,10 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.VectorFeatures import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.pushers.RegisterUnifiedPushUseCase
import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase
import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.AnalyticsConfig
import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.extensions.toAnalyticsType import im.vector.app.features.analytics.extensions.toAnalyticsType
@ -48,12 +50,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.UserPasswordAuth
@ -62,9 +62,7 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.session.getUserOrDefault
import org.matrix.android.sdk.api.session.pushrules.RuleIds import org.matrix.android.sdk.api.session.pushrules.RuleIds
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
@ -89,8 +87,11 @@ class HomeActivityViewModel @AssistedInject constructor(
private val analyticsTracker: AnalyticsTracker, private val analyticsTracker: AnalyticsTracker,
private val analyticsConfig: AnalyticsConfig, private val analyticsConfig: AnalyticsConfig,
private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore, private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore,
private val vectorFeatures: VectorFeatures,
private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase, private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase,
private val pushersManager: PushersManager,
private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase,
) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) { ) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
@ -114,17 +115,44 @@ class HomeActivityViewModel @AssistedInject constructor(
private fun initialize() { private fun initialize() {
if (isInitialized) return if (isInitialized) return
isInitialized = true isInitialized = true
registerUnifiedPushIfNeeded()
cleanupFiles() cleanupFiles()
observeInitialSync() observeInitialSync()
checkSessionPushIsOn() checkSessionPushIsOn()
observeCrossSigningReset() observeCrossSigningReset()
observeAnalytics() observeAnalytics()
observeReleaseNotes() observeReleaseNotes()
observeLocalNotificationsSilenced()
initThreadsMigration() initThreadsMigration()
viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() } viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() }
} }
private fun registerUnifiedPushIfNeeded() {
if (vectorPreferences.areNotificationEnabledForDevice()) {
registerUnifiedPush(distributor = "")
} else {
unregisterUnifiedPush()
}
}
private fun registerUnifiedPush(distributor: String) {
viewModelScope.launch {
when (registerUnifiedPushUseCase.execute(distributor = distributor)) {
is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> {
_viewEvents.post(HomeActivityViewEvents.AskUserForPushDistributor)
}
RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> {
ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = vectorPreferences.areNotificationEnabledForDevice())
}
}
}
}
private fun unregisterUnifiedPush() {
viewModelScope.launch {
unregisterUnifiedPushUseCase.execute(pushersManager)
}
}
private fun observeReleaseNotes() = withState { state -> private fun observeReleaseNotes() = withState { state ->
if (vectorPreferences.isNewAppLayoutEnabled()) { if (vectorPreferences.isNewAppLayoutEnabled()) {
// we don't want to show release notes for new users or after relogin // we don't want to show release notes for new users or after relogin
@ -143,26 +171,6 @@ class HomeActivityViewModel @AssistedInject constructor(
} }
} }
fun shouldAddHttpPusher() = if (vectorPreferences.areNotificationEnabledForDevice()) {
val currentSession = activeSessionHolder.getActiveSession()
val currentPushers = currentSession.pushersService().getPushers()
currentPushers.none { it.deviceId == currentSession.sessionParams.deviceId }
} else {
false
}
fun observeLocalNotificationsSilenced() {
val currentSession = activeSessionHolder.getActiveSession()
val deviceId = currentSession.cryptoService().getMyCryptoDevice().deviceId
viewModelScope.launch {
currentSession.accountDataService()
.getLiveUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
.asFlow()
.map { it.getOrNull()?.content?.toModel<LocalNotificationSettingsContent>()?.isSilenced ?: false }
.onEach { setState { copy(areNotificationsSilenced = it) } }
}
}
private fun observeAnalytics() { private fun observeAnalytics() {
if (analyticsConfig.isEnabled) { if (analyticsConfig.isEnabled) {
analyticsStore.didAskUserConsentFlow analyticsStore.didAskUserConsentFlow
@ -496,6 +504,9 @@ class HomeActivityViewModel @AssistedInject constructor(
HomeActivityViewActions.ViewStarted -> { HomeActivityViewActions.ViewStarted -> {
initialize() initialize()
} }
is HomeActivityViewActions.RegisterPushDistributor -> {
registerUnifiedPush(distributor = action.distributor)
}
} }
} }
} }

View file

@ -23,5 +23,4 @@ import org.matrix.android.sdk.api.session.sync.SyncRequestState
data class HomeActivityViewState( data class HomeActivityViewState(
val syncRequestState: SyncRequestState = SyncRequestState.Idle, val syncRequestState: SyncRequestState = SyncRequestState.Idle,
val authenticationDescription: AuthenticationDescription? = null, val authenticationDescription: AuthenticationDescription? = null,
val areNotificationsSilenced: Boolean = false,
) : MavericksState ) : MavericksState

View file

@ -17,5 +17,18 @@
package im.vector.app.features.home package im.vector.app.features.home
interface NightlyProxy { interface NightlyProxy {
fun onHomeResumed() /**
* Return true if this is a nightly build (checking the package of the app), and only once a day.
*/
fun canDisplayPopup(): Boolean
/**
* Return true if this is a nightly build (checking the package of the app).
*/
fun isNightlyBuild(): Boolean
/**
* Try to update the application, if update is available. Will also take care of the user sign in.
*/
fun updateApplication()
} }

View file

@ -60,6 +60,7 @@ import im.vector.app.core.utils.onPermissionDeniedDialog
import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.FragmentComposerBinding import im.vector.app.databinding.FragmentComposerBinding
import im.vector.app.features.VectorFeatures import im.vector.app.features.VectorFeatures
import im.vector.app.features.analytics.errors.ErrorTracker
import im.vector.app.features.attachments.AttachmentType import im.vector.app.features.attachments.AttachmentType
import im.vector.app.features.attachments.AttachmentTypeSelectorBottomSheet import im.vector.app.features.attachments.AttachmentTypeSelectorBottomSheet
import im.vector.app.features.attachments.AttachmentTypeSelectorSharedAction import im.vector.app.features.attachments.AttachmentTypeSelectorSharedAction
@ -116,6 +117,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
@Inject lateinit var vectorFeatures: VectorFeatures @Inject lateinit var vectorFeatures: VectorFeatures
@Inject lateinit var buildMeta: BuildMeta @Inject lateinit var buildMeta: BuildMeta
@Inject lateinit var session: Session @Inject lateinit var session: Session
@Inject lateinit var errorTracker: ErrorTracker
private val roomId: String get() = withState(timelineViewModel) { it.roomId } private val roomId: String get() = withState(timelineViewModel) { it.roomId }
@ -171,6 +173,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
views.composerLayout.isGone = vectorPreferences.isRichTextEditorEnabled() views.composerLayout.isGone = vectorPreferences.isRichTextEditorEnabled()
views.richTextComposerLayout.isVisible = vectorPreferences.isRichTextEditorEnabled() views.richTextComposerLayout.isVisible = vectorPreferences.isRichTextEditorEnabled()
views.richTextComposerLayout.setOnErrorListener(errorTracker::trackError)
messageComposerViewModel.observeViewEvents { messageComposerViewModel.observeViewEvents {
when (it) { when (it) {

View file

@ -49,10 +49,11 @@ import im.vector.app.databinding.ComposerRichTextLayoutBinding
import im.vector.app.databinding.ViewRichTextMenuButtonBinding import im.vector.app.databinding.ViewRichTextMenuButtonBinding
import io.element.android.wysiwyg.EditorEditText import io.element.android.wysiwyg.EditorEditText
import io.element.android.wysiwyg.inputhandlers.models.InlineFormat import io.element.android.wysiwyg.inputhandlers.models.InlineFormat
import io.element.android.wysiwyg.utils.RustErrorCollector
import uniffi.wysiwyg_composer.ActionState import uniffi.wysiwyg_composer.ActionState
import uniffi.wysiwyg_composer.ComposerAction import uniffi.wysiwyg_composer.ComposerAction
class RichTextComposerLayout @JvmOverloads constructor( internal class RichTextComposerLayout @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0 defStyleAttr: Int = 0
@ -248,10 +249,15 @@ class RichTextComposerLayout @JvmOverloads constructor(
updateMenuStateFor(action, state) updateMenuStateFor(action, state)
} }
} }
updateEditTextVisibility() updateEditTextVisibility()
} }
fun setOnErrorListener(onError: (e: RichTextEditorException) -> Unit) {
views.richTextComposerEditText.rustErrorCollector = RustErrorCollector {
onError(RichTextEditorException(it))
}
}
private fun updateEditTextVisibility() { private fun updateEditTextVisibility() {
views.richTextComposerEditText.isVisible = isTextFormattingEnabled views.richTextComposerEditText.isVisible = isTextFormattingEnabled
views.richTextMenu.isVisible = isTextFormattingEnabled views.richTextMenu.isVisible = isTextFormattingEnabled

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.composer
internal class RichTextEditorException(
cause: Throwable,
) : Exception(cause)

View file

@ -22,6 +22,7 @@ import android.os.Parcelable
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.startForegroundCompat
import im.vector.app.core.services.VectorAndroidService import im.vector.app.core.services.VectorAndroidService
import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationData
import im.vector.app.features.location.LocationTracker import im.vector.app.features.location.LocationTracker
@ -105,7 +106,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
if (foregroundModeStarted) { if (foregroundModeStarted) {
NotificationManagerCompat.from(this).notify(FOREGROUND_SERVICE_NOTIFICATION_ID, notification) NotificationManagerCompat.from(this).notify(FOREGROUND_SERVICE_NOTIFICATION_ID, notification)
} else { } else {
startForeground(FOREGROUND_SERVICE_NOTIFICATION_ID, notification) startForegroundCompat(FOREGROUND_SERVICE_NOTIFICATION_ID, notification)
foregroundModeStarted = true foregroundModeStarted = true
} }

View file

@ -22,10 +22,13 @@ import androidx.preference.SeekBarPreference
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.preference.VectorPreferenceCategory import im.vector.app.core.preference.VectorPreferenceCategory
import im.vector.app.core.preference.VectorSwitchPreference import im.vector.app.core.preference.VectorSwitchPreference
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.home.NightlyProxy
import im.vector.app.features.rageshake.RageShake import im.vector.app.features.rageshake.RageShake
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class VectorSettingsAdvancedSettingsFragment : class VectorSettingsAdvancedSettingsFragment :
@ -34,6 +37,8 @@ class VectorSettingsAdvancedSettingsFragment :
override var titleRes = R.string.settings_advanced_settings override var titleRes = R.string.settings_advanced_settings
override val preferenceXmlRes = R.xml.vector_settings_advanced_settings override val preferenceXmlRes = R.xml.vector_settings_advanced_settings
@Inject lateinit var nightlyProxy: NightlyProxy
private var rageshake: RageShake? = null private var rageshake: RageShake? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -57,6 +62,11 @@ class VectorSettingsAdvancedSettingsFragment :
} }
override fun bindPref() { override fun bindPref() {
setupRageShakeSection()
setupNightlySection()
}
private fun setupRageShakeSection() {
val isRageShakeAvailable = RageShake.isAvailable(requireContext()) val isRageShakeAvailable = RageShake.isAvailable(requireContext())
if (isRageShakeAvailable) { if (isRageShakeAvailable) {
@ -86,4 +96,12 @@ class VectorSettingsAdvancedSettingsFragment :
findPreference<VectorPreferenceCategory>("SETTINGS_RAGE_SHAKE_CATEGORY_KEY")!!.isVisible = false findPreference<VectorPreferenceCategory>("SETTINGS_RAGE_SHAKE_CATEGORY_KEY")!!.isVisible = false
} }
} }
private fun setupNightlySection() {
findPreference<VectorPreferenceCategory>("SETTINGS_NIGHTLY_BUILD_PREFERENCE_KEY")?.isVisible = nightlyProxy.isNightlyBuild()
findPreference<VectorPreference>("SETTINGS_NIGHTLY_BUILD_UPDATE_PREFERENCE_KEY")?.setOnPreferenceClickListener {
nightlyProxy.updateApplication()
true
}
}
} }

View file

@ -28,6 +28,8 @@ import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.singletonEntryPoint import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
@ -60,6 +62,19 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick
protected lateinit var session: Session protected lateinit var session: Session
protected lateinit var errorFormatter: ErrorFormatter protected lateinit var errorFormatter: ErrorFormatter
/* ==========================================================================================
* ViewEvents
* ========================================================================================== */
protected fun <T : VectorViewEvents> VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) {
viewEvents
.stream()
.onEach {
observer(it)
}
.launchIn(viewLifecycleOwner.lifecycleScope)
}
/* ========================================================================================== /* ==========================================================================================
* Views * Views
* ========================================================================================== */ * ========================================================================================== */
@ -148,7 +163,7 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), Maverick
} }
} }
protected fun displayErrorDialog(throwable: Throwable) { protected fun displayErrorDialog(throwable: Throwable?) {
displayErrorDialog(errorFormatter.toHumanReadable(throwable)) displayErrorDialog(errorFormatter.toHumanReadable(throwable))
} }

View file

@ -85,9 +85,9 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>(R.layout.item_de
trusted trusted
) )
holder.trustIcon.render(shield) holder.trustIcon.renderDeviceShield(shield)
} else { } else {
holder.trustIcon.render(null) holder.trustIcon.renderDeviceShield(null)
} }
val detailedModeLabels = listOf( val detailedModeLabels = listOf(

View file

@ -28,7 +28,6 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.features.auth.PendingAuthHandler import im.vector.app.features.auth.PendingAuthHandler
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
@ -48,7 +47,6 @@ class DevicesViewModel @AssistedInject constructor(
private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase, private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase,
private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase,
private val signoutSessionsUseCase: SignoutSessionsUseCase, private val signoutSessionsUseCase: SignoutSessionsUseCase,
private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase,
private val pendingAuthHandler: PendingAuthHandler, private val pendingAuthHandler: PendingAuthHandler,
refreshDevicesUseCase: RefreshDevicesUseCase, refreshDevicesUseCase: RefreshDevicesUseCase,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,

View file

@ -51,7 +51,9 @@ import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationVie
import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase
import im.vector.app.features.workers.signout.SignOutUiWorker
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import javax.inject.Inject import javax.inject.Inject
@ -98,6 +100,8 @@ class VectorSettingsDevicesFragment :
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initWaitingView() initWaitingView()
initCurrentSessionHeaderView()
initCurrentSessionView()
initOtherSessionsHeaderView() initOtherSessionsHeaderView()
initOtherSessionsView() initOtherSessionsView()
initSecurityRecommendationsView() initSecurityRecommendationsView()
@ -139,6 +143,46 @@ class VectorSettingsDevicesFragment :
views.waitingView.waitingStatusText.isVisible = true views.waitingView.waitingStatusText.isVisible = true
} }
private fun initCurrentSessionHeaderView() {
views.deviceListHeaderCurrentSession.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.currentSessionHeaderRename -> {
navigateToRenameCurrentSession()
true
}
R.id.currentSessionHeaderSignout -> {
confirmSignoutCurrentSession()
true
}
R.id.currentSessionHeaderSignoutOtherSessions -> {
confirmMultiSignoutOtherSessions()
true
}
else -> false
}
}
}
private fun navigateToRenameCurrentSession() = withState(viewModel) { state ->
val currentDeviceId = state.currentSessionCrossSigningInfo.deviceId
if (currentDeviceId.isNotEmpty()) {
viewNavigator.navigateToRenameSession(
context = requireActivity(),
deviceId = currentDeviceId,
)
}
}
private fun confirmSignoutCurrentSession() {
activity?.let { SignOutUiWorker(it).perform() }
}
private fun initCurrentSessionView() {
views.deviceListCurrentSession.viewVerifyButton.debouncedClicks {
viewModel.handle(DevicesAction.VerifyCurrentSession)
}
}
private fun initOtherSessionsHeaderView() { private fun initOtherSessionsHeaderView() {
views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) { when (menuItem.itemId) {
@ -247,7 +291,7 @@ class VectorSettingsDevicesFragment :
val otherDevices = devices?.filter { it.deviceInfo.deviceId != currentDeviceId } val otherDevices = devices?.filter { it.deviceInfo.deviceId != currentDeviceId }
renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount, isCurrentSessionVerified) renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount, isCurrentSessionVerified)
renderCurrentDevice(currentDeviceInfo) renderCurrentSessionView(currentDeviceInfo, hasOtherDevices = otherDevices?.isNotEmpty().orFalse())
renderOtherSessionsView(otherDevices, state.isShowingIpAddress) renderOtherSessionsView(otherDevices, state.isShowingIpAddress)
} else { } else {
hideSecurityRecommendations() hideSecurityRecommendations()
@ -310,11 +354,11 @@ class VectorSettingsDevicesFragment :
hideOtherSessionsView() hideOtherSessionsView()
} else { } else {
views.deviceListHeaderOtherSessions.isVisible = true views.deviceListHeaderOtherSessions.isVisible = true
val color = colorProvider.getColorFromAttribute(R.attr.colorError) val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError)
val multiSignoutItem = views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout) val multiSignoutItem = views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout)
val nbDevices = otherDevices.size val nbDevices = otherDevices.size
multiSignoutItem.title = stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices) multiSignoutItem.title = stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices)
multiSignoutItem.setTextColor(color) multiSignoutItem.setTextColor(colorDestructive)
views.deviceListOtherSessions.isVisible = true views.deviceListOtherSessions.isVisible = true
val devices = if (isShowingIpAddress) otherDevices else otherDevices.map { it.copy(deviceInfo = it.deviceInfo.copy(lastSeenIp = null)) } val devices = if (isShowingIpAddress) otherDevices else otherDevices.map { it.copy(deviceInfo = it.deviceInfo.copy(lastSeenIp = null)) }
views.deviceListOtherSessions.render( views.deviceListOtherSessions.render(
@ -327,7 +371,7 @@ class VectorSettingsDevicesFragment :
} else { } else {
stringProvider.getString(R.string.device_manager_other_sessions_show_ip_address) stringProvider.getString(R.string.device_manager_other_sessions_show_ip_address)
} }
} }
} }
private fun hideOtherSessionsView() { private fun hideOtherSessionsView() {
@ -335,29 +379,40 @@ class VectorSettingsDevicesFragment :
views.deviceListOtherSessions.isVisible = false views.deviceListOtherSessions.isVisible = false
} }
private fun renderCurrentDevice(currentDeviceInfo: DeviceFullInfo?) { private fun renderCurrentSessionView(currentDeviceInfo: DeviceFullInfo?, hasOtherDevices: Boolean) {
currentDeviceInfo?.let { currentDeviceInfo?.let {
views.deviceListHeaderCurrentSession.isVisible = true renderCurrentSessionHeaderView(hasOtherDevices)
views.deviceListCurrentSession.isVisible = true renderCurrentSessionListView(it)
val viewState = SessionInfoViewState(
isCurrentSession = true,
deviceFullInfo = it
)
views.deviceListCurrentSession.render(viewState, dateFormatter, drawableProvider, colorProvider, stringProvider)
views.deviceListCurrentSession.debouncedClicks {
currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) }
}
views.deviceListCurrentSession.viewDetailsButton.debouncedClicks {
currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) }
}
views.deviceListCurrentSession.viewVerifyButton.debouncedClicks {
viewModel.handle(DevicesAction.VerifyCurrentSession)
}
} ?: run { } ?: run {
hideCurrentSessionView() hideCurrentSessionView()
} }
} }
private fun renderCurrentSessionHeaderView(hasOtherDevices: Boolean) {
views.deviceListHeaderCurrentSession.isVisible = true
val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError)
val signoutSessionItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignout)
signoutSessionItem.setTextColor(colorDestructive)
val signoutOtherSessionsItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignoutOtherSessions)
signoutOtherSessionsItem.setTextColor(colorDestructive)
signoutOtherSessionsItem.isVisible = hasOtherDevices
}
private fun renderCurrentSessionListView(currentDeviceInfo: DeviceFullInfo) {
views.deviceListCurrentSession.isVisible = true
val viewState = SessionInfoViewState(
isCurrentSession = true,
deviceFullInfo = currentDeviceInfo
)
views.deviceListCurrentSession.render(viewState, dateFormatter, drawableProvider, colorProvider, stringProvider)
views.deviceListCurrentSession.debouncedClicks {
currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) }
}
views.deviceListCurrentSession.viewDetailsButton.debouncedClicks {
currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) }
}
}
private fun navigateToSessionOverview(deviceId: String) { private fun navigateToSessionOverview(deviceId: String) {
viewNavigator.navigateToSessionOverview( viewNavigator.navigateToSessionOverview(
context = requireActivity(), context = requireActivity(),

View file

@ -20,6 +20,7 @@ import android.content.Context
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity
import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity
import im.vector.app.features.settings.devices.v2.rename.RenameSessionActivity
import javax.inject.Inject import javax.inject.Inject
class VectorSettingsDevicesViewNavigator @Inject constructor() { class VectorSettingsDevicesViewNavigator @Inject constructor() {
@ -38,4 +39,8 @@ class VectorSettingsDevicesViewNavigator @Inject constructor() {
OtherSessionsActivity.newIntent(context, titleResourceId, defaultFilter, excludeCurrentDevice) OtherSessionsActivity.newIntent(context, titleResourceId, defaultFilter, excludeCurrentDevice)
) )
} }
fun navigateToRenameSession(context: Context, deviceId: String) {
context.startActivity(RenameSessionActivity.newIntent(context, deviceId))
}
} }

View file

@ -97,7 +97,7 @@ abstract class OtherSessionItem : VectorEpoxyModel<OtherSessionItem.Holder>(R.la
} else { } else {
setDeviceTypeIconUseCase.execute(deviceType, holder.otherSessionDeviceTypeImageView, stringProvider) setDeviceTypeIconUseCase.execute(deviceType, holder.otherSessionDeviceTypeImageView, stringProvider)
} }
holder.otherSessionVerificationStatusImageView.render(roomEncryptionTrustLevel) holder.otherSessionVerificationStatusImageView.renderDeviceShield(roomEncryptionTrustLevel)
holder.otherSessionNameTextView.text = sessionName holder.otherSessionNameTextView.text = sessionName
holder.otherSessionDescriptionTextView.text = sessionDescription holder.otherSessionDescriptionTextView.text = sessionDescription
sessionDescriptionColor?.let { sessionDescriptionColor?.let {

View file

@ -90,7 +90,7 @@ class SessionInfoView @JvmOverloads constructor(
hasLearnMoreLink: Boolean, hasLearnMoreLink: Boolean,
isVerifyButtonVisible: Boolean, isVerifyButtonVisible: Boolean,
) { ) {
views.sessionInfoVerificationStatusImageView.render(encryptionTrustLevel) views.sessionInfoVerificationStatusImageView.renderDeviceShield(encryptionTrustLevel)
when { when {
encryptionTrustLevel == RoomEncryptionTrustLevel.Trusted -> renderCrossSigningVerified(isCurrentSession) encryptionTrustLevel == RoomEncryptionTrustLevel.Trusted -> renderCrossSigningVerified(isCurrentSession)
encryptionTrustLevel == RoomEncryptionTrustLevel.Default && !isCurrentSession -> renderCrossSigningUnknown() encryptionTrustLevel == RoomEncryptionTrustLevel.Default && !isCurrentSession -> renderCrossSigningUnknown()

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.notification
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject
class CanToggleNotificationsViaAccountDataUseCase @Inject constructor(
private val getNotificationSettingsAccountDataUpdatesUseCase: GetNotificationSettingsAccountDataUpdatesUseCase,
) {
fun execute(session: Session, deviceId: String): Flow<Boolean> {
return getNotificationSettingsAccountDataUpdatesUseCase.execute(session, deviceId)
.map { it?.isSilenced != null }
}
}

View file

@ -24,7 +24,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.flow.unwrap import org.matrix.android.sdk.flow.unwrap
import javax.inject.Inject import javax.inject.Inject
class CanTogglePushNotificationsViaPusherUseCase @Inject constructor() { class CanToggleNotificationsViaPusherUseCase @Inject constructor() {
fun execute(session: Session): Flow<Boolean> { fun execute(session: Session): Flow<Boolean> {
return session return session

View file

@ -17,14 +17,13 @@
package im.vector.app.features.settings.devices.v2.notification package im.vector.app.features.settings.devices.v2.notification
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
import javax.inject.Inject import javax.inject.Inject
class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor() { class CheckIfCanToggleNotificationsViaAccountDataUseCase @Inject constructor(
private val getNotificationSettingsAccountDataUseCase: GetNotificationSettingsAccountDataUseCase,
) {
fun execute(session: Session, deviceId: String): Boolean { fun execute(session: Session, deviceId: String): Boolean {
return session return getNotificationSettingsAccountDataUseCase.execute(session, deviceId)?.isSilenced != null
.accountDataService()
.getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null
} }
} }

View file

@ -19,7 +19,7 @@ package im.vector.app.features.settings.devices.v2.notification
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject import javax.inject.Inject
class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor() { class CheckIfCanToggleNotificationsViaPusherUseCase @Inject constructor() {
fun execute(session: Session): Boolean { fun execute(session: Session): Boolean {
return session return session

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.notification
import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject
/**
* Delete the content of any associated notification settings to the current session.
*/
class DeleteNotificationSettingsAccountDataUseCase @Inject constructor(
private val getNotificationSettingsAccountDataUseCase: GetNotificationSettingsAccountDataUseCase,
private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase,
) {
suspend fun execute(session: Session) {
val deviceId = session.sessionParams.deviceId ?: return
if (getNotificationSettingsAccountDataUseCase.execute(session, deviceId)?.isSilenced != null) {
val emptyNotificationSettingsContent = LocalNotificationSettingsContent(
isSilenced = null
)
setNotificationSettingsAccountDataUseCase.execute(session, deviceId, emptyNotificationSettingsContent)
}
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.notification
import androidx.lifecycle.asFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
import org.matrix.android.sdk.api.session.events.model.toModel
import javax.inject.Inject
class GetNotificationSettingsAccountDataUpdatesUseCase @Inject constructor() {
fun execute(session: Session, deviceId: String): Flow<LocalNotificationSettingsContent?> {
return session
.accountDataService()
.getLiveUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
.asFlow()
.map { it.getOrNull()?.content?.toModel<LocalNotificationSettingsContent>() }
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.settings.devices.v2.notification
import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
import org.matrix.android.sdk.api.session.events.model.toModel
import javax.inject.Inject
class GetNotificationSettingsAccountDataUseCase @Inject constructor() {
fun execute(session: Session, deviceId: String): LocalNotificationSettingsContent? {
return session
.accountDataService()
.getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
?.content
.toModel<LocalNotificationSettingsContent>()
}
}

View file

@ -30,33 +30,47 @@ import org.matrix.android.sdk.flow.unwrap
import javax.inject.Inject import javax.inject.Inject
class GetNotificationsStatusUseCase @Inject constructor( class GetNotificationsStatusUseCase @Inject constructor(
private val canTogglePushNotificationsViaPusherUseCase: CanTogglePushNotificationsViaPusherUseCase, private val canToggleNotificationsViaPusherUseCase: CanToggleNotificationsViaPusherUseCase,
private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, private val canToggleNotificationsViaAccountDataUseCase: CanToggleNotificationsViaAccountDataUseCase,
) { ) {
fun execute(session: Session, deviceId: String): Flow<NotificationsStatus> { fun execute(session: Session, deviceId: String): Flow<NotificationsStatus> {
return when { return canToggleNotificationsViaAccountDataUseCase.execute(session, deviceId)
checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId) -> { .flatMapLatest { canToggle ->
session.flow() if (canToggle) {
.liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) notificationStatusFromAccountData(session, deviceId)
.unwrap() } else {
.map { it.content.toModel<LocalNotificationSettingsContent>()?.isSilenced?.not() } notificationStatusFromPusher(session, deviceId)
.map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } }
.distinctUntilChanged() }
} .distinctUntilChanged()
else -> canTogglePushNotificationsViaPusherUseCase.execute(session) }
private fun notificationStatusFromAccountData(session: Session, deviceId: String) =
session.flow()
.liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId)
.unwrap()
.map { it.content.toModel<LocalNotificationSettingsContent>()?.isSilenced?.not() }
.map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED }
private fun notificationStatusFromPusher(session: Session, deviceId: String) =
canToggleNotificationsViaPusherUseCase.execute(session)
.flatMapLatest { canToggle -> .flatMapLatest { canToggle ->
if (canToggle) { if (canToggle) {
session.flow() session.flow()
.livePushers() .livePushers()
.map { it.filter { pusher -> pusher.deviceId == deviceId } } .map { it.filter { pusher -> pusher.deviceId == deviceId } }
.map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } } .map { it.takeIf { it.isNotEmpty() }?.any { pusher -> pusher.enabled } }
.map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } .map {
when (it) {
true -> NotificationsStatus.ENABLED
false -> NotificationsStatus.DISABLED
else -> NotificationsStatus.NOT_SUPPORTED
}
}
.distinctUntilChanged() .distinctUntilChanged()
} else { } else {
flowOf(NotificationsStatus.NOT_SUPPORTED) flowOf(NotificationsStatus.NOT_SUPPORTED)
} }
} }
}
}
} }

Some files were not shown because too many files have changed in this diff Show more