diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 30b6600c94..e5226d0723 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -11,7 +11,7 @@ jobs: - run: | npm install --save-dev @babel/plugin-transform-flow-strip-types - name: Danger - uses: danger/danger-js@11.1.4 + uses: danger/danger-js@11.2.0 with: args: "--dangerfile tools/danger/dangerfile.js" env: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 9d9e8e76e8..57dd5a6a45 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -66,7 +66,7 @@ jobs: yarn add danger-plugin-lint-report --dev - name: Danger lint if: always() - uses: danger/danger-js@11.1.4 + uses: danger/danger-js@11.2.0 with: args: "--dangerfile tools/danger/dangerfile-lint.js" env: diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml index f1458a1d11..41cd274b93 100644 --- a/.github/workflows/triage-labelled.yml +++ b/.github/workflows/triage-labelled.yml @@ -17,7 +17,8 @@ jobs: contains(github.event.issue.labels.*.name, 'Z-IA') || contains(github.event.issue.labels.*.name, 'A-Themes-Custom') || contains(github.event.issue.labels.*.name, 'A-E2EE-Dehydration') || - contains(github.event.issue.labels.*.name, 'A-Tags') + contains(github.event.issue.labels.*.name, 'A-Tags') || + contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') steps: - uses: actions/github-script@v5 with: diff --git a/build.gradle b/build.gradle index 51604b67a8..2abb2a9072 100644 --- a/build.gradle +++ b/build.gradle @@ -45,10 +45,10 @@ plugins { // Detekt id "io.gitlab.arturbosch.detekt" version "1.22.0" // 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 - id 'com.autonomousapps.dependency-analysis' version "1.16.0" + id 'com.autonomousapps.dependency-analysis' version "1.17.0" // Gradle doctor id "com.osacky.doctor" version "0.8.1" } diff --git a/changelog.d/7274.bugfix b/changelog.d/7274.bugfix new file mode 100644 index 0000000000..e99daceb89 --- /dev/null +++ b/changelog.d/7274.bugfix @@ -0,0 +1 @@ +Fix bad pills color background. For light and dark theme the color is now 61708B (iso EleWeb) diff --git a/changelog.d/7477.misc b/changelog.d/7477.misc new file mode 100644 index 0000000000..2ea83ce81d --- /dev/null +++ b/changelog.d/7477.misc @@ -0,0 +1 @@ +Add Z-Labs label for rich text editor and migrate to new label naming. \ No newline at end of file diff --git a/changelog.d/7596.feature b/changelog.d/7596.feature new file mode 100644 index 0000000000..022d86342b --- /dev/null +++ b/changelog.d/7596.feature @@ -0,0 +1 @@ +Save m.local_notification_settings. event in account_data diff --git a/changelog.d/7632.feature b/changelog.d/7632.feature new file mode 100644 index 0000000000..460f987756 --- /dev/null +++ b/changelog.d/7632.feature @@ -0,0 +1 @@ +Update notifications setting when m.local_notification_settings. event changes for current device diff --git a/changelog.d/7653.bugfix b/changelog.d/7653.bugfix new file mode 100644 index 0000000000..ae49c4ed4e --- /dev/null +++ b/changelog.d/7653.bugfix @@ -0,0 +1 @@ +ANR when asking to select the notification method diff --git a/7658.bugfix b/changelog.d/7658.bugfix similarity index 100% rename from 7658.bugfix rename to changelog.d/7658.bugfix diff --git a/changelog.d/7659.bugfix b/changelog.d/7659.bugfix new file mode 100644 index 0000000000..38be1008ef --- /dev/null +++ b/changelog.d/7659.bugfix @@ -0,0 +1 @@ +[Rich text editor] Fix keyboard closing after collapsing editor diff --git a/changelog.d/7680.bugfix b/changelog.d/7680.bugfix new file mode 100644 index 0000000000..2e3b4b2e48 --- /dev/null +++ b/changelog.d/7680.bugfix @@ -0,0 +1,3 @@ +Rich Text Editor: fix several issues related to insets: +* Empty space displayed at the bottom when you don't have permissions to send messages into a room. +* Wrong insets being kept when you exit the room screen and the keyboard is displayed, then come back to it. diff --git a/changelog.d/7683.bugfix b/changelog.d/7683.bugfix new file mode 100644 index 0000000000..3922253ba6 --- /dev/null +++ b/changelog.d/7683.bugfix @@ -0,0 +1,2 @@ +Fix crash in message composer when room is missing + diff --git a/changelog.d/7684.bugfix b/changelog.d/7684.bugfix new file mode 100644 index 0000000000..4a9af884a1 --- /dev/null +++ b/changelog.d/7684.bugfix @@ -0,0 +1 @@ + Fix crash when invalid homeserver url is entered. diff --git a/changelog.d/7693.feature b/changelog.d/7693.feature new file mode 100644 index 0000000000..271964db82 --- /dev/null +++ b/changelog.d/7693.feature @@ -0,0 +1 @@ +[Session manager] Add action to signout all the other session diff --git a/changelog.d/7694.feature b/changelog.d/7694.feature new file mode 100644 index 0000000000..408925974e --- /dev/null +++ b/changelog.d/7694.feature @@ -0,0 +1 @@ +Remind unverified sessions with a banner once a week diff --git a/changelog.d/7710.bugfix b/changelog.d/7710.bugfix new file mode 100644 index 0000000000..9e75a03e1b --- /dev/null +++ b/changelog.d/7710.bugfix @@ -0,0 +1 @@ +Fix usage of unknown shield in room summary diff --git a/changelog.d/7725.bugfix b/changelog.d/7725.bugfix new file mode 100644 index 0000000000..b701451505 --- /dev/null +++ b/changelog.d/7725.bugfix @@ -0,0 +1 @@ +Fix crash when the network is not available. diff --git a/dependencies.gradle b/dependencies.gradle index b0dc1820b5..cc3c939f98 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -17,7 +17,7 @@ def markwon = "4.6.2" def moshi = "1.14.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" -def flipper = "0.174.0" +def flipper = "0.176.0" def epoxy = "5.0.0" def mavericks = "3.0.1" def glide = "4.14.2" @@ -26,7 +26,7 @@ def jjwt = "0.11.5" // Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert // the whole commit which set version 0.16.0-SNAPSHOT def vanniktechEmoji = "0.16.0-SNAPSHOT" -def sentry = "6.7.0" +def sentry = "6.9.0" def fragment = "1.5.4" // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 @@ -99,7 +99,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:0.7.0.1" + 'wysiwyg' : "io.element.android:wysiwyg:0.8.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", diff --git a/fastlane/metadata/android/cs-CZ/changelogs/40105100.txt b/fastlane/metadata/android/cs-CZ/changelogs/40105100.txt new file mode 100644 index 0000000000..8c51742e06 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/40105100.txt @@ -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 diff --git a/fastlane/metadata/android/de-DE/changelogs/40105100.txt b/fastlane/metadata/android/de-DE/changelogs/40105100.txt new file mode 100644 index 0000000000..de5f4d90e8 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/40105100.txt @@ -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 diff --git a/fastlane/metadata/android/et/changelogs/40105100.txt b/fastlane/metadata/android/et/changelogs/40105100.txt new file mode 100644 index 0000000000..f6212db01b --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40105100.txt @@ -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 diff --git a/fastlane/metadata/android/id/changelogs/40105100.txt b/fastlane/metadata/android/id/changelogs/40105100.txt new file mode 100644 index 0000000000..0c7d2f5262 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/40105100.txt @@ -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 diff --git a/fastlane/metadata/android/sk/changelogs/40105100.txt b/fastlane/metadata/android/sk/changelogs/40105100.txt new file mode 100644 index 0000000000..c286f155d4 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/40105100.txt @@ -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 diff --git a/fastlane/metadata/android/sq/changelogs/40105080.txt b/fastlane/metadata/android/sq/changelogs/40105080.txt new file mode 100644 index 0000000000..b059e86cbd --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/40105080.txt @@ -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 diff --git a/fastlane/metadata/android/sv-SE/changelogs/40105080.txt b/fastlane/metadata/android/sv-SE/changelogs/40105080.txt new file mode 100644 index 0000000000..cee589ed35 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/40105080.txt @@ -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 diff --git a/fastlane/metadata/android/uk/changelogs/40105100.txt b/fastlane/metadata/android/uk/changelogs/40105100.txt new file mode 100644 index 0000000000..6bb3ab95c7 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40105100.txt @@ -0,0 +1,2 @@ +Основні зміни в цій версії: Нова реалізація повноекранного режиму для редактора розширеного тексту та виправлення помилок. +Перелік усіх змін: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/zh-TW/changelogs/40105100.txt b/fastlane/metadata/android/zh-TW/changelogs/40105100.txt new file mode 100644 index 0000000000..20341b84fe --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/40105100.txt @@ -0,0 +1,2 @@ +此版本中的主要變動:格式化文字編輯器的全螢幕模式新實作與臭蟲修復。 +完整的變更紀錄:https://github.com/vector-im/element-android/releases diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f0..943f0cbfa7 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f7189a776c..bc073f6761 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=db9c8211ed63f61f60292c69e80d89196f9eb36665e369e7f00ac4cc841c2219 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +distributionSha256Sum=312eb12875e1747e05c2f81a4789902d7e4ec5defbd1eefeaccc08acf096505d +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index a69d9cb6c2..65dcd68d65 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,10 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' @@ -143,12 +143,16 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac diff --git a/gradlew.bat b/gradlew.bat index 53a6b238d4..6689b85bee 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml index f260a129fc..67cc3353aa 100644 --- a/library/ui-strings/src/main/res/values-cs/strings.xml +++ b/library/ui-strings/src/main/res/values-cs/strings.xml @@ -2789,7 +2789,7 @@ Pravost této šifrované zprávy nelze v tomto zařízení zaručit. 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. Inkognito klávesnice - Přidá znaky (╯°□°)╯︵ ┻━┻ před zprávy ve formátu obyčejného textu + Přidá znaky (╯°□°)╯︵ ┻━┻ před zprávy ve formátu prostého textu Hlasové vysílání Otevřít nástroje pro vývojáře 🔒 V nastavení zabezpečení jste povolili šifrování pouze do ověřených relací pro všechny místnosti. @@ -2824,8 +2824,8 @@ ${app_name} potřebuje oprávnění k zobrazování oznámení. Oznámení mohou zobrazovat vaše zprávy, pozvánky atd. \n \nPro zobrazování oznámení povolte přístup na dalších vyskakovacích oknech. - Vyzkoušejte rozšířený textový editor (textový režim již brzy) - Povolit rozšířený textový editor + Vyzkoušejte editor formátovaného textu (režim prostého textu již brzy) + Povolit editor formátovaného textu 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. Potvrdit Zkuste to znovu @@ -2868,7 +2868,7 @@ Druhé zařízení je již přihlášeno. 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í; Žádost se nezdařila. - Ukládání do vyrovnávací paměti + Ukládání do vyrovnávací paměti… Pozastavit hlasové vysílání Přehrát nebo obnovit hlasové vysílání Ukončit záznam hlasového vysílání @@ -2922,4 +2922,6 @@ Citace Odpovídám na %s Úpravy + Zobrazit poslední chaty v nabídce sdílení systému + Povolit přímé sdílení \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml index be53c15026..809ee477fc 100644 --- a/library/ui-strings/src/main/res/values-de/strings.xml +++ b/library/ui-strings/src/main/res/values-de/strings.xml @@ -2815,7 +2815,7 @@ Die Anfrage ist fehlgeschlagen. Abspielen oder fortsetzen der Sprachübertragung Fortsetzen der Sprachübertragung - Puffere + Puffere … Pausiere Sprachübertragung Stoppe Aufzeichnung der Sprachübertragung Pausiere Aufzeichnung der Sprachübertragung @@ -2865,4 +2865,6 @@ %s antworten IP-Adresse ausblenden IP-Adresse anzeigen + Kürzliche Unterhaltungen im Teilen-Menü des Systems anzeigen + Direktes Teilen aktivieren \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-et/strings.xml b/library/ui-strings/src/main/res/values-et/strings.xml index 156221379d..96d9650ceb 100644 --- a/library/ui-strings/src/main/res/values-et/strings.xml +++ b/library/ui-strings/src/main/res/values-et/strings.xml @@ -2805,7 +2805,7 @@ Teine seade on juba võrku loginud. 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; Päring ei õnnestunud. - Andmed on puhverdamisel + Andmed on puhverdamisel… Alusta või jätka ringhäälingukõne esitamist Lõpeta ringhäälingukõne salvestamine Peata ringhäälingukõne salvestamine @@ -2857,4 +2857,6 @@ saatis video. saatis kleepsu. koostas küsitluse. + Kasuta otsejagamist + Näita viimaseid vestlusi süsteemses jagamisvaates \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-fa/strings.xml b/library/ui-strings/src/main/res/values-fa/strings.xml index cc8d60a87b..a3a74df10f 100644 --- a/library/ui-strings/src/main/res/values-fa/strings.xml +++ b/library/ui-strings/src/main/res/values-fa/strings.xml @@ -943,7 +943,7 @@ \n \nپیام‌هایتان با قفل‌هایی امن شده‌اند و فقط شما و گیرندگان دیگر، کلیدهای یکتا را برای قفل‌گشاییشان دارید. امنیت - بثیش‌تر بدانید + بیش‌تر بدانید بیش‌تر کنش‌های مدیر تنظمیات اتاق @@ -2783,7 +2783,7 @@ نظرسنجی‌ها پیوست‌ها برچسب‌ها - میانگیری + میانگیری… زنده تأیید ۳ @@ -2844,4 +2844,9 @@ نقل کردن پاسخ دادن به %s ویرایش کردن + می‌توانید با یک رمز QR از این افزاره برای ورود به افزاره‌ای همراه یا روی وب استفاده کنید. دو راه برای این کار وجود دارد: + مشکلی امنیتی در برپایی پیام‌رسانی امن وجود داشت. ممکن است یکی از موارد زیر دستکاری شده باشند: کارساز خانیگیتان؛ اتّصال اینترنتیتان؛ افزاره(های)تان؛ + لطفاً مطمئن شوید که مبدأ این کد را می‌دانید. با پیوند دادن افزاره‌ها، دسترسی کامل را به حسابتان می‌دهید. + نمایش گپ‌های اخیر در فهرست هم رسانی سامانه + به کار انداختن هم‌رسانی مستقیم \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml index cf49733bdf..d74d3bac71 100644 --- a/library/ui-strings/src/main/res/values-fr/strings.xml +++ b/library/ui-strings/src/main/res/values-fr/strings.xml @@ -2814,7 +2814,7 @@ 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 : Se connecter avec un QR code Scanner le QR code - Mise en mémoire tampon + Mise en mémoire tampon… Mettre en pause la diffusion audio Lire ou continuer la diffusion audio Arrêter l’enregistrement de la diffusion audio @@ -2866,4 +2866,6 @@ Citation de Réponse à %s Modification + Affiche les conversations récentes dans le menu de partage du système + Activer le partage direct \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-in/strings.xml b/library/ui-strings/src/main/res/values-in/strings.xml index ce9e524067..da4c474689 100644 --- a/library/ui-strings/src/main/res/values-in/strings.xml +++ b/library/ui-strings/src/main/res/values-in/strings.xml @@ -2762,7 +2762,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Permintaan gagal. Memungkinkan untuk merekam dan mengirim siaran suara dalam linimasa ruangan. Aktifkan siaran suara (dalam pengembangan aktif) - Memuat + Memuat… Jeda siaran suara Mainkan atau lanjutkan siaran suara Hentikan rekaman siaran suara @@ -2812,4 +2812,6 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Mengedit Tampilkan alamat IP Membalas ke %s + Tampilkan obrolan terkini dalam menu pembagian sistem + Aktifkan pembagian langsung \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-it/strings.xml b/library/ui-strings/src/main/res/values-it/strings.xml index d244f26a43..d6a7858ebc 100644 --- a/library/ui-strings/src/main/res/values-it/strings.xml +++ b/library/ui-strings/src/main/res/values-it/strings.xml @@ -2805,7 +2805,7 @@ L\'altro dispositivo ha già fatto l\'accesso. 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; La richiesta è fallita. - Buffering + Buffer… Sospendi trasmissione vocale Avvia o riprendi trasmissione vocale Ferma registrazione trasmissione vocale @@ -2857,4 +2857,6 @@ Citazione Risposta a %s Modifica + Mostra chat recenti nel menu di condivisione di sistema + Attiva condivisione diretta \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml index 8baba5df53..8129a234fb 100644 --- a/library/ui-strings/src/main/res/values-pt-rBR/strings.xml +++ b/library/ui-strings/src/main/res/values-pt-rBR/strings.xml @@ -2723,7 +2723,7 @@ Sessões não-verificadas 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 -\nRemover sessões inativas melhora segurança e performance, e torna-o mais fácil para você identificar se uma nova sessão é suspeita. +\nRemover sessões inativas melhora segurança e performance, e torna mais fácil para você identificar se uma nova sessão é suspeita. Sessões inativas Por favor esteja ciente que nomes de sessões também são visíveis a pessoas com quem você se comunica. Nomes de sessões personalizadas podem ajudar você a reconhecer seus dispositivos mais facilmente. @@ -2844,9 +2844,9 @@ Não dá pra começar um novo broadcast de voz Avançar rápido 30 segundos Retroceder 30 segundos - 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. + 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 -\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. +\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. Fazer signout de %1$d sessão Fazer signout de %1$d sessões diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml index 078ffc44eb..f59073c5db 100644 --- a/library/ui-strings/src/main/res/values-sk/strings.xml +++ b/library/ui-strings/src/main/res/values-sk/strings.xml @@ -2868,7 +2868,7 @@ Žiadosť zlyhala. Možnosť nahrávania a odosielania hlasového vysielania v časovej osi miestnosti. Zapnúť hlasové vysielanie (v štádiu aktívneho vývoja) - Načítavanie do vyrovnávacej pamäte + Načítavanie do vyrovnávacej pamäte… Pozastaviť hlasové vysielanie Prehrať alebo pokračovať v nahrávaní hlasového vysielania Zastaviť nahrávanie hlasového vysielania @@ -2922,4 +2922,6 @@ Zobraziť IP adresu Odpoveď na %s Úprava + Zobraziť posledné konverzácie v systémovej ponuke zdieľania + Povoliť priame zdieľanie \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-sq/strings.xml b/library/ui-strings/src/main/res/values-sq/strings.xml index 800ec17dcf..58214e8a22 100644 --- a/library/ui-strings/src/main/res/values-sq/strings.xml +++ b/library/ui-strings/src/main/res/values-sq/strings.xml @@ -2659,7 +2659,7 @@ \nKy shërbyes Home mund të mos jetë formësuar të shfaqë harta. Përfundimet do të jenë të dukshme pasi të ketë përfunduar pyetësori 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. - Përdo + Ndal transmetim zanor Luani ose vazhdoni luajtje transmetimi zanor Ndal incizim transmetimi zanor @@ -2851,4 +2851,6 @@ Kthim prapa 30 sekonda Si përgjigje për %s Aktivizo MD të lënë për më vonë + Tkurr pjella të %s + Zgjero pjella të %s \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-sv/strings.xml b/library/ui-strings/src/main/res/values-sv/strings.xml index 65318096c7..45cfe4338b 100644 --- a/library/ui-strings/src/main/res/values-sv/strings.xml +++ b/library/ui-strings/src/main/res/values-sv/strings.xml @@ -2852,4 +2852,18 @@ Kan inte starta en ny röstsändning Spola framåt 30 sekunder Spola tillbaka 30 sekunder + skickade en omröstning. + skickade en dekal. + skickade en video. + skickade en bild. + skickade ett röstmeddelande. + skickade en ljudfil. + skickade en fil. + Svar på + Dölj IP-adress + Visa IP-adress + %1$s kvar + Citerar + Besvarar %s + Redigerar \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml index 19889892ff..6a1c5355ab 100644 --- a/library/ui-strings/src/main/res/values-uk/strings.xml +++ b/library/ui-strings/src/main/res/values-uk/strings.xml @@ -2922,7 +2922,7 @@ Запит не виконаний. Можливість записувати та надсилати голосові трансляції до стрічки кімнати. Увімкнути голосові трансляції (в активній розробці) - Буферизація + Буферизація… Призупинити голосову трансляцію Відтворити або поновити відтворення голосової трансляції Припинити запис голосової трансляції @@ -2966,16 +2966,18 @@ Вийти Залишилося %1$s надсилає аудіофайл. - відправив файл. + надсилає файл. У відповідь на Сховати IP-адресу - створив голосування. - відправив наліпку. - відправив відео. - відправив зображення. - відправив голосове повідомлення. + створює опитування. + надсилає наліпку. + надсилає відео. + надсилає зображення. + надсилає голосове повідомлення. Показати IP-адресу Цитуючи - У відповідь на %s + У відповідь %s Редагування + Показувати останні бесіди в системному меню загального доступу + Увімкнути пряме поширення \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml index 5ab8a351d1..0a01610c36 100644 --- a/library/ui-strings/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rCN/strings.xml @@ -1007,7 +1007,7 @@ 您当前在身份服务器 %1$s 上共享电子邮件地址或电话号码。您需要重新连接到 %2$s 才能停止共享它们。 同意身份服务器 (%s) 服务条款使你可以通过电子邮件地址或电话号码被发现。 启用详细日志。 - 详细日志将通过在您发送 RageShake 时提供更多日志来帮助开发人员。即使启用,应用程序也不会记录消息内容或任何其他私人数据。 + 详细日志将通过在您发送愤怒摇动(RageShake)时提供更多日志来帮助开发人员。即使启用,应用程序也不会记录消息内容或任何其他私人数据。 接收你的主服务器条款和条件后请重试。 服务器似乎响应时间太长,这可能是由于连接不良或服务器错误引起的。请稍后再试。 发送附件 @@ -1205,7 +1205,7 @@ 高级设置 开发者模式 开发者模式激活隐藏的功能,也可能使应用不稳定。仅供开发者使用! - 摇一摇 + 愤怒摇动(Rageshake) 检测阈值 摇动手机以测试检测阈值 检测到摇动! @@ -1213,7 +1213,7 @@ 当前会话 其它会话 仅显示第一个结果,请输入更多字符… - 快速失败 + 快速失败(Fail-fast) 发生意外错误时,${app_name} 可能更经常崩溃 在明文消息前添加 ¯\\_(ツ)_/¯ 启用加密 @@ -2694,7 +2694,7 @@ 验证您当前的会话以显示此会话的验证状态。 未知的验证状态 开始语音广播 - 缓冲 + 正在缓冲…… 暂停语音广播 实时 知道了 @@ -2789,4 +2789,19 @@ 已选择 %1$d + 已创建投票。 + 已发送贴纸。 + 已发送视频。 + 已发送图片。 + 已发送语音消息。 + 已发送音频文件。 + 已发送文件。 + 已验证的会话是在输入你的口令词组或用另一个已验证的会话确认你的身份之后你使用此账户的任何地方。 +\n +\n这意味着你拥有解锁你的已加密消息和向其他用户证明你信任此会话所需的全部密钥。 + + 登出%1$d个会话 + + 登出 + 剩余%1$s \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml index dc5f6d85e3..9a5439b2ae 100644 --- a/library/ui-strings/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui-strings/src/main/res/values-zh-rTW/strings.xml @@ -2760,7 +2760,7 @@ 請求失敗。 可以在聊天室時間軸中錄製並傳送語音廣播。 啟用語音廣播(正在積極開發中) - 正在緩衝 + 正在緩衝…… 暫停語音廣播 播放或繼續語音廣播 停止語音廣播錄製 @@ -2810,4 +2810,6 @@ 引用 回覆給 %s 正在編輯 + 在系統分享選單中顯示最近聊天 + 啟用直接分享 \ No newline at end of file diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 58fc62b347..609cdac233 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -2649,8 +2649,12 @@ Unencrypted Encrypted by an unverified device The authenticity of this encrypted message can\'t be guaranteed on this device. - Review where you’re logged in - Verify all your sessions to ensure your account & messages are safe + + Review where you’re logged in + + Verify all your sessions to ensure your account & messages are safe + You have unverified sessions + Review to ensure your account is safe Verify the new login accessing your account: %1$s @@ -3359,6 +3363,7 @@ Sign out of %1$d session Sign out of %1$d sessions + Sign out of all other sessions Show IP address Hide IP address Sign out of this session diff --git a/library/ui-styles/src/main/res/values/palette.xml b/library/ui-styles/src/main/res/values/palette.xml index 73ac768919..999dccf167 100644 --- a/library/ui-styles/src/main/res/values/palette.xml +++ b/library/ui-styles/src/main/res/values/palette.xml @@ -44,4 +44,4 @@ #15191E #21262C - \ No newline at end of file + diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml index f81c005d2c..b30a4dd91c 100644 --- a/library/ui-styles/src/main/res/values/theme_dark.xml +++ b/library/ui-styles/src/main/res/values/theme_dark.xml @@ -53,7 +53,7 @@ ?vctr_content_quinary ?vctr_system ?vctr_system - ?vctr_content_tertiary + ?vctr_notice_secondary @color/element_accent_dark diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml index 693118ff67..4bbb081922 100644 --- a/library/ui-styles/src/main/res/values/theme_light.xml +++ b/library/ui-styles/src/main/res/values/theme_light.xml @@ -53,7 +53,7 @@ ?vctr_content_quinary ?vctr_system ?vctr_system - ?vctr_content_tertiary + ?vctr_notice_secondary @color/element_accent_light diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 0b5dc1aacf..4be8d55614 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -62,7 +62,7 @@ android { // that the app's state is completely cleared between tests. testInstrumentationRunnerArguments clearPackageData: 'true' - buildConfigField "String", "SDK_VERSION", "\"1.5.11\"" + buildConfigField "String", "SDK_VERSION", "\"1.5.12\"" buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\"" buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\"" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt index 2a95ccce7a..75d04f340a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt @@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class LocalNotificationSettingsContent( - @Json(name = "is_silenced") val isSilenced: Boolean = false + @Json(name = "is_silenced") + val isSilenced: Boolean? ) diff --git a/tools/release/releaseScript.sh b/tools/release/releaseScript.sh index d8980b9da7..d76cd98061 100755 --- a/tools/release/releaseScript.sh +++ b/tools/release/releaseScript.sh @@ -19,43 +19,81 @@ # Ignore any error to not stop the script set +e -printf "\n" -printf "================================================================================\n" +printf "\n================================================================================\n" printf "| Welcome to the release script! |\n" printf "================================================================================\n" -releaseScriptLocation="${RELEASE_SCRIPT_PATH}" +printf "Checking environment...\n" +envError=0 -if [[ -z "${releaseScriptLocation}" ]]; then - printf "Fatal: RELEASE_SCRIPT_PATH is not defined in the environment. Please set to the path of your local file 'releaseElement2.sh'.\n" - exit 1 +# Path of the key store (it's a file) +keyStorePath="${ELEMENT_KEYSTORE_PATH}" +if [[ -z "${keyStorePath}" ]]; then + printf "Fatal: ELEMENT_KEYSTORE_PATH is not defined in the environment.\n" + envError=1 +fi +# Keystore password +keyStorePassword="${ELEMENT_KEYSTORE_PASSWORD}" +if [[ -z "${keyStorePassword}" ]]; then + printf "Fatal: ELEMENT_KEYSTORE_PASSWORD is not defined in the environment.\n" + envError=1 +fi +# Key password +keyPassword="${ELEMENT_KEY_PASSWORD}" +if [[ -z "${keyPassword}" ]]; then + printf "Fatal: ELEMENT_KEY_PASSWORD is not defined in the environment.\n" + envError=1 +fi +# GitHub token +gitHubToken="${ELEMENT_GITHUB_TOKEN}" +if [[ -z "${gitHubToken}" ]]; then + printf "Fatal: ELEMENT_GITHUB_TOKEN is not defined in the environment.\n" + envError=1 +fi +# Android home +androidHome="${ANDROID_HOME}" +if [[ -z "${androidHome}" ]]; then + printf "Fatal: ANDROID_HOME is not defined in the environment.\n" + envError=1 +fi +# @elementbot:matrix.org matrix token / Not mandatory +elementBotToken="${ELEMENT_BOT_MATRIX_TOKEN}" +if [[ -z "${elementBotToken}" ]]; then + printf "Warning: ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment.\n" fi -releaseScriptFullPath="${releaseScriptLocation}/releaseElement2.sh" - -if [[ ! -f ${releaseScriptFullPath} ]]; then - printf "Fatal: release script not found at ${releaseScriptFullPath}.\n" +if [ ${envError} == 1 ]; then exit 1 fi +buildToolsVersion="30.0.2" +buildToolsPath="${androidHome}/build-tools/${buildToolsVersion}" + +if [[ ! -d ${buildToolsPath} ]]; then + printf "Fatal: ${buildToolsPath} folder not found, ensure that you have installed the SDK version ${buildToolsVersion}.\n" + exit 1 +fi + # Check if git flow is enabled git flow config >/dev/null 2>&1 if [[ $? == 0 ]] then - printf "Git flow is initialized" + printf "Git flow is initialized\n" else printf "Git flow is not initialized. Initializing...\n" # All default value, just set 'v' for tag prefix git flow init -d -t 'v' fi +printf "OK\n" + +printf "\n================================================================================\n" # Guessing version to propose a default version versionMajorCandidate=`grep "ext.versionMajor" ./vector-app/build.gradle | cut -d " " -f3` versionMinorCandidate=`grep "ext.versionMinor" ./vector-app/build.gradle | cut -d " " -f3` versionPatchCandidate=`grep "ext.versionPatch" ./vector-app/build.gradle | cut -d " " -f3` versionCandidate="${versionMajorCandidate}.${versionMinorCandidate}.${versionPatchCandidate}" -printf "\n" read -p "Please enter the release version (example: ${versionCandidate}). Just press enter if ${versionCandidate} is correct. " version version=${version:-${versionCandidate}} @@ -225,17 +263,93 @@ else fi printf "\n================================================================================\n" -read -p "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Amain to build the 'main' branch. Press enter when it's done." +printf "Wait for the GitHub action https://github.com/vector-im/element-android/actions/workflows/build.yml?query=branch%3Amain to build the 'main' branch.\n" +read -p "After GHA is finished, please enter the artifact URL (for 'vector-gplay-release-unsigned'): " artifactUrl printf "\n================================================================================\n" -printf "Running the release script...\n" -cd ${releaseScriptLocation} -${releaseScriptFullPath} "v${version}" -cd - +printf "Downloading the artifact...\n" + +# Download files +targetPath="./tmp/Element/${version}" + +# Ignore error +set +e + +python3 ./tools/release/download_github_artifacts.py \ + --token ${gitHubToken} \ + --artifactUrl ${artifactUrl} \ + --directory ${targetPath} \ + --ignoreErrors + +# Do not ignore error +set -e printf "\n================================================================================\n" -apkPath="${releaseScriptLocation}/Element/v${version}/vector-gplay-arm64-v8a-release-signed.apk" -printf "Installing apk on a real device...\n" +printf "Unzipping the artifact...\n" + +unzip ${targetPath}/vector-gplay-release-unsigned.zip -d ${targetPath} + +# Flatten folder hierarchy +mv ${targetPath}/gplay/release/* ${targetPath} +rm -rf ${targetPath}/gplay + +printf "\n================================================================================\n" +printf "Signing the APKs...\n" + +cp ${targetPath}/vector-gplay-arm64-v8a-release-unsigned.apk \ + ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk +./tools/release/sign_apk_unsafe.sh \ + ${keyStorePath} \ + ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk \ + ${keyStorePassword} \ + ${keyPassword} + +cp ${targetPath}/vector-gplay-armeabi-v7a-release-unsigned.apk \ + ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk +./tools/release/sign_apk_unsafe.sh \ + ${keyStorePath} \ + ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk \ + ${keyStorePassword} \ + ${keyPassword} + +cp ${targetPath}/vector-gplay-x86-release-unsigned.apk \ + ${targetPath}/vector-gplay-x86-release-signed.apk +./tools/release/sign_apk_unsafe.sh \ + ${keyStorePath} \ + ${targetPath}/vector-gplay-x86-release-signed.apk \ + ${keyStorePassword} \ + ${keyPassword} + +cp ${targetPath}/vector-gplay-x86_64-release-unsigned.apk \ + ${targetPath}/vector-gplay-x86_64-release-signed.apk +./tools/release/sign_apk_unsafe.sh \ + ${keyStorePath} \ + ${targetPath}/vector-gplay-x86_64-release-signed.apk \ + ${keyStorePassword} \ + ${keyPassword} + +# Ref: https://docs.fastlane.tools/getting-started/android/beta-deployment/#uploading-your-app +# set SUPPLY_APK_PATHS="${targetPath}/vector-gplay-arm64-v8a-release-unsigned.apk,${targetPath}/vector-gplay-armeabi-v7a-release-unsigned.apk,${targetPath}/vector-gplay-x86-release-unsigned.apk,${targetPath}/vector-gplay-x86_64-release-unsigned.apk" +# +# ./fastlane beta + +printf "\n================================================================================\n" +printf "Please check the information below:\n" + +printf "File vector-gplay-arm64-v8a-release-signed.apk:\n" +${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-arm64-v8a-release-signed.apk | grep package +printf "File vector-gplay-armeabi-v7a-release-signed.apk:\n" +${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-armeabi-v7a-release-signed.apk | grep package +printf "File vector-gplay-x86-release-signed.apk:\n" +${buildToolsPath}/aapt dump badging ${targetPath}/vector-gplay-x86-release-signed.apk | grep package +printf "File vector-gplay-x86_64-release-signed.apk:\n" +${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================================================================================\n" +read -p "Installing apk on a real device, press enter when a real device is connected. " +apkPath="${targetPath}/vector-gplay-arm64-v8a-release-signed.apk" adb -d install ${apkPath} read -p "Please run the APK on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done." @@ -245,9 +359,25 @@ read -p "Create the release on gitHub from the tag https://github.com/vector-im/ read -p "Add the 4 signed APKs to the GitHub release. Press enter when it's done." printf "\n================================================================================\n" -printf "Ping the Android Internal room. Here is an example of message which can be sent:\n\n" -printf "@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!\n\n" -read -p "Press enter when it's done." +printf "Message for the Android internal room:\n\n" +message="@room Element Android ${version} is ready to be tested. You can get if from https://github.com/vector-im/element-android/releases/tag/v${version}. Please report any feedback here. Thanks!" +printf "${message}\n\n" + +if [[ -z "${elementBotToken}" ]]; then + read -p "ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter when it's done " +else + read -p "Send this message to the room (yes/no) default to yes? " doSend + doSend=${doSend:-yes} + if [ ${doSend} == "yes" ]; then + printf "Sending message...\n" + transactionId=`openssl rand -hex 16` + # Element Android internal + matrixRoomId="!LiSLXinTDCsepePiYW:matrix.org" + curl -X PUT --data $"{\"msgtype\":\"m.text\",\"body\":\"${message}\"}" -H "Authorization: Bearer ${elementBotToken}" https://matrix-client.matrix.org/_matrix/client/r0/rooms/${matrixRoomId}/send/m.room.message/\$local.${transactionId} + else + printf "Message not sent, please send it manually!\n" + fi +fi printf "\n================================================================================\n" printf "Congratulation! Kudos for using this script! Have a nice day!\n" diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 75b70a01ac..14b2c929e4 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -37,7 +37,7 @@ ext.versionMinor = 5 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -ext.versionPatch = 11 +ext.versionPatch = 12 ext.scVersion = 62 @@ -396,14 +396,14 @@ dependencies { // Plant Timber tree for test androidTestImplementation libs.tests.timberJunitRule // "The one who serves a great Espresso" - androidTestImplementation('com.adevinta.android:barista:4.2.0') { + androidTestImplementation('com.adevinta.android:barista:4.3.0') { exclude group: 'org.jetbrains.kotlin' } androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator androidTestImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22" debugImplementation libs.androidx.fragmentTesting - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' } diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index 5c497c24ec..2134c8cf2c 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -88,6 +88,9 @@ class DebugVectorFeatures( override fun isVoiceBroadcastEnabled(): Boolean = read(DebugFeatureKeys.voiceBroadcastEnabled) ?: vectorFeatures.isVoiceBroadcastEnabled() + override fun isUnverifiedSessionsAlertEnabled(): Boolean = read(DebugFeatureKeys.unverifiedSessionsAlertEnabled) + ?: vectorFeatures.isUnverifiedSessionsAlertEnabled() + fun override(value: T?, key: Preferences.Key) = updatePreferences { if (value == null) { it.remove(key) @@ -151,4 +154,5 @@ object DebugFeatureKeys { val qrCodeLoginForAllServers = booleanPreferencesKey("qr-code-login-for-all-servers") val reciprocateQrCodeLogin = booleanPreferencesKey("reciprocate-qr-code-login") val voiceBroadcastEnabled = booleanPreferencesKey("voice-broadcast-enabled") + val unverifiedSessionsAlertEnabled = booleanPreferencesKey("unverified-sessions-alert-enabled") } diff --git a/vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt b/vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt index 5b83769116..44fd92953e 100755 --- a/vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt +++ b/vector-app/src/fdroid/java/im/vector/app/push/fcm/FdroidFcmHelper.kt @@ -17,7 +17,6 @@ package im.vector.app.push.fcm -import android.app.Activity import android.content.Context import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.FcmHelper @@ -44,7 +43,7 @@ class FdroidFcmHelper @Inject constructor( // No op } - override fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) { + override fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) { // No op } diff --git a/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt b/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt index 26b12fe3e3..83984b2bee 100755 --- a/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt +++ b/vector-app/src/gplay/java/im/vector/app/push/fcm/GoogleFcmHelper.kt @@ -15,7 +15,6 @@ */ package im.vector.app.push.fcm -import android.app.Activity import android.content.Context import android.content.SharedPreferences 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.GoogleApiAvailability import com.google.firebase.messaging.FirebaseMessaging +import dagger.hilt.android.qualifiers.ApplicationContext import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.DefaultPreferences @@ -36,8 +36,8 @@ import javax.inject.Inject * It has an alter ego in the fdroid variant. */ class GoogleFcmHelper @Inject constructor( - @DefaultPreferences - private val sharedPrefs: SharedPreferences, + @ApplicationContext private val context: Context, + @DefaultPreferences private val sharedPrefs: SharedPreferences, ) : FcmHelper { companion object { 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) { - // if (TextUtils.isEmpty(getFcmToken(activity))) { + override fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) { // '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 { FirebaseMessaging.getInstance().token .addOnSuccessListener { token -> @@ -75,7 +74,7 @@ class GoogleFcmHelper @Inject constructor( Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") } } 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.") } } diff --git a/vector-config/src/main/java/im/vector/app/config/Config.kt b/vector-config/src/main/java/im/vector/app/config/Config.kt index d03af2959f..ac6c6fab5b 100644 --- a/vector-config/src/main/java/im/vector/app/config/Config.kt +++ b/vector-config/src/main/java/im/vector/app/config/Config.kt @@ -16,6 +16,8 @@ package im.vector.app.config +import kotlin.time.Duration.Companion.days + /** * Set of flags to configure the application. */ @@ -93,4 +95,6 @@ object Config { * Can be disabled by providing Analytics.Disabled */ val NIGHTLY_ANALYTICS_CONFIG = RELEASE_ANALYTICS_CONFIG.copy(sentryEnvironment = "NIGHTLY") + + val SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS = 7.days.inWholeMilliseconds // 1 Week } diff --git a/vector/build.gradle b/vector/build.gradle index 56da280cb9..9038fa6af0 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -327,11 +327,11 @@ dependencies { // Plant Timber tree for test androidTestImplementation libs.tests.timberJunitRule // "The one who serves a great Espresso" - androidTestImplementation('com.adevinta.android:barista:4.2.0') { + androidTestImplementation('com.adevinta.android:barista:4.3.0') { exclude group: 'org.jetbrains.kotlin' } androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator debugImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.21" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22" } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index c67bbbcd88..66361c1ca1 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -72,7 +72,9 @@ - + @@ -346,6 +349,7 @@ = AtomicReference() @@ -86,7 +86,7 @@ class ActiveSessionHolder @Inject constructor( incomingVerificationRequestHandler.stop() pushRuleTriggerListener.stop() // 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() } diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index 2242abb7aa..b58d584dad 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -105,6 +105,7 @@ import im.vector.app.features.settings.ignored.IgnoredUsersViewModel import im.vector.app.features.settings.labs.VectorSettingsLabsViewModel import im.vector.app.features.settings.legals.LegalsViewModel 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.threepids.ThreePidsSettingsViewModel import im.vector.app.features.share.IncomingShareViewModel @@ -683,4 +684,11 @@ interface MavericksViewModelModule { @IntoMap @MavericksViewModelKey(AttachmentTypeSelectorViewModel::class) fun attachmentTypeSelectorViewModelFactory(factory: AttachmentTypeSelectorViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(VectorSettingsNotificationPreferenceViewModel::class) + fun vectorSettingsNotificationPreferenceViewModelFactory( + factory: VectorSettingsNotificationPreferenceViewModel.Factory + ): MavericksAssistedViewModelFactory<*, *> } diff --git a/vector/src/main/java/im/vector/app/core/extensions/Service.kt b/vector/src/main/java/im/vector/app/core/extensions/Service.kt new file mode 100644 index 0000000000..de301df85e --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/Service.kt @@ -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) + } +} diff --git a/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt b/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt similarity index 77% rename from vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt rename to vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt index 81b524cde9..a4d18baa64 100644 --- a/vector/src/main/java/im/vector/app/core/notification/EnableNotificationsSettingUpdater.kt +++ b/vector/src/main/java/im/vector/app/core/notification/NotificationsSettingUpdater.kt @@ -23,14 +23,21 @@ import org.matrix.android.sdk.api.session.Session import javax.inject.Inject import javax.inject.Singleton +/** + * Listen changes in Pusher or Account Data to update the local setting for notification toggle. + */ @Singleton -class EnableNotificationsSettingUpdater @Inject constructor( +class NotificationsSettingUpdater @Inject constructor( private val updateEnableNotificationsSettingOnChangeUseCase: UpdateEnableNotificationsSettingOnChangeUseCase, ) { private var job: Job? = null - fun onSessionsStarted(session: Session) { + fun onSessionStarted(session: Session) { + updateEnableNotificationsSettingOnChange(session) + } + + private fun updateEnableNotificationsSettingOnChange(session: Session) { job?.cancel() job = session.coroutineScope.launch { updateEnableNotificationsSettingOnChangeUseCase.execute(session) diff --git a/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt new file mode 100644 index 0000000000..cb955e01f7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCase.kt @@ -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 + } +} diff --git a/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt index 7b2c5e3959..381348638d 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/FcmHelper.kt @@ -16,7 +16,6 @@ package im.vector.app.core.pushers -import android.app.Activity import im.vector.app.core.di.ActiveSessionHolder interface FcmHelper { @@ -39,11 +38,10 @@ interface FcmHelper { /** * 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 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) diff --git a/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt new file mode 100644 index 0000000000..aa3652a54f --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCase.kt @@ -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) + } +} diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt index 6050016025..95824db60b 100644 --- a/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt +++ b/vector/src/main/java/im/vector/app/core/pushers/UnifiedPushHelper.kt @@ -17,18 +17,14 @@ package im.vector.app.core.pushers import android.content.Context -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.lifecycleScope +import androidx.annotation.MainThread import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import im.vector.app.R import im.vector.app.core.resources.StringProvider 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.cache.CacheStrategy import org.matrix.android.sdk.api.util.MatrixJsonParser @@ -43,88 +39,13 @@ class UnifiedPushHelper @Inject constructor( private val stringProvider: StringProvider, private val vectorPreferences: VectorPreferences, private val matrix: Matrix, - private val vectorFeatures: VectorFeatures, private val fcmHelper: FcmHelper, ) { - // Called when the home activity starts - // or when notifications are enabled - fun register( - activity: FragmentActivity, - 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 + @MainThread + fun showSelectDistributorDialog( + context: Context, + onDistributorSelected: (String) -> Unit, ) { val internalDistributorName = stringProvider.getString( if (fcmHelper.isFirebaseAvailable()) { @@ -134,6 +55,7 @@ class UnifiedPushHelper @Inject constructor( } ) + val distributors = UnifiedPush.getDistributors(context) val distributorsName = distributors.map { if (it == context.packageName) { internalDistributorName @@ -142,44 +64,23 @@ class UnifiedPushHelper @Inject constructor( } } - MaterialAlertDialogBuilder(activity) + MaterialAlertDialogBuilder(context) .setTitle(stringProvider.getString(R.string.unifiedpush_getdistributors_dialog_title)) .setItems(distributorsName.toTypedArray()) { _, which -> val distributor = distributors[which] - - activity.lifecycleScope.launch { - UnifiedPush.saveDistributor(context, distributor) - Timber.i("Saving distributor: $distributor") - UnifiedPush.registerApp(context) - onDoneRunnable?.run() - } + onDistributorSelected(distributor) } .setOnCancelListener { - // By default, use internal solution (fcm/background sync) - UnifiedPush.saveDistributor(context, context.packageName) - UnifiedPush.registerApp(context) - onDoneRunnable?.run() + // we do not want to change the distributor on behalf of the user + if (UnifiedPush.getDistributor(context).isEmpty()) { + // By default, use internal solution (fcm/background sync) + onDistributorSelected(context.packageName) + } } .setCancelable(true) .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) internal data class DiscoveryResponse( @Json(name = "unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush() diff --git a/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt b/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt new file mode 100644 index 0000000000..acad3e649f --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCase.kt @@ -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) + } +} diff --git a/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt b/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt index 85ea7f1a1b..a4e3872e0f 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt @@ -28,6 +28,7 @@ import androidx.media.session.MediaButtonReceiver import com.airbnb.mvrx.Mavericks import dagger.hilt.android.AndroidEntryPoint 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.VectorCallActivity import im.vector.app.features.call.telecom.CallConnection @@ -181,7 +182,7 @@ class CallAndroidService : VectorAndroidService() { fromBg = fromBg ) if (knownCalls.isEmpty()) { - startForeground(callId.hashCode(), notification) + startForegroundCompat(callId.hashCode(), notification) } else { notificationManager.notify(callId.hashCode(), notification) } @@ -201,7 +202,7 @@ class CallAndroidService : VectorAndroidService() { } val notification = notificationUtils.buildCallEndedNotification(false) val notificationId = callId.hashCode() - startForeground(notificationId, notification) + startForegroundCompat(notificationId, notification) if (knownCalls.isEmpty()) { Timber.tag(loggerTag.value).v("No more call, stop the service") stopForegroundCompat() @@ -236,7 +237,7 @@ class CallAndroidService : VectorAndroidService() { title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId ) if (knownCalls.isEmpty()) { - startForeground(callId.hashCode(), notification) + startForegroundCompat(callId.hashCode(), notification) } else { notificationManager.notify(callId.hashCode(), notification) } @@ -260,7 +261,7 @@ class CallAndroidService : VectorAndroidService() { title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId ) if (knownCalls.isEmpty()) { - startForeground(callId.hashCode(), notification) + startForegroundCompat(callId.hashCode(), notification) } else { notificationManager.notify(callId.hashCode(), notification) } @@ -273,9 +274,9 @@ class CallAndroidService : VectorAndroidService() { callRingPlayerOutgoing?.stop() val notification = notificationUtils.buildCallEndedNotification(false) if (callId != null) { - startForeground(callId.hashCode(), notification) + startForegroundCompat(callId.hashCode(), notification) } else { - startForeground(DEFAULT_NOTIFICATION_ID, notification) + startForegroundCompat(DEFAULT_NOTIFICATION_ID, notification) } if (knownCalls.isEmpty()) { mediaSession?.isActive = false diff --git a/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt b/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt index 864f69a136..f746c0749b 100644 --- a/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt +++ b/vector/src/main/java/im/vector/app/core/services/VectorSyncAndroidService.kt @@ -32,6 +32,7 @@ import androidx.work.Worker import androidx.work.WorkerParameters import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.extensions.startForegroundCompat import im.vector.app.core.platform.PendingIntentCompat import im.vector.app.core.time.Clock import im.vector.app.core.time.DefaultClock @@ -98,7 +99,7 @@ class VectorSyncAndroidService : SyncAndroidService() { R.string.notification_listening_for_notifications } val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false) - startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification) + startForegroundCompat(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification) } override fun onRescheduleAsked( diff --git a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt index 96c3f8a6ce..fbf89b76a4 100644 --- a/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/core/session/ConfigureAndStartSessionUseCase.kt @@ -19,11 +19,12 @@ package im.vector.app.core.session import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext 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.features.call.webrtc.WebRtcCallManager import im.vector.app.features.session.coroutineScope 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 kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session @@ -35,7 +36,8 @@ class ConfigureAndStartSessionUseCase @Inject constructor( private val webRtcCallManager: WebRtcCallManager, private val updateMatrixClientInfoUseCase: UpdateMatrixClientInfoUseCase, private val vectorPreferences: VectorPreferences, - private val enableNotificationsSettingUpdater: EnableNotificationsSettingUpdater, + private val notificationsSettingUpdater: NotificationsSettingUpdater, + private val updateNotificationSettingsAccountDataUseCase: UpdateNotificationSettingsAccountDataUseCase, ) { fun execute(session: Session, startSyncing: Boolean = true) { @@ -49,11 +51,22 @@ class ConfigureAndStartSessionUseCase @Inject constructor( } session.pushersService().refreshPushers() webRtcCallManager.checkForProtocolsSupportIfNeeded() + updateMatrixClientInfoIfNeeded(session) + createNotificationSettingsAccountDataIfNeeded(session) + notificationsSettingUpdater.onSessionStarted(session) + } + + private fun updateMatrixClientInfoIfNeeded(session: Session) { session.coroutineScope.launch { if (vectorPreferences.isClientInfoRecordingEnabled()) { updateMatrixClientInfoUseCase.execute(session) } } - enableNotificationsSettingUpdater.onSessionsStarted(session) + } + + private fun createNotificationSettingsAccountDataIfNeeded(session: Session) { + session.coroutineScope.launch { + updateNotificationSettingsAccountDataUseCase.execute(session) + } } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt index 6327daec86..0570bbe4d7 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/ShieldImageView.kt @@ -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) { isVisible = roomEncryptionTrustLevel != null @@ -45,8 +64,8 @@ class ShieldImageView @JvmOverloads constructor( 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 + if (borderLess) R.drawable.ic_shield_black_no_border + else R.drawable.ic_shield_black ) } RoomEncryptionTrustLevel.Warning -> { @@ -137,7 +156,7 @@ class ShieldImageView @JvmOverloads constructor( @DrawableRes fun RoomEncryptionTrustLevel.toDrawableRes(): Int { 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.Trusted -> R.drawable.ic_shield_trusted RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm -> R.drawable.ic_warning_badge diff --git a/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt b/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt index 0474cdea7e..47326bca76 100644 --- a/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt +++ b/vector/src/main/java/im/vector/app/core/utils/ExpandingBottomSheetBehavior.kt @@ -608,26 +608,33 @@ class ExpandingBottomSheetBehavior : CoordinatorLayout.Behavior { initialPaddingBottom = view.paddingBottom // This should only be used to set initial insets and other edge cases where the insets can't be applied using an animation. - var applyInsetsFromAnimation = false + var isAnimating = false - // This will animated inset changes, making them look a lot better. However, it won't update initial insets. + // This will animate inset changes, making them look a lot better. However, it won't update initial insets. ViewCompat.setWindowInsetsAnimationCallback(view, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) { + override fun onPrepare(animation: WindowInsetsAnimationCompat) { + isAnimating = true + } + override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList): WindowInsetsCompat { - return applyInsets(view, insets) + return if (isAnimating) { + applyInsets(view, insets) + } else { + insets + } } override fun onEnd(animation: WindowInsetsAnimationCompat) { - applyInsetsFromAnimation = false + isAnimating = false view.requestApplyInsets() } }) ViewCompat.setOnApplyWindowInsetsListener(view) { _: View, insets: WindowInsetsCompat -> - if (!applyInsetsFromAnimation) { - applyInsetsFromAnimation = true - applyInsets(view, insets) - } else { + if (isAnimating) { insets + } else { + applyInsets(view, insets) } } diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index 95cf272abd..99abc15f81 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -44,6 +44,7 @@ interface VectorFeatures { fun isQrCodeLoginForAllServers(): Boolean fun isReciprocateQrCodeLogin(): Boolean fun isVoiceBroadcastEnabled(): Boolean + fun isUnverifiedSessionsAlertEnabled(): Boolean } class DefaultVectorFeatures : VectorFeatures { @@ -63,4 +64,5 @@ class DefaultVectorFeatures : VectorFeatures { override fun isQrCodeLoginForAllServers(): Boolean = false override fun isReciprocateQrCodeLogin(): Boolean = false override fun isVoiceBroadcastEnabled(): Boolean = true + override fun isUnverifiedSessionsAlertEnabled(): Boolean = true } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt index e7cebfb9c9..00b6bc40d2 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureAndroidService.kt @@ -20,6 +20,7 @@ import android.content.Intent import android.os.Binder import android.os.IBinder import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.startForegroundCompat import im.vector.app.core.services.VectorAndroidService import im.vector.app.core.time.Clock import im.vector.app.features.notifications.NotificationUtils @@ -41,7 +42,7 @@ class ScreenCaptureAndroidService : VectorAndroidService() { private fun showStickyNotification() { val notificationId = clock.epochMillis().toInt() val notification = notificationUtils.buildScreenSharingNotification() - startForeground(notificationId, notification) + startForegroundCompat(notificationId, notification) } override fun onBind(intent: Intent?): IBinder { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index d2bdbd71ae..0d98ffcab1 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -45,8 +45,6 @@ import im.vector.app.core.extensions.restart import im.vector.app.core.extensions.validateBackPressed import im.vector.app.core.platform.VectorBaseActivity 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.resources.ColorProvider import im.vector.app.core.utils.registerForPermissionsResult @@ -134,7 +132,6 @@ class HomeActivity : private val serverBackupStatusViewModel: ServerBackupStatusViewModel by viewModel() @Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler - @Inject lateinit var pushersManager: PushersManager @Inject lateinit var notificationDrawerManager: NotificationDrawerManager @Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var shortcutsHandler: ShortcutsHandler @@ -143,7 +140,6 @@ class HomeActivity : @Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter @Inject lateinit var spaceStateHandler: SpaceStateHandler @Inject lateinit var unifiedPushHelper: UnifiedPushHelper - @Inject lateinit var fcmHelper: FcmHelper @Inject lateinit var nightlyProxy: NightlyProxy @Inject lateinit var disclaimerDialog: DisclaimerDialog @Inject lateinit var notificationPermissionManager: NotificationPermissionManager @@ -215,16 +211,6 @@ class HomeActivity : isNewAppLayoutEnabled = vectorPreferences.isNewAppLayoutEnabled() analyticsScreenName = MobileScreen.ScreenName.Home supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) - unifiedPushHelper.register(this) { - if (unifiedPushHelper.isEmbeddedDistributor()) { - fcmHelper.ensureFcmTokenIsRetrieved( - this, - pushersManager, - homeActivityViewModel.shouldAddHttpPusher() - ) - } - } - sharedActionViewModel = viewModelProvider[HomeSharedActionViewModel::class.java] roomListSharedActionViewModel = viewModelProvider[RoomListSharedActionViewModel::class.java] views.drawerLayout.addDrawerListener(drawerListener) @@ -286,6 +272,7 @@ class HomeActivity : HomeActivityViewEvents.ShowReleaseNotes -> handleShowReleaseNotes() HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration() is HomeActivityViewEvents.MigrateThreads -> migrateThreadsIfNeeded(it.checkSession) + is HomeActivityViewEvents.AskUserForPushDistributor -> askUserToSelectPushDistributor() } } homeActivityViewModel.onEach { renderState(it) } @@ -298,6 +285,12 @@ class HomeActivity : homeActivityViewModel.handle(HomeActivityViewActions.ViewStarted) } + private fun askUserToSelectPushDistributor() { + unifiedPushHelper.showSelectDistributorDialog(this) { selection -> + homeActivityViewModel.handle(HomeActivityViewActions.RegisterPushDistributor(selection)) + } + } + private fun handleShowNotificationDialog() { notificationPermissionManager.eventuallyRequestPermission(this, postPermissionLauncher) } @@ -430,14 +423,6 @@ class HomeActivity : } private fun renderState(state: HomeActivityViewState) { - lifecycleScope.launch { - if (state.areNotificationsSilenced) { - unifiedPushHelper.unregister(pushersManager) - } else { - unifiedPushHelper.register(this@HomeActivity) - } - } - when (val status = state.syncRequestState) { is SyncRequestState.InitialSyncProgressing -> { val initSyncStepStr = initSyncStepFormatter.format(status.initialSyncStep) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt index 5f89c89bc9..54392d5f56 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewActions.kt @@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction sealed interface HomeActivityViewActions : VectorViewModelAction { object ViewStarted : HomeActivityViewActions object PushPromptHasBeenReviewed : HomeActivityViewActions + data class RegisterPushDistributor(val distributor: String) : HomeActivityViewActions } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt index e548fdb2f3..be5aa7def0 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewEvents.kt @@ -25,9 +25,11 @@ sealed interface HomeActivityViewEvents : VectorViewEvents { val userItem: MatrixItem.UserItem, val waitForIncomingRequest: Boolean = true, ) : HomeActivityViewEvents + data class CurrentSessionCannotBeVerified( val userItem: MatrixItem.UserItem, ) : HomeActivityViewEvents + data class OnCrossSignedInvalidated(val userItem: MatrixItem.UserItem) : HomeActivityViewEvents object PromptToEnableSessionPush : HomeActivityViewEvents object ShowAnalyticsOptIn : HomeActivityViewEvents @@ -37,4 +39,5 @@ sealed interface HomeActivityViewEvents : VectorViewEvents { data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents object StartRecoverySetupFlow : HomeActivityViewEvents data class ForceVerification(val sendRequest: Boolean) : HomeActivityViewEvents + object AskUserForPushDistributor : HomeActivityViewEvents } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 182424e9ae..9dde54514a 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -16,7 +16,6 @@ package im.vector.app.features.home -import androidx.lifecycle.asFlow import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.MavericksViewModelFactory 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.hiltMavericksViewModelFactory 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.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsType @@ -48,12 +50,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.takeWhile 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.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.auth.UserPasswordAuth @@ -62,11 +62,9 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.extensions.tryOrNull 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.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap -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.pushrules.RuleIds import org.matrix.android.sdk.api.session.room.model.Membership @@ -92,8 +90,11 @@ class HomeActivityViewModel @AssistedInject constructor( private val analyticsTracker: AnalyticsTracker, private val analyticsConfig: AnalyticsConfig, private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore, - private val vectorFeatures: VectorFeatures, private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase, + private val pushersManager: PushersManager, + private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, + private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, + private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase, ) : VectorViewModel(initialState) { @AssistedFactory @@ -117,17 +118,44 @@ class HomeActivityViewModel @AssistedInject constructor( private fun initialize() { if (isInitialized) return isInitialized = true + registerUnifiedPushIfNeeded() cleanupFiles() observeInitialSync() checkSessionPushIsOn() observeCrossSigningReset() observeAnalytics() //observeReleaseNotes() - observeLocalNotificationsSilenced() initThreadsMigration() 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 -> if (vectorPreferences.isNewAppLayoutEnabled()) { @@ -148,26 +176,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().getMyDevice().deviceId - viewModelScope.launch { - currentSession.accountDataService() - .getLiveUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) - .asFlow() - .map { it.getOrNull()?.content?.toModel()?.isSilenced ?: false } - .onEach { setState { copy(areNotificationsSilenced = it) } } - } - } - private fun observeAnalytics() { if (analyticsConfig.isEnabled) { analyticsStore.didAskUserConsentFlow @@ -503,6 +511,9 @@ class HomeActivityViewModel @AssistedInject constructor( HomeActivityViewActions.ViewStarted -> { initialize() } + is HomeActivityViewActions.RegisterPushDistributor -> { + registerUnifiedPush(distributor = action.distributor) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt index 4df2957cbc..f9c1b37ed5 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewState.kt @@ -23,5 +23,4 @@ import org.matrix.android.sdk.api.session.sync.SyncRequestState data class HomeActivityViewState( val syncRequestState: SyncRequestState = SyncRequestState.Idle, val authenticationDescription: AuthenticationDescription? = null, - val areNotificationsSilenced: Boolean = false, ) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index f5bad25f6e..0b42e5cc45 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -487,12 +487,12 @@ class HomeDetailFragment : .requestSessionVerification(vectorBaseActivity, newest.deviceId ?: "") } unknownDeviceDetectorSharedViewModel.handle( - UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) + UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty()) ) } dismissedAction = Runnable { unknownDeviceDetectorSharedViewModel.handle( - UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) + UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty()) ) } } @@ -504,8 +504,8 @@ class HomeDetailFragment : alertManager.postVectorAlert( VerificationVectorAlert( uid = uid, - title = getString(R.string.review_logins), - description = getString(R.string.verify_other_sessions), + title = getString(R.string.review_unverified_sessions_title), + description = getString(R.string.review_unverified_sessions_description), iconId = R.drawable.ic_shield_warning ).apply { viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer) diff --git a/vector/src/main/java/im/vector/app/features/home/IsNewLoginAlertShownUseCase.kt b/vector/src/main/java/im/vector/app/features/home/IsNewLoginAlertShownUseCase.kt new file mode 100644 index 0000000000..5a0d4743dc --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/IsNewLoginAlertShownUseCase.kt @@ -0,0 +1,31 @@ +/* + * 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 + +import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject + +class IsNewLoginAlertShownUseCase @Inject constructor( + private val vectorPreferences: VectorPreferences, +) { + + fun execute(deviceId: String?): Boolean { + deviceId ?: return false + + return vectorPreferences.isNewLoginAlertShownForDevice(deviceId) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt index 7cd2509857..80047767d1 100644 --- a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt @@ -264,12 +264,12 @@ class NewHomeDetailFragment : .requestSessionVerification(vectorBaseActivity, newest.deviceId ?: "") } unknownDeviceDetectorSharedViewModel.handle( - UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) + UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty()) ) } dismissedAction = Runnable { unknownDeviceDetectorSharedViewModel.handle( - UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) + UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty()) ) } } @@ -281,8 +281,8 @@ class NewHomeDetailFragment : alertManager.postVectorAlert( VerificationVectorAlert( uid = uid, - title = getString(R.string.review_logins), - description = getString(R.string.verify_other_sessions), + title = getString(R.string.review_unverified_sessions_title), + description = getString(R.string.review_unverified_sessions_description), iconId = R.drawable.ic_shield_warning ).apply { viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer) diff --git a/vector/src/main/java/im/vector/app/features/home/SetNewLoginAlertShownUseCase.kt b/vector/src/main/java/im/vector/app/features/home/SetNewLoginAlertShownUseCase.kt new file mode 100644 index 0000000000..d313f93043 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/SetNewLoginAlertShownUseCase.kt @@ -0,0 +1,31 @@ +/* + * 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 + +import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject + +class SetNewLoginAlertShownUseCase @Inject constructor( + private val vectorPreferences: VectorPreferences, +) { + + fun execute(deviceIds: List) { + deviceIds.forEach { + vectorPreferences.setNewLoginAlertShownForDevice(it) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt b/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt new file mode 100644 index 0000000000..4580ac0f31 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt @@ -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.home + +import im.vector.app.core.time.Clock +import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject + +class SetUnverifiedSessionsAlertShownUseCase @Inject constructor( + private val vectorPreferences: VectorPreferences, + private val clock: Clock, +) { + + fun execute(deviceIds: List) { + val epochMillis = clock.epochMillis() + deviceIds.forEach { + vectorPreferences.setUnverifiedSessionsAlertLastShownMillis(it, epochMillis) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt b/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt new file mode 100644 index 0000000000..18c7ed9689 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt @@ -0,0 +1,39 @@ +/* + * 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 + +import im.vector.app.config.Config +import im.vector.app.core.time.Clock +import im.vector.app.features.VectorFeatures +import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject + +class ShouldShowUnverifiedSessionsAlertUseCase @Inject constructor( + private val vectorFeatures: VectorFeatures, + private val vectorPreferences: VectorPreferences, + private val clock: Clock, +) { + + fun execute(deviceId: String?): Boolean { + deviceId ?: return false + + val isUnverifiedSessionsAlertEnabled = vectorFeatures.isUnverifiedSessionsAlertEnabled() + val unverifiedSessionsAlertLastShownMillis = vectorPreferences.getUnverifiedSessionsAlertLastShownMillis(deviceId) + return isUnverifiedSessionsAlertEnabled && + clock.epochMillis() - unverifiedSessionsAlertLastShownMillis >= Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt index 855c47f4bb..21c7bd6ea1 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -19,7 +19,6 @@ package im.vector.app.features.home import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksViewModelFactory -import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted @@ -33,7 +32,6 @@ import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.time.Clock -import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn @@ -63,12 +61,16 @@ data class DeviceDetectionInfo( class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( @Assisted initialState: UnknownDevicesState, session: Session, - private val vectorPreferences: VectorPreferences, clock: Clock, + private val shouldShowUnverifiedSessionsAlertUseCase: ShouldShowUnverifiedSessionsAlertUseCase, + private val setUnverifiedSessionsAlertShownUseCase: SetUnverifiedSessionsAlertShownUseCase, + private val isNewLoginAlertShownUseCase: IsNewLoginAlertShownUseCase, + private val setNewLoginAlertShownUseCase: SetNewLoginAlertShownUseCase, ) : VectorViewModel(initialState) { sealed class Action : VectorViewModelAction { data class IgnoreDevice(val deviceIds: List) : Action() + data class IgnoreNewLogin(val deviceIds: List) : Action() } @AssistedFactory @@ -86,8 +88,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( } } - private val ignoredDeviceList = ArrayList() - init { val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId) .firstOrNull { it.deviceId == session.sessionParams.deviceId } @@ -95,12 +95,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( ?: clock.epochMillis() Timber.v("## Detector - Current Session first time seen $currentSessionTs") - ignoredDeviceList.addAll( - vectorPreferences.getUnknownDeviceDismissedList().also { - Timber.v("## Detector - Remembered ignored list $it") - } - ) - combine( session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveMyDevicesInfo(), @@ -114,13 +108,15 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse() } // filter out ignored devices - .filter { !ignoredDeviceList.contains(it.deviceId) } + .filter { shouldShowUnverifiedSessionsAlertUseCase.execute(it.deviceId) } .sortedByDescending { it.lastSeenTs } .map { deviceInfo -> val deviceKnownSince = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }?.firstTimeSeenLocalTs ?: 0 + val isNew = isNewLoginAlertShownUseCase.execute(deviceInfo.deviceId).not() && deviceKnownSince > currentSessionTs + DeviceDetectionInfo( deviceInfo, - deviceKnownSince > currentSessionTs + 60_000, // short window to avoid false positive, + isNew, pInfo.getOrNull()?.selfSigned != null // adding this to pass distinct when cross sign change ) } @@ -150,22 +146,11 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( override fun handle(action: Action) { when (action) { is Action.IgnoreDevice -> { - ignoredDeviceList.addAll(action.deviceIds) - // local echo - withState { state -> - state.unknownSessions.invoke()?.let { detectedSessions -> - val updated = detectedSessions.filter { !action.deviceIds.contains(it.deviceInfo.deviceId) } - setState { - copy(unknownSessions = Success(updated)) - } - } - } + setUnverifiedSessionsAlertShownUseCase.execute(action.deviceIds) + } + is Action.IgnoreNewLogin -> { + setNewLoginAlertShownUseCase.execute(action.deviceIds) } } } - - override fun onCleared() { - vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList) - super.onCleared() - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index 2b11d4052f..791b250517 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -268,7 +268,7 @@ class MessageComposerFragment : VectorBaseFragment(), A ) { mainState, messageComposerState, attachmentState -> if (mainState.tombstoneEvent != null) return@withState - (composer as? View)?.isInvisible = !messageComposerState.isComposerVisible + (composer as? View)?.isVisible = messageComposerState.isComposerVisible composer.sendButton.isInvisible = !messageComposerState.isSendButtonVisible (composer as? RichTextComposerLayout)?.isTextFormattingEnabled = attachmentState.isTextFormattingEnabled } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 45c56e9137..0e71875f91 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -59,6 +59,7 @@ import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoomSummary +import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent @@ -89,39 +90,44 @@ class MessageComposerViewModel @AssistedInject constructor( private val voiceBroadcastHelper: VoiceBroadcastHelper, ) : VectorViewModel(initialState) { - private val room = session.getRoom(initialState.roomId)!! + private val room = session.getRoom(initialState.roomId) // Keep it out of state to avoid invalidate being called private var currentComposerText: CharSequence = "" init { - loadDraftIfAny() - observePowerLevelAndEncryption() - observeVoiceBroadcast() - subscribeToStateInternal() + if (room != null) { + loadDraftIfAny(room) + observePowerLevelAndEncryption(room) + observeVoiceBroadcast(room) + subscribeToStateInternal() + } else { + onRoomError() + } } override fun handle(action: MessageComposerAction) { + val room = this.room ?: return when (action) { - is MessageComposerAction.EnterEditMode -> handleEnterEditMode(action) - is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(action) + is MessageComposerAction.EnterEditMode -> handleEnterEditMode(room, action) + is MessageComposerAction.EnterQuoteMode -> handleEnterQuoteMode(room, action) is MessageComposerAction.EnterRegularMode -> handleEnterRegularMode(action) - is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(action) - is MessageComposerAction.SendMessage -> handleSendMessage(action) - is MessageComposerAction.UserIsTyping -> handleUserIsTyping(action) + is MessageComposerAction.EnterReplyMode -> handleEnterReplyMode(room, action) + is MessageComposerAction.SendMessage -> handleSendMessage(room, action) + is MessageComposerAction.UserIsTyping -> handleUserIsTyping(room, action) is MessageComposerAction.OnTextChanged -> handleOnTextChanged(action) is MessageComposerAction.OnVoiceRecordingUiStateChanged -> handleOnVoiceRecordingUiStateChanged(action) - is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage() - is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.isCancelled, action.rootThreadEventId) + is MessageComposerAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage(room) + is MessageComposerAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(room, action.isCancelled, action.rootThreadEventId) is MessageComposerAction.PlayOrPauseVoicePlayback -> handlePlayOrPauseVoicePlayback(action) MessageComposerAction.PauseRecordingVoiceMessage -> handlePauseRecordingVoiceMessage() MessageComposerAction.PlayOrPauseRecordingPlayback -> handlePlayOrPauseRecordingPlayback() - is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData) - is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText) + is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(room, action.attachmentData) + is MessageComposerAction.OnEntersBackground -> handleEntersBackground(room, action.composerText) is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action) is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action) is MessageComposerAction.AudioSeekBarMovedTo -> handleAudioSeekBarMovedTo(action) - is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(action) + is MessageComposerAction.SlashCommandConfirmed -> handleSlashCommandConfirmed(room, action) is MessageComposerAction.InsertUserDisplayName -> handleInsertUserDisplayName(action) is MessageComposerAction.SetFullScreen -> handleSetFullScreen(action) // SC @@ -162,7 +168,7 @@ class MessageComposerViewModel @AssistedInject constructor( copy(sendMode = SendMode.Regular(newText, action.fromSharing)) } - private fun handleEnterEditMode(action: MessageComposerAction.EnterEditMode) { + private fun handleEnterEditMode(room: Room, action: MessageComposerAction.EnterEditMode) { room.getTimelineEvent(action.eventId)?.let { timelineEvent -> val formatted = vectorPreferences.isRichTextEditorEnabled() setState { copy(sendMode = SendMode.Edit(timelineEvent, timelineEvent.getTextEditableContent(formatted))) } @@ -173,7 +179,7 @@ class MessageComposerViewModel @AssistedInject constructor( setState { copy(isFullScreen = action.isFullScreen) } } - private fun observePowerLevelAndEncryption() { + private fun observePowerLevelAndEncryption(room: Room) { combine( PowerLevelsFlowFactory(room).createFlow(), room.flow().liveRoomSummary().unwrap() @@ -199,7 +205,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun observeVoiceBroadcast() { + private fun observeVoiceBroadcast(room: Room) { room.stateService().getStateEventLive(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(session.myUserId)) .asFlow() .unwrap() @@ -209,19 +215,19 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) { + private fun handleEnterQuoteMode(room: Room, action: MessageComposerAction.EnterQuoteMode) { room.getTimelineEvent(action.eventId)?.let { timelineEvent -> setState { copy(sendMode = SendMode.Quote(timelineEvent, currentComposerText)) } } } - private fun handleEnterReplyMode(action: MessageComposerAction.EnterReplyMode) { + private fun handleEnterReplyMode(room: Room, action: MessageComposerAction.EnterReplyMode) { room.getTimelineEvent(action.eventId)?.let { timelineEvent -> setState { copy(sendMode = SendMode.Reply(timelineEvent, currentComposerText)) } } } - private fun handleSendMessage(action: MessageComposerAction.SendMessage) { + private fun handleSendMessage(room: Room, action: MessageComposerAction.SendMessage) { withState { state -> analyticsTracker.capture(state.toAnalyticsComposer()).also { setState { copy(startsThread = false) } @@ -251,7 +257,7 @@ class MessageComposerViewModel @AssistedInject constructor( } _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() + popDraft(room) } is ParsedCommand.ErrorSyntax -> { _viewEvents.post(MessageComposerViewEvents.SlashCommandError(parsedCommand.command)) @@ -277,7 +283,7 @@ class MessageComposerViewModel @AssistedInject constructor( room.sendService().sendTextMessage(parsedCommand.message, autoMarkdown = false) } _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() + popDraft(room) } is ParsedCommand.SendFormattedText -> { // Send the text message to the room, without markdown @@ -295,23 +301,23 @@ class MessageComposerViewModel @AssistedInject constructor( ) } _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() + popDraft(room) } is ParsedCommand.ChangeRoomName -> { - handleChangeRoomNameSlashCommand(parsedCommand) + handleChangeRoomNameSlashCommand(room, parsedCommand) } is ParsedCommand.Invite -> { - handleInviteSlashCommand(parsedCommand) + handleInviteSlashCommand(room, parsedCommand) } is ParsedCommand.Invite3Pid -> { - handleInvite3pidSlashCommand(parsedCommand) + handleInvite3pidSlashCommand(room, parsedCommand) } is ParsedCommand.SetUserPowerLevel -> { - handleSetUserPowerLevel(parsedCommand) + handleSetUserPowerLevel(room, parsedCommand) } is ParsedCommand.DevTools -> { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.ClearScalarToken -> { // TODO @@ -320,29 +326,29 @@ class MessageComposerViewModel @AssistedInject constructor( is ParsedCommand.SetMarkdown -> { vectorPreferences.setMarkdownEnabled(parsedCommand.enable) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.BanUser -> { - handleBanSlashCommand(parsedCommand) + handleBanSlashCommand(room, parsedCommand) } is ParsedCommand.UnbanUser -> { - handleUnbanSlashCommand(parsedCommand) + handleUnbanSlashCommand(room, parsedCommand) } is ParsedCommand.IgnoreUser -> { - handleIgnoreSlashCommand(parsedCommand) + handleIgnoreSlashCommand(room, parsedCommand) } is ParsedCommand.UnignoreUser -> { handleUnignoreSlashCommand(parsedCommand) } is ParsedCommand.RemoveUser -> { - handleRemoveSlashCommand(parsedCommand) + handleRemoveSlashCommand(room, parsedCommand) } is ParsedCommand.JoinRoom -> { handleJoinToAnotherRoomSlashCommand(parsedCommand) - popDraft() + popDraft(room) } is ParsedCommand.PartRoom -> { - handlePartSlashCommand(parsedCommand) + handlePartSlashCommand(room, parsedCommand) } is ParsedCommand.SendEmote -> { if (state.rootThreadEventId != null) { @@ -360,7 +366,7 @@ class MessageComposerViewModel @AssistedInject constructor( ) } _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.SendRainbow -> { val message = parsedCommand.message.toString() @@ -374,7 +380,7 @@ class MessageComposerViewModel @AssistedInject constructor( room.sendService().sendFormattedTextMessage(message, rainbowGenerator.generate(message)) } _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.SendRainbowEmote -> { val message = parsedCommand.message.toString() @@ -390,7 +396,7 @@ class MessageComposerViewModel @AssistedInject constructor( } _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.SendSpoiler -> { val text = "[${stringProvider.getString(R.string.spoiler)}](${parsedCommand.message})" @@ -408,53 +414,53 @@ class MessageComposerViewModel @AssistedInject constructor( ) } _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.SendShrug -> { - sendPrefixedMessage("¯\\_(ツ)_/¯", parsedCommand.message, state.rootThreadEventId) + sendPrefixedMessage(room, "¯\\_(ツ)_/¯", parsedCommand.message, state.rootThreadEventId) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.SendLenny -> { - sendPrefixedMessage("( ͡° ͜ʖ ͡°)", parsedCommand.message, state.rootThreadEventId) + sendPrefixedMessage(room, "( ͡° ͜ʖ ͡°)", parsedCommand.message, state.rootThreadEventId) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.SendTableFlip -> { - sendPrefixedMessage("(╯°□°)╯︵ ┻━┻", parsedCommand.message, state.rootThreadEventId) + sendPrefixedMessage(room, "(╯°□°)╯︵ ┻━┻", parsedCommand.message, state.rootThreadEventId) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.SendChatEffect -> { - sendChatEffect(parsedCommand) + sendChatEffect(room, parsedCommand) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } is ParsedCommand.ChangeTopic -> { - handleChangeTopicSlashCommand(parsedCommand) + handleChangeTopicSlashCommand(room, parsedCommand) } is ParsedCommand.ChangeDisplayName -> { - handleChangeDisplayNameSlashCommand(parsedCommand) + handleChangeDisplayNameSlashCommand(room, parsedCommand) } is ParsedCommand.ChangeDisplayNameForRoom -> { - handleChangeDisplayNameForRoomSlashCommand(parsedCommand) + handleChangeDisplayNameForRoomSlashCommand(room, parsedCommand) } is ParsedCommand.ChangeRoomAvatar -> { - handleChangeRoomAvatarSlashCommand(parsedCommand) + handleChangeRoomAvatarSlashCommand(room, parsedCommand) } is ParsedCommand.ChangeAvatarForRoom -> { - handleChangeAvatarForRoomSlashCommand(parsedCommand) + handleChangeAvatarForRoomSlashCommand(room, parsedCommand) } is ParsedCommand.ShowUser -> { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) handleWhoisSlashCommand(parsedCommand) - popDraft() + popDraft(room) } is ParsedCommand.DiscardSession -> { if (room.roomCryptoService().isEncrypted()) { session.cryptoService().discardOutboundSession(room.roomId) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } else { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) _viewEvents.post( @@ -479,7 +485,7 @@ class MessageComposerViewModel @AssistedInject constructor( null, true ) - popDraft() + popDraft(room) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) @@ -498,7 +504,7 @@ class MessageComposerViewModel @AssistedInject constructor( null, false ) - popDraft() + popDraft(room) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) @@ -511,7 +517,7 @@ class MessageComposerViewModel @AssistedInject constructor( viewModelScope.launch(Dispatchers.IO) { try { session.spaceService().joinSpace(parsedCommand.spaceIdOrAlias) - popDraft() + popDraft(room) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) @@ -523,7 +529,7 @@ class MessageComposerViewModel @AssistedInject constructor( viewModelScope.launch(Dispatchers.IO) { try { session.roomService().leaveRoom(parsedCommand.roomId) - popDraft() + popDraft(room) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) } catch (failure: Throwable) { _viewEvents.post(MessageComposerViewEvents.SlashCommandResultError(failure)) @@ -539,7 +545,7 @@ class MessageComposerViewModel @AssistedInject constructor( ) ) _viewEvents.post(MessageComposerViewEvents.SlashCommandResultOk(parsedCommand)) - popDraft() + popDraft(room) } } } @@ -588,7 +594,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() + popDraft(room) } is SendMode.Quote -> { room.sendService().sendQuotedTextMessage( @@ -599,7 +605,7 @@ class MessageComposerViewModel @AssistedInject constructor( rootThreadEventId = state.rootThreadEventId ) _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() + popDraft(room) } is SendMode.Reply -> { val timelineEvent = state.sendMode.timelineEvent @@ -624,7 +630,7 @@ class MessageComposerViewModel @AssistedInject constructor( ) _viewEvents.post(MessageComposerViewEvents.MessageSent) - popDraft() + popDraft(room) } is SendMode.Voice -> { // do nothing @@ -633,10 +639,10 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun popDraft() = withState { + private fun popDraft(room: Room) = withState { if (it.sendMode is SendMode.Regular && it.sendMode.fromSharing) { // If we were sharing, we want to get back our last value from draft - loadDraftIfAny() + loadDraftIfAny(room) } else { // Otherwise we clear the composer and remove the draft from db setState { copy(sendMode = SendMode.Regular("", false)) } @@ -646,7 +652,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun loadDraftIfAny() { + private fun loadDraftIfAny(room: Room) { val currentDraft = room.draftService().getDraft() setState { copy( @@ -675,7 +681,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleUserIsTyping(action: MessageComposerAction.UserIsTyping) { + private fun handleUserIsTyping(room: Room, action: MessageComposerAction.UserIsTyping) { if (vectorPreferences.sendTypingNotifs()) { if (action.isTyping) { room.typingService().userIsTyping() @@ -685,7 +691,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun sendChatEffect(sendChatEffect: ParsedCommand.SendChatEffect) { + private fun sendChatEffect(room: Room, sendChatEffect: ParsedCommand.SendChatEffect) { // If message is blank, convert to an emote, with default message if (sendChatEffect.message.isBlank()) { val defaultMessage = stringProvider.getString( @@ -737,25 +743,25 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) { - launchSlashCommandFlowSuspendable(changeTopic) { + private fun handleChangeTopicSlashCommand(room: Room, changeTopic: ParsedCommand.ChangeTopic) { + launchSlashCommandFlowSuspendable(room, changeTopic) { room.stateService().updateTopic(changeTopic.topic) } } - private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { - launchSlashCommandFlowSuspendable(invite) { + private fun handleInviteSlashCommand(room: Room, invite: ParsedCommand.Invite) { + launchSlashCommandFlowSuspendable(room, invite) { room.membershipService().invite(invite.userId, invite.reason) } } - private fun handleInvite3pidSlashCommand(invite: ParsedCommand.Invite3Pid) { - launchSlashCommandFlowSuspendable(invite) { + private fun handleInvite3pidSlashCommand(room: Room, invite: ParsedCommand.Invite3Pid) { + launchSlashCommandFlowSuspendable(room, invite) { room.membershipService().invite3pid(invite.threePid) } } - private fun handleSetUserPowerLevel(setUserPowerLevel: ParsedCommand.SetUserPowerLevel) { + private fun handleSetUserPowerLevel(room: Room, setUserPowerLevel: ParsedCommand.SetUserPowerLevel) { val newPowerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty) ?.content ?.toModel() @@ -763,19 +769,19 @@ class MessageComposerViewModel @AssistedInject constructor( ?.toContent() ?: return - launchSlashCommandFlowSuspendable(setUserPowerLevel) { + launchSlashCommandFlowSuspendable(room, setUserPowerLevel) { room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent) } } - private fun handleChangeDisplayNameSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayName) { - launchSlashCommandFlowSuspendable(changeDisplayName) { + private fun handleChangeDisplayNameSlashCommand(room: Room, changeDisplayName: ParsedCommand.ChangeDisplayName) { + launchSlashCommandFlowSuspendable(room, changeDisplayName) { session.profileService().setDisplayName(session.myUserId, changeDisplayName.displayName) } } - private fun handlePartSlashCommand(command: ParsedCommand.PartRoom) { - launchSlashCommandFlowSuspendable(command) { + private fun handlePartSlashCommand(room: Room, command: ParsedCommand.PartRoom) { + launchSlashCommandFlowSuspendable(room, command) { if (command.roomAlias == null) { // Leave the current room room @@ -790,39 +796,39 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleRemoveSlashCommand(removeUser: ParsedCommand.RemoveUser) { - launchSlashCommandFlowSuspendable(removeUser) { + private fun handleRemoveSlashCommand(room: Room, removeUser: ParsedCommand.RemoveUser) { + launchSlashCommandFlowSuspendable(room, removeUser) { room.membershipService().remove(removeUser.userId, removeUser.reason) } } - private fun handleBanSlashCommand(ban: ParsedCommand.BanUser) { - launchSlashCommandFlowSuspendable(ban) { + private fun handleBanSlashCommand(room: Room, ban: ParsedCommand.BanUser) { + launchSlashCommandFlowSuspendable(room, ban) { room.membershipService().ban(ban.userId, ban.reason) } } - private fun handleUnbanSlashCommand(unban: ParsedCommand.UnbanUser) { - launchSlashCommandFlowSuspendable(unban) { + private fun handleUnbanSlashCommand(room: Room, unban: ParsedCommand.UnbanUser) { + launchSlashCommandFlowSuspendable(room, unban) { room.membershipService().unban(unban.userId, unban.reason) } } - private fun handleChangeRoomNameSlashCommand(changeRoomName: ParsedCommand.ChangeRoomName) { - launchSlashCommandFlowSuspendable(changeRoomName) { + private fun handleChangeRoomNameSlashCommand(room: Room, changeRoomName: ParsedCommand.ChangeRoomName) { + launchSlashCommandFlowSuspendable(room, changeRoomName) { room.stateService().updateName(changeRoomName.name) } } - private fun getMyRoomMemberContent(): RoomMemberContent? { + private fun getMyRoomMemberContent(room: Room): RoomMemberContent? { return room.getStateEvent(EventType.STATE_ROOM_MEMBER, QueryStringValue.Equals(session.myUserId)) ?.content ?.toModel() } - private fun handleChangeDisplayNameForRoomSlashCommand(changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) { - launchSlashCommandFlowSuspendable(changeDisplayName) { - getMyRoomMemberContent() + private fun handleChangeDisplayNameForRoomSlashCommand(room: Room, changeDisplayName: ParsedCommand.ChangeDisplayNameForRoom) { + launchSlashCommandFlowSuspendable(room, changeDisplayName) { + getMyRoomMemberContent(room) ?.copy(displayName = changeDisplayName.displayName) ?.toContent() ?.let { @@ -831,15 +837,15 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleChangeRoomAvatarSlashCommand(changeAvatar: ParsedCommand.ChangeRoomAvatar) { - launchSlashCommandFlowSuspendable(changeAvatar) { + private fun handleChangeRoomAvatarSlashCommand(room: Room, changeAvatar: ParsedCommand.ChangeRoomAvatar) { + launchSlashCommandFlowSuspendable(room, changeAvatar) { room.stateService().sendStateEvent(EventType.STATE_ROOM_AVATAR, stateKey = "", RoomAvatarContent(changeAvatar.url).toContent()) } } - private fun handleChangeAvatarForRoomSlashCommand(changeAvatar: ParsedCommand.ChangeAvatarForRoom) { - launchSlashCommandFlowSuspendable(changeAvatar) { - getMyRoomMemberContent() + private fun handleChangeAvatarForRoomSlashCommand(room: Room, changeAvatar: ParsedCommand.ChangeAvatarForRoom) { + launchSlashCommandFlowSuspendable(room, changeAvatar) { + getMyRoomMemberContent(room) ?.copy(avatarUrl = changeAvatar.url) ?.toContent() ?.let { @@ -848,8 +854,8 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleIgnoreSlashCommand(ignore: ParsedCommand.IgnoreUser) { - launchSlashCommandFlowSuspendable(ignore) { + private fun handleIgnoreSlashCommand(room: Room, ignore: ParsedCommand.IgnoreUser) { + launchSlashCommandFlowSuspendable(room, ignore) { session.userService().ignoreUserIds(listOf(ignore.userId)) } } @@ -858,15 +864,15 @@ class MessageComposerViewModel @AssistedInject constructor( _viewEvents.post(MessageComposerViewEvents.SlashCommandConfirmationRequest(unignore)) } - private fun handleSlashCommandConfirmed(action: MessageComposerAction.SlashCommandConfirmed) { + private fun handleSlashCommandConfirmed(room: Room, action: MessageComposerAction.SlashCommandConfirmed) { when (action.parsedCommand) { - is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(action.parsedCommand) + is ParsedCommand.UnignoreUser -> handleUnignoreSlashCommandConfirmed(room, action.parsedCommand) else -> TODO("Not handled yet") } } - private fun handleUnignoreSlashCommandConfirmed(unignore: ParsedCommand.UnignoreUser) { - launchSlashCommandFlowSuspendable(unignore) { + private fun handleUnignoreSlashCommandConfirmed(room: Room, unignore: ParsedCommand.UnignoreUser) { + launchSlashCommandFlowSuspendable(room, unignore) { session.userService().unIgnoreUserIds(listOf(unignore.userId)) } } @@ -875,7 +881,7 @@ class MessageComposerViewModel @AssistedInject constructor( _viewEvents.post(MessageComposerViewEvents.OpenRoomMemberProfile(whois.userId)) } - private fun sendPrefixedMessage(prefix: String, message: CharSequence, rootThreadEventId: String?) { + private fun sendPrefixedMessage(room: Room, prefix: String, message: CharSequence, rootThreadEventId: String?) { val sequence = buildString { append(prefix) if (message.isNotEmpty()) { @@ -891,7 +897,7 @@ class MessageComposerViewModel @AssistedInject constructor( /** * Convert a send mode to a draft and save the draft. */ - private fun handleSaveTextDraft(draft: String) = withState { + private fun handleSaveTextDraft(room: Room, draft: String) = withState { session.coroutineScope.launch { when { it.sendMode is SendMode.Regular && !it.sendMode.fromSharing -> { @@ -914,7 +920,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleStartRecordingVoiceMessage() { + private fun handleStartRecordingVoiceMessage(room: Room) { try { audioMessageHelper.startRecording(room.roomId) } catch (failure: Throwable) { @@ -922,7 +928,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } - private fun handleEndRecordingVoiceMessage(isCancelled: Boolean, rootThreadEventId: String? = null) { + private fun handleEndRecordingVoiceMessage(room: Room, isCancelled: Boolean, rootThreadEventId: String? = null) { audioMessageHelper.stopPlayback() if (isCancelled) { audioMessageHelper.deleteRecording() @@ -969,7 +975,7 @@ class MessageComposerViewModel @AssistedInject constructor( audioMessageHelper.stopAllVoiceActions(deleteRecord) } - private fun handleInitializeVoiceRecorder(attachmentData: ContentAttachmentData) { + private fun handleInitializeVoiceRecorder(room: Room, attachmentData: ContentAttachmentData) { audioMessageHelper.initializeRecorder(room.roomId, attachmentData) setState { copy(voiceRecordingUiState = VoiceMessageRecorderView.RecordingUiState.Draft) } } @@ -990,7 +996,7 @@ class MessageComposerViewModel @AssistedInject constructor( audioMessageHelper.movePlaybackTo(action.eventId, action.percentage, action.duration) } - private fun handleEntersBackground(composerText: String) { + private fun handleEntersBackground(room: Room, composerText: String) { // Always stop all voice actions. It may be playing in timeline or active recording val playingAudioContent = audioMessageHelper.stopAllVoiceActions(deleteRecord = false) // TODO remove this when there will be a listening indicator outside of the timeline @@ -1006,7 +1012,7 @@ class MessageComposerViewModel @AssistedInject constructor( } } } else { - handleSaveTextDraft(draft = composerText) + handleSaveTextDraft(room = room, draft = composerText) } } @@ -1014,12 +1020,12 @@ class MessageComposerViewModel @AssistedInject constructor( _viewEvents.post(MessageComposerViewEvents.InsertUserDisplayName(action.userId)) } - private fun launchSlashCommandFlowSuspendable(parsedCommand: ParsedCommand, block: suspend () -> Unit) { + private fun launchSlashCommandFlowSuspendable(room: Room, parsedCommand: ParsedCommand, block: suspend () -> Unit) { _viewEvents.post(MessageComposerViewEvents.SlashCommandLoading) viewModelScope.launch { val event = try { block() - popDraft() + popDraft(room) MessageComposerViewEvents.SlashCommandResultOk(parsedCommand) } catch (failure: Throwable) { MessageComposerViewEvents.SlashCommandResultError(failure) @@ -1028,6 +1034,10 @@ class MessageComposerViewModel @AssistedInject constructor( } } + private fun onRoomError() = setState { + copy(isRoomError = true) + } + @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { override fun create(initialState: MessageComposerViewState): MessageComposerViewModel diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt index 102fa513ab..9f9d2b3844 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt @@ -62,6 +62,7 @@ fun CanSendStatus.boolean(): Boolean { data class MessageComposerViewState( val roomId: String, + val isRoomError: Boolean = false, val canSendMessage: CanSendStatus = CanSendStatus.Allowed, val isSendButtonActive: Boolean = false, val isSendButtonVisible: Boolean = false, @@ -89,8 +90,8 @@ data class MessageComposerViewState( val isVoiceMessageIdle = !isVoiceRecording - val isComposerVisible = canSendMessage.boolean() && !isVoiceRecording - val isVoiceMessageRecorderVisible = canSendMessage.boolean() && !isSendButtonVisible + val isComposerVisible = canSendMessage.boolean() && !isVoiceRecording && !isRoomError + val isVoiceMessageRecorderVisible = canSendMessage.boolean() && !isSendButtonVisible && !isRoomError constructor(args: TimelineArgs) : this( roomId = args.roomId, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt index be92c4fbb4..dfc125d933 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/RichTextComposerLayout.kt @@ -42,7 +42,6 @@ import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import com.google.android.material.shape.MaterialShapeDrawable import im.vector.app.R -import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.setTextIfDifferent import im.vector.app.core.extensions.showKeyboard import im.vector.app.core.utils.DimensionConverter @@ -133,8 +132,6 @@ class RichTextComposerLayout @JvmOverloads constructor( views.bottomSheetHandle.isVisible = isFullScreen if (isFullScreen) { editText.showKeyboard(true) - } else { - editText.hideKeyboard() } this.isFullScreen = isFullScreen } diff --git a/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt b/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt index ccab23a83b..d77a87f756 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/tracking/LocationSharingAndroidService.kt @@ -22,6 +22,7 @@ import android.os.Parcelable import androidx.core.app.NotificationManagerCompat import dagger.hilt.android.AndroidEntryPoint 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.features.location.LocationData import im.vector.app.features.location.LocationTracker @@ -105,7 +106,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca if (foregroundModeStarted) { NotificationManagerCompat.from(this).notify(FOREGROUND_SERVICE_NOTIFICATION_ID, notification) } else { - startForeground(FOREGROUND_SERVICE_NOTIFICATION_ID, notification) + startForegroundCompat(FOREGROUND_SERVICE_NOTIFICATION_ID, notification) foregroundModeStarted = true } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 022fea5ed1..7fe73f8087 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -118,7 +118,7 @@ class OnboardingViewModel @AssistedInject constructor( } } - private fun checkQrCodeLoginCapability(homeServerUrl: String) { + private suspend fun checkQrCodeLoginCapability(config: HomeServerConnectionConfig) { if (!vectorFeatures.isQrCodeLoginEnabled()) { setState { copy( @@ -133,16 +133,12 @@ class OnboardingViewModel @AssistedInject constructor( ) } } else { - viewModelScope.launch { - // check if selected server supports MSC3882 first - homeServerConnectionConfigFactory.create(homeServerUrl)?.let { - val canLoginWithQrCode = authenticationService.isQrLoginSupported(it) - setState { - copy( - canLoginWithQrCode = canLoginWithQrCode - ) - } - } + // check if selected server supports MSC3882 first + val canLoginWithQrCode = authenticationService.isQrLoginSupported(config) + setState { + copy( + canLoginWithQrCode = canLoginWithQrCode + ) } } } @@ -710,7 +706,6 @@ class OnboardingViewModel @AssistedInject constructor( _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig"))) } else { startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction) - checkQrCodeLoginCapability(homeServerConnectionConfig.homeServerUri.toString()) } } @@ -769,6 +764,8 @@ class OnboardingViewModel @AssistedInject constructor( _viewEvents.post(OnboardingViewEvents.OutdatedHomeserver) } + checkQrCodeLoginCapability(config) + when (trigger) { is OnboardingAction.HomeServerChange.SelectHomeServer -> { onHomeServerSelected(config, serverTypeOverride, authResult) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index e204f4526e..d2ceaaab50 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -268,8 +268,6 @@ class VectorPreferences @Inject constructor( private const val MEDIA_SAVING_1_MONTH = 2 private const val MEDIA_SAVING_FOREVER = 3 - private const val SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST = "SETTINGS_UNKNWON_DEVICE_DISMISSED_LIST" - private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE" private const val SETTINGS_LABS_ENABLE_LIVE_LOCATION = "SETTINGS_LABS_ENABLE_LIVE_LOCATION" @@ -286,6 +284,9 @@ class VectorPreferences @Inject constructor( // This key will be used to enable user for displaying live user info or not. const val SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO = "SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO" + const val SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS = "SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS_" + const val SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE = "SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE_" + // Possible values for TAKE_PHOTO_VIDEO_MODE const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0 const val TAKE_PHOTO_VIDEO_MODE_PHOTO = 1 @@ -573,18 +574,6 @@ class VectorPreferences @Inject constructor( return defaultPrefs.getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true) } - fun storeUnknownDeviceDismissedList(deviceIds: List) { - defaultPrefs.edit(true) { - putStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, deviceIds.toSet()) - } - } - - fun getUnknownDeviceDismissedList(): List { - return tryOrNull { - defaultPrefs.getStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, null)?.toList() - }.orEmpty() - } - /** * Update the notification ringtone. * @@ -1526,7 +1515,27 @@ class VectorPreferences @Inject constructor( fun setIpAddressVisibilityInDeviceManagerScreens(isVisible: Boolean) { defaultPrefs.edit { - putBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, isVisible) + putBoolean(SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, isVisible) + } + } + + fun getUnverifiedSessionsAlertLastShownMillis(deviceId: String): Long { + return defaultPrefs.getLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS + deviceId, 0) + } + + fun setUnverifiedSessionsAlertLastShownMillis(deviceId: String, lastShownMillis: Long) { + defaultPrefs.edit { + putLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS + deviceId, lastShownMillis) + } + } + + fun isNewLoginAlertShownForDevice(deviceId: String): Boolean { + return defaultPrefs.getBoolean(SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE + deviceId, false) + } + + fun setNewLoginAlertShownForDevice(deviceId: String) { + defaultPrefs.edit { + putBoolean(SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE + deviceId, true) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt index c685c84991..8f991ecd79 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsBaseFragment.kt @@ -29,6 +29,8 @@ import im.vector.app.R import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.singletonEntryPoint 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.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.plan.MobileScreen @@ -61,6 +63,19 @@ abstract class VectorSettingsBaseFragment : ScPreferenceFragment(), MavericksVie protected lateinit var session: Session protected lateinit var errorFormatter: ErrorFormatter + /* ========================================================================================== + * ViewEvents + * ========================================================================================== */ + + protected fun VectorViewModel<*, *, T>.observeViewEvents(observer: (T) -> Unit) { + viewEvents + .stream() + .onEach { + observer(it) + } + .launchIn(viewLifecycleOwner.lifecycleScope) + } + /* ========================================================================================== * Views * ========================================================================================== */ @@ -149,7 +164,7 @@ abstract class VectorSettingsBaseFragment : ScPreferenceFragment(), MavericksVie } } - protected fun displayErrorDialog(throwable: Throwable) { + protected fun displayErrorDialog(throwable: Throwable?) { displayErrorDialog(errorFormatter.toHumanReadable(throwable)) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt index 6486b8a3ca..5924742c26 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DeviceItem.kt @@ -85,9 +85,9 @@ abstract class DeviceItem : VectorEpoxyModel(R.layout.item_de trusted ) - holder.trustIcon.render(shield) + holder.trustIcon.renderDeviceShield(shield) } else { - holder.trustIcon.render(null) + holder.trustIcon.renderDeviceShield(null) } val detailedModeLabels = listOf( diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index f42d5af398..b7a6c5df30 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -28,7 +28,6 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.features.auth.PendingAuthHandler 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.signout.InterceptSignoutFlowResponseUseCase 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.verification.CheckIfCurrentSessionCanBeVerifiedUseCase @@ -48,7 +47,6 @@ class DevicesViewModel @AssistedInject constructor( private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, private val signoutSessionsUseCase: SignoutSessionsUseCase, - private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase, private val vectorPreferences: VectorPreferences, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index b27d8a7270..d748600416 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -53,6 +53,7 @@ import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationVie import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState import im.vector.app.features.settings.devices.v2.signout.BuildConfirmSignoutDialogUseCase 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 javax.inject.Inject @@ -99,6 +100,7 @@ class VectorSettingsDevicesFragment : super.onViewCreated(view, savedInstanceState) initWaitingView() + initCurrentSessionHeaderView() initOtherSessionsHeaderView() initOtherSessionsView() initSecurityRecommendationsView() @@ -139,6 +141,18 @@ class VectorSettingsDevicesFragment : views.waitingView.waitingStatusText.isVisible = true } + private fun initCurrentSessionHeaderView() { + views.deviceListHeaderCurrentSession.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.currentSessionHeaderSignoutOtherSessions -> { + confirmMultiSignoutOtherSessions() + true + } + else -> false + } + } + } + private fun initOtherSessionsHeaderView() { views.deviceListHeaderOtherSessions.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { @@ -247,7 +261,7 @@ class VectorSettingsDevicesFragment : val otherDevices = devices?.filter { it.deviceInfo.deviceId != currentDeviceId } renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount, isCurrentSessionVerified) - renderCurrentDevice(currentDeviceInfo) + renderCurrentSessionView(currentDeviceInfo, hasOtherDevices = otherDevices?.isNotEmpty().orFalse()) renderOtherSessionsView(otherDevices, state.isShowingIpAddress) } else { hideSecurityRecommendations() @@ -310,11 +324,11 @@ class VectorSettingsDevicesFragment : hideOtherSessionsView() } else { 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 nbDevices = otherDevices.size 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 val devices = if (isShowingIpAddress) otherDevices else otherDevices.map { it.copy(deviceInfo = it.deviceInfo.copy(lastSeenIp = null)) } views.deviceListOtherSessions.render( @@ -327,7 +341,7 @@ class VectorSettingsDevicesFragment : } else { stringProvider.getString(R.string.device_manager_other_sessions_show_ip_address) } - } + } } private fun hideOtherSessionsView() { @@ -335,9 +349,13 @@ class VectorSettingsDevicesFragment : views.deviceListOtherSessions.isVisible = false } - private fun renderCurrentDevice(currentDeviceInfo: DeviceFullInfo?) { + private fun renderCurrentSessionView(currentDeviceInfo: DeviceFullInfo?, hasOtherDevices: Boolean) { currentDeviceInfo?.let { views.deviceListHeaderCurrentSession.isVisible = true + val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError) + val signoutOtherSessionsItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignoutOtherSessions) + signoutOtherSessionsItem.setTextColor(colorDestructive) + signoutOtherSessionsItem.isVisible = hasOtherDevices views.deviceListCurrentSession.isVisible = true val viewState = SessionInfoViewState( isCurrentSession = true, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt index 9d9cb15c28..68cae344cd 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt @@ -97,7 +97,7 @@ abstract class OtherSessionItem : VectorEpoxyModel(R.la } else { setDeviceTypeIconUseCase.execute(deviceType, holder.otherSessionDeviceTypeImageView, stringProvider) } - holder.otherSessionVerificationStatusImageView.render(roomEncryptionTrustLevel) + holder.otherSessionVerificationStatusImageView.renderDeviceShield(roomEncryptionTrustLevel) holder.otherSessionNameTextView.text = sessionName holder.otherSessionDescriptionTextView.text = sessionDescription sessionDescriptionColor?.let { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt index c6044d04a4..7727cee4fa 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt @@ -90,7 +90,7 @@ class SessionInfoView @JvmOverloads constructor( hasLearnMoreLink: Boolean, isVerifyButtonVisible: Boolean, ) { - views.sessionInfoVerificationStatusImageView.render(encryptionTrustLevel) + views.sessionInfoVerificationStatusImageView.renderDeviceShield(encryptionTrustLevel) when { encryptionTrustLevel == RoomEncryptionTrustLevel.Trusted -> renderCrossSigningVerified(isCurrentSession) encryptionTrustLevel == RoomEncryptionTrustLevel.Default && !isCurrentSession -> renderCrossSigningUnknown() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt new file mode 100644 index 0000000000..18ee9ad937 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCase.kt @@ -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 { + return getNotificationSettingsAccountDataUpdatesUseCase.execute(session, deviceId) + .map { it?.isSilenced != null } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCase.kt similarity index 94% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCase.kt index 0125d92ba6..96521ec78c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCase.kt @@ -24,7 +24,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.flow.unwrap import javax.inject.Inject -class CanTogglePushNotificationsViaPusherUseCase @Inject constructor() { +class CanToggleNotificationsViaPusherUseCase @Inject constructor() { fun execute(session: Session): Flow { return session diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt similarity index 70% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt index 194a2aebbf..58289495a4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCase.kt @@ -17,14 +17,13 @@ package im.vector.app.features.settings.devices.v2.notification import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import javax.inject.Inject -class CheckIfCanTogglePushNotificationsViaAccountDataUseCase @Inject constructor() { +class CheckIfCanToggleNotificationsViaAccountDataUseCase @Inject constructor( + private val getNotificationSettingsAccountDataUseCase: GetNotificationSettingsAccountDataUseCase, +) { fun execute(session: Session, deviceId: String): Boolean { - return session - .accountDataService() - .getUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) != null + return getNotificationSettingsAccountDataUseCase.execute(session, deviceId)?.isSilenced != null } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCase.kt similarity index 92% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCase.kt index ca314bf145..1dc186be7c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCase.kt @@ -19,7 +19,7 @@ package im.vector.app.features.settings.devices.v2.notification import org.matrix.android.sdk.api.session.Session import javax.inject.Inject -class CheckIfCanTogglePushNotificationsViaPusherUseCase @Inject constructor() { +class CheckIfCanToggleNotificationsViaPusherUseCase @Inject constructor() { fun execute(session: Session): Boolean { return session diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt new file mode 100644 index 0000000000..3c086fe111 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCase.kt @@ -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) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCase.kt new file mode 100644 index 0000000000..308aeec5f2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCase.kt @@ -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 { + return session + .accountDataService() + .getLiveUserAccountDataEvent(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) + .asFlow() + .map { it.getOrNull()?.content?.toModel() } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCase.kt new file mode 100644 index 0000000000..5517fa0978 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCase.kt @@ -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() + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt index 03e4e31f2e..8cf684975e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCase.kt @@ -30,33 +30,47 @@ import org.matrix.android.sdk.flow.unwrap import javax.inject.Inject class GetNotificationsStatusUseCase @Inject constructor( - private val canTogglePushNotificationsViaPusherUseCase: CanTogglePushNotificationsViaPusherUseCase, - private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, + private val canToggleNotificationsViaPusherUseCase: CanToggleNotificationsViaPusherUseCase, + private val canToggleNotificationsViaAccountDataUseCase: CanToggleNotificationsViaAccountDataUseCase, ) { fun execute(session: Session, deviceId: String): Flow { - return when { - checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId) -> { - session.flow() - .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) - .unwrap() - .map { it.content.toModel()?.isSilenced?.not() } - .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } - .distinctUntilChanged() - } - else -> canTogglePushNotificationsViaPusherUseCase.execute(session) + return canToggleNotificationsViaAccountDataUseCase.execute(session, deviceId) + .flatMapLatest { canToggle -> + if (canToggle) { + notificationStatusFromAccountData(session, deviceId) + } else { + notificationStatusFromPusher(session, deviceId) + } + } + .distinctUntilChanged() + } + + private fun notificationStatusFromAccountData(session: Session, deviceId: String) = + session.flow() + .liveUserAccountData(UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId) + .unwrap() + .map { it.content.toModel()?.isSilenced?.not() } + .map { if (it == true) NotificationsStatus.ENABLED else NotificationsStatus.DISABLED } + + private fun notificationStatusFromPusher(session: Session, deviceId: String) = + canToggleNotificationsViaPusherUseCase.execute(session) .flatMapLatest { canToggle -> if (canToggle) { session.flow() .livePushers() .map { it.filter { pusher -> pusher.deviceId == deviceId } } .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() } else { flowOf(NotificationsStatus.NOT_SUPPORTED) } } - } - } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt new file mode 100644 index 0000000000..7306794f16 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCase.kt @@ -0,0 +1,33 @@ +/* + * 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.toContent +import javax.inject.Inject + +class SetNotificationSettingsAccountDataUseCase @Inject constructor() { + + suspend fun execute(session: Session, deviceId: String, localNotificationSettingsContent: LocalNotificationSettingsContent) { + session.accountDataService().updateUserAccountData( + UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId, + localNotificationSettingsContent.toContent(), + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCase.kt similarity index 61% rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCase.kt index 7969bbbe9b..77195ea950 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCase.kt @@ -18,32 +18,28 @@ package im.vector.app.features.settings.devices.v2.notification import im.vector.app.core.di.ActiveSessionHolder import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes -import org.matrix.android.sdk.api.session.events.model.toContent import javax.inject.Inject -class TogglePushNotificationUseCase @Inject constructor( +class ToggleNotificationsUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, - private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, - private val checkIfCanTogglePushNotificationsViaAccountDataUseCase: CheckIfCanTogglePushNotificationsViaAccountDataUseCase, + private val checkIfCanToggleNotificationsViaPusherUseCase: CheckIfCanToggleNotificationsViaPusherUseCase, + private val checkIfCanToggleNotificationsViaAccountDataUseCase: CheckIfCanToggleNotificationsViaAccountDataUseCase, + private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase, ) { suspend fun execute(deviceId: String, enabled: Boolean) { val session = activeSessionHolder.getSafeActiveSession() ?: return - if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) { + if (checkIfCanToggleNotificationsViaPusherUseCase.execute(session)) { val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } devicePusher?.let { pusher -> session.pushersService().togglePusher(pusher, enabled) } } - if (checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(session, deviceId)) { + if (checkIfCanToggleNotificationsViaAccountDataUseCase.execute(session, deviceId)) { val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled) - session.accountDataService().updateUserAccountData( - UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + deviceId, - newNotificationSettingsContent.toContent(), - ) + setNotificationSettingsAccountDataUseCase.execute(session, deviceId, newNotificationSettingsContent) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt new file mode 100644 index 0000000000..9296bcd912 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCase.kt @@ -0,0 +1,60 @@ +/* + * 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 im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.features.settings.VectorPreferences +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject + +/** + * Update the notification settings account data for the current session depending on whether + * the background sync is enabled or not. + */ +class UpdateNotificationSettingsAccountDataUseCase @Inject constructor( + private val vectorPreferences: VectorPreferences, + private val unifiedPushHelper: UnifiedPushHelper, + private val getNotificationSettingsAccountDataUseCase: GetNotificationSettingsAccountDataUseCase, + private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase, + private val deleteNotificationSettingsAccountDataUseCase: DeleteNotificationSettingsAccountDataUseCase, +) { + + suspend fun execute(session: Session) { + if (unifiedPushHelper.isBackgroundSync()) { + setCurrentNotificationStatus(session) + } else { + deleteCurrentNotificationStatus(session) + } + } + + private suspend fun setCurrentNotificationStatus(session: Session) { + val deviceId = session.sessionParams.deviceId ?: return + val areNotificationsSilenced = !vectorPreferences.areNotificationEnabledForDevice() + val isSilencedAccountData = getNotificationSettingsAccountDataUseCase.execute(session, deviceId)?.isSilenced + if (areNotificationsSilenced != isSilencedAccountData) { + val notificationSettingsContent = LocalNotificationSettingsContent( + isSilenced = areNotificationsSilenced + ) + setNotificationSettingsAccountDataUseCase.execute(session, deviceId, notificationSettingsContent) + } + } + + private suspend fun deleteCurrentNotificationStatus(session: Session) { + deleteNotificationSettingsAccountDataUseCase.execute(session) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index 472e0a4269..55866cb8c4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -31,8 +31,7 @@ import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase -import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase +import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationsUseCase 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.verification.CheckIfCurrentSessionCanBeVerifiedUseCase @@ -51,10 +50,9 @@ class SessionOverviewViewModel @AssistedInject constructor( private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase, private val checkIfCurrentSessionCanBeVerifiedUseCase: CheckIfCurrentSessionCanBeVerifiedUseCase, private val signoutSessionsUseCase: SignoutSessionsUseCase, - private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val pendingAuthHandler: PendingAuthHandler, private val activeSessionHolder: ActiveSessionHolder, - private val togglePushNotificationUseCase: TogglePushNotificationUseCase, + private val toggleNotificationsUseCase: ToggleNotificationsUseCase, private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase, refreshDevicesUseCase: RefreshDevicesUseCase, private val vectorPreferences: VectorPreferences, @@ -228,7 +226,7 @@ class SessionOverviewViewModel @AssistedInject constructor( private fun handleTogglePusherAction(action: SessionOverviewAction.TogglePushNotifications) { viewModelScope.launch { - togglePushNotificationUseCase.execute(action.deviceId, action.enabled) + toggleNotificationsUseCase.execute(action.deviceId, action.enabled) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt index 61c884f0bc..0c50a296f3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCase.kt @@ -16,28 +16,18 @@ package im.vector.app.features.settings.notifications -import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.PushersManager -import im.vector.app.core.pushers.UnifiedPushHelper -import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase -import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import javax.inject.Inject class DisableNotificationsForCurrentSessionUseCase @Inject constructor( - private val activeSessionHolder: ActiveSessionHolder, - private val unifiedPushHelper: UnifiedPushHelper, private val pushersManager: PushersManager, - private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, - private val togglePushNotificationUseCase: TogglePushNotificationUseCase, + private val toggleNotificationsForCurrentSessionUseCase: ToggleNotificationsForCurrentSessionUseCase, + private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, ) { suspend fun execute() { - val session = activeSessionHolder.getSafeActiveSession() ?: return - val deviceId = session.sessionParams.deviceId ?: return - if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) { - togglePushNotificationUseCase.execute(deviceId, enabled = false) - } else { - unifiedPushHelper.unregister(pushersManager) - } + toggleNotificationsForCurrentSessionUseCase.execute(enabled = false) + unregisterUnifiedPushUseCase.execute(pushersManager) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt index 180627a15f..daf3890e33 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCase.kt @@ -16,56 +16,38 @@ package im.vector.app.features.settings.notifications -import androidx.fragment.app.FragmentActivity -import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.pushers.FcmHelper +import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase import im.vector.app.core.pushers.PushersManager -import im.vector.app.core.pushers.UnifiedPushHelper -import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase -import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import im.vector.app.core.pushers.RegisterUnifiedPushUseCase import javax.inject.Inject -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine class EnableNotificationsForCurrentSessionUseCase @Inject constructor( - private val activeSessionHolder: ActiveSessionHolder, - private val unifiedPushHelper: UnifiedPushHelper, private val pushersManager: PushersManager, - private val fcmHelper: FcmHelper, - private val checkIfCanTogglePushNotificationsViaPusherUseCase: CheckIfCanTogglePushNotificationsViaPusherUseCase, - private val togglePushNotificationUseCase: TogglePushNotificationUseCase, + private val toggleNotificationsForCurrentSessionUseCase: ToggleNotificationsForCurrentSessionUseCase, + private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, + private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase, ) { - suspend fun execute(fragmentActivity: FragmentActivity) { + sealed interface EnableNotificationsResult { + object Success : EnableNotificationsResult + object NeedToAskUserForDistributor : EnableNotificationsResult + } + + suspend fun execute(distributor: String = ""): EnableNotificationsResult { val pusherForCurrentSession = pushersManager.getPusherForCurrentSession() if (pusherForCurrentSession == null) { - registerPusher(fragmentActivity) - } - - val session = activeSessionHolder.getSafeActiveSession() ?: return - if (checkIfCanTogglePushNotificationsViaPusherUseCase.execute(session)) { - val deviceId = session.sessionParams.deviceId ?: return - togglePushNotificationUseCase.execute(deviceId, enabled = true) - } - } - - private suspend fun registerPusher(fragmentActivity: FragmentActivity) { - suspendCoroutine { continuation -> - try { - unifiedPushHelper.register(fragmentActivity) { - if (unifiedPushHelper.isEmbeddedDistributor()) { - fcmHelper.ensureFcmTokenIsRetrieved( - fragmentActivity, - pushersManager, - registerPusher = true - ) - } - continuation.resume(Unit) + when (registerUnifiedPushUseCase.execute(distributor)) { + is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> { + return EnableNotificationsResult.NeedToAskUserForDistributor + } + RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> { + ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = true) } - } catch (error: Exception) { - continuation.resumeWithException(error) } } + + toggleNotificationsForCurrentSessionUseCase.execute(enabled = true) + + return EnableNotificationsResult.Success } } diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt new file mode 100644 index 0000000000..3dc73f0a31 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCase.kt @@ -0,0 +1,56 @@ +/* + * 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.notifications + +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase +import im.vector.app.features.settings.devices.v2.notification.DeleteNotificationSettingsAccountDataUseCase +import im.vector.app.features.settings.devices.v2.notification.SetNotificationSettingsAccountDataUseCase +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import timber.log.Timber +import javax.inject.Inject + +class ToggleNotificationsForCurrentSessionUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val unifiedPushHelper: UnifiedPushHelper, + private val checkIfCanToggleNotificationsViaPusherUseCase: CheckIfCanToggleNotificationsViaPusherUseCase, + private val setNotificationSettingsAccountDataUseCase: SetNotificationSettingsAccountDataUseCase, + private val deleteNotificationSettingsAccountDataUseCase: DeleteNotificationSettingsAccountDataUseCase, +) { + + suspend fun execute(enabled: Boolean) { + val session = activeSessionHolder.getSafeActiveSession() ?: return + val deviceId = session.sessionParams.deviceId ?: return + + if (unifiedPushHelper.isBackgroundSync()) { + Timber.d("background sync is enabled, setting account data event") + val newNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = !enabled) + setNotificationSettingsAccountDataUseCase.execute(session, deviceId, newNotificationSettingsContent) + } else { + Timber.d("push notif is enabled, deleting any account data and updating pusher") + deleteNotificationSettingsAccountDataUseCase.execute(session) + + if (checkIfCanToggleNotificationsViaPusherUseCase.execute(session)) { + val devicePusher = session.pushersService().getPushers().firstOrNull { it.deviceId == deviceId } + devicePusher?.let { pusher -> + session.pushersService().togglePusher(pusher, enabled) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt index a2cfe2cf98..a18fe030f2 100644 --- a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceFragment.kt @@ -24,6 +24,7 @@ import android.net.Uri import android.os.Bundle import android.os.Handler import android.os.Looper +import android.view.View import android.widget.Toast import androidx.lifecycle.LiveData import androidx.lifecycle.distinctUntilChanged @@ -31,6 +32,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.map import androidx.preference.Preference import androidx.preference.SwitchPreference +import com.airbnb.mvrx.fragmentViewModel import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder @@ -39,6 +41,7 @@ import im.vector.app.core.preference.VectorEditTextPreference import im.vector.app.core.preference.VectorPreference import im.vector.app.core.preference.VectorPreferenceCategory import im.vector.app.core.preference.VectorSwitchPreference +import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase import im.vector.app.core.pushers.FcmHelper import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.UnifiedPushHelper @@ -82,14 +85,15 @@ class VectorSettingsNotificationPreferenceFragment : @Inject lateinit var guardServiceStarter: GuardServiceStarter @Inject lateinit var vectorFeatures: VectorFeatures @Inject lateinit var notificationPermissionManager: NotificationPermissionManager - @Inject lateinit var disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase - @Inject lateinit var enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase + @Inject lateinit var ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase override var titleRes: Int = R.string.settings_notifications override val preferenceXmlRes = R.xml.vector_settings_notifications private var interactionListener: VectorSettingsFragmentInteractionListener? = null + private val viewModel: VectorSettingsNotificationPreferenceViewModel by fragmentViewModel() + private val notificationStartForActivityResult = registerStartForActivityResult { _ -> // No op } @@ -106,6 +110,22 @@ class VectorSettingsNotificationPreferenceFragment : analyticsScreenName = MobileScreen.ScreenName.SettingsNotifications } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + observeViewEvents() + } + + private fun observeViewEvents() { + viewModel.observeViewEvents { + when (it) { + VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled -> onNotificationsForDeviceEnabled() + VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled -> onNotificationsForDeviceDisabled() + is VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor -> askUserToSelectPushDistributor() + VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged -> onNotificationMethodChanged() + } + } + } + override fun bindPref() { findPreference(VectorPreferences.SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY)!!.let { pref -> val pushRuleService = session.pushRuleService() @@ -123,23 +143,15 @@ class VectorSettingsNotificationPreferenceFragment : } findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) - ?.setTransactionalSwitchChangeListener(lifecycleScope) { isChecked -> - if (isChecked) { - enableNotificationsForCurrentSessionUseCase.execute(requireActivity()) - - findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY) - ?.summary = unifiedPushHelper.getCurrentDistributorName() - - notificationPermissionManager.eventuallyRequestPermission( - requireActivity(), - postPermissionLauncher, - showRationale = false, - ignorePreference = true - ) + ?.setOnPreferenceChangeListener { _, isChecked -> + val action = if (isChecked as Boolean) { + VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(pushDistributor = "") } else { - disableNotificationsForCurrentSessionUseCase.execute() - notificationPermissionManager.eventuallyRevokePermission(requireActivity()) + VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice } + viewModel.handle(action) + // preference will be updated on ViewEvent reception + false } // SC addition @@ -200,18 +212,7 @@ class VectorSettingsNotificationPreferenceFragment : if (vectorFeatures.allowExternalUnifiedPushDistributors()) { it.summary = unifiedPushHelper.getCurrentDistributorName() it.onPreferenceClickListener = Preference.OnPreferenceClickListener { - unifiedPushHelper.forceRegister(requireActivity(), pushersManager) { - if (unifiedPushHelper.isEmbeddedDistributor()) { - fcmHelper.ensureFcmTokenIsRetrieved( - requireActivity(), - pushersManager, - vectorPreferences.areNotificationEnabledForDevice() - ) - } - it.summary = unifiedPushHelper.getCurrentDistributorName() - session.pushersService().refreshPushers() - refreshBackgroundSyncPrefs() - } + askUserToSelectPushDistributor(withUnregister = true) true } } else { @@ -225,6 +226,42 @@ class VectorSettingsNotificationPreferenceFragment : handleSystemPreference() } + private fun onNotificationsForDeviceEnabled() { + findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) + ?.isChecked = true + findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY) + ?.summary = unifiedPushHelper.getCurrentDistributorName() + + notificationPermissionManager.eventuallyRequestPermission( + requireActivity(), + postPermissionLauncher, + showRationale = false, + ignorePreference = true + ) + } + + private fun onNotificationsForDeviceDisabled() { + findPreference(VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) + ?.isChecked = false + notificationPermissionManager.eventuallyRevokePermission(requireActivity()) + } + + private fun askUserToSelectPushDistributor(withUnregister: Boolean = false) { + unifiedPushHelper.showSelectDistributorDialog(requireContext()) { selection -> + if (withUnregister) { + viewModel.handle(VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(selection)) + } else { + viewModel.handle(VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(selection)) + } + } + } + + private fun onNotificationMethodChanged() { + findPreference(VectorPreferences.SETTINGS_NOTIFICATION_METHOD_KEY)?.summary = unifiedPushHelper.getCurrentDistributorName() + session.pushersService().refreshPushers() + refreshBackgroundSyncPrefs() + } + private fun bindEmailNotifications() { val initialEmails = session.getEmailsWithPushInformation() bindEmailNotificationCategory(initialEmails) diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewAction.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewAction.kt new file mode 100644 index 0000000000..949dc99993 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewAction.kt @@ -0,0 +1,25 @@ +/* + * 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.notifications + +import im.vector.app.core.platform.VectorViewModelAction + +sealed interface VectorSettingsNotificationPreferenceViewAction : VectorViewModelAction { + data class EnableNotificationsForDevice(val pushDistributor: String) : VectorSettingsNotificationPreferenceViewAction + object DisableNotificationsForDevice : VectorSettingsNotificationPreferenceViewAction + data class RegisterPushDistributor(val pushDistributor: String) : VectorSettingsNotificationPreferenceViewAction +} diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt new file mode 100644 index 0000000000..b0ee107769 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewEvent.kt @@ -0,0 +1,26 @@ +/* + * 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.notifications + +import im.vector.app.core.platform.VectorViewEvents + +sealed interface VectorSettingsNotificationPreferenceViewEvent : VectorViewEvents { + object NotificationsForDeviceEnabled : VectorSettingsNotificationPreferenceViewEvent + object NotificationsForDeviceDisabled : VectorSettingsNotificationPreferenceViewEvent + object AskUserForPushDistributor : VectorSettingsNotificationPreferenceViewEvent + object NotificationMethodChanged : VectorSettingsNotificationPreferenceViewEvent +} diff --git a/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt new file mode 100644 index 0000000000..9530be599e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModel.kt @@ -0,0 +1,124 @@ +/* + * 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.notifications + +import android.content.SharedPreferences +import androidx.annotation.VisibleForTesting +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.VectorDummyViewState +import im.vector.app.core.platform.VectorViewModel +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.settings.VectorPreferences +import kotlinx.coroutines.launch + +class VectorSettingsNotificationPreferenceViewModel @AssistedInject constructor( + @Assisted initialState: VectorDummyViewState, + private val pushersManager: PushersManager, + private val vectorPreferences: VectorPreferences, + private val enableNotificationsForCurrentSessionUseCase: EnableNotificationsForCurrentSessionUseCase, + private val disableNotificationsForCurrentSessionUseCase: DisableNotificationsForCurrentSessionUseCase, + private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, + private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, + private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase, + private val toggleNotificationsForCurrentSessionUseCase: ToggleNotificationsForCurrentSessionUseCase, +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: VectorDummyViewState): VectorSettingsNotificationPreferenceViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + @VisibleForTesting + val notificationsPreferenceListener: SharedPreferences.OnSharedPreferenceChangeListener = + SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (key == VectorPreferences.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) { + if (vectorPreferences.areNotificationEnabledForDevice()) { + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled) + } else { + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled) + } + } + } + + init { + observeNotificationsEnabledPreference() + } + + private fun observeNotificationsEnabledPreference() { + vectorPreferences.subscribeToChanges(notificationsPreferenceListener) + } + + override fun onCleared() { + vectorPreferences.unsubscribeToChanges(notificationsPreferenceListener) + super.onCleared() + } + + override fun handle(action: VectorSettingsNotificationPreferenceViewAction) { + when (action) { + VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice -> handleDisableNotificationsForDevice() + is VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice -> handleEnableNotificationsForDevice(action.pushDistributor) + is VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor -> handleRegisterPushDistributor(action.pushDistributor) + } + } + + private fun handleDisableNotificationsForDevice() { + viewModelScope.launch { + disableNotificationsForCurrentSessionUseCase.execute() + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled) + } + } + + private fun handleEnableNotificationsForDevice(distributor: String) { + viewModelScope.launch { + when (enableNotificationsForCurrentSessionUseCase.execute(distributor)) { + is EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor -> { + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor) + } + EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Success -> { + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled) + } + } + } + } + + private fun handleRegisterPushDistributor(distributor: String) { + viewModelScope.launch { + unregisterUnifiedPushUseCase.execute(pushersManager) + when (registerUnifiedPushUseCase.execute(distributor)) { + RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> { + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor) + } + RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> { + val areNotificationsEnabled = vectorPreferences.areNotificationEnabledForDevice() + ensureFcmTokenIsRetrievedUseCase.execute(pushersManager, registerPusher = areNotificationsEnabled) + toggleNotificationsForCurrentSessionUseCase.execute(enabled = areNotificationsEnabled) + _viewEvents.post(VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt index 3bbff0f2fe..b355b55903 100644 --- a/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt +++ b/vector/src/main/java/im/vector/app/features/settings/troubleshoot/TestEndpointAsTokenRegistration.kt @@ -17,14 +17,17 @@ package im.vector.app.features.settings.troubleshoot import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.Observer import androidx.work.WorkInfo import androidx.work.WorkManager import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.pushers.PushersManager +import im.vector.app.core.pushers.RegisterUnifiedPushUseCase import im.vector.app.core.pushers.UnifiedPushHelper +import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.core.resources.StringProvider +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.pushers.PusherState import javax.inject.Inject @@ -34,6 +37,8 @@ class TestEndpointAsTokenRegistration @Inject constructor( private val pushersManager: PushersManager, private val activeSessionHolder: ActiveSessionHolder, private val unifiedPushHelper: UnifiedPushHelper, + private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, + private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, ) : TroubleshootTest(R.string.settings_troubleshoot_test_endpoint_registration_title) { override fun perform(testParameters: TestParameters) { @@ -56,27 +61,52 @@ class TestEndpointAsTokenRegistration @Inject constructor( ) quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_endpoint_registration_quick_fix) { override fun doFix() { - unifiedPushHelper.forceRegister( - context, - pushersManager - ) - val workId = pushersManager.enqueueRegisterPusherWithFcmKey(endpoint) - WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context, Observer { workInfo -> - if (workInfo != null) { - if (workInfo.state == WorkInfo.State.SUCCEEDED) { - manager?.retry(testParameters) - } else if (workInfo.state == WorkInfo.State.FAILED) { - manager?.retry(testParameters) - } - } - }) + unregisterThenRegister(testParameters, endpoint) } } - status = TestStatus.FAILED } else { description = stringProvider.getString(R.string.settings_troubleshoot_test_endpoint_registration_success) status = TestStatus.SUCCESS } } + + private fun unregisterThenRegister(testParameters: TestParameters, pushKey: String) { + activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch { + unregisterUnifiedPushUseCase.execute(pushersManager) + registerUnifiedPush(distributor = "", testParameters, pushKey) + } + } + + private fun registerUnifiedPush( + distributor: String, + testParameters: TestParameters, + pushKey: String, + ) { + when (registerUnifiedPushUseCase.execute(distributor)) { + is RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor -> + askUserForDistributor(testParameters, pushKey) + RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success -> { + val workId = pushersManager.enqueueRegisterPusherWithFcmKey(pushKey) + WorkManager.getInstance(context).getWorkInfoByIdLiveData(workId).observe(context) { workInfo -> + if (workInfo != null) { + if (workInfo.state == WorkInfo.State.SUCCEEDED) { + manager?.retry(testParameters) + } else if (workInfo.state == WorkInfo.State.FAILED) { + manager?.retry(testParameters) + } + } + } + } + } + } + + private fun askUserForDistributor( + testParameters: TestParameters, + pushKey: String, + ) { + unifiedPushHelper.showSelectDistributorDialog(context) { selection -> + registerUnifiedPush(distributor = selection, testParameters, pushKey) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt b/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt index e8e0eac863..a14967a931 100644 --- a/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt +++ b/vector/src/main/java/im/vector/app/features/start/StartAppAndroidService.kt @@ -20,6 +20,7 @@ import android.content.Intent import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.NamedGlobalScope +import im.vector.app.core.extensions.startForegroundCompat import im.vector.app.core.services.VectorAndroidService import im.vector.app.features.notifications.NotificationUtils import kotlinx.coroutines.CoroutineScope @@ -58,6 +59,6 @@ class StartAppAndroidService : VectorAndroidService() { private fun showStickyNotification() { val notificationId = Random.nextInt() val notification = notificationUtils.buildStartAppNotification() - startForeground(notificationId, notification) + startForegroundCompat(notificationId, notification) } } diff --git a/vector/src/main/res/layout/fragment_settings_devices.xml b/vector/src/main/res/layout/fragment_settings_devices.xml index 8134774887..731049f3a2 100644 --- a/vector/src/main/res/layout/fragment_settings_devices.xml +++ b/vector/src/main/res/layout/fragment_settings_devices.xml @@ -67,6 +67,7 @@ app:layout_constraintTop_toBottomOf="@id/deviceListSecurityRecommendationsDivider" app:sessionsListHeaderDescription="" app:sessionsListHeaderHasLearnMoreLink="false" + app:sessionsListHeaderMenu="@menu/menu_current_session_header" app:sessionsListHeaderTitle="@string/device_manager_current_session_title" /> + + + + + diff --git a/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt b/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt new file mode 100644 index 0000000000..0920ee4716 --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/notification/NotificationsSettingUpdaterTest.kt @@ -0,0 +1,65 @@ +/* + * 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.notification + +import im.vector.app.features.session.coroutineScope +import im.vector.app.test.fakes.FakeSession +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +class NotificationsSettingUpdaterTest { + + private val fakeUpdateEnableNotificationsSettingOnChangeUseCase = mockk() + + private val notificationsSettingUpdater = NotificationsSettingUpdater( + updateEnableNotificationsSettingOnChangeUseCase = fakeUpdateEnableNotificationsSettingOnChangeUseCase, + ) + + @Before + fun setup() { + mockkStatic("im.vector.app.features.session.SessionCoroutineScopesKt") + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given a session when calling onSessionStarted then update enable notification on change`() = runTest { + // Given + val aSession = FakeSession() + every { aSession.coroutineScope } returns this + coJustRun { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(any()) } + + // When + notificationsSettingUpdater.onSessionStarted(aSession) + advanceUntilIdle() + + // Then + coVerify { fakeUpdateEnableNotificationsSettingOnChangeUseCase.execute(aSession) } + } +} diff --git a/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt new file mode 100644 index 0000000000..fca49adc9b --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/pushers/EnsureFcmTokenIsRetrievedUseCaseTest.kt @@ -0,0 +1,105 @@ +/* + * 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.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeFcmHelper +import im.vector.app.test.fakes.FakePushersManager +import im.vector.app.test.fakes.FakeUnifiedPushHelper +import im.vector.app.test.fixtures.PusherFixture +import org.junit.Test + +class EnsureFcmTokenIsRetrievedUseCaseTest { + + private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() + private val fakeFcmHelper = FakeFcmHelper() + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val ensureFcmTokenIsRetrievedUseCase = EnsureFcmTokenIsRetrievedUseCase( + unifiedPushHelper = fakeUnifiedPushHelper.instance, + fcmHelper = fakeFcmHelper.instance, + activeSessionHolder = fakeActiveSessionHolder.instance, + ) + + @Test + fun `given no registered pusher and distributor as embedded when execute then ensure the FCM token is retrieved with register pusher option`() { + // Given + val aPushersManager = FakePushersManager() + fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true) + fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(aPushersManager.instance) + val aSessionId = "aSessionId" + fakeActiveSessionHolder.fakeSession.givenSessionId(aSessionId) + val expectedPusher = PusherFixture.aPusher(deviceId = "") + fakeActiveSessionHolder.fakeSession.fakePushersService.givenGetPushers(listOf(expectedPusher)) + + // When + ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = true) + + // Then + fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = true) + } + + @Test + fun `given a registered pusher and distributor as embedded when execute then ensure the FCM token is retrieved without register pusher option`() { + // Given + val aPushersManager = FakePushersManager() + fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true) + fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(aPushersManager.instance) + val aSessionId = "aSessionId" + fakeActiveSessionHolder.fakeSession.givenSessionId(aSessionId) + val expectedPusher = PusherFixture.aPusher(deviceId = aSessionId) + fakeActiveSessionHolder.fakeSession.fakePushersService.givenGetPushers(listOf(expectedPusher)) + + // When + ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = true) + + // Then + fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = false) + } + + @Test + fun `given no registering asked and distributor as embedded when execute then ensure the FCM token is retrieved without register pusher option`() { + // Given + val aPushersManager = FakePushersManager() + fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true) + fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(aPushersManager.instance) + val aSessionId = "aSessionId" + fakeActiveSessionHolder.fakeSession.givenSessionId(aSessionId) + val expectedPusher = PusherFixture.aPusher(deviceId = aSessionId) + fakeActiveSessionHolder.fakeSession.fakePushersService.givenGetPushers(listOf(expectedPusher)) + + // When + ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = false) + + // Then + fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = false) + } + + @Test + fun `given distributor as not embedded when execute then nothing is done`() { + // Given + val aPushersManager = FakePushersManager() + fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(false) + + // When + ensureFcmTokenIsRetrievedUseCase.execute(aPushersManager.instance, registerPusher = true) + + // Then + fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = true, inverse = true) + fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(aPushersManager.instance, registerPusher = false, inverse = true) + } +} diff --git a/vector/src/test/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCaseTest.kt new file mode 100644 index 0000000000..c72c519172 --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/pushers/RegisterUnifiedPushUseCaseTest.kt @@ -0,0 +1,158 @@ +/* + * 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.test.fakes.FakeContext +import im.vector.app.test.fakes.FakeVectorFeatures +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import io.mockk.verifyAll +import io.mockk.verifyOrder +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.unifiedpush.android.connector.UnifiedPush + +class RegisterUnifiedPushUseCaseTest { + + private val fakeContext = FakeContext() + private val fakeVectorFeatures = FakeVectorFeatures() + + private val registerUnifiedPushUseCase = RegisterUnifiedPushUseCase( + context = fakeContext.instance, + vectorFeatures = fakeVectorFeatures, + ) + + @Before + fun setup() { + mockkStatic(UnifiedPush::class) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given non empty distributor when execute then distributor is saved and app is registered`() = runTest { + // Given + val aDistributor = "distributor" + justRun { UnifiedPush.registerApp(any()) } + justRun { UnifiedPush.saveDistributor(any(), any()) } + + // When + val result = registerUnifiedPushUseCase.execute(aDistributor) + + // Then + result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success + verifyOrder { + UnifiedPush.saveDistributor(fakeContext.instance, aDistributor) + UnifiedPush.registerApp(fakeContext.instance) + } + } + + @Test + fun `given external distributors are not allowed when execute then internal distributor is saved and app is registered`() = runTest { + // Given + val aPackageName = "packageName" + fakeContext.givenPackageName(aPackageName) + justRun { UnifiedPush.registerApp(any()) } + justRun { UnifiedPush.saveDistributor(any(), any()) } + fakeVectorFeatures.givenExternalDistributorsAreAllowed(false) + + // When + val result = registerUnifiedPushUseCase.execute() + + // Then + result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success + verifyOrder { + UnifiedPush.saveDistributor(fakeContext.instance, aPackageName) + UnifiedPush.registerApp(fakeContext.instance) + } + } + + @Test + fun `given a saved distributor and external distributors are allowed when execute then app is registered`() = runTest { + // Given + justRun { UnifiedPush.registerApp(any()) } + val aDistributor = "distributor" + every { UnifiedPush.getDistributor(any()) } returns aDistributor + fakeVectorFeatures.givenExternalDistributorsAreAllowed(true) + + // When + val result = registerUnifiedPushUseCase.execute() + + // Then + result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success + verifyAll { + UnifiedPush.getDistributor(fakeContext.instance) + UnifiedPush.registerApp(fakeContext.instance) + } + } + + @Test + fun `given no saved distributor and a unique distributor available when execute then the distributor is saved and app is registered`() = runTest { + // Given + justRun { UnifiedPush.registerApp(any()) } + justRun { UnifiedPush.saveDistributor(any(), any()) } + every { UnifiedPush.getDistributor(any()) } returns "" + fakeVectorFeatures.givenExternalDistributorsAreAllowed(true) + val aDistributor = "distributor" + every { UnifiedPush.getDistributors(any()) } returns listOf(aDistributor) + + // When + val result = registerUnifiedPushUseCase.execute() + + // Then + result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success + verifyOrder { + UnifiedPush.getDistributor(fakeContext.instance) + UnifiedPush.getDistributors(fakeContext.instance) + UnifiedPush.saveDistributor(fakeContext.instance, aDistributor) + UnifiedPush.registerApp(fakeContext.instance) + } + } + + @Test + fun `given no saved distributor and multiple distributors available when execute then result is to ask user`() = runTest { + // Given + every { UnifiedPush.getDistributor(any()) } returns "" + fakeVectorFeatures.givenExternalDistributorsAreAllowed(true) + val aDistributor1 = "distributor1" + val aDistributor2 = "distributor2" + every { UnifiedPush.getDistributors(any()) } returns listOf(aDistributor1, aDistributor2) + + // When + val result = registerUnifiedPushUseCase.execute() + + // Then + result shouldBe RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor + verifyOrder { + UnifiedPush.getDistributor(fakeContext.instance) + UnifiedPush.getDistributors(fakeContext.instance) + } + verify(inverse = true) { + UnifiedPush.saveDistributor(any(), any()) + UnifiedPush.registerApp(any()) + } + } +} diff --git a/vector/src/test/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCaseTest.kt new file mode 100644 index 0000000000..bee545b3e1 --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/pushers/UnregisterUnifiedPushUseCaseTest.kt @@ -0,0 +1,83 @@ +/* + * 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.features.settings.BackgroundSyncMode +import im.vector.app.test.fakes.FakeContext +import im.vector.app.test.fakes.FakePushersManager +import im.vector.app.test.fakes.FakeUnifiedPushHelper +import im.vector.app.test.fakes.FakeUnifiedPushStore +import im.vector.app.test.fakes.FakeVectorPreferences +import io.mockk.justRun +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verifyAll +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.unifiedpush.android.connector.UnifiedPush + +class UnregisterUnifiedPushUseCaseTest { + + private val fakeContext = FakeContext() + private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeUnifiedPushStore = FakeUnifiedPushStore() + private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() + + private val unregisterUnifiedPushUseCase = UnregisterUnifiedPushUseCase( + context = fakeContext.instance, + vectorPreferences = fakeVectorPreferences.instance, + unifiedPushStore = fakeUnifiedPushStore.instance, + unifiedPushHelper = fakeUnifiedPushHelper.instance, + ) + + @Before + fun setup() { + mockkStatic(UnifiedPush::class) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given pushersManager when execute then unregister and clean everything which is needed`() = runTest { + // Given + val aEndpoint = "endpoint" + fakeUnifiedPushHelper.givenGetEndpointOrTokenReturns(aEndpoint) + val aPushersManager = FakePushersManager() + aPushersManager.givenUnregisterPusher(aEndpoint) + justRun { UnifiedPush.unregisterApp(any()) } + fakeVectorPreferences.givenSetFdroidSyncBackgroundMode(BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME) + fakeUnifiedPushStore.givenStorePushGateway(null) + fakeUnifiedPushStore.givenStoreUpEndpoint(null) + + // When + unregisterUnifiedPushUseCase.execute(aPushersManager.instance) + + // Then + fakeVectorPreferences.verifySetFdroidSyncBackgroundMode(BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME) + aPushersManager.verifyUnregisterPusher(aEndpoint) + verifyAll { + UnifiedPush.unregisterApp(fakeContext.instance) + } + fakeUnifiedPushStore.verifyStorePushGateway(null) + fakeUnifiedPushStore.verifyStoreUpEndpoint(null) + } +} diff --git a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt index 01596e796d..3fb128c759 100644 --- a/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/core/session/ConfigureAndStartSessionUseCaseTest.kt @@ -19,9 +19,10 @@ package im.vector.app.core.session import im.vector.app.core.extensions.startSyncing import im.vector.app.core.session.clientinfo.UpdateMatrixClientInfoUseCase import im.vector.app.features.session.coroutineScope +import im.vector.app.features.settings.devices.v2.notification.UpdateNotificationSettingsAccountDataUseCase import im.vector.app.features.sync.SyncUtils import im.vector.app.test.fakes.FakeContext -import im.vector.app.test.fakes.FakeEnableNotificationsSettingUpdater +import im.vector.app.test.fakes.FakeNotificationsSettingUpdater import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeVectorPreferences import im.vector.app.test.fakes.FakeWebRtcCallManager @@ -46,14 +47,16 @@ class ConfigureAndStartSessionUseCaseTest { private val fakeWebRtcCallManager = FakeWebRtcCallManager() private val fakeUpdateMatrixClientInfoUseCase = mockk() private val fakeVectorPreferences = FakeVectorPreferences() - private val fakeEnableNotificationsSettingUpdater = FakeEnableNotificationsSettingUpdater() + private val fakeNotificationsSettingUpdater = FakeNotificationsSettingUpdater() + private val fakeUpdateNotificationSettingsAccountDataUseCase = mockk() private val configureAndStartSessionUseCase = ConfigureAndStartSessionUseCase( context = fakeContext.instance, webRtcCallManager = fakeWebRtcCallManager.instance, updateMatrixClientInfoUseCase = fakeUpdateMatrixClientInfoUseCase, vectorPreferences = fakeVectorPreferences.instance, - enableNotificationsSettingUpdater = fakeEnableNotificationsSettingUpdater.instance, + notificationsSettingUpdater = fakeNotificationsSettingUpdater.instance, + updateNotificationSettingsAccountDataUseCase = fakeUpdateNotificationSettingsAccountDataUseCase, ) @Before @@ -70,67 +73,80 @@ class ConfigureAndStartSessionUseCaseTest { @Test fun `given start sync needed and client info recording enabled when execute then it should be configured properly`() = runTest { // Given - val fakeSession = givenASession() - every { fakeSession.coroutineScope } returns this + val aSession = givenASession() + every { aSession.coroutineScope } returns this fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } + coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true) - fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession) + fakeNotificationsSettingUpdater.givenOnSessionsStarted(aSession) // When - configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true) + configureAndStartSessionUseCase.execute(aSession, startSyncing = true) advanceUntilIdle() // Then - verify { fakeSession.startSyncing(fakeContext.instance) } - fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder()) - fakeSession.fakePushersService.verifyRefreshPushers() + verify { aSession.startSyncing(fakeContext.instance) } + aSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder()) + aSession.fakePushersService.verifyRefreshPushers() fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded() - coVerify { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) } + coVerify { + fakeUpdateMatrixClientInfoUseCase.execute(aSession) + fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession) + } } @Test fun `given start sync needed and client info recording disabled when execute then it should be configured properly`() = runTest { // Given - val fakeSession = givenASession() - every { fakeSession.coroutineScope } returns this + val aSession = givenASession() + every { aSession.coroutineScope } returns this fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() - coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } + coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = false) - fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession) + fakeNotificationsSettingUpdater.givenOnSessionsStarted(aSession) // When - configureAndStartSessionUseCase.execute(fakeSession, startSyncing = true) + configureAndStartSessionUseCase.execute(aSession, startSyncing = true) advanceUntilIdle() // Then - verify { fakeSession.startSyncing(fakeContext.instance) } - fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder()) - fakeSession.fakePushersService.verifyRefreshPushers() + verify { aSession.startSyncing(fakeContext.instance) } + aSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder()) + aSession.fakePushersService.verifyRefreshPushers() fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded() - coVerify(inverse = true) { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) } + coVerify(inverse = true) { + fakeUpdateMatrixClientInfoUseCase.execute(aSession) + } + coVerify { + fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession) + } } @Test fun `given a session and no start sync needed when execute then it should be configured properly`() = runTest { // Given - val fakeSession = givenASession() - every { fakeSession.coroutineScope } returns this + val aSession = givenASession() + every { aSession.coroutineScope } returns this fakeWebRtcCallManager.givenCheckForProtocolsSupportIfNeededSucceeds() coJustRun { fakeUpdateMatrixClientInfoUseCase.execute(any()) } + coJustRun { fakeUpdateNotificationSettingsAccountDataUseCase.execute(any()) } fakeVectorPreferences.givenIsClientInfoRecordingEnabled(isEnabled = true) - fakeEnableNotificationsSettingUpdater.givenOnSessionsStarted(fakeSession) + fakeNotificationsSettingUpdater.givenOnSessionsStarted(aSession) // When - configureAndStartSessionUseCase.execute(fakeSession, startSyncing = false) + configureAndStartSessionUseCase.execute(aSession, startSyncing = false) advanceUntilIdle() // Then - verify(inverse = true) { fakeSession.startSyncing(fakeContext.instance) } - fakeSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder()) - fakeSession.fakePushersService.verifyRefreshPushers() + verify(inverse = true) { aSession.startSyncing(fakeContext.instance) } + aSession.fakeFilterService.verifySetSyncFilter(SyncUtils.getSyncFilterBuilder()) + aSession.fakePushersService.verifyRefreshPushers() fakeWebRtcCallManager.verifyCheckForProtocolsSupportIfNeeded() - coVerify { fakeUpdateMatrixClientInfoUseCase.execute(fakeSession) } + coVerify { + fakeUpdateMatrixClientInfoUseCase.execute(aSession) + fakeUpdateNotificationSettingsAccountDataUseCase.execute(aSession) + } } private fun givenASession(): FakeSession { diff --git a/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt new file mode 100644 index 0000000000..5d08499e32 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt @@ -0,0 +1,75 @@ +/* + * 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 + +import im.vector.app.config.Config +import im.vector.app.test.fakes.FakeClock +import im.vector.app.test.fakes.FakeVectorFeatures +import im.vector.app.test.fakes.FakeVectorPreferences +import org.amshove.kluent.shouldBe +import org.junit.Test + +private val AN_EPOCH = Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS.toLong() +private const val A_DEVICE_ID = "A_DEVICE_ID" + +class ShouldShowUnverifiedSessionsAlertUseCaseTest { + + private val fakeVectorFeatures = FakeVectorFeatures() + private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeClock = FakeClock() + + private val shouldShowUnverifiedSessionsAlertUseCase = ShouldShowUnverifiedSessionsAlertUseCase( + vectorFeatures = fakeVectorFeatures, + vectorPreferences = fakeVectorPreferences.instance, + clock = fakeClock, + ) + + @Test + fun `given the feature is disabled then the use case returns false`() { + fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(false) + fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(0L) + + shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe false + } + + @Test + fun `given the feature in enabled and there is not a saved preference then the use case returns true`() { + fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(true) + fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(0L) + fakeClock.givenEpoch(AN_EPOCH + 1) + + shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe true + } + + @Test + fun `given the feature in enabled and last shown is a long time ago then the use case returns true`() { + fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(true) + fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(AN_EPOCH) + fakeClock.givenEpoch(AN_EPOCH * 2 + 1) + + shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe true + } + + @Test + fun `given the feature in enabled and last shown is not a long time ago then the use case returns false`() { + fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(true) + fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(AN_EPOCH) + fakeClock.givenEpoch(AN_EPOCH + 1) + + shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe false + } +} diff --git a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt index 718f1ec7a9..92083eb50b 100644 --- a/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/onboarding/OnboardingViewModelTest.kt @@ -160,6 +160,28 @@ class OnboardingViewModelTest { .finish() } + @Test + fun `given combined login enabled, when handling sign in splash action, then emits OpenCombinedLogin with default homeserver qrCode supported`() = runTest { + val test = viewModel.test() + fakeVectorFeatures.givenCombinedLoginEnabled() + givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE, canLoginWithQrCode = true) + + viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(OnboardingFlow.SignIn)) + + test + .assertStatesChanges( + initialState, + { copy(onboardingFlow = OnboardingFlow.SignIn) }, + { copy(isLoading = true) }, + { copy(canLoginWithQrCode = true) }, + { copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE) }, + { copy(signMode = SignMode.SignIn) }, + { copy(isLoading = false) } + ) + .assertEvents(OnboardingViewEvents.OpenCombinedLogin) + .finish() + } + @Test fun `given can successfully login in with token, when logging in with token, then emits AccountSignedIn`() = runTest { val test = viewModel.test() @@ -1152,11 +1174,13 @@ class OnboardingViewModelTest { resultingState: SelectedHomeserverState, config: HomeServerConnectionConfig = A_HOMESERVER_CONFIG, fingerprint: Fingerprint? = null, + canLoginWithQrCode: Boolean = false, ) { fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint, config) fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState)) givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.StartRegistration) fakeHomeServerHistoryService.expectUrlToBeAdded(config.homeServerUri.toString()) + fakeAuthenticationService.givenIsQrLoginSupported(config, canLoginWithQrCode) } private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) { @@ -1164,6 +1188,7 @@ class OnboardingViewModelTest { fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState)) givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error)) fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString()) + fakeAuthenticationService.givenIsQrLoginSupported(A_HOMESERVER_CONFIG, false) } private fun givenUserNameIsAvailable(userName: String) { diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 03177aac47..4bfd5c4496 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -22,7 +22,6 @@ import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.core.session.clientinfo.MatrixClientInfoContent import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo import im.vector.app.features.settings.devices.v2.list.DeviceType -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase @@ -72,7 +71,6 @@ class DevicesViewModelTest { private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk(relaxed = true) private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() - private val fakeInterceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() private val fakeRefreshDevicesUseCase = mockk(relaxUnitFun = true) private val fakeVectorPreferences = FakeVectorPreferences() @@ -87,7 +85,6 @@ class DevicesViewModelTest { refreshDevicesOnCryptoDevicesChangeUseCase = refreshDevicesOnCryptoDevicesChangeUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, - interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCase, vectorPreferences = fakeVectorPreferences.instance, diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt new file mode 100644 index 0000000000..b85acb1e69 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaAccountDataUseCaseTest.kt @@ -0,0 +1,88 @@ +/* + * 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 im.vector.app.test.fakes.FakeSession +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent + +class CanToggleNotificationsViaAccountDataUseCaseTest { + + private val fakeGetNotificationSettingsAccountDataUpdatesUseCase = mockk() + + private val canToggleNotificationsViaAccountDataUseCase = CanToggleNotificationsViaAccountDataUseCase( + getNotificationSettingsAccountDataUpdatesUseCase = fakeGetNotificationSettingsAccountDataUpdatesUseCase, + ) + + @Test + fun `given current session and content for account data when execute then true is returned`() = runTest { + // Given + val aSession = FakeSession() + val aDeviceId = "aDeviceId" + val localNotificationSettingsContent = LocalNotificationSettingsContent( + isSilenced = true, + ) + every { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(any(), any()) } returns flowOf(localNotificationSettingsContent) + + // When + val result = canToggleNotificationsViaAccountDataUseCase.execute(aSession, aDeviceId).firstOrNull() + + // Then + result shouldBe true + verify { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId) } + } + + @Test + fun `given current session and empty content for account data when execute then false is returned`() = runTest { + // Given + val aSession = FakeSession() + val aDeviceId = "aDeviceId" + val localNotificationSettingsContent = LocalNotificationSettingsContent( + isSilenced = null, + ) + every { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(any(), any()) } returns flowOf(localNotificationSettingsContent) + + // When + val result = canToggleNotificationsViaAccountDataUseCase.execute(aSession, aDeviceId).firstOrNull() + + // Then + result shouldBe false + verify { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId) } + } + + @Test + fun `given current session and no related account data when execute then false is returned`() = runTest { + // Given + val aSession = FakeSession() + val aDeviceId = "aDeviceId" + every { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(any(), any()) } returns flowOf(null) + + // When + val result = canToggleNotificationsViaAccountDataUseCase.execute(aSession, aDeviceId).firstOrNull() + + // Then + result shouldBe false + verify { fakeGetNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId) } + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCaseTest.kt similarity index 87% rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCaseTest.kt index 997fa827f5..3284adb32d 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanTogglePushNotificationsViaPusherUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CanToggleNotificationsViaPusherUseCaseTest.kt @@ -30,13 +30,13 @@ import org.junit.Test private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyTogglePushNotificationsOfDevices = true) -class CanTogglePushNotificationsViaPusherUseCaseTest { +class CanToggleNotificationsViaPusherUseCaseTest { private val fakeSession = FakeSession() private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions() - private val canTogglePushNotificationsViaPusherUseCase = - CanTogglePushNotificationsViaPusherUseCase() + private val canToggleNotificationsViaPusherUseCase = + CanToggleNotificationsViaPusherUseCase() @Before fun setUp() { @@ -57,7 +57,7 @@ class CanTogglePushNotificationsViaPusherUseCaseTest { .givenAsFlow() // When - val result = canTogglePushNotificationsViaPusherUseCase.execute(fakeSession).firstOrNull() + val result = canToggleNotificationsViaPusherUseCase.execute(fakeSession).firstOrNull() // Then result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt new file mode 100644 index 0000000000..f97e326a02 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaAccountDataUseCaseTest.kt @@ -0,0 +1,76 @@ +/* + * 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 im.vector.app.test.fakes.FakeSession +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent + +private const val A_DEVICE_ID = "device-id" + +class CheckIfCanToggleNotificationsViaAccountDataUseCaseTest { + + private val fakeGetNotificationSettingsAccountDataUseCase = mockk() + private val fakeSession = FakeSession() + + private val checkIfCanToggleNotificationsViaAccountDataUseCase = + CheckIfCanToggleNotificationsViaAccountDataUseCase( + getNotificationSettingsAccountDataUseCase = fakeGetNotificationSettingsAccountDataUseCase, + ) + + @Test + fun `given current session and an account data with a content for the device id when execute then result is true`() { + // Given + val content = LocalNotificationSettingsContent(isSilenced = true) + every { fakeGetNotificationSettingsAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns content + + // When + val result = checkIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) + + // Then + result shouldBeEqualTo true + } + + @Test + fun `given current session and an account data with empty content for the device id when execute then result is false`() { + // Given + val content = LocalNotificationSettingsContent(isSilenced = null) + every { fakeGetNotificationSettingsAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns content + + // When + val result = checkIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) + + // Then + result shouldBeEqualTo false + } + + @Test + fun `given current session and NO account data for the device id when execute then result is false`() { + // Given + val content = null + every { fakeGetNotificationSettingsAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns content + + // When + val result = checkIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) + + // Then + result shouldBeEqualTo false + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCaseTest.kt similarity index 82% rename from vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt rename to vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCaseTest.kt index 508a05acd6..64beb7b8e2 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaPusherUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanToggleNotificationsViaPusherUseCaseTest.kt @@ -23,12 +23,12 @@ import org.junit.Test private val A_HOMESERVER_CAPABILITIES = aHomeServerCapabilities(canRemotelyTogglePushNotificationsOfDevices = true) -class CheckIfCanTogglePushNotificationsViaPusherUseCaseTest { +class CheckIfCanToggleNotificationsViaPusherUseCaseTest { private val fakeSession = FakeSession() - private val checkIfCanTogglePushNotificationsViaPusherUseCase = - CheckIfCanTogglePushNotificationsViaPusherUseCase() + private val checkIfCanToggleNotificationsViaPusherUseCase = + CheckIfCanToggleNotificationsViaPusherUseCase() @Test fun `given current session when execute then toggle capability is returned`() { @@ -38,7 +38,7 @@ class CheckIfCanTogglePushNotificationsViaPusherUseCaseTest { .givenCapabilities(A_HOMESERVER_CAPABILITIES) // When - val result = checkIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) + val result = checkIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) // Then result shouldBeEqualTo A_HOMESERVER_CAPABILITIES.canRemotelyTogglePushNotificationsOfDevices diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt deleted file mode 100644 index 37433364e8..0000000000 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 im.vector.app.test.fakes.FakeSession -import io.mockk.mockk -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes - -private const val A_DEVICE_ID = "device-id" - -class CheckIfCanTogglePushNotificationsViaAccountDataUseCaseTest { - - private val fakeSession = FakeSession() - - private val checkIfCanTogglePushNotificationsViaAccountDataUseCase = - CheckIfCanTogglePushNotificationsViaAccountDataUseCase() - - @Test - fun `given current session and an account data for the device id when execute then result is true`() { - // Given - fakeSession - .accountDataService() - .givenGetUserAccountDataEventReturns( - type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, - content = mockk(), - ) - - // When - val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) - - // Then - result shouldBeEqualTo true - } - - @Test - fun `given current session and NO account data for the device id when execute then result is false`() { - // Given - fakeSession - .accountDataService() - .givenGetUserAccountDataEventReturns( - type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + A_DEVICE_ID, - content = null, - ) - - // When - val result = checkIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) - - // Then - result shouldBeEqualTo false - } -} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt new file mode 100644 index 0000000000..600ba2ba48 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/DeleteNotificationSettingsAccountDataUseCaseTest.kt @@ -0,0 +1,78 @@ +/* + * 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 im.vector.app.test.fakes.FakeSession +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent + +class DeleteNotificationSettingsAccountDataUseCaseTest { + + private val fakeSetNotificationSettingsAccountDataUseCase = mockk() + private val fakeGetNotificationSettingsAccountDataUseCase = mockk() + + private val deleteNotificationSettingsAccountDataUseCase = DeleteNotificationSettingsAccountDataUseCase( + setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase, + getNotificationSettingsAccountDataUseCase = fakeGetNotificationSettingsAccountDataUseCase, + ) + + @Test + fun `given a device id and existing account data content when execute then empty content is set for the account data`() = runTest { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + aSession.givenSessionId(aDeviceId) + every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns LocalNotificationSettingsContent( + isSilenced = true, + ) + coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } + val expectedContent = LocalNotificationSettingsContent( + isSilenced = null + ) + + // When + deleteNotificationSettingsAccountDataUseCase.execute(aSession) + + // Then + verify { fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) } + coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, expectedContent) } + } + + @Test + fun `given a device id and empty existing account data content when execute then nothing is done`() = runTest { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + aSession.givenSessionId(aDeviceId) + every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns LocalNotificationSettingsContent( + isSilenced = null, + ) + + // When + deleteNotificationSettingsAccountDataUseCase.execute(aSession) + + // Then + verify { fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) } + coVerify(inverse = true) { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, any()) } + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCaseTest.kt new file mode 100644 index 0000000000..50940b9d34 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUpdatesUseCaseTest.kt @@ -0,0 +1,109 @@ +/* + * 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 im.vector.app.test.fakes.FakeFlowLiveDataConversions +import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.givenAsFlow +import io.mockk.unmockkAll +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toContent + +class GetNotificationSettingsAccountDataUpdatesUseCaseTest { + + private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions() + private val getNotificationSettingsAccountDataUpdatesUseCase = GetNotificationSettingsAccountDataUpdatesUseCase() + + @Before + fun setUp() { + fakeFlowLiveDataConversions.setup() + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given a device id when execute then retrieve the account data event corresponding to this id if any`() = runTest { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + val expectedContent = LocalNotificationSettingsContent(isSilenced = true) + aSession + .accountDataService() + .givenGetLiveUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + aDeviceId, + content = expectedContent.toContent(), + ) + .givenAsFlow() + + // When + val result = getNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId).firstOrNull() + + // Then + result shouldBeEqualTo expectedContent + } + + @Test + fun `given a device id and no content for account data when execute then retrieve the account data event corresponding to this id if any`() = runTest { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + aSession + .accountDataService() + .givenGetLiveUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + aDeviceId, + content = null, + ) + .givenAsFlow() + + // When + val result = getNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId).firstOrNull() + + // Then + result shouldBeEqualTo null + } + + @Test + fun `given a device id and empty content for account data when execute then retrieve the account data event corresponding to this id if any`() = runTest { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + val expectedContent = LocalNotificationSettingsContent(isSilenced = null) + aSession + .accountDataService() + .givenGetLiveUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + aDeviceId, + content = expectedContent.toContent(), + ) + .givenAsFlow() + + // When + val result = getNotificationSettingsAccountDataUpdatesUseCase.execute(aSession, aDeviceId).firstOrNull() + + // Then + result shouldBeEqualTo expectedContent + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt new file mode 100644 index 0000000000..2adb0d8599 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationSettingsAccountDataUseCaseTest.kt @@ -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.features.settings.devices.v2.notification + +import im.vector.app.test.fakes.FakeSession +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toContent + +class GetNotificationSettingsAccountDataUseCaseTest { + + private val getNotificationSettingsAccountDataUseCase = GetNotificationSettingsAccountDataUseCase() + + @Test + fun `given a device id when execute then retrieve the account data event corresponding to this id if any`() { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + val expectedContent = LocalNotificationSettingsContent(isSilenced = true) + aSession + .accountDataService() + .givenGetUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + aDeviceId, + content = expectedContent.toContent(), + ) + + // When + val result = getNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) + + // Then + result shouldBeEqualTo expectedContent + } + + @Test + fun `given a device id and empty content when execute then retrieve the account data event corresponding to this id if any`() { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + val expectedContent = LocalNotificationSettingsContent(isSilenced = null) + aSession + .accountDataService() + .givenGetUserAccountDataEventReturns( + type = UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + aDeviceId, + content = expectedContent.toContent(), + ) + + // When + val result = getNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) + + // Then + result shouldBeEqualTo expectedContent + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt index b38367b098..e4b681c5ec 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/GetNotificationsStatusUseCaseTest.kt @@ -22,6 +22,7 @@ import im.vector.app.test.fixtures.PusherFixture import im.vector.app.test.testDispatcher import io.mockk.every import io.mockk.mockk +import io.mockk.verify import io.mockk.verifyOrder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull @@ -46,15 +47,15 @@ class GetNotificationsStatusUseCaseTest { val instantTaskExecutorRule = InstantTaskExecutorRule() private val fakeSession = FakeSession() - private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase = - mockk() - private val fakeCanTogglePushNotificationsViaPusherUseCase = - mockk() + private val fakeCanToggleNotificationsViaAccountDataUseCase = + mockk() + private val fakeCanToggleNotificationsViaPusherUseCase = + mockk() private val getNotificationsStatusUseCase = GetNotificationsStatusUseCase( - checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase, - canTogglePushNotificationsViaPusherUseCase = fakeCanTogglePushNotificationsViaPusherUseCase, + canToggleNotificationsViaAccountDataUseCase = fakeCanToggleNotificationsViaAccountDataUseCase, + canToggleNotificationsViaPusherUseCase = fakeCanToggleNotificationsViaPusherUseCase, ) @Before @@ -70,8 +71,8 @@ class GetNotificationsStatusUseCaseTest { @Test fun `given current session and toggle is not supported when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { // Given - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false - every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false) + every { fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns flowOf(false) + every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false) // When val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID) @@ -80,8 +81,8 @@ class GetNotificationsStatusUseCaseTest { result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED verifyOrder { // we should first check account data - fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) - fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) + fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) + fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } } @@ -95,8 +96,8 @@ class GetNotificationsStatusUseCaseTest { ) ) fakeSession.pushersService().givenPushersLive(pushers) - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns false - every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true) + every { fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns flowOf(false) + every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true) // When val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID) @@ -106,7 +107,21 @@ class GetNotificationsStatusUseCaseTest { } @Test - fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on settings value`() = runTest { + fun `given toggle via pusher is supported and no registered pusher when execute then resulting flow contains NOT_SUPPORTED value`() = runTest { + // Given + fakeSession.pushersService().givenPushersLive(emptyList()) + every { fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns flowOf(false) + every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(true) + + // When + val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID) + + // Then + result.firstOrNull() shouldBeEqualTo NotificationsStatus.NOT_SUPPORTED + } + + @Test + fun `given current session and toggle via account data is supported when execute then resulting flow contains status based on account data`() = runTest { // Given fakeSession .accountDataService() @@ -116,13 +131,19 @@ class GetNotificationsStatusUseCaseTest { isSilenced = false ).toContent(), ) - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns true - every { fakeCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false) + every { fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) } returns flowOf(true) + every { fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns flowOf(false) // When val result = getNotificationsStatusUseCase.execute(fakeSession, A_DEVICE_ID) // Then result.firstOrNull() shouldBeEqualTo NotificationsStatus.ENABLED + verify { + fakeCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, A_DEVICE_ID) + } + verify(inverse = true) { + fakeCanToggleNotificationsViaPusherUseCase.execute(fakeSession) + } } } diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt new file mode 100644 index 0000000000..89fcd5e512 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/SetNotificationSettingsAccountDataUseCaseTest.kt @@ -0,0 +1,47 @@ +/* + * 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 im.vector.app.test.fakes.FakeSession +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes +import org.matrix.android.sdk.api.session.events.model.toContent + +class SetNotificationSettingsAccountDataUseCaseTest { + + private val setNotificationSettingsAccountDataUseCase = SetNotificationSettingsAccountDataUseCase() + + @Test + fun `given a content when execute then update local notification settings with this content`() = runTest { + // Given + val sessionId = "a_session_id" + val localNotificationSettingsContent = LocalNotificationSettingsContent(isSilenced = true) + val fakeSession = FakeSession() + fakeSession.accountDataService().givenUpdateUserAccountDataEventSucceeds() + + // When + setNotificationSettingsAccountDataUseCase.execute(fakeSession, sessionId, localNotificationSettingsContent) + + // Then + fakeSession.accountDataService().verifyUpdateUserAccountDataEventSucceeds( + UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId, + localNotificationSettingsContent.toContent(), + ) + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCaseTest.kt new file mode 100644 index 0000000000..90afbe9045 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/ToggleNotificationsUseCaseTest.kt @@ -0,0 +1,88 @@ +/* + * 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 im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fixtures.PusherFixture +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent + +class ToggleNotificationsUseCaseTest { + + private val activeSessionHolder = FakeActiveSessionHolder() + private val fakeCheckIfCanToggleNotificationsViaPusherUseCase = + mockk() + private val fakeCheckIfCanToggleNotificationsViaAccountDataUseCase = + mockk() + private val fakeSetNotificationSettingsAccountDataUseCase = + mockk() + + private val toggleNotificationsUseCase = + ToggleNotificationsUseCase( + activeSessionHolder = activeSessionHolder.instance, + checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase, + checkIfCanToggleNotificationsViaAccountDataUseCase = fakeCheckIfCanToggleNotificationsViaAccountDataUseCase, + setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase, + ) + + @Test + fun `when execute, then toggle enabled for device pushers`() = runTest { + // Given + val sessionId = "a_session_id" + val pushers = listOf( + PusherFixture.aPusher(deviceId = sessionId, enabled = false), + PusherFixture.aPusher(deviceId = "another id", enabled = false) + ) + val fakeSession = activeSessionHolder.fakeSession + fakeSession.pushersService().givenPushersLive(pushers) + fakeSession.pushersService().givenGetPushers(pushers) + every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns true + every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns false + + // When + toggleNotificationsUseCase.execute(sessionId, true) + + // Then + activeSessionHolder.fakeSession.pushersService().verifyTogglePusherCalled(pushers.first(), true) + } + + @Test + fun `when execute, then toggle local notification settings`() = runTest { + // Given + val sessionId = "a_session_id" + val fakeSession = activeSessionHolder.fakeSession + every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeSession) } returns false + every { fakeCheckIfCanToggleNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns true + coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } + val expectedLocalNotificationSettingsContent = LocalNotificationSettingsContent( + isSilenced = false + ) + + // When + toggleNotificationsUseCase.execute(sessionId, true) + + // Then + coVerify { + fakeSetNotificationSettingsAccountDataUseCase.execute(fakeSession, sessionId, expectedLocalNotificationSettingsContent) + } + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt deleted file mode 100644 index 35c5979e53..0000000000 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/TogglePushNotificationUseCaseTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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 im.vector.app.test.fakes.FakeActiveSessionHolder -import im.vector.app.test.fixtures.PusherFixture -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes -import org.matrix.android.sdk.api.session.events.model.toContent - -class TogglePushNotificationUseCaseTest { - - private val activeSessionHolder = FakeActiveSessionHolder() - private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = - mockk() - private val fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase = - mockk() - - private val togglePushNotificationUseCase = - TogglePushNotificationUseCase( - activeSessionHolder = activeSessionHolder.instance, - checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, - checkIfCanTogglePushNotificationsViaAccountDataUseCase = fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase, - ) - - @Test - fun `when execute, then toggle enabled for device pushers`() = runTest { - // Given - val sessionId = "a_session_id" - val pushers = listOf( - PusherFixture.aPusher(deviceId = sessionId, enabled = false), - PusherFixture.aPusher(deviceId = "another id", enabled = false) - ) - val fakeSession = activeSessionHolder.fakeSession - fakeSession.pushersService().givenPushersLive(pushers) - fakeSession.pushersService().givenGetPushers(pushers) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns true - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns false - - // When - togglePushNotificationUseCase.execute(sessionId, true) - - // Then - activeSessionHolder.fakeSession.pushersService().verifyTogglePusherCalled(pushers.first(), true) - } - - @Test - fun `when execute, then toggle local notification settings`() = runTest { - // Given - val sessionId = "a_session_id" - val pushers = listOf( - PusherFixture.aPusher(deviceId = sessionId, enabled = false), - PusherFixture.aPusher(deviceId = "another id", enabled = false) - ) - val fakeSession = activeSessionHolder.fakeSession - fakeSession.pushersService().givenPushersLive(pushers) - fakeSession.accountDataService().givenGetUserAccountDataEventReturns( - UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId, - LocalNotificationSettingsContent(isSilenced = true).toContent() - ) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false - every { fakeCheckIfCanTogglePushNotificationsViaAccountDataUseCase.execute(fakeSession, sessionId) } returns true - - // When - togglePushNotificationUseCase.execute(sessionId, true) - - // Then - activeSessionHolder.fakeSession.accountDataService().verifyUpdateUserAccountDataEventSucceeds( - UserAccountDataTypes.TYPE_LOCAL_NOTIFICATION_SETTINGS + sessionId, - LocalNotificationSettingsContent(isSilenced = false).toContent(), - ) - } -} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt new file mode 100644 index 0000000000..0075be02d2 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/notification/UpdateNotificationSettingsAccountDataUseCaseTest.kt @@ -0,0 +1,128 @@ +/* + * 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 im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.FakeUnifiedPushHelper +import im.vector.app.test.fakes.FakeVectorPreferences +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent + +class UpdateNotificationSettingsAccountDataUseCaseTest { + + private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() + private val fakeGetNotificationSettingsAccountDataUseCase = mockk() + private val fakeSetNotificationSettingsAccountDataUseCase = mockk() + private val fakeDeleteNotificationSettingsAccountDataUseCase = mockk() + + private val updateNotificationSettingsAccountDataUseCase = UpdateNotificationSettingsAccountDataUseCase( + vectorPreferences = fakeVectorPreferences.instance, + unifiedPushHelper = fakeUnifiedPushHelper.instance, + getNotificationSettingsAccountDataUseCase = fakeGetNotificationSettingsAccountDataUseCase, + setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase, + deleteNotificationSettingsAccountDataUseCase = fakeDeleteNotificationSettingsAccountDataUseCase, + ) + + @Test + fun `given back sync enabled, a device id and a different local setting compared to remote when execute then content is updated`() = runTest { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + aSession.givenSessionId(aDeviceId) + coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } + val areNotificationsEnabled = true + fakeVectorPreferences.givenAreNotificationsEnabledForDevice(areNotificationsEnabled) + fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(true) + every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns + LocalNotificationSettingsContent( + isSilenced = null + ) + val expectedContent = LocalNotificationSettingsContent( + isSilenced = !areNotificationsEnabled + ) + + // When + updateNotificationSettingsAccountDataUseCase.execute(aSession) + + // Then + verify { + fakeUnifiedPushHelper.instance.isBackgroundSync() + fakeVectorPreferences.instance.areNotificationEnabledForDevice() + fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) + } + coVerify(inverse = true) { fakeDeleteNotificationSettingsAccountDataUseCase.execute(aSession) } + coVerify { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, expectedContent) } + } + + @Test + fun `given back sync enabled, a device id and a same local setting compared to remote when execute then content is not updated`() = runTest { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + aSession.givenSessionId(aDeviceId) + coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } + val areNotificationsEnabled = true + fakeVectorPreferences.givenAreNotificationsEnabledForDevice(areNotificationsEnabled) + fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(true) + every { fakeGetNotificationSettingsAccountDataUseCase.execute(any(), any()) } returns + LocalNotificationSettingsContent( + isSilenced = false + ) + val expectedContent = LocalNotificationSettingsContent( + isSilenced = !areNotificationsEnabled + ) + + // When + updateNotificationSettingsAccountDataUseCase.execute(aSession) + + // Then + verify { + fakeUnifiedPushHelper.instance.isBackgroundSync() + fakeVectorPreferences.instance.areNotificationEnabledForDevice() + fakeGetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId) + } + coVerify(inverse = true) { fakeDeleteNotificationSettingsAccountDataUseCase.execute(aSession) } + coVerify(inverse = true) { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, expectedContent) } + } + + @Test + fun `given back sync disabled and a device id when execute then content is deleted`() = runTest { + // Given + val aDeviceId = "device-id" + val aSession = FakeSession() + aSession.givenSessionId(aDeviceId) + coJustRun { fakeDeleteNotificationSettingsAccountDataUseCase.execute(any()) } + fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(false) + + // When + updateNotificationSettingsAccountDataUseCase.execute(aSession) + + // Then + verify { + fakeUnifiedPushHelper.instance.isBackgroundSync() + } + coVerify { fakeDeleteNotificationSettingsAccountDataUseCase.execute(aSession) } + coVerify(inverse = true) { fakeSetNotificationSettingsAccountDataUseCase.execute(aSession, aDeviceId, any()) } + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 287bdd159c..b0f7a774f2 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -24,13 +24,12 @@ import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCase import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus -import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakeGetNotificationsStatusUseCase import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionsUseCase -import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase +import im.vector.app.test.fakes.FakeToggleNotificationUseCase import im.vector.app.test.fakes.FakeVectorPreferences import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test @@ -73,10 +72,9 @@ class SessionOverviewViewModelTest { private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk() private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() - private val interceptSignoutFlowResponseUseCase = mockk() private val fakePendingAuthHandler = FakePendingAuthHandler() private val refreshDevicesUseCase = mockk(relaxed = true) - private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase() + private val toggleNotificationUseCase = FakeToggleNotificationUseCase() private val fakeGetNotificationsStatusUseCase = FakeGetNotificationsStatusUseCase() private val notificationsStatus = NotificationsStatus.ENABLED private val fakeVectorPreferences = FakeVectorPreferences() @@ -87,11 +85,10 @@ class SessionOverviewViewModelTest { getDeviceFullInfoUseCase = getDeviceFullInfoUseCase, checkIfCurrentSessionCanBeVerifiedUseCase = checkIfCurrentSessionCanBeVerifiedUseCase, signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, - interceptSignoutFlowResponseUseCase = interceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, activeSessionHolder = fakeActiveSessionHolder.instance, refreshDevicesUseCase = refreshDevicesUseCase, - togglePushNotificationUseCase = togglePushNotificationUseCase.instance, + toggleNotificationsUseCase = toggleNotificationUseCase.instance, getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance, vectorPreferences = fakeVectorPreferences.instance, toggleIpAddressVisibilityUseCase = toggleIpAddressVisibilityUseCase, @@ -436,7 +433,7 @@ class SessionOverviewViewModelTest { viewModel.handle(SessionOverviewAction.TogglePushNotifications(A_SESSION_ID_1, true)) - togglePushNotificationUseCase.verifyExecute(A_SESSION_ID_1, true) + toggleNotificationUseCase.verifyExecute(A_SESSION_ID_1, true) viewModel.test().assertLatestState { state -> state.notificationsStatus == NotificationsStatus.ENABLED }.finish() } } diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt index e460413a39..669b20fc1a 100644 --- a/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/DisableNotificationsForCurrentSessionUseCaseTest.kt @@ -16,63 +16,39 @@ package im.vector.app.features.settings.notifications -import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase -import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase -import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase import im.vector.app.test.fakes.FakePushersManager -import im.vector.app.test.fakes.FakeUnifiedPushHelper import io.mockk.coJustRun import io.mockk.coVerify -import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Test -private const val A_SESSION_ID = "session-id" - class DisableNotificationsForCurrentSessionUseCaseTest { - private val fakeActiveSessionHolder = FakeActiveSessionHolder() - private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() private val fakePushersManager = FakePushersManager() - private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk() - private val fakeTogglePushNotificationUseCase = mockk() + private val fakeToggleNotificationsForCurrentSessionUseCase = mockk() + private val fakeUnregisterUnifiedPushUseCase = mockk() private val disableNotificationsForCurrentSessionUseCase = DisableNotificationsForCurrentSessionUseCase( - activeSessionHolder = fakeActiveSessionHolder.instance, - unifiedPushHelper = fakeUnifiedPushHelper.instance, pushersManager = fakePushersManager.instance, - checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, - togglePushNotificationUseCase = fakeTogglePushNotificationUseCase, + toggleNotificationsForCurrentSessionUseCase = fakeToggleNotificationsForCurrentSessionUseCase, + unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase, ) @Test - fun `given toggle via pusher is possible when execute then disable notification via toggle of existing pusher`() = runTest { + fun `when execute then disable notifications and unregister the pusher`() = runTest { // Given - val fakeSession = fakeActiveSessionHolder.fakeSession - fakeSession.givenSessionId(A_SESSION_ID) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns true - coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) } + coJustRun { fakeToggleNotificationsForCurrentSessionUseCase.execute(any()) } + coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) } // When disableNotificationsForCurrentSessionUseCase.execute() // Then - coVerify { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, false) } - } - - @Test - fun `given toggle via pusher is NOT possible when execute then disable notification by unregistering the pusher`() = runTest { - // Given - val fakeSession = fakeActiveSessionHolder.fakeSession - fakeSession.givenSessionId(A_SESSION_ID) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeSession) } returns false - fakeUnifiedPushHelper.givenUnregister(fakePushersManager.instance) - - // When - disableNotificationsForCurrentSessionUseCase.execute() - - // Then - fakeUnifiedPushHelper.verifyUnregister(fakePushersManager.instance) + coVerify { + fakeToggleNotificationsForCurrentSessionUseCase.execute(false) + fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance) + } } } diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt index eb6629cb13..d58ba7645c 100644 --- a/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/EnableNotificationsForCurrentSessionUseCaseTest.kt @@ -16,72 +16,70 @@ package im.vector.app.features.settings.notifications -import androidx.fragment.app.FragmentActivity -import im.vector.app.features.settings.devices.v2.notification.CheckIfCanTogglePushNotificationsViaPusherUseCase -import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase -import im.vector.app.test.fakes.FakeActiveSessionHolder -import im.vector.app.test.fakes.FakeFcmHelper +import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase +import im.vector.app.core.pushers.RegisterUnifiedPushUseCase import im.vector.app.test.fakes.FakePushersManager -import im.vector.app.test.fakes.FakeUnifiedPushHelper import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.every +import io.mockk.justRun import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe import org.junit.Test -private const val A_SESSION_ID = "session-id" - class EnableNotificationsForCurrentSessionUseCaseTest { - private val fakeActiveSessionHolder = FakeActiveSessionHolder() - private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() private val fakePushersManager = FakePushersManager() - private val fakeFcmHelper = FakeFcmHelper() - private val fakeCheckIfCanTogglePushNotificationsViaPusherUseCase = mockk() - private val fakeTogglePushNotificationUseCase = mockk() + private val fakeToggleNotificationsForCurrentSessionUseCase = mockk() + private val fakeRegisterUnifiedPushUseCase = mockk() + private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk() private val enableNotificationsForCurrentSessionUseCase = EnableNotificationsForCurrentSessionUseCase( - activeSessionHolder = fakeActiveSessionHolder.instance, - unifiedPushHelper = fakeUnifiedPushHelper.instance, pushersManager = fakePushersManager.instance, - fcmHelper = fakeFcmHelper.instance, - checkIfCanTogglePushNotificationsViaPusherUseCase = fakeCheckIfCanTogglePushNotificationsViaPusherUseCase, - togglePushNotificationUseCase = fakeTogglePushNotificationUseCase, + toggleNotificationsForCurrentSessionUseCase = fakeToggleNotificationsForCurrentSessionUseCase, + registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase, + ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase, ) @Test - fun `given no existing pusher for current session when execute then a new pusher is registered`() = runTest { + fun `given no existing pusher and a registered distributor when execute then a new pusher is registered and result is success`() = runTest { // Given - val fragmentActivity = mockk() + val aDistributor = "distributor" fakePushersManager.givenGetPusherForCurrentSessionReturns(null) - fakeUnifiedPushHelper.givenRegister(fragmentActivity) - fakeUnifiedPushHelper.givenIsEmbeddedDistributorReturns(true) - fakeFcmHelper.givenEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns false + every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success + justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) } + coJustRun { fakeToggleNotificationsForCurrentSessionUseCase.execute(any()) } // When - enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity) + val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor) // Then - fakeUnifiedPushHelper.verifyRegister(fragmentActivity) - fakeFcmHelper.verifyEnsureFcmTokenIsRetrieved(fragmentActivity, fakePushersManager.instance, registerPusher = true) + result shouldBe EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Success + verify { + fakeRegisterUnifiedPushUseCase.execute(aDistributor) + fakeEnsureFcmTokenIsRetrievedUseCase.execute(fakePushersManager.instance, registerPusher = true) + } + coVerify { + fakeToggleNotificationsForCurrentSessionUseCase.execute(enabled = true) + } } @Test - fun `given toggle via Pusher is possible when execute then current pusher is toggled to true`() = runTest { + fun `given no existing pusher and a no registered distributor when execute then result is need to ask user for distributor`() = runTest { // Given - val fragmentActivity = mockk() - fakePushersManager.givenGetPusherForCurrentSessionReturns(mockk()) - val fakeSession = fakeActiveSessionHolder.fakeSession - fakeSession.givenSessionId(A_SESSION_ID) - every { fakeCheckIfCanTogglePushNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) } returns true - coJustRun { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, any()) } + val aDistributor = "distributor" + fakePushersManager.givenGetPusherForCurrentSessionReturns(null) + every { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor // When - enableNotificationsForCurrentSessionUseCase.execute(fragmentActivity) + val result = enableNotificationsForCurrentSessionUseCase.execute(aDistributor) // Then - coVerify { fakeTogglePushNotificationUseCase.execute(A_SESSION_ID, true) } + result shouldBe EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor + verify { + fakeRegisterUnifiedPushUseCase.execute(aDistributor) + } } } diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCaseTest.kt new file mode 100644 index 0000000000..f49aafab8a --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/ToggleNotificationsForCurrentSessionUseCaseTest.kt @@ -0,0 +1,117 @@ +/* + * 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.notifications + +import im.vector.app.features.settings.devices.v2.notification.CheckIfCanToggleNotificationsViaPusherUseCase +import im.vector.app.features.settings.devices.v2.notification.DeleteNotificationSettingsAccountDataUseCase +import im.vector.app.features.settings.devices.v2.notification.SetNotificationSettingsAccountDataUseCase +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeUnifiedPushHelper +import im.vector.app.test.fixtures.PusherFixture +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.api.account.LocalNotificationSettingsContent + +class ToggleNotificationsForCurrentSessionUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + private val fakeUnifiedPushHelper = FakeUnifiedPushHelper() + private val fakeCheckIfCanToggleNotificationsViaPusherUseCase = mockk() + private val fakeSetNotificationSettingsAccountDataUseCase = mockk() + private val fakeDeleteNotificationSettingsAccountDataUseCase = mockk() + + private val toggleNotificationsForCurrentSessionUseCase = ToggleNotificationsForCurrentSessionUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance, + unifiedPushHelper = fakeUnifiedPushHelper.instance, + checkIfCanToggleNotificationsViaPusherUseCase = fakeCheckIfCanToggleNotificationsViaPusherUseCase, + setNotificationSettingsAccountDataUseCase = fakeSetNotificationSettingsAccountDataUseCase, + deleteNotificationSettingsAccountDataUseCase = fakeDeleteNotificationSettingsAccountDataUseCase, + ) + + @Test + fun `given background sync is enabled when execute then set the related account data with correct value`() = runTest { + // Given + val enabled = true + val aDeviceId = "deviceId" + fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(true) + fakeActiveSessionHolder.fakeSession.givenSessionId(aDeviceId) + coJustRun { fakeSetNotificationSettingsAccountDataUseCase.execute(any(), any(), any()) } + val expectedNotificationContent = LocalNotificationSettingsContent(isSilenced = !enabled) + + // When + toggleNotificationsForCurrentSessionUseCase.execute(enabled) + + // Then + coVerify { + fakeSetNotificationSettingsAccountDataUseCase.execute( + fakeActiveSessionHolder.fakeSession, + aDeviceId, + expectedNotificationContent + ) + } + } + + @Test + fun `given background sync is not enabled and toggle pusher is possible when execute then delete any related account data and toggle pusher`() = runTest { + // Given + val enabled = true + val aDeviceId = "deviceId" + fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(false) + fakeActiveSessionHolder.fakeSession.givenSessionId(aDeviceId) + coJustRun { fakeDeleteNotificationSettingsAccountDataUseCase.execute(any()) } + every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(any()) } returns true + val aPusher = PusherFixture.aPusher(deviceId = aDeviceId) + fakeActiveSessionHolder.fakeSession.fakePushersService.givenGetPushers(listOf(aPusher)) + + // When + toggleNotificationsForCurrentSessionUseCase.execute(enabled) + + // Then + coVerify { + fakeDeleteNotificationSettingsAccountDataUseCase.execute(fakeActiveSessionHolder.fakeSession) + fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) + } + fakeActiveSessionHolder.fakeSession.fakePushersService.verifyTogglePusherCalled(aPusher, enabled) + } + + @Test + fun `given background sync is not enabled and toggle pusher is not possible when execute then only delete any related account data`() = runTest { + // Given + val enabled = true + val aDeviceId = "deviceId" + fakeUnifiedPushHelper.givenIsBackgroundSyncReturns(false) + fakeActiveSessionHolder.fakeSession.givenSessionId(aDeviceId) + coJustRun { fakeDeleteNotificationSettingsAccountDataUseCase.execute(any()) } + every { fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(any()) } returns false + + // When + toggleNotificationsForCurrentSessionUseCase.execute(enabled) + + // Then + coVerify { + fakeDeleteNotificationSettingsAccountDataUseCase.execute(fakeActiveSessionHolder.fakeSession) + fakeCheckIfCanToggleNotificationsViaPusherUseCase.execute(fakeActiveSessionHolder.fakeSession) + } + coVerify(inverse = true) { + fakeActiveSessionHolder.fakeSession.fakePushersService.togglePusher(any(), any()) + } + } +} diff --git a/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt new file mode 100644 index 0000000000..ae36ee7600 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/settings/notifications/VectorSettingsNotificationPreferenceViewModelTest.kt @@ -0,0 +1,218 @@ +/* + * 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.notifications + +import com.airbnb.mvrx.test.MavericksTestRule +import im.vector.app.core.platform.VectorDummyViewState +import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase +import im.vector.app.core.pushers.RegisterUnifiedPushUseCase +import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase +import im.vector.app.features.settings.VectorPreferences.Companion.SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY +import im.vector.app.test.fakes.FakePushersManager +import im.vector.app.test.fakes.FakeVectorPreferences +import im.vector.app.test.test +import im.vector.app.test.testDispatcher +import io.mockk.coEvery +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.justRun +import io.mockk.mockk +import org.junit.Rule +import org.junit.Test + +class VectorSettingsNotificationPreferenceViewModelTest { + + @get:Rule + val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher) + + private val fakePushersManager = FakePushersManager() + private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeEnableNotificationsForCurrentSessionUseCase = mockk() + private val fakeDisableNotificationsForCurrentSessionUseCase = mockk() + private val fakeUnregisterUnifiedPushUseCase = mockk() + private val fakeRegisterUnifiedPushUseCase = mockk() + private val fakeEnsureFcmTokenIsRetrievedUseCase = mockk() + private val fakeToggleNotificationsForCurrentSessionUseCase = mockk() + + private fun createViewModel() = VectorSettingsNotificationPreferenceViewModel( + initialState = VectorDummyViewState(), + pushersManager = fakePushersManager.instance, + vectorPreferences = fakeVectorPreferences.instance, + enableNotificationsForCurrentSessionUseCase = fakeEnableNotificationsForCurrentSessionUseCase, + disableNotificationsForCurrentSessionUseCase = fakeDisableNotificationsForCurrentSessionUseCase, + unregisterUnifiedPushUseCase = fakeUnregisterUnifiedPushUseCase, + registerUnifiedPushUseCase = fakeRegisterUnifiedPushUseCase, + ensureFcmTokenIsRetrievedUseCase = fakeEnsureFcmTokenIsRetrievedUseCase, + toggleNotificationsForCurrentSessionUseCase = fakeToggleNotificationsForCurrentSessionUseCase, + ) + + @Test + fun `given view model init when notifications are enabled in preferences then view event is posted`() { + // Given + fakeVectorPreferences.givenAreNotificationsEnabledForDevice(true) + val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled + val viewModel = createViewModel() + + // When + val viewModelTest = viewModel.test() + viewModel.notificationsPreferenceListener.onSharedPreferenceChanged(mockk(), SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) + + // Then + viewModelTest + .assertEvent { event -> event == expectedEvent } + .finish() + } + + @Test + fun `given view model init when notifications are disabled in preferences then view event is posted`() { + // Given + fakeVectorPreferences.givenAreNotificationsEnabledForDevice(false) + val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled + val viewModel = createViewModel() + + // When + val viewModelTest = viewModel.test() + viewModel.notificationsPreferenceListener.onSharedPreferenceChanged(mockk(), SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY) + + // Then + viewModelTest + .assertEvent { event -> event == expectedEvent } + .finish() + } + + @Test + fun `given DisableNotificationsForDevice action when handling action then disable use case is called`() { + // Given + val viewModel = createViewModel() + val action = VectorSettingsNotificationPreferenceViewAction.DisableNotificationsForDevice + coJustRun { fakeDisableNotificationsForCurrentSessionUseCase.execute() } + val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceDisabled + + // When + val viewModelTest = viewModel.test() + viewModel.handle(action) + + // Then + viewModelTest + .assertEvent { event -> event == expectedEvent } + .finish() + coVerify { + fakeDisableNotificationsForCurrentSessionUseCase.execute() + } + } + + @Test + fun `given EnableNotificationsForDevice action and enable success when handling action then enable use case is called`() { + // Given + val viewModel = createViewModel() + val aDistributor = "aDistributor" + val action = VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(aDistributor) + coEvery { fakeEnableNotificationsForCurrentSessionUseCase.execute(any()) } returns + EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.Success + val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationsForDeviceEnabled + + // When + val viewModelTest = viewModel.test() + viewModel.handle(action) + + // Then + viewModelTest + .assertEvent { event -> event == expectedEvent } + .finish() + coVerify { + fakeEnableNotificationsForCurrentSessionUseCase.execute(aDistributor) + } + } + + @Test + fun `given EnableNotificationsForDevice action and enable needs user choice when handling action then enable use case is called`() { + // Given + val viewModel = createViewModel() + val aDistributor = "aDistributor" + val action = VectorSettingsNotificationPreferenceViewAction.EnableNotificationsForDevice(aDistributor) + coEvery { fakeEnableNotificationsForCurrentSessionUseCase.execute(any()) } returns + EnableNotificationsForCurrentSessionUseCase.EnableNotificationsResult.NeedToAskUserForDistributor + val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor + + // When + val viewModelTest = viewModel.test() + viewModel.handle(action) + + // Then + viewModelTest + .assertEvent { event -> event == expectedEvent } + .finish() + coVerify { + fakeEnableNotificationsForCurrentSessionUseCase.execute(aDistributor) + } + } + + @Test + fun `given RegisterPushDistributor action and register success when handling action then register use case is called`() { + // Given + val viewModel = createViewModel() + val aDistributor = "aDistributor" + val action = VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(aDistributor) + coEvery { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.Success + coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) } + val areNotificationsEnabled = true + fakeVectorPreferences.givenAreNotificationsEnabledForDevice(areNotificationsEnabled) + coJustRun { fakeToggleNotificationsForCurrentSessionUseCase.execute(any()) } + justRun { fakeEnsureFcmTokenIsRetrievedUseCase.execute(any(), any()) } + val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.NotificationMethodChanged + + // When + val viewModelTest = viewModel.test() + viewModel.handle(action) + + // Then + viewModelTest + .assertEvent { event -> event == expectedEvent } + .finish() + coVerifyOrder { + fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance) + fakeRegisterUnifiedPushUseCase.execute(aDistributor) + fakeEnsureFcmTokenIsRetrievedUseCase.execute(fakePushersManager.instance, registerPusher = areNotificationsEnabled) + fakeToggleNotificationsForCurrentSessionUseCase.execute(enabled = areNotificationsEnabled) + } + } + + @Test + fun `given RegisterPushDistributor action and register needs user choice when handling action then register use case is called`() { + // Given + val viewModel = createViewModel() + val aDistributor = "aDistributor" + val action = VectorSettingsNotificationPreferenceViewAction.RegisterPushDistributor(aDistributor) + coEvery { fakeRegisterUnifiedPushUseCase.execute(any()) } returns RegisterUnifiedPushUseCase.RegisterUnifiedPushResult.NeedToAskUserForDistributor + coJustRun { fakeUnregisterUnifiedPushUseCase.execute(any()) } + val expectedEvent = VectorSettingsNotificationPreferenceViewEvent.AskUserForPushDistributor + + // When + val viewModelTest = viewModel.test() + viewModel.handle(action) + + // Then + viewModelTest + .assertEvent { event -> event == expectedEvent } + .finish() + coVerifyOrder { + fakeUnregisterUnifiedPushUseCase.execute(fakePushersManager.instance) + fakeRegisterUnifiedPushUseCase.execute(aDistributor) + } + } +} diff --git a/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt b/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt index 65f89dcc6a..58658651cf 100644 --- a/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt +++ b/vector/src/test/java/im/vector/app/screenshot/PaparazziExampleScreenshotTest.kt @@ -16,14 +16,9 @@ package im.vector.app.screenshot -import android.os.Build import android.widget.ImageView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout -import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_3 -import app.cash.paparazzi.Paparazzi -import app.cash.paparazzi.androidHome -import app.cash.paparazzi.detectEnvironment import im.vector.app.R import org.junit.Rule import org.junit.Test @@ -31,16 +26,7 @@ import org.junit.Test class PaparazziExampleScreenshotTest { @get:Rule - val paparazzi = Paparazzi( - // Apply trick from https://github.com/cashapp/paparazzi/issues/489#issuecomment-1195674603 - environment = detectEnvironment().copy( - platformDir = "${androidHome()}/platforms/android-32", - compileSdkVersion = Build.VERSION_CODES.S_V2 /* 32 */ - ), - deviceConfig = PIXEL_3, - theme = "Theme.Vector.Light", - maxPercentDifference = 0.0, - ) + val paparazzi = createPaparazziRule() @Test fun `example paparazzi test`() { diff --git a/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt b/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt new file mode 100644 index 0000000000..a5cba20561 --- /dev/null +++ b/vector/src/test/java/im/vector/app/screenshot/PaparazziRule.kt @@ -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.screenshot + +import android.os.Build +import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_3 +import app.cash.paparazzi.Paparazzi +import app.cash.paparazzi.androidHome +import app.cash.paparazzi.detectEnvironment + +fun createPaparazziRule() = Paparazzi( + // Apply trick from https://github.com/cashapp/paparazzi/issues/489#issuecomment-1195674603 + environment = detectEnvironment().copy( + platformDir = "${androidHome()}/platforms/android-32", + compileSdkVersion = Build.VERSION_CODES.S_V2 /* 32 */ + ), + deviceConfig = PIXEL_3, + theme = "Theme.Vector.Light", + maxPercentDifference = 0.0, +) diff --git a/vector/src/test/java/im/vector/app/screenshot/RoomItemScreenshotTest.kt b/vector/src/test/java/im/vector/app/screenshot/RoomItemScreenshotTest.kt new file mode 100644 index 0000000000..d1f4034f43 --- /dev/null +++ b/vector/src/test/java/im/vector/app/screenshot/RoomItemScreenshotTest.kt @@ -0,0 +1,66 @@ +/* + * 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.screenshot + +import android.view.View +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.view.isVisible +import im.vector.app.R +import im.vector.app.features.home.room.list.UnreadCounterBadgeView +import org.junit.Rule +import org.junit.Test + +class RoomItemScreenshotTest { + + @get:Rule + val paparazzi = createPaparazziRule() + + @Test + fun `item room test`() { + val view = paparazzi.inflate(R.layout.item_room) + + view.findViewById(R.id.roomUnreadIndicator).isVisible = true + view.findViewById(R.id.roomNameView).text = "Room name" + view.findViewById(R.id.roomLastEventTimeView).text = "12:34" + view.findViewById(R.id.subtitleView).text = "Latest message" + view.findViewById(R.id.roomDraftBadge).isVisible = true + view.findViewById(R.id.roomUnreadCounterBadgeView).let { + it.isVisible = true + it.render(UnreadCounterBadgeView.State.Count(8, false)) + } + + paparazzi.snapshot(view) + } + + @Test + fun `item room two line and highlight test`() { + val view = paparazzi.inflate(R.layout.item_room) + + view.findViewById(R.id.roomUnreadIndicator).isVisible = true + view.findViewById(R.id.roomNameView).text = "Room name" + view.findViewById(R.id.roomLastEventTimeView).text = "23:59" + view.findViewById(R.id.subtitleView).text = "Latest message\nOn two lines" + view.findViewById(R.id.roomDraftBadge).isVisible = true + view.findViewById(R.id.roomUnreadCounterBadgeView).let { + it.isVisible = true + it.render(UnreadCounterBadgeView.State.Count(88, true)) + } + + paparazzi.snapshot(view) + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt index af53913169..5d0e317c57 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeAuthenticationService.kt @@ -58,6 +58,10 @@ class FakeAuthenticationService : AuthenticationService by mockk() { coEvery { getWellKnownData(matrixId, config) } returns result } + fun givenIsQrLoginSupported(config: HomeServerConnectionConfig, result: Boolean) { + coEvery { isQrLoginSupported(config) } returns result + } + fun givenWellKnownThrows(matrixId: String, config: HomeServerConnectionConfig?, cause: Throwable) { coEvery { getWellKnownData(matrixId, config) } throws cause } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt index 9a94313fec..f8c568e908 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt @@ -81,4 +81,8 @@ class FakeContext( givenService(Context.CLIPBOARD_SERVICE, ClipboardManager::class.java, fakeClipboardManager.instance) return fakeClipboardManager } + + fun givenPackageName(name: String) { + every { instance.packageName } returns name + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt index 11abf18794..4c210215ec 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeFcmHelper.kt @@ -16,7 +16,6 @@ package im.vector.app.test.fakes -import androidx.fragment.app.FragmentActivity import im.vector.app.core.pushers.FcmHelper import im.vector.app.core.pushers.PushersManager import io.mockk.justRun @@ -27,18 +26,15 @@ class FakeFcmHelper { val instance = mockk() - fun givenEnsureFcmTokenIsRetrieved( - fragmentActivity: FragmentActivity, - pushersManager: PushersManager, - ) { - justRun { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, any()) } + fun givenEnsureFcmTokenIsRetrieved(pushersManager: PushersManager) { + justRun { instance.ensureFcmTokenIsRetrieved(pushersManager, any()) } } fun verifyEnsureFcmTokenIsRetrieved( - fragmentActivity: FragmentActivity, pushersManager: PushersManager, registerPusher: Boolean, + inverse: Boolean = false, ) { - verify { instance.ensureFcmTokenIsRetrieved(fragmentActivity, pushersManager, registerPusher) } + verify(inverse = inverse) { instance.ensureFcmTokenIsRetrieved(pushersManager, registerPusher) } } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt similarity index 77% rename from vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt rename to vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt index a78dd1a34b..2e397763f8 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeEnableNotificationsSettingUpdater.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeNotificationsSettingUpdater.kt @@ -16,16 +16,16 @@ package im.vector.app.test.fakes -import im.vector.app.core.notification.EnableNotificationsSettingUpdater +import im.vector.app.core.notification.NotificationsSettingUpdater import io.mockk.justRun import io.mockk.mockk import org.matrix.android.sdk.api.session.Session -class FakeEnableNotificationsSettingUpdater { +class FakeNotificationsSettingUpdater { - val instance = mockk() + val instance = mockk() fun givenOnSessionsStarted(session: Session) { - justRun { instance.onSessionsStarted(session) } + justRun { instance.onSessionStarted(session) } } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt index 46d852f4f8..3dd3854a18 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePushersManager.kt @@ -17,6 +17,8 @@ package im.vector.app.test.fakes import im.vector.app.core.pushers.PushersManager +import io.mockk.coJustRun +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import org.matrix.android.sdk.api.session.pushers.Pusher @@ -28,4 +30,12 @@ class FakePushersManager { fun givenGetPusherForCurrentSessionReturns(pusher: Pusher?) { every { instance.getPusherForCurrentSession() } returns pusher } + + fun givenUnregisterPusher(pushKey: String) { + coJustRun { instance.unregisterPusher(pushKey) } + } + + fun verifyUnregisterPusher(pushKey: String) { + coVerify { instance.unregisterPusher(pushKey) } + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt index c44fc4a497..f1a0ae7452 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSessionAccountDataService.kt @@ -16,6 +16,8 @@ package im.vector.app.test.fakes +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -25,6 +27,8 @@ import io.mockk.runs import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional class FakeSessionAccountDataService : SessionAccountDataService by mockk(relaxed = true) { @@ -32,6 +36,13 @@ class FakeSessionAccountDataService : SessionAccountDataService by mockk(relaxed every { getUserAccountDataEvent(type) } returns content?.let { UserAccountDataEvent(type, it) } } + fun givenGetLiveUserAccountDataEventReturns(type: String, content: Content?): LiveData> { + return MutableLiveData(content?.let { UserAccountDataEvent(type, it) }.toOptional()) + .also { + every { getLiveUserAccountDataEvent(type) } returns it + } + } + fun givenUpdateUserAccountDataEventSucceeds() { coEvery { updateUserAccountData(any(), any()) } just runs } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt similarity index 88% rename from vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt rename to vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt index bfbbb87705..3d2179bc2d 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeTogglePushNotificationUseCase.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeToggleNotificationUseCase.kt @@ -16,14 +16,14 @@ package im.vector.app.test.fakes -import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase +import im.vector.app.features.settings.devices.v2.notification.ToggleNotificationsUseCase import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.mockk -class FakeTogglePushNotificationUseCase { +class FakeToggleNotificationUseCase { - val instance = mockk { + val instance = mockk { coJustRun { execute(any(), any()) } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt index 1f2cc8a1ce..1a09783fad 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushHelper.kt @@ -16,38 +16,23 @@ package im.vector.app.test.fakes -import androidx.fragment.app.FragmentActivity -import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.UnifiedPushHelper -import io.mockk.coJustRun -import io.mockk.coVerify import io.mockk.every import io.mockk.mockk -import io.mockk.verify class FakeUnifiedPushHelper { val instance = mockk() - fun givenRegister(fragmentActivity: FragmentActivity) { - every { instance.register(fragmentActivity, any()) } answers { - secondArg().run() - } - } - - fun verifyRegister(fragmentActivity: FragmentActivity) { - verify { instance.register(fragmentActivity, any()) } - } - - fun givenUnregister(pushersManager: PushersManager) { - coJustRun { instance.unregister(pushersManager) } - } - - fun verifyUnregister(pushersManager: PushersManager) { - coVerify { instance.unregister(pushersManager) } - } - fun givenIsEmbeddedDistributorReturns(isEmbedded: Boolean) { every { instance.isEmbeddedDistributor() } returns isEmbedded } + + fun givenGetEndpointOrTokenReturns(endpoint: String?) { + every { instance.getEndpointOrToken() } returns endpoint + } + + fun givenIsBackgroundSyncReturns(enabled: Boolean) { + every { instance.isBackgroundSync() } returns enabled + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushStore.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushStore.kt new file mode 100644 index 0000000000..9b09bec688 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeUnifiedPushStore.kt @@ -0,0 +1,43 @@ +/* + * 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.test.fakes + +import im.vector.app.core.pushers.UnifiedPushStore +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify + +class FakeUnifiedPushStore { + + val instance = mockk() + + fun givenStoreUpEndpoint(endpoint: String?) { + justRun { instance.storeUpEndpoint(endpoint) } + } + + fun verifyStoreUpEndpoint(endpoint: String?) { + verify { instance.storeUpEndpoint(endpoint) } + } + + fun givenStorePushGateway(gateway: String?) { + justRun { instance.storePushGateway(gateway) } + } + + fun verifyStorePushGateway(gateway: String?) { + verify { instance.storePushGateway(gateway) } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt index d989abc214..b399f0baa4 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt @@ -50,4 +50,12 @@ class FakeVectorFeatures : VectorFeatures by spyk() { fun givenVoiceBroadcast(isEnabled: Boolean) { every { isVoiceBroadcastEnabled() } returns isEnabled } + + fun givenUnverifiedSessionsAlertEnabled(isEnabled: Boolean) { + every { isUnverifiedSessionsAlertEnabled() } returns isEnabled + } + + fun givenExternalDistributorsAreAllowed(allowed: Boolean) { + every { allowExternalUnifiedPushDistributors() } returns allowed + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index d89764a77e..58bc1a18b8 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -16,6 +16,7 @@ package im.vector.app.test.fakes +import im.vector.app.features.settings.BackgroundSyncMode import im.vector.app.features.settings.VectorPreferences import io.mockk.every import io.mockk.justRun @@ -56,4 +57,24 @@ class FakeVectorPreferences { fun givenSessionManagerShowIpAddress(showIpAddress: Boolean) { every { instance.showIpAddressInSessionManagerScreens() } returns showIpAddress } + + fun givenUnverifiedSessionsAlertLastShownMillis(lastShownMillis: Long) { + every { instance.getUnverifiedSessionsAlertLastShownMillis(any()) } returns lastShownMillis + } + + fun givenSetFdroidSyncBackgroundMode(mode: BackgroundSyncMode) { + justRun { instance.setFdroidSyncBackgroundMode(mode) } + } + + fun verifySetFdroidSyncBackgroundMode(mode: BackgroundSyncMode) { + verify { instance.setFdroidSyncBackgroundMode(mode) } + } + + fun givenAreNotificationsEnabledForDevice(notificationsEnabled: Boolean) { + every { instance.areNotificationEnabledForDevice() } returns notificationsEnabled + } + + fun givenIsBackgroundSyncEnabled(isEnabled: Boolean) { + every { instance.isBackgroundSyncEnabled() } returns isEnabled + } } diff --git a/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room test.png b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room test.png new file mode 100644 index 0000000000..1e87449b3c --- /dev/null +++ b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room test.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d33e82c6647bab9dcb3745d8c5a5448d60049279c365b9f64816eb9c958360d2 +size 15015 diff --git a/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room two line and highlight test.png b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room two line and highlight test.png new file mode 100644 index 0000000000..83fcb8d000 --- /dev/null +++ b/vector/src/test/snapshots/images/im.vector.app.screenshot_RoomItemScreenshotTest_item room two line and highlight test.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91a106e2a3f7310ac05425a2413ccec0aaa07720609d77a2ecd9a9d0d602b296 +size 17232