diff --git a/CHANGES.md b/CHANGES.md index c1d7998e78..514b0b9bdc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,20 +1,23 @@ -Changes in Element 1.0.18 (2020-XX-XX) +Changes in Element 1.0.18 (2021-XX-XX) =================================================== Features ✨: - - - + - VoIP : support for VoIP V1 protocol, transfer call and dial-pad + Improvements 🙌: - - + - VoIP : new tiles in timeline + - Improve room profile UX + - Upgrade Jitsi library from 2.9.3 to 3.1.0 Bugfix 🐛: - - + - VoIP : fix audio devices output + - Fix crash after initial sync on Dendrite Translations 🗣: - SDK API changes ⚠️: - - + - Build 🧱: - @@ -24,8 +27,9 @@ Test: Other changes: - New Dev Tools panel for developers + - Fix typos in CHANGES.md (#2811) -Changes in Element 1.0.17 (2020-02-09) +Changes in Element 1.0.17 (2021-02-09) =================================================== Improvements 🙌: @@ -47,13 +51,13 @@ Build 🧱: Other changes: - Change app name from "Element (Riot.im)" to "Element" -Changes in Element 1.0.16 (2020-02-04) +Changes in Element 1.0.16 (2021-02-04) =================================================== Bugfix 🐛: - Fix crash on API < 30 and light theme (#2774) -Changes in Element 1.0.15 (2020-02-03) +Changes in Element 1.0.15 (2021-02-03) =================================================== Features ✨: @@ -84,7 +88,7 @@ Build 🧱: Other changes: - Update Dagger to 2.31 version so we can use the embedded AssistedInject feature -Changes in Element 1.0.14 (2020-01-15) +Changes in Element 1.0.14 (2021-01-15) =================================================== Features ✨: @@ -1196,7 +1200,7 @@ Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-a ======================================================= -Changes in Element 1.X.X (2020-XX-XX) +Changes in Element 1.X.X (2021-XX-XX) =================================================== Features ✨: diff --git a/build.gradle b/build.gradle index 625ed348be..3da87093ec 100644 --- a/build.gradle +++ b/build.gradle @@ -58,9 +58,9 @@ allprojects { maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } // Jitsi repo maven { - url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-2.9.3" + url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-3.1.0" // Note: to test Jitsi release you can use a local file like this: - // url "file:///Users/bmarty/workspaces/jitsi_libre_maven/android-sdk-2.9.3" + // url "file:///Users/bmarty/workspaces/jitsi_libre_maven/android-sdk-3.1.0" } google() jcenter() diff --git a/docs/jitsi.md b/docs/jitsi.md index 071470b040..389e7d71ec 100644 --- a/docs/jitsi.md +++ b/docs/jitsi.md @@ -18,7 +18,7 @@ The generated maven repository is then host in the project https://github.com/ve Update the script `./tools/jitsi/build_jisti_libs.sh` with the tag of the project `https://github.com/jitsi/jitsi-meet`. -Currently we are building the version with the tag `android-sdk-2.9.3`. +Currently we are building the version with the tag `android-sdk-3.1.0`. ### Run the build script @@ -35,21 +35,21 @@ It will build the Jitsi Meet Android library and put every generated files in th - Update the file `./build.gradle` to use the previously created local Maven repository. Currently we have this line: ```groovy -url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-2.9.3" +url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-3.1.0" ``` You can uncomment and update the line starting with `// url "file://...` and comment the line starting with `url`, to test the library using the locally generated Maven repository. -- Update the dependency of the WebRTC library in the file `./matrix-sdk-android/build.gradle`. Currently we have this line: - -```groovy -implementation('com.facebook.react:react-native-webrtc:1.84.0-jitsi-5112273@aar') -``` - - Update the dependency of the Jitsi Meet library in the file `./vector/build.gradle`. Currently we have this line: ```groovy -implementation('org.jitsi.react:jitsi-meet-sdk:2.9.3') { transitive = true } +implementation('org.jitsi.react:jitsi-meet-sdk:3.1.0') +``` + +- Update the dependency of the WebRTC library in the file `./vector/build.gradle`. Currently we have this line: + +```groovy +implementation('com.facebook.react:react-native-webrtc:1.87.3-jitsi-6624067@aar') ``` - Perform a gradle sync and build the project @@ -74,9 +74,9 @@ If all the tests are passed, you can export the generated Jitsi library to our M - Update the file `./build.gradle` to use the previously created Maven repository. Currently we have this line: ```groovy -url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-2.9.3" +url "https://github.com/vector-im/jitsi_libre_maven/raw/master/android-sdk-3.1.0" ``` - Build the project and perform the sanity tests again. -- Update the file `/CANGES.md` to notify about the library upgrade, and create a regular PR for project Element Android. \ No newline at end of file +- Update the file `/CHANGES.md` to notify about the library upgrade, and create a regular PR for project Element Android. \ No newline at end of file diff --git a/fastlane/metadata/android/ca/changelogs/40100130.txt b/fastlane/metadata/android/ca/changelogs/40100130.txt index e2e967215f..c63b5fb793 100644 --- a/fastlane/metadata/android/ca/changelogs/40100130.txt +++ b/fastlane/metadata/android/ca/changelogs/40100130.txt @@ -1,2 +1,2 @@ Canvis principals d'aquesta versió: previsualització d'URL, nou teclat d'emoticones, noves funcions de configuració de les sales i neu pel Nadal! -Registre de canvis complet: https://github.com/vector-im/element-android/releases/tag/v1.0.12 +Registre de canvis complet: https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/ca/changelogs/40100140.txt b/fastlane/metadata/android/ca/changelogs/40100140.txt new file mode 100644 index 0000000000..1823c1abd7 --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/40100140.txt @@ -0,0 +1,2 @@ +Canvis principals d'aquesta versió: modificació dels permisos de sala, tema clar/fosc automàtic, correcció d'errors. +Registre de canvis complet: https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/ca/changelogs/40100150.txt b/fastlane/metadata/android/ca/changelogs/40100150.txt new file mode 100644 index 0000000000..430b311b0e --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/40100150.txt @@ -0,0 +1,2 @@ +Canvis principals d'aquesta versió: inici de sessió amb xarxes socials. +Registre de canvis complet: https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/ca/changelogs/40100160.txt b/fastlane/metadata/android/ca/changelogs/40100160.txt new file mode 100644 index 0000000000..8a5b6e5d9d --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/40100160.txt @@ -0,0 +1,2 @@ +Canvis principals d'aquesta versió: inici de sessió amb xarxes socials. +Registre de canvis complet: https://github.com/vector-im/element-android/releases/tag/v1.0.15 i https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/ca/short_description.txt b/fastlane/metadata/android/ca/short_description.txt index 1e842ec64e..136e9c7ae3 100644 --- a/fastlane/metadata/android/ca/short_description.txt +++ b/fastlane/metadata/android/ca/short_description.txt @@ -1 +1 @@ -Xat i VoIP segurs i descentralitzats. Protegeix les teves dades de tercers. +Xats i VoIP segurs i descentralitzats. Protegeix les teves dades de tercers. diff --git a/fastlane/metadata/android/ca/title.txt b/fastlane/metadata/android/ca/title.txt index adc831006a..9dc1e0d277 100644 --- a/fastlane/metadata/android/ca/title.txt +++ b/fastlane/metadata/android/ca/title.txt @@ -1 +1 @@ -Element (anteriorment Riot.im) +Element (abans Riot.im) diff --git a/fastlane/metadata/android/de/changelogs/40100130.txt b/fastlane/metadata/android/de/changelogs/40100130.txt index 305329ff8c..142003d607 100644 --- a/fastlane/metadata/android/de/changelogs/40100130.txt +++ b/fastlane/metadata/android/de/changelogs/40100130.txt @@ -1,2 +1,2 @@ Hauptänderungen in dieser Version: URL-Vorschau, neue Emoji-Tastatur, neue Raumeinstellungen und Schnee für Weihnachten! -Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases/tag/v1.0.12 +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/de/changelogs/40100140.txt b/fastlane/metadata/android/de/changelogs/40100140.txt new file mode 100644 index 0000000000..1e4725411b --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/40100140.txt @@ -0,0 +1,2 @@ +Hauptänderungen in dieser Version: Bearbeiten von Raumberechtigungen, automatisches Hell/Dunkel-Design und eine Reihe von Fehlerkorrekturen. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/de/changelogs/40100150.txt b/fastlane/metadata/android/de/changelogs/40100150.txt new file mode 100644 index 0000000000..f3335da144 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/40100150.txt @@ -0,0 +1,2 @@ +Hauptänderungen in dieser Version: Unterstützung für soziale Anmeldungen. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/de/changelogs/40100160.txt b/fastlane/metadata/android/de/changelogs/40100160.txt new file mode 100644 index 0000000000..1c4ce91ae2 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/40100160.txt @@ -0,0 +1,2 @@ +Hauptänderungen in dieser Version: Unterstützung für soziale Anmeldungen. +Vollständiges Änderungsprotokoll: https://github.com/vector-im/element-android/releases/tag/v1.0.15 and https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/et/changelogs/40100130.txt b/fastlane/metadata/android/et/changelogs/40100130.txt index 583bc3fc42..0dae99a63e 100644 --- a/fastlane/metadata/android/et/changelogs/40100130.txt +++ b/fastlane/metadata/android/et/changelogs/40100130.txt @@ -1,2 +1,2 @@ Olulisemad muutused selles versioonis: URLide eelvaade, uus klahvistik emojide jaoks, jututubade uued seadistused ja natuke lund jõuludeks! -Muudatuste logi täismahus: https://github.com/vector-im/element-android/releases/tag/v1.0.12 +Muudatuste logi täismahus: https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/et/changelogs/40100140.txt b/fastlane/metadata/android/et/changelogs/40100140.txt new file mode 100644 index 0000000000..88b6fa56cd --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40100140.txt @@ -0,0 +1,2 @@ +Olulisemad muutused selles versioonis: Jututoa õiguste muutmine, automaatne tumeda ja heleda teema vahetamine ning märgatav kogus veaparandusi. +Muudatuste logi täismahus: https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/et/changelogs/40100150.txt b/fastlane/metadata/android/et/changelogs/40100150.txt new file mode 100644 index 0000000000..5df920b96c --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40100150.txt @@ -0,0 +1,2 @@ +Olulisemad muutused selles versioonis: Sisselogimine sotsiaalmeediakontode abil. +Muudatuste logi täismahus: https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/et/changelogs/40100160.txt b/fastlane/metadata/android/et/changelogs/40100160.txt new file mode 100644 index 0000000000..902394552a --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/40100160.txt @@ -0,0 +1,2 @@ +Olulisemad muutused selles versioonis: Sisselogimine sotsiaalmeediakontode abil. +Muudatuste logi täismahus: https://github.com/vector-im/element-android/releases/tag/v1.0.15 ja https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/pt_BR/changelogs/40100130.txt b/fastlane/metadata/android/pt_BR/changelogs/40100130.txt index 834b512304..8f5a3d4b21 100644 --- a/fastlane/metadata/android/pt_BR/changelogs/40100130.txt +++ b/fastlane/metadata/android/pt_BR/changelogs/40100130.txt @@ -1,2 +1,2 @@ Principais mudanças nessa versão: Prévia do endereço URL, novo teclado de Emojis, novos recursos de configuração da sala, e neve para o Natal! -Registro de alterações completo: https://github.com/vector-im/element-android/releases/tag/v1.0.12 +Registro de alterações completo: https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/pt_BR/changelogs/40100140.txt b/fastlane/metadata/android/pt_BR/changelogs/40100140.txt new file mode 100644 index 0000000000..3533e8dd9c --- /dev/null +++ b/fastlane/metadata/android/pt_BR/changelogs/40100140.txt @@ -0,0 +1,2 @@ +Principais mudanças nessa versão: editar permissões da sala, tema automaticamente claro/escuro e várias correções de erros. +Registro de alterações completo: https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/pt_BR/changelogs/40100150.txt b/fastlane/metadata/android/pt_BR/changelogs/40100150.txt new file mode 100644 index 0000000000..fb13732507 --- /dev/null +++ b/fastlane/metadata/android/pt_BR/changelogs/40100150.txt @@ -0,0 +1,2 @@ +Principais mudanças nessa versão: suporte para Login Social. +Registro de alterações completo: https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/pt_BR/changelogs/40100160.txt b/fastlane/metadata/android/pt_BR/changelogs/40100160.txt new file mode 100644 index 0000000000..561ceb3f25 --- /dev/null +++ b/fastlane/metadata/android/pt_BR/changelogs/40100160.txt @@ -0,0 +1,2 @@ +Principais mudanças nessa versão: suporte para Login Social. +Registro de alterações completo: https://github.com/vector-im/element-android/releases/tag/v1.0.15 e https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/ru/changelogs/40100130.txt b/fastlane/metadata/android/ru/changelogs/40100130.txt index 5a594af4cf..a5a090d06a 100644 --- a/fastlane/metadata/android/ru/changelogs/40100130.txt +++ b/fastlane/metadata/android/ru/changelogs/40100130.txt @@ -1,2 +1,2 @@ Основные изменения в этой версии: предварительный просмотр URL, новая клавиатура эмодзи, новые возможности настройки комнаты и снег на Рождество! -Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.0.12 +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/ru/changelogs/40100140.txt b/fastlane/metadata/android/ru/changelogs/40100140.txt new file mode 100644 index 0000000000..8769f971eb --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/40100140.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Редактирование разрешений для комнаты, автоматическая светлая/темная тема и множество исправлений ошибок. +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/ru/changelogs/40100150.txt b/fastlane/metadata/android/ru/changelogs/40100150.txt new file mode 100644 index 0000000000..1de07b1b13 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/40100150.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Поддержка входа в социальные сети. +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/ru/changelogs/40100160.txt b/fastlane/metadata/android/ru/changelogs/40100160.txt new file mode 100644 index 0000000000..346a3d75e1 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/40100160.txt @@ -0,0 +1,2 @@ +Основные изменения в этой версии: Поддержка входа в социальные сети. +Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.0.15 and https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/sr/changelogs/40100130.txt b/fastlane/metadata/android/sr/changelogs/40100130.txt index 07ab721c42..ba9ff5ad43 100644 --- a/fastlane/metadata/android/sr/changelogs/40100130.txt +++ b/fastlane/metadata/android/sr/changelogs/40100130.txt @@ -1,2 +1,2 @@ -Главне измене у овој верзији: УРЛ преглед, нова емоџи тастатура, нове могућности у поставкама собе и снег за Божић ! -Дневник свих измена: https://github.com/vector-im/element-android/releases/tag/v1.0.12 +Главне измене у овој верзији: УРЛ преглед, нова емоџи тастатура, нове могућности у поставкама собе и снег за Божић! +Дневник свих измена: https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/sr/changelogs/40100140.txt b/fastlane/metadata/android/sr/changelogs/40100140.txt new file mode 100644 index 0000000000..8fe5bed02d --- /dev/null +++ b/fastlane/metadata/android/sr/changelogs/40100140.txt @@ -0,0 +1,2 @@ +Главна измена у овој верзији: уређивање дозвола у соби, аутоматска светла/тамна тема и гомила исправљених грешака. +Цео дневник измена: https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/sr/changelogs/40100150.txt b/fastlane/metadata/android/sr/changelogs/40100150.txt new file mode 100644 index 0000000000..6300f1f026 --- /dev/null +++ b/fastlane/metadata/android/sr/changelogs/40100150.txt @@ -0,0 +1,2 @@ +Главна измена у овој верзији: подршка за пријављивање са друштвених мрежа. +Цео дневник измена: https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/sr/changelogs/40100160.txt b/fastlane/metadata/android/sr/changelogs/40100160.txt new file mode 100644 index 0000000000..b4dbbd763a --- /dev/null +++ b/fastlane/metadata/android/sr/changelogs/40100160.txt @@ -0,0 +1,2 @@ +Главна измена у овој верзији: подршка за пријављивање са друштвених мрежа. +Цео дневник измена: https://github.com/vector-im/element-android/releases/tag/v1.0.15 и https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/sv/changelogs/40100130.txt b/fastlane/metadata/android/sv/changelogs/40100130.txt index 5aa3351aaf..a2adbf9746 100644 --- a/fastlane/metadata/android/sv/changelogs/40100130.txt +++ b/fastlane/metadata/android/sv/changelogs/40100130.txt @@ -1,2 +1,2 @@ Huvudsakliga ändringar i den här versionen: URL-förhandsgranskning, nya rumsinställningsförmågor, och en vit jul! -Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.0.12 +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/sv/changelogs/40100140.txt b/fastlane/metadata/android/sv/changelogs/40100140.txt new file mode 100644 index 0000000000..15596de911 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/40100140.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Redigering av rumsbehörigheter, automatiskt ljust/mörkt tema, och en hög buggfixar. +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/sv/changelogs/40100150.txt b/fastlane/metadata/android/sv/changelogs/40100150.txt new file mode 100644 index 0000000000..f280e5ed89 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/40100150.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Stöd för social inloggning. +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/sv/changelogs/40100160.txt b/fastlane/metadata/android/sv/changelogs/40100160.txt new file mode 100644 index 0000000000..adb520ecab --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/40100160.txt @@ -0,0 +1,2 @@ +Huvudsakliga ändringar i den här versionen: Stöd för social inloggning. +Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.0.15 och https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/uk/changelogs/40100130.txt b/fastlane/metadata/android/uk/changelogs/40100130.txt index 7342934333..c1ccdd7500 100644 --- a/fastlane/metadata/android/uk/changelogs/40100130.txt +++ b/fastlane/metadata/android/uk/changelogs/40100130.txt @@ -1,2 +1,2 @@ -Основні зміни в цій версії: попередній перегляд URL-адреси, нова клавіатура Emoji, нові можливості налаштування кімнати та сніг на Різдво! -Повний журнал змін: https://github.com/vector-im/element-android/releases/tag/v1.0.12 +Основні зміни в цій версії: попередній перегляд URL-адрес, нова клавіатура Emoji, нові можливості налаштування кімнати та сніг на Різдво! +Повний перелік змін: https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/uk/changelogs/40100140.txt b/fastlane/metadata/android/uk/changelogs/40100140.txt new file mode 100644 index 0000000000..f61bebc71c --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40100140.txt @@ -0,0 +1,2 @@ +Основні зміни цієї версії: Керування дозволами кімнати, автоперемикання між світлою/темною темами та виправлення багатьох вад. +Повний перелік змін: https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/uk/changelogs/40100150.txt b/fastlane/metadata/android/uk/changelogs/40100150.txt new file mode 100644 index 0000000000..5b76163ab0 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40100150.txt @@ -0,0 +1,2 @@ +Основні зміни цієї версії: підтримка входу за допомогою суспільних мереж. +Повний перелік змін: https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/uk/changelogs/40100160.txt b/fastlane/metadata/android/uk/changelogs/40100160.txt new file mode 100644 index 0000000000..f3fa048e24 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/40100160.txt @@ -0,0 +1,2 @@ +Основні зміни цієї версії: підтримка входу за допомогою суспільних мереж. +Повний перелік змін: https://github.com/vector-im/element-android/releases/tag/v1.0.15 та https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/fastlane/metadata/android/zh_Hant/changelogs/40100130.txt b/fastlane/metadata/android/zh_Hant/changelogs/40100130.txt index 846126af63..f42e9d3101 100644 --- a/fastlane/metadata/android/zh_Hant/changelogs/40100130.txt +++ b/fastlane/metadata/android/zh_Hant/changelogs/40100130.txt @@ -1,2 +1,2 @@ 此版本中的主要變更:URL 預覽、新的表情符號鍵盤、新的聊天室設定功能以及聖誕節降雪! -完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.12 +完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.13 diff --git a/fastlane/metadata/android/zh_Hant/changelogs/40100140.txt b/fastlane/metadata/android/zh_Hant/changelogs/40100140.txt new file mode 100644 index 0000000000..9ed2152127 --- /dev/null +++ b/fastlane/metadata/android/zh_Hant/changelogs/40100140.txt @@ -0,0 +1,2 @@ +此版本的主要變動:編輯聊天室權限、自動淺色/深色佈景主題與許多臭蟲修復。 +完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.14 diff --git a/fastlane/metadata/android/zh_Hant/changelogs/40100150.txt b/fastlane/metadata/android/zh_Hant/changelogs/40100150.txt new file mode 100644 index 0000000000..09a67d544b --- /dev/null +++ b/fastlane/metadata/android/zh_Hant/changelogs/40100150.txt @@ -0,0 +1,2 @@ +此版本的主要變動:社群網路登入支援。 +完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.15 diff --git a/fastlane/metadata/android/zh_Hant/changelogs/40100160.txt b/fastlane/metadata/android/zh_Hant/changelogs/40100160.txt new file mode 100644 index 0000000000..77606636d3 --- /dev/null +++ b/fastlane/metadata/android/zh_Hant/changelogs/40100160.txt @@ -0,0 +1,2 @@ +此版本的主要變動:社群網路登入支援。 +完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.15 以及 https://github.com/vector-im/element-android/releases/tag/v1.0.16 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 087983c1d6..30d5f428d7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,3 +1,4 @@ +#Fri Jan 29 18:05:42 CET 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionSha256Sum=1433372d903ffba27496f8d5af24265310d2da0d78bf6b4e5138831d4fe066e9 diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 836b49b3f2..dca19e7755 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -168,12 +168,6 @@ dependencies { // Phone number https://github.com/google/libphonenumber implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23' - // Web RTC - // org.webrtc:google-webrtc is for development purposes only. See http://webrtc.github.io/webrtc-org/native-code/android/ - // implementation 'org.webrtc:google-webrtc:1.0.+' - // Use the same WebRTC library than the one used by Jitsi library - implementation('com.facebook.react:react-native-webrtc:1.84.0-jitsi-5112273@aar') - testImplementation 'junit:junit:4.13' testImplementation 'org.robolectric:robolectric:4.3' //testImplementation 'org.robolectric:shadows-support-v4:3.0' diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index 725fd08d3b..93a1b962ed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -35,7 +35,11 @@ data class MatrixConfiguration( * Optional proxy to connect to the matrix servers * You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port) */ - val proxy: Proxy? = null + val proxy: Proxy? = null, + /** + * True to advertise support for call transfers to other parties on Matrix calls. + */ + val supportsCallTransfer: Boolean = false ) { /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index ff7c9f0521..039025e0df 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -48,6 +48,7 @@ import org.matrix.android.sdk.api.session.signout.SignOutService import org.matrix.android.sdk.api.session.sync.FilterService import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.terms.TermsService +import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService import org.matrix.android.sdk.api.session.typing.TypingUsersTracker import org.matrix.android.sdk.api.session.user.UserService import org.matrix.android.sdk.api.session.widgets.WidgetService @@ -212,6 +213,11 @@ interface Session : */ fun searchService(): SearchService + /** + * Returns the third party service associated with the session + */ + fun thirdPartyService(): ThirdPartyService + /** * Add a listener to the session. * @param listener the listener to add. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallsListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallListener.kt similarity index 67% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallsListener.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallListener.kt index 37ab198487..303add747f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallsListener.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallListener.kt @@ -20,8 +20,11 @@ import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent +import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent +import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent -interface CallsListener { +interface CallListener { /** * Called when there is an incoming call within the room. */ @@ -39,5 +42,23 @@ interface CallsListener { */ fun onCallHangupReceived(callHangupContent: CallHangupContent) + /** + * Called when a called has been rejected + */ + fun onCallRejectReceived(callRejectContent: CallRejectContent) + + /** + * Called when an answer has been selected + */ + fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) + + /** + * Called when a negotiation is sent + */ + fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) + + /** + * Called when the call has been managed by an other session + */ fun onCallManagedByOtherSession(callId: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt index e28c1fa595..c6bdcd19c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt @@ -28,9 +28,9 @@ interface CallSignalingService { */ fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall - fun addCallListener(listener: CallsListener) + fun addCallListener(listener: CallListener) - fun removeCallListener(listener: CallsListener) + fun removeCallListener(listener: CallListener) fun getCallWithId(callId: String): MxCall? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt index 757a09fb3f..2dbd1c9b01 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt @@ -16,13 +16,16 @@ package org.matrix.android.sdk.api.session.call -import org.webrtc.PeerConnection - sealed class CallState { /** Idle, setting up objects */ object Idle : CallState() + /** + * CreateOffer. Intermediate state between Idle and Dialing. + */ + object CreateOffer: CallState() + /** Dialing. Outgoing call is signaling the remote peer */ object Dialing : CallState() @@ -36,8 +39,8 @@ sealed class CallState { * Connected. Incoming/Outgoing call, ice layer connecting or connected * Notice that the PeerState failed is not always final, if you switch network, new ice candidtates * could be exchanged, and the connection could go back to connected - */ - data class Connected(val iceConnectionState: PeerConnection.PeerConnectionState) : CallState() + * */ + data class Connected(val iceConnectionState: MxPeerConnectionState) : CallState() /** Terminated. Incoming/Outgoing call, the call is terminated */ object Terminated : CallState() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt index a1ab687894..7533619eb0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt @@ -16,14 +16,17 @@ package org.matrix.android.sdk.api.session.call -import org.webrtc.IceCandidate -import org.webrtc.SessionDescription +import org.matrix.android.sdk.api.session.room.model.call.CallCandidate +import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities +import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.SdpType +import org.matrix.android.sdk.api.util.Optional interface MxCallDetail { val callId: String val isOutgoing: Boolean val roomId: String - val otherUserId: String + val opponentUserId: String val isVideoCall: Boolean } @@ -32,40 +35,64 @@ interface MxCallDetail { */ interface MxCall : MxCallDetail { + companion object { + const val VOIP_PROTO_VERSION = 1 + } + + val ourPartyId: String + var opponentPartyId: Optional<String>? + var opponentVersion: Int + + var capabilities: CallCapabilities? + var state: CallState /** * Pick Up the incoming call * It has no effect on outgoing call */ - fun accept(sdp: SessionDescription) + fun accept(sdpString: String) + + /** + * SDP negotiation for media pause, hold/resume, ICE restarts and voice/video call up/downgrading + */ + fun negotiate(sdpString: String, type: SdpType) + + /** + * This has to be sent by the caller's client once it has chosen an answer. + */ + fun selectAnswer() /** * Reject an incoming call - * It's an alias to hangUp */ - fun reject() = hangUp() + fun reject() /** * End the call */ - fun hangUp() + fun hangUp(reason: CallHangupContent.Reason? = null) /** * Start a call * Send offer SDP to the other participant. */ - fun offerSdp(sdp: SessionDescription) + fun offerSdp(sdpString: String) /** - * Send Ice candidate to the other participant. + * Send Call candidate to the other participant. */ - fun sendLocalIceCandidates(candidates: List<IceCandidate>) + fun sendLocalCallCandidates(candidates: List<CallCandidate>) /** * Send removed ICE candidates to the other participant. */ - fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>) + fun sendLocalIceCandidateRemovals(candidates: List<CallCandidate>) + + /** + * Send a m.call.replaces event to initiate call transfer. + */ + suspend fun transfer(targetUserId: String, targetRoomId: String?) fun addListener(listener: StateListener) fun removeListener(listener: StateListener) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxPeerConnectionState.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxPeerConnectionState.java new file mode 100644 index 0000000000..7ea0765809 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxPeerConnectionState.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.call; + +/** + * This is a copy of https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionState + * to avoid having the dependency over WebRtc library on sdk. + */ +public enum MxPeerConnectionState { + NEW, + CONNECTING, + CONNECTED, + DISCONNECTED, + FAILED, + CLOSED +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 68874a1fb1..d79117ad87 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -68,7 +68,12 @@ object EventType { const val CALL_INVITE = "m.call.invite" const val CALL_CANDIDATES = "m.call.candidates" const val CALL_ANSWER = "m.call.answer" + const val CALL_SELECT_ANSWER = "m.call.select_answer" + const val CALL_NEGOTIATE = "m.call.negotiate" + const val CALL_REJECT = "m.call.reject" const val CALL_HANGUP = "m.call.hangup" + // This type is not processed by the client, just sent to the server + const val CALL_REPLACES = "m.call.replaces" // Key share events const val ROOM_KEY_REQUEST = "m.room_key_request" @@ -98,5 +103,9 @@ object EventType { || type == CALL_CANDIDATES || type == CALL_ANSWER || type == CALL_HANGUP + || type == CALL_SELECT_ANSWER + || type == CALL_NEGOTIATE + || type == CALL_REJECT + || type == CALL_REPLACES } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt index 61970ce848..55f3b76760 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt @@ -20,7 +20,6 @@ import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse -import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.util.Cancelable /** @@ -35,12 +34,6 @@ interface RoomDirectoryService { publicRoomsParams: PublicRoomsParams, callback: MatrixCallback<PublicRoomsResponse>): Cancelable - /** - * Fetches the overall metadata about protocols supported by the homeserver. - * Includes both the available protocols and all fields required for queries against each protocol. - */ - fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable - /** * Get the visibility of a room in the directory */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt index c4d1f6486f..45a54bb379 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt @@ -27,16 +27,24 @@ data class CallAnswerContent( /** * Required. The ID of the call this event relates to. */ - @Json(name = "call_id") val callId: String, + @Json(name = "call_id") override val callId: String, + /** + * Required. ID to let user identify remote echo of their own events + */ + @Json(name = "party_id") override val partyId: String? = null, /** * Required. The session description object */ @Json(name = "answer") val answer: Answer, /** - * Required. The version of the VoIP specification this messages adheres to. This specification is version 0. + * Required. The version of the VoIP specification this messages adheres to. */ - @Json(name = "version") val version: Int = 0 -) { + @Json(name = "version") override val version: String?, + /** + * Capability advertisement. + */ + @Json(name = "capabilities") val capabilities: CallCapabilities? = null +): CallSignallingContent { @JsonClass(generateAdapter = true) data class Answer( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidate.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidate.kt new file mode 100644 index 0000000000..ace8c5a757 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidate.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class CallCandidate( + /** + * Required. The SDP media type this candidate is intended for. + */ + @Json(name = "sdpMid") val sdpMid: String? = null, + /** + * Required. The index of the SDP 'm' line this candidate is intended for. + */ + @Json(name = "sdpMLineIndex") val sdpMLineIndex: Int = 0, + /** + * Required. The SDP 'a' line of the candidate. + */ + @Json(name = "candidate") val candidate: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt index cad2356c2d..7bfe7a97ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt @@ -28,30 +28,17 @@ data class CallCandidatesContent( /** * Required. The ID of the call this event relates to. */ - @Json(name = "call_id") val callId: String, + @Json(name = "call_id") override val callId: String, + /** + * Required. ID to let user identify remote echo of their own events + */ + @Json(name = "party_id") override val partyId: String? = null, /** * Required. Array of objects describing the candidates. */ - @Json(name = "candidates") val candidates: List<Candidate> = emptyList(), + @Json(name = "candidates") val candidates: List<CallCandidate> = emptyList(), /** - * Required. The version of the VoIP specification this messages adheres to. This specification is version 0. + * Required. The version of the VoIP specification this messages adheres to. */ - @Json(name = "version") val version: Int = 0 -) { - - @JsonClass(generateAdapter = true) - data class Candidate( - /** - * Required. The SDP media type this candidate is intended for. - */ - @Json(name = "sdpMid") val sdpMid: String, - /** - * Required. The index of the SDP 'm' line this candidate is intended for. - */ - @Json(name = "sdpMLineIndex") val sdpMLineIndex: Int, - /** - * Required. The SDP 'a' line of the candidate. - */ - @Json(name = "candidate") val candidate: String - ) -} + @Json(name = "version") override val version: String? +): CallSignallingContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCapabilities.kt new file mode 100644 index 0000000000..d911ca3b88 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCapabilities.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.extensions.orFalse + +@JsonClass(generateAdapter = true) +data class CallCapabilities( + /** + * If set to true, states that the sender of the event supports the m.call.replaces event and therefore supports + * being transferred to another destination + */ + @Json(name = "m.call.transferee") val transferee: Boolean? = null +) + +fun CallCapabilities?.supportCallTransfer() = this?.transferee.orFalse() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt index d30441df4b..0acc409053 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt @@ -28,24 +28,41 @@ data class CallHangupContent( /** * Required. The ID of the call this event relates to. */ - @Json(name = "call_id") val callId: String, + @Json(name = "call_id") override val callId: String, /** - * Required. The version of the VoIP specification this message adheres to. This specification is version 0. + * Required. ID to let user identify remote echo of their own events */ - @Json(name = "version") val version: Int = 0, + @Json(name = "party_id") override val partyId: String? = null, + /** + * Required. The version of the VoIP specification this message adheres to. + */ + @Json(name = "version") override val version: String?, /** * Optional error reason for the hangup. This should not be provided when the user naturally ends or rejects the call. * When there was an error in the call negotiation, this should be `ice_failed` for when ICE negotiation fails - * or `invite_timeout` for when the other party did not answer in time. One of: ["ice_failed", "invite_timeout"] + * or `invite_timeout` for when the other party did not answer in time. + * One of: ["ice_failed", "invite_timeout"] */ @Json(name = "reason") val reason: Reason? = null -) { +) : CallSignallingContent { @JsonClass(generateAdapter = false) enum class Reason { @Json(name = "ice_failed") ICE_FAILED, + @Json(name = "ice_timeout") + ICE_TIMEOUT, + + @Json(name = "user_hangup") + USER_HANGUP, + + @Json(name = "user_media_failed") + USER_MEDIA_FAILED, + @Json(name = "invite_timeout") - INVITE_TIMEOUT + INVITE_TIMEOUT, + + @Json(name = "unknown_error") + UNKWOWN_ERROR } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt index b961a6f654..42489bc0ce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt @@ -27,22 +27,35 @@ data class CallInviteContent( /** * Required. A unique identifier for the call. */ - @Json(name = "call_id") val callId: String?, + @Json(name = "call_id") override val callId: String?, + /** + * Required. ID to let user identify remote echo of their own events + */ + @Json(name = "party_id") override val partyId: String? = null, /** * Required. The session description object */ @Json(name = "offer") val offer: Offer?, /** - * Required. The version of the VoIP specification this message adheres to. This specification is version 0. + * Required. The version of the VoIP specification this message adheres to. */ - @Json(name = "version") val version: Int? = 0, + @Json(name = "version") override val version: String?, /** * Required. The time in milliseconds that the invite is valid for. * Once the invite age exceeds this value, clients should discard it. * They should also no longer show the call as awaiting an answer in the UI. */ - @Json(name = "lifetime") val lifetime: Int? -) { + @Json(name = "lifetime") val lifetime: Int?, + /** + * The field should be added for all invites where the target is a specific user + */ + @Json(name = "invitee") val invitee: String? = null, + /** + * Capability advertisement. + */ + @Json(name = "capabilities") val capabilities: CallCapabilities? = null + +): CallSignallingContent { @JsonClass(generateAdapter = true) data class Offer( /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallNegotiateContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallNegotiateContent.kt new file mode 100644 index 0000000000..040993bb51 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallNegotiateContent.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This introduces SDP negotiation semantics for media pause, hold/resume, ICE restarts and voice/video call up/downgrading. + */ +@JsonClass(generateAdapter = true) +data class CallNegotiateContent( + /** + * Required. The ID of the call this event relates to. + */ + @Json(name = "call_id") override val callId: String, + /** + * Required. ID to let user identify remote echo of their own events + */ + @Json(name = "party_id") override val partyId: String? = null, + /** + * Required. The time in milliseconds that the negotiation is valid for. Once exceeded the sender + * of the negotiate event should consider the negotiation failed (timed out) and the recipient should ignore it. + **/ + @Json(name = "lifetime") val lifetime: Int?, + /** + * Required. The session description object + */ + @Json(name = "description") val description: Description? = null, + + /** + * Required. The version of the VoIP specification this message adheres to. + */ + @Json(name = "version") override val version: String? + + ): CallSignallingContent { + @JsonClass(generateAdapter = true) + data class Description( + /** + * Required. The type of session description. + */ + @Json(name = "type") val type: SdpType?, + /** + * Required. The SDP text of the session description. + */ + @Json(name = "sdp") val sdp: String? + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt new file mode 100644 index 0000000000..1da229b179 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Sent by either party to signal their termination of the call. This can be sent either once + * the call has been established or before to abort the call. + */ +@JsonClass(generateAdapter = true) +data class CallRejectContent( + /** + * Required. The ID of the call this event relates to. + */ + @Json(name = "call_id") override val callId: String, + /** + * Required. ID to let user identify remote echo of their own events + */ + @Json(name = "party_id") override val partyId: String? = null, + /** + * Required. The version of the VoIP specification this message adheres to. + */ + @Json(name = "version") override val version: String? +) : CallSignallingContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt new file mode 100644 index 0000000000..97a3b8c7a7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This event is sent to signal the intent of a participant in a call to replace the call with another, + * such that the other participant ends up in a call with a new user. + */ +@JsonClass(generateAdapter = true) +data class CallReplacesContent( + /** + * Required. The ID of the call this event relates to. + */ + @Json(name = "call_id") override val callId: String, + /** + * Required. ID to let user identify remote echo of their own events + */ + @Json(name = "party_id") override val partyId: String? = null, + /** + * An identifier for the call replacement itself, generated by the transferor. + */ + @Json(name = "replacement_id") val replacementId: String? = null, + /** + * Optional. If specified, the transferee client waits for an invite to this room and joins it + * (possibly waiting for user confirmation) and then continues the transfer in this room. + * If absent, the transferee contacts the Matrix User ID given in the target_user field in a room of its choosing. + */ + @Json(name = "target_room") val targerRoomId: String? = null, + /** + * An object giving information about the transfer target + */ + @Json(name = "target_user") val targetUser: TargetUser? = null, + /** + * If specified, gives the call ID for the transferee's client to use when placing the replacement call. + * Mutually exclusive with await_call + */ + @Json(name = "create_call") val createCall: String? = null, + /** + * If specified, gives the call ID that the transferee's client should wait for. + * Mutually exclusive with create_call. + */ + @Json(name = "await_call") val awaitCall: String? = null, + /** + * Required. The version of the VoIP specification this messages adheres to. + */ + @Json(name = "version") override val version: String? +): CallSignallingContent { + + @JsonClass(generateAdapter = true) + data class TargetUser( + /** + * Required. The matrix user ID of the transfer target + */ + @Json(name = "id") val id: String, + /** + * Optional. The display name of the transfer target. + */ + @Json(name = "display_name") val displayName: String?, + /** + * Optional. The avatar URL of the transfer target. + */ + @Json(name = "avatar_url") val avatarUrl: String? + + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSelectAnswerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSelectAnswerContent.kt new file mode 100644 index 0000000000..6ea70ac990 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSelectAnswerContent.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This event is sent by the callee when they wish to answer the call. + */ +@JsonClass(generateAdapter = true) +data class CallSelectAnswerContent( + /** + * Required. The ID of the call this event relates to. + */ + @Json(name = "call_id") override val callId: String, + /** + * Required. ID to let user identify remote echo of their own events + */ + @Json(name = "party_id") override val partyId: String? = null, + /** + * Required. Indicates the answer user has chosen. + */ + @Json(name = "selected_party_id") val selectedPartyId: String? = null, + + /** + * Required. The version of the VoIP specification this message adheres to. + */ + @Json(name = "version") override val version: String? +): CallSignallingContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSignallingContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSignallingContent.kt new file mode 100644 index 0000000000..f8d8c2a5e8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSignallingContent.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +interface CallSignallingContent { + /** + * Required. A unique identifier for the call. + */ + val callId: String? + + /** + * Required. ID to let user identify remote echo of their own events + */ + val partyId: String? + + /** + * Required. The version of the VoIP specification this message adheres to. This specification is version 0. + */ + val version: String? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/SdpType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/SdpType.kt index ff393135ea..9b55ab80c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/SdpType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/SdpType.kt @@ -25,5 +25,5 @@ enum class SdpType { OFFER, @Json(name = "answer") - ANSWER + ANSWER; } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt index dcaf5f3276..ef6300eae2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt @@ -20,11 +20,15 @@ import org.matrix.android.sdk.api.session.events.model.EventType object RoomSummaryConstants { + /** + * + */ val PREVIEWABLE_TYPES = listOf( // TODO filter message type (KEY_VERIFICATION_READY, etc.) EventType.MESSAGE, EventType.CALL_INVITE, EventType.CALL_HANGUP, + EventType.CALL_REJECT, EventType.CALL_ANSWER, EventType.ENCRYPTED, EventType.STICKER, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index b10fb540e1..53f0e5a8d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -52,6 +52,8 @@ data class TimelineEvent( } } + val roomId = root.roomId ?: "" + val metadata = HashMap<String, Any>() /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt new file mode 100644 index 0000000000..2ae4562b0b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.thirdparty + +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser + +/** + * See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-thirdparty-protocols + */ +interface ThirdPartyService { + + /** + * Fetches the overall metadata about protocols supported by the homeserver. + * Includes both the available protocols and all fields required for queries against each protocol. + */ + suspend fun getThirdPartyProtocols(): Map<String, ThirdPartyProtocol> + + /** + * Retrieve a Matrix User ID linked to a user on the third party service, given a set of user parameters. + * @param protocol Required. The name of the protocol. + * @param fields One or more custom fields that are passed to the AS to help identify the user. + */ + suspend fun getThirdPartyUser(protocol: String, fields: Map<String, String> = emptyMap()): List<ThirdPartyUser> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/model/ThirdPartyUser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/model/ThirdPartyUser.kt new file mode 100644 index 0000000000..d77dfcfe35 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/model/ThirdPartyUser.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.thirdparty.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +@JsonClass(generateAdapter = true) +data class ThirdPartyUser( + /* + Required. A Matrix User ID represting a third party user. + */ + @Json(name = "userid") val userId: String, + /* + Required. The protocol ID that the third party location is a part of. + */ + @Json(name = "protocol") val protocol: String, + /* + Required. Information used to identify this third party location. + */ + @Json(name = "fields") val fields: JsonDict +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt index 0310a3d001..bf3ff8959d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt @@ -56,6 +56,11 @@ interface WidgetService { excludedTypes: Set<String>? = null ): List<Widget> + /** + * Return the computed URL of a widget + */ + fun getWidgetComputedUrl(widget: Widget, isLightTheme: Boolean): String? + /** * Returns the live room widgets so you can listen to them. * Some widgets can be deactivated, so be sure to check for isActive. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/Widget.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/Widget.kt index c8465d4d2e..86aaba7f6f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/Widget.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/Widget.kt @@ -25,7 +25,6 @@ data class Widget( val widgetId: String, val senderInfo: SenderInfo?, val isAddedByMe: Boolean, - val computedUrl: String?, val type: WidgetType ) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt index 71f978c03c..88aa432fb3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt @@ -71,7 +71,6 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real return@forEach } val domainEvent = event.asDomain() -// decryptIfNeeded(domainEvent) processors.filter { it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType) }.forEach { @@ -83,6 +82,7 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real .findAll() .deleteAllFromRealm() } + processors.forEach { it.onPostProcess() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index e09c051c81..890f3a6ac3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -49,6 +49,7 @@ import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageServi import org.matrix.android.sdk.api.session.signout.SignOutService import org.matrix.android.sdk.api.session.sync.FilterService import org.matrix.android.sdk.api.session.terms.TermsService +import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService import org.matrix.android.sdk.api.session.typing.TypingUsersTracker import org.matrix.android.sdk.api.session.user.UserService import org.matrix.android.sdk.api.session.widgets.WidgetService @@ -114,6 +115,7 @@ internal class DefaultSession @Inject constructor( private val accountService: Lazy<AccountService>, private val defaultIdentityService: DefaultIdentityService, private val integrationManagerService: IntegrationManagerService, + private val thirdPartyService: Lazy<ThirdPartyService>, private val callSignalingService: Lazy<CallSignalingService>, @UnauthenticatedWithCertificate private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>, @@ -258,6 +260,8 @@ internal class DefaultSession @Inject constructor( override fun searchService(): SearchService = searchService.get() + override fun thirdPartyService(): ThirdPartyService = thirdPartyService.get() + override fun getOkHttpClient(): OkHttpClient { return unauthenticatedWithCertificateOkHttpClient.get() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt index 53b1a73544..7a687b774b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt @@ -25,4 +25,12 @@ internal interface EventInsertLiveProcessor { fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean suspend fun process(realm: Realm, event: Event) + + /** + * Called after transaction. + * Maybe you prefer to process the events outside of the realm transaction. + */ + suspend fun onPostProcess() { + // Noop by default + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index f5eade1704..9279c5c97a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -56,6 +56,7 @@ import org.matrix.android.sdk.internal.session.sync.SyncTask import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.session.sync.job.SyncWorker import org.matrix.android.sdk.internal.session.terms.TermsModule +import org.matrix.android.sdk.internal.session.thirdparty.ThirdPartyModule import org.matrix.android.sdk.internal.session.user.UserModule import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataModule import org.matrix.android.sdk.internal.session.widgets.WidgetModule @@ -87,7 +88,8 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers ProfileModule::class, AccountModule::class, CallModule::class, - SearchModule::class + SearchModule::class, + ThirdPartyModule::class ] ) @SessionScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt index f789a64500..4887351709 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt @@ -16,28 +16,30 @@ package org.matrix.android.sdk.internal.session.call +import io.realm.Realm import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.internal.database.model.EventInsertType -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor -import io.realm.Realm import timber.log.Timber import javax.inject.Inject -internal class CallEventProcessor @Inject constructor( - @UserId private val userId: String, - private val callService: DefaultCallSignalingService -) : EventInsertLiveProcessor { +internal class CallEventProcessor @Inject constructor(private val callSignalingHandler: CallSignalingHandler) + : EventInsertLiveProcessor { private val allowedTypes = listOf( EventType.CALL_ANSWER, + EventType.CALL_SELECT_ANSWER, + EventType.CALL_REJECT, + EventType.CALL_NEGOTIATE, EventType.CALL_CANDIDATES, EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.ENCRYPTED ) + private val eventsToPostProcess = mutableListOf<Event>() + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { if (insertType != EventInsertType.INCREMENTAL_SYNC) { return false @@ -46,10 +48,17 @@ internal class CallEventProcessor @Inject constructor( } override suspend fun process(realm: Realm, event: Event) { - update(realm, event) + eventsToPostProcess.add(event) } - private fun update(realm: Realm, event: Event) { + override suspend fun onPostProcess() { + eventsToPostProcess.forEach { + dispatchToCallSignalingHandlerIfNeeded(it) + } + eventsToPostProcess.clear() + } + + private fun dispatchToCallSignalingHandlerIfNeeded(event: Event) { val now = System.currentTimeMillis() // TODO might check if an invite is not closed (hangup/answsered) in the same event batch? event.roomId ?: return Unit.also { @@ -60,10 +69,6 @@ internal class CallEventProcessor @Inject constructor( // To old to ring? return } - event.ageLocalTs - if (EventType.isCallEvent(event.getClearType())) { - callService.onCallEvent(event) - } - Timber.v("$realm : $userId") + callSignalingHandler.onCallEvent(event) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallListenersDispatcher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallListenersDispatcher.kt new file mode 100644 index 0000000000..1de2d8a106 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallListenersDispatcher.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.call + +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.call.CallListener +import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent +import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent +import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent +import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent + +/** + * Dispatch each method safely to all listeners. + */ +internal class CallListenersDispatcher(private val listeners: Set<CallListener>) : CallListener { + + override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) = dispatch { + it.onCallInviteReceived(mxCall, callInviteContent) + } + + override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) = dispatch { + it.onCallIceCandidateReceived(mxCall, iceCandidatesContent) + } + + override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) = dispatch { + it.onCallAnswerReceived(callAnswerContent) + } + + override fun onCallHangupReceived(callHangupContent: CallHangupContent) = dispatch { + it.onCallHangupReceived(callHangupContent) + } + + override fun onCallRejectReceived(callRejectContent: CallRejectContent) = dispatch { + it.onCallRejectReceived(callRejectContent) + } + + override fun onCallManagedByOtherSession(callId: String) = dispatch { + it.onCallManagedByOtherSession(callId) + } + + override fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) = dispatch { + it.onCallSelectAnswerReceived(callSelectAnswerContent) + } + + override fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) = dispatch { + it.onCallNegotiateReceived(callNegotiateContent) + } + + private fun dispatch(lambda: (CallListener) -> Unit) { + listeners.toList().forEach { + tryOrNull { + lambda(it) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt new file mode 100644 index 0000000000..7e54301f63 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.call + +import org.matrix.android.sdk.api.session.call.CallListener +import org.matrix.android.sdk.api.session.call.CallState +import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent +import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities +import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent +import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent +import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallSignallingContent +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionScope +import timber.log.Timber +import java.math.BigDecimal +import javax.inject.Inject + +@SessionScope +internal class CallSignalingHandler @Inject constructor(private val activeCallHandler: ActiveCallHandler, + private val mxCallFactory: MxCallFactory, + @UserId private val userId: String) { + + private val callListeners = mutableSetOf<CallListener>() + private val callListenersDispatcher = CallListenersDispatcher(callListeners) + + fun addCallListener(listener: CallListener) { + callListeners.add(listener) + } + + fun removeCallListener(listener: CallListener) { + callListeners.remove(listener) + } + + fun onCallEvent(event: Event) { + when (event.getClearType()) { + EventType.CALL_ANSWER -> { + handleCallAnswerEvent(event) + } + EventType.CALL_INVITE -> { + handleCallInviteEvent(event) + } + EventType.CALL_HANGUP -> { + handleCallHangupEvent(event) + } + EventType.CALL_REJECT -> { + handleCallRejectEvent(event) + } + EventType.CALL_CANDIDATES -> { + handleCallCandidatesEvent(event) + } + EventType.CALL_SELECT_ANSWER -> { + handleCallSelectAnswerEvent(event) + } + EventType.CALL_NEGOTIATE -> { + handleCallNegotiateEvent(event) + } + } + } + + private fun handleCallNegotiateEvent(event: Event) { + val content = event.getClearContent().toModel<CallNegotiateContent>() ?: return + val call = content.getCall() ?: return + if (call.ourPartyId == content.partyId) { + // Ignore remote echo + return + } + callListenersDispatcher.onCallNegotiateReceived(content) + } + + private fun handleCallSelectAnswerEvent(event: Event) { + val content = event.getClearContent().toModel<CallSelectAnswerContent>() ?: return + val call = content.getCall() ?: return + if (call.ourPartyId == content.partyId) { + // Ignore remote echo + return + } + if (call.isOutgoing) { + Timber.v("Got selectAnswer for an outbound call: ignoring") + return + } + val selectedPartyId = content.selectedPartyId + if (selectedPartyId == null) { + Timber.w("Got nonsensical select_answer with null selected_party_id: ignoring") + return + } + callListenersDispatcher.onCallSelectAnswerReceived(content) + } + + private fun handleCallCandidatesEvent(event: Event) { + val content = event.getClearContent().toModel<CallCandidatesContent>() ?: return + val call = content.getCall() ?: return + if (call.ourPartyId == content.partyId) { + // Ignore remote echo + return + } + if (call.opponentPartyId != null && !call.partyIdsMatches(content)) { + Timber.v("Ignoring candidates from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}") + return + } + callListenersDispatcher.onCallIceCandidateReceived(call, content) + } + + private fun handleCallRejectEvent(event: Event) { + val content = event.getClearContent().toModel<CallRejectContent>() ?: return + val call = content.getCall() ?: return + if (call.ourPartyId == content.partyId) { + // Ignore remote echo + return + } + activeCallHandler.removeCall(content.callId) + if (event.senderId == userId) { + // discard current call, it's rejected by another of my session + callListenersDispatcher.onCallManagedByOtherSession(content.callId) + return + } + // No need to check party_id for reject because if we'd received either + // an answer or reject, we wouldn't be in state InviteSent + if (call.state != CallState.Dialing) { + return + } + callListenersDispatcher.onCallRejectReceived(content) + } + + private fun handleCallHangupEvent(event: Event) { + val content = event.getClearContent().toModel<CallHangupContent>() ?: return + val call = content.getCall() ?: return + // party ID must match (our chosen partner hanging up the call) or be undefined (we haven't chosen + // a partner yet but we're treating the hangup as a reject as per VoIP v0) + if (call.opponentPartyId != null && !call.partyIdsMatches(content)) { + Timber.v("Ignoring hangup from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}") + return + } + if (call.state != CallState.Terminated) { + activeCallHandler.removeCall(content.callId) + callListenersDispatcher.onCallHangupReceived(content) + } + } + + private fun handleCallInviteEvent(event: Event) { + if (event.senderId == userId) { + // ignore invites you send + return + } + if (event.roomId == null || event.senderId == null) { + return + } + val content = event.getClearContent().toModel<CallInviteContent>() ?: return + val incomingCall = mxCallFactory.createIncomingCall( + roomId = event.roomId, + opponentUserId = event.senderId, + content = content + ) ?: return + activeCallHandler.addCall(incomingCall) + callListenersDispatcher.onCallInviteReceived(incomingCall, content) + } + + private fun handleCallAnswerEvent(event: Event) { + val content = event.getClearContent().toModel<CallAnswerContent>() ?: return + val call = content.getCall() ?: return + if (call.ourPartyId == content.partyId) { + // Ignore remote echo + return + } + if (event.senderId == userId) { + // discard current call, it's answered by another of my session + activeCallHandler.removeCall(call.callId) + callListenersDispatcher.onCallManagedByOtherSession(content.callId) + } else { + if (call.opponentPartyId != null) { + Timber.v("Ignoring answer from party ID ${content.partyId} we already have an answer from ${call.opponentPartyId}") + return + } + call.apply { + opponentPartyId = Optional.from(content.partyId) + opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION + capabilities = content.capabilities ?: CallCapabilities() + } + callListenersDispatcher.onCallAnswerReceived(content) + } + } + + private fun MxCall.partyIdsMatches(contentSignallingContent: CallSignallingContent): Boolean { + return opponentPartyId?.getOrNull() == contentSignallingContent.partyId + } + + private fun CallSignallingContent.getCall(): MxCall? { + val currentCall = callId?.let { + activeCallHandler.getCallWithId(it) + } + if (currentCall == null) { + Timber.v("Call with id $callId is null") + } + return currentCall + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt index 019da27d27..10690c59de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt @@ -16,106 +16,46 @@ package org.matrix.android.sdk.internal.session.call -import android.os.SystemClock +import kotlinx.coroutines.Dispatchers import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.call.CallListener import org.matrix.android.sdk.api.session.call.CallSignalingService -import org.matrix.android.sdk.api.session.call.CallState -import org.matrix.android.sdk.api.session.call.CallsListener import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.TurnServerResponse -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent -import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent -import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent -import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.util.Cancelable -import org.matrix.android.sdk.api.util.NoOpCancellable -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.session.call.model.MxCallImpl -import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor -import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith +import org.matrix.android.sdk.internal.task.launchToCallback import timber.log.Timber -import java.util.UUID import javax.inject.Inject @SessionScope internal class DefaultCallSignalingService @Inject constructor( - @UserId - private val userId: String, + private val callSignalingHandler: CallSignalingHandler, + private val mxCallFactory: MxCallFactory, private val activeCallHandler: ActiveCallHandler, - private val localEchoEventFactory: LocalEchoEventFactory, - private val eventSenderProcessor: EventSenderProcessor, private val taskExecutor: TaskExecutor, - private val turnServerTask: GetTurnServerTask + private val turnServerDataSource: TurnServerDataSource ) : CallSignalingService { - private val callListeners = mutableSetOf<CallsListener>() - - private val cachedTurnServerResponse = object { - // Keep one minute safe to avoid considering the data is valid and then actually it is not when effectively using it. - private val MIN_TTL = 60 - - private val now = { SystemClock.elapsedRealtime() / 1000 } - - private var expiresAt: Long = 0 - - var data: TurnServerResponse? = null - get() = if (expiresAt > now()) field else null - set(value) { - expiresAt = now() + (value?.ttl ?: 0) - MIN_TTL - field = value - } - } - override fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable { - if (cachedTurnServerResponse.data != null) { - cachedTurnServerResponse.data?.let { callback.onSuccess(it) } - return NoOpCancellable + return taskExecutor.executorScope.launchToCallback(Dispatchers.Default, callback) { + turnServerDataSource.getTurnServer() } - return turnServerTask - .configureWith(GetTurnServerTask.Params) { - this.callback = object : MatrixCallback<TurnServerResponse> { - override fun onSuccess(data: TurnServerResponse) { - cachedTurnServerResponse.data = data - callback.onSuccess(data) - } - - override fun onFailure(failure: Throwable) { - callback.onFailure(failure) - } - } - } - .executeBy(taskExecutor) } override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall { - val call = MxCallImpl( - callId = UUID.randomUUID().toString(), - isOutgoing = true, - roomId = roomId, - userId = userId, - otherUserId = otherUserId, - isVideoCall = isVideoCall, - localEchoEventFactory = localEchoEventFactory, - eventSenderProcessor = eventSenderProcessor - ) - activeCallHandler.addCall(call).also { - return call + return mxCallFactory.createOutgoingCall(roomId, otherUserId, isVideoCall).also { + activeCallHandler.addCall(it) } } - override fun addCallListener(listener: CallsListener) { - callListeners.add(listener) + override fun addCallListener(listener: CallListener) { + callSignalingHandler.addCallListener(listener) } - override fun removeCallListener(listener: CallsListener) { - callListeners.remove(listener) + override fun removeCallListener(listener: CallListener) { + callSignalingHandler.removeCallListener(listener) } override fun getCallWithId(callId: String): MxCall? { @@ -127,129 +67,6 @@ internal class DefaultCallSignalingService @Inject constructor( return activeCallHandler.getActiveCallsLiveData().value?.isNotEmpty() == true } - internal fun onCallEvent(event: Event) { - when (event.getClearType()) { - EventType.CALL_ANSWER -> { - event.getClearContent().toModel<CallAnswerContent>()?.let { - if (event.senderId == userId) { - // ok it's an answer from me.. is it remote echo or other session - val knownCall = getCallWithId(it.callId) - if (knownCall == null) { - Timber.d("## VOIP onCallEvent ${event.getClearType()} id ${it.callId} send by me") - } else if (!knownCall.isOutgoing) { - // incoming call - // if it was anwsered by this session, the call state would be in Answering(or connected) state - if (knownCall.state == CallState.LocalRinging) { - // discard current call, it's answered by another of my session - onCallManageByOtherSession(it.callId) - } - } - return - } - - onCallAnswer(it) - } - } - EventType.CALL_INVITE -> { - if (event.senderId == userId) { - // Always ignore local echos of invite - return - } - - event.getClearContent().toModel<CallInviteContent>()?.let { content -> - val incomingCall = MxCallImpl( - callId = content.callId ?: return@let, - isOutgoing = false, - roomId = event.roomId ?: return@let, - userId = userId, - otherUserId = event.senderId ?: return@let, - isVideoCall = content.isVideo(), - localEchoEventFactory = localEchoEventFactory, - eventSenderProcessor = eventSenderProcessor - ) - activeCallHandler.addCall(incomingCall) - onCallInvite(incomingCall, content) - } - } - EventType.CALL_HANGUP -> { - event.getClearContent().toModel<CallHangupContent>()?.let { content -> - - if (event.senderId == userId) { - // ok it's an answer from me.. is it remote echo or other session - val knownCall = getCallWithId(content.callId) - if (knownCall == null) { - Timber.d("## VOIP onCallEvent ${event.getClearType()} id ${content.callId} send by me") - } else if (!knownCall.isOutgoing) { - // incoming call - if (knownCall.state == CallState.LocalRinging) { - // discard current call, it's answered by another of my session - onCallManageByOtherSession(content.callId) - } - } - return - } - - activeCallHandler.removeCall(content.callId) - onCallHangup(content) - } - } - EventType.CALL_CANDIDATES -> { - if (event.senderId == userId) { - // Always ignore local echos of invite - return - } - event.getClearContent().toModel<CallCandidatesContent>()?.let { content -> - activeCallHandler.getCallWithId(content.callId)?.let { - onCallIceCandidate(it, content) - } - } - } - } - } - - private fun onCallHangup(hangup: CallHangupContent) { - callListeners.toList().forEach { - tryOrNull { - it.onCallHangupReceived(hangup) - } - } - } - - private fun onCallAnswer(answer: CallAnswerContent) { - callListeners.toList().forEach { - tryOrNull { - it.onCallAnswerReceived(answer) - } - } - } - - private fun onCallManageByOtherSession(callId: String) { - callListeners.toList().forEach { - tryOrNull { - it.onCallManagedByOtherSession(callId) - } - } - } - - private fun onCallInvite(incomingCall: MxCall, invite: CallInviteContent) { - // Ignore the invitation from current user - if (incomingCall.otherUserId == userId) return - - callListeners.toList().forEach { - tryOrNull { - it.onCallInviteReceived(incomingCall, invite) - } - } - } - - private fun onCallIceCandidate(incomingCall: MxCall, candidates: CallCandidatesContent) { - callListeners.toList().forEach { - tryOrNull { - it.onCallIceCandidateReceived(incomingCall, candidates) - } - } - } - companion object { const val CALL_TIMEOUT_MS = 120_000 } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt new file mode 100644 index 0000000000..b14cdca63c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.call + +import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities +import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.di.DeviceId +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.call.model.MxCallImpl +import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor +import java.math.BigDecimal +import java.util.UUID +import javax.inject.Inject + +internal class MxCallFactory @Inject constructor( + @DeviceId private val deviceId: String?, + private val localEchoEventFactory: LocalEchoEventFactory, + private val eventSenderProcessor: EventSenderProcessor, + private val matrixConfiguration: MatrixConfiguration, + private val getProfileInfoTask: GetProfileInfoTask, + @UserId private val userId: String +) { + + fun createIncomingCall(roomId: String, opponentUserId: String, content: CallInviteContent): MxCall? { + content.callId ?: return null + return MxCallImpl( + callId = content.callId, + isOutgoing = false, + roomId = roomId, + userId = userId, + ourPartyId = deviceId ?: "", + opponentUserId = opponentUserId, + isVideoCall = content.isVideo(), + localEchoEventFactory = localEchoEventFactory, + eventSenderProcessor = eventSenderProcessor, + matrixConfiguration = matrixConfiguration, + getProfileInfoTask = getProfileInfoTask + ).apply { + opponentPartyId = Optional.from(content.partyId) + opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION + capabilities = content.capabilities ?: CallCapabilities() + } + } + + fun createOutgoingCall(roomId: String, opponentUserId: String, isVideoCall: Boolean): MxCall { + return MxCallImpl( + callId = UUID.randomUUID().toString(), + isOutgoing = true, + roomId = roomId, + userId = userId, + ourPartyId = deviceId ?: "", + opponentUserId = opponentUserId, + isVideoCall = isVideoCall, + localEchoEventFactory = localEchoEventFactory, + eventSenderProcessor = eventSenderProcessor, + matrixConfiguration = matrixConfiguration, + getProfileInfoTask = getProfileInfoTask + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/TurnServerDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/TurnServerDataSource.kt new file mode 100644 index 0000000000..8e2ac5e17e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/TurnServerDataSource.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.call + +import android.os.SystemClock +import org.matrix.android.sdk.api.session.call.TurnServerResponse +import javax.inject.Inject + +internal class TurnServerDataSource @Inject constructor(private val turnServerTask: GetTurnServerTask) { + + private val cachedTurnServerResponse = object { + // Keep one minute safe to avoid considering the data is valid and then actually it is not when effectively using it. + private val MIN_TTL = 60 + + private val now = { SystemClock.elapsedRealtime() / 1000 } + + private var expiresAt: Long = 0 + + var data: TurnServerResponse? = null + get() = if (expiresAt > now()) field else null + set(value) { + expiresAt = now() + (value?.ttl ?: 0) - MIN_TTL + field = value + } + } + + suspend fun getTurnServer(): TurnServerResponse { + return cachedTurnServerResponse.data ?: turnServerTask.execute(GetTurnServerTask.Params).also { + cachedTurnServerResponse.data = it + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt index 6c0d437a60..88fba0ea85 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.call.model +import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.events.model.Content @@ -24,28 +25,44 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.UnsignedData import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallCandidate import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent +import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent +import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent +import org.matrix.android.sdk.api.session.room.model.call.CallReplacesContent +import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.SdpType +import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService -import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor +import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory -import org.webrtc.IceCandidate -import org.webrtc.SessionDescription +import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import timber.log.Timber +import java.util.UUID internal class MxCallImpl( override val callId: String, override val isOutgoing: Boolean, override val roomId: String, private val userId: String, - override val otherUserId: String, + override val opponentUserId: String, override val isVideoCall: Boolean, + override val ourPartyId: String, private val localEchoEventFactory: LocalEchoEventFactory, - private val eventSenderProcessor: EventSenderProcessor + private val eventSenderProcessor: EventSenderProcessor, + private val matrixConfiguration: MatrixConfiguration, + private val getProfileInfoTask: GetProfileInfoTask ) : MxCall { + override var opponentPartyId: Optional<String>? = null + override var opponentVersion: Int = MxCall.VOIP_PROTO_VERSION + override var capabilities: CallCapabilities? = null + override var state: CallState = CallState.Idle set(value) { field = value @@ -81,60 +98,135 @@ internal class MxCallImpl( } } - override fun offerSdp(sdp: SessionDescription) { + override fun offerSdp(sdpString: String) { if (!isOutgoing) return Timber.v("## VOIP offerSdp $callId") state = CallState.Dialing CallInviteContent( callId = callId, + partyId = ourPartyId, lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS, - offer = CallInviteContent.Offer(sdp = sdp.description) + offer = CallInviteContent.Offer(sdp = sdpString), + version = MxCall.VOIP_PROTO_VERSION.toString(), + capabilities = buildCapabilities() ) .let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) } .also { eventSenderProcessor.postEvent(it) } } - override fun sendLocalIceCandidates(candidates: List<IceCandidate>) { + override fun sendLocalCallCandidates(candidates: List<CallCandidate>) { + Timber.v("Send local call canditates $callId: $candidates") CallCandidatesContent( callId = callId, - candidates = candidates.map { - CallCandidatesContent.Candidate( - sdpMid = it.sdpMid, - sdpMLineIndex = it.sdpMLineIndex, - candidate = it.sdp - ) - } + partyId = ourPartyId, + candidates = candidates, + version = MxCall.VOIP_PROTO_VERSION.toString() ) .let { createEventAndLocalEcho(type = EventType.CALL_CANDIDATES, roomId = roomId, content = it.toContent()) } .also { eventSenderProcessor.postEvent(it) } } - override fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>) { + override fun sendLocalIceCandidateRemovals(candidates: List<CallCandidate>) { // For now we don't support this flow } - override fun hangUp() { + override fun reject() { + if (opponentVersion < 1) { + Timber.v("Opponent version is less than 1 ($opponentVersion): sending hangup instead of reject") + hangUp() + return + } + Timber.v("## VOIP reject $callId") + CallRejectContent( + callId = callId, + partyId = ourPartyId, + version = MxCall.VOIP_PROTO_VERSION.toString() + ) + .let { createEventAndLocalEcho(type = EventType.CALL_REJECT, roomId = roomId, content = it.toContent()) } + .also { eventSenderProcessor.postEvent(it) } + state = CallState.Terminated + } + + override fun hangUp(reason: CallHangupContent.Reason?) { Timber.v("## VOIP hangup $callId") CallHangupContent( - callId = callId + callId = callId, + partyId = ourPartyId, + reason = reason ?: CallHangupContent.Reason.USER_HANGUP, + version = MxCall.VOIP_PROTO_VERSION.toString() ) .let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) } .also { eventSenderProcessor.postEvent(it) } state = CallState.Terminated } - override fun accept(sdp: SessionDescription) { + override fun accept(sdpString: String) { Timber.v("## VOIP accept $callId") if (isOutgoing) return state = CallState.Answering CallAnswerContent( callId = callId, - answer = CallAnswerContent.Answer(sdp = sdp.description) + partyId = ourPartyId, + answer = CallAnswerContent.Answer(sdp = sdpString), + version = MxCall.VOIP_PROTO_VERSION.toString(), + capabilities = buildCapabilities() ) .let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) } .also { eventSenderProcessor.postEvent(it) } } + override fun negotiate(sdpString: String, type: SdpType) { + Timber.v("## VOIP negotiate $callId") + CallNegotiateContent( + callId = callId, + partyId = ourPartyId, + lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS, + description = CallNegotiateContent.Description(sdp = sdpString, type = type), + version = MxCall.VOIP_PROTO_VERSION.toString() + ) + .let { createEventAndLocalEcho(type = EventType.CALL_NEGOTIATE, roomId = roomId, content = it.toContent()) } + .also { eventSenderProcessor.postEvent(it) } + } + + override fun selectAnswer() { + Timber.v("## VOIP select answer $callId") + if (isOutgoing) return + state = CallState.Answering + CallSelectAnswerContent( + callId = callId, + partyId = ourPartyId, + selectedPartyId = opponentPartyId?.getOrNull(), + version = MxCall.VOIP_PROTO_VERSION.toString() + ) + .let { createEventAndLocalEcho(type = EventType.CALL_SELECT_ANSWER, roomId = roomId, content = it.toContent()) } + .also { eventSenderProcessor.postEvent(it) } + } + + override suspend fun transfer(targetUserId: String, targetRoomId: String?) { + val profileInfoParams = GetProfileInfoTask.Params(targetUserId) + val profileInfo = try { + getProfileInfoTask.execute(profileInfoParams) + } catch (failure: Throwable) { + Timber.v("Fail fetching profile info of $targetUserId while transferring call") + null + } + CallReplacesContent( + callId = callId, + partyId = ourPartyId, + replacementId = UUID.randomUUID().toString(), + version = MxCall.VOIP_PROTO_VERSION.toString(), + targetUser = CallReplacesContent.TargetUser( + id = targetUserId, + displayName = profileInfo?.get(ProfileService.DISPLAY_NAME_KEY) as? String, + avatarUrl = profileInfo?.get(ProfileService.AVATAR_URL_KEY) as? String + ), + targerRoomId = targetRoomId, + createCall = UUID.randomUUID().toString() + ) + .let { createEventAndLocalEcho(type = EventType.CALL_REPLACES, roomId = roomId, content = it.toContent()) } + .also { eventSenderProcessor.postEvent(it) } + } + private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { return Event( roomId = roomId, @@ -147,4 +239,12 @@ internal class MxCallImpl( ) .also { localEchoEventFactory.createLocalEcho(it) } } + + private fun buildCapabilities(): CallCapabilities? { + return if (matrixConfiguration.supportsCallTransfer) { + CallCapabilities(true) + } else { + null + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt index ebd57ce657..19a87103f4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt @@ -37,7 +37,6 @@ import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataDataS import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask import org.matrix.android.sdk.internal.session.widgets.helper.WidgetFactory import org.matrix.android.sdk.internal.session.widgets.helper.extractWidgetSequence -import org.matrix.android.sdk.internal.task.TaskExecutor import timber.log.Timber import javax.inject.Inject @@ -55,7 +54,6 @@ import javax.inject.Inject */ @SessionScope internal class IntegrationManager @Inject constructor(matrixConfiguration: MatrixConfiguration, - private val taskExecutor: TaskExecutor, @SessionDatabase private val monarchy: Monarchy, private val updateUserAccountDataTask: UpdateUserAccountDataTask, private val accountDataDataSource: AccountDataDataSource, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt index 0d41c6f35e..f9047fdf3c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt @@ -21,11 +21,9 @@ import org.matrix.android.sdk.api.session.room.RoomDirectoryService import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse -import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask -import org.matrix.android.sdk.internal.session.room.directory.GetThirdPartyProtocolsTask import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith @@ -33,7 +31,6 @@ import javax.inject.Inject internal class DefaultRoomDirectoryService @Inject constructor( private val getPublicRoomTask: GetPublicRoomTask, - private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask, private val getRoomDirectoryVisibilityTask: GetRoomDirectoryVisibilityTask, private val setRoomDirectoryVisibilityTask: SetRoomDirectoryVisibilityTask, private val taskExecutor: TaskExecutor) : RoomDirectoryService { @@ -48,14 +45,6 @@ internal class DefaultRoomDirectoryService @Inject constructor( .executeBy(taskExecutor) } - override fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable { - return getThirdPartyProtocolsTask - .configureWith { - this.callback = callback - } - .executeBy(taskExecutor) - } - override suspend fun getRoomDirectoryVisibility(roomId: String): RoomDirectoryVisibility { return getRoomDirectoryVisibilityTask.execute(GetRoomDirectoryVisibilityTask.Params(roomId)) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index aa92c1cb3b..20cb49ee8a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -20,7 +20,6 @@ import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse -import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.session.room.alias.GetAliasesResponse @@ -50,14 +49,6 @@ import retrofit2.http.Query internal interface RoomAPI { - /** - * Get the third party server protocols. - * - * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-thirdparty-protocols - */ - @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols") - fun thirdPartyProtocols(): Call<Map<String, ThirdPartyProtocol>> - /** * Lists the public rooms on the server, with optional filter. * This API returns paginated responses. The rooms are ordered by the number of joined members, with the largest rooms first. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 92f4ea2aea..66b7272360 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -39,11 +39,9 @@ import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomTask import org.matrix.android.sdk.internal.session.room.directory.DefaultGetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.DefaultGetRoomDirectoryVisibilityTask -import org.matrix.android.sdk.internal.session.room.directory.DefaultGetThirdPartyProtocolsTask import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask -import org.matrix.android.sdk.internal.session.room.directory.GetThirdPartyProtocolsTask import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask @@ -153,9 +151,6 @@ internal abstract class RoomModule { @Binds abstract fun bindSetRoomDirectoryVisibilityTask(task: DefaultSetRoomDirectoryVisibilityTask): SetRoomDirectoryVisibilityTask - @Binds - abstract fun bindGetThirdPartyProtocolsTask(task: DefaultGetThirdPartyProtocolsTask): GetThirdPartyProtocolsTask - @Binds abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt index dfbac347d9..a6836c8086 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt @@ -49,8 +49,10 @@ internal class QueueMemento @Inject constructor(context: Context, } fun unTrack(task: QueuedTask) { - managedTaskInfos.remove(task) - persist() + synchronized(managedTaskInfos) { + managedTaskInfos.remove(task) + persist() + } } private fun persist() { @@ -64,19 +66,17 @@ internal class QueueMemento @Inject constructor(context: Context, } private fun toTaskInfo(task: QueuedTask, order: Int): TaskInfo? { - synchronized(managedTaskInfos) { - return when (task) { - is SendEventQueuedTask -> SendEventTaskInfo( - localEchoId = task.event.eventId ?: "", - encrypt = task.encrypt, - order = order - ) - is RedactQueuedTask -> RedactEventTaskInfo( - redactionLocalEcho = task.redactionLocalEchoId, - order = order - ) - else -> null - } + return when (task) { + is SendEventQueuedTask -> SendEventTaskInfo( + localEchoId = task.event.eventId ?: "", + encrypt = task.encrypt, + order = order + ) + is RedactQueuedTask -> RedactEventTaskInfo( + redactionLocalEcho = task.redactionLocalEchoId, + order = order + ) + else -> null } } @@ -90,7 +90,7 @@ internal class QueueMemento @Inject constructor(context: Context, ?.forEach { info -> try { when (info) { - is SendEventTaskInfo -> { + is SendEventTaskInfo -> { localEchoRepository.getUpToDateEcho(info.localEchoId)?.let { if (it.sendState.isSending() && it.eventId != null && it.roomId != null) { localEchoRepository.updateSendState(it.eventId, it.roomId, SendState.UNSENT) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt new file mode 100644 index 0000000000..13829c400a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.thirdparty + +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService +import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser +import javax.inject.Inject + +internal class DefaultThirdPartyService @Inject constructor(private val getThirdPartyProtocolTask: GetThirdPartyProtocolsTask, + private val getThirdPartyUserTask: GetThirdPartyUserTask) + : ThirdPartyService { + + override suspend fun getThirdPartyProtocols(): Map<String, ThirdPartyProtocol> { + return getThirdPartyProtocolTask.execute(Unit) + } + + override suspend fun getThirdPartyUser(protocol: String, fields: Map<String, String>): List<ThirdPartyUser> { + val taskParams = GetThirdPartyUserTask.Params( + protocol = protocol, + fields = fields + ) + return getThirdPartyUserTask.execute(taskParams) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetThirdPartyProtocolsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyProtocolsTask.kt similarity index 86% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetThirdPartyProtocolsTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyProtocolsTask.kt index 3477aa671e..fd1ed741e9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetThirdPartyProtocolsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyProtocolsTask.kt @@ -14,25 +14,24 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.room.directory +package org.matrix.android.sdk.internal.session.thirdparty import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest -import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject internal interface GetThirdPartyProtocolsTask : Task<Unit, Map<String, ThirdPartyProtocol>> internal class DefaultGetThirdPartyProtocolsTask @Inject constructor( - private val roomAPI: RoomAPI, + private val thirdPartyAPI: ThirdPartyAPI, private val globalErrorReceiver: GlobalErrorReceiver ) : GetThirdPartyProtocolsTask { override suspend fun execute(params: Unit): Map<String, ThirdPartyProtocol> { return executeRequest(globalErrorReceiver) { - apiCall = roomAPI.thirdPartyProtocols() + apiCall = thirdPartyAPI.thirdPartyProtocols() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyUserTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyUserTask.kt new file mode 100644 index 0000000000..01a8b57678 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyUserTask.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.thirdparty + +import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface GetThirdPartyUserTask : Task<GetThirdPartyUserTask.Params, List<ThirdPartyUser>> { + + data class Params( + val protocol: String, + val fields: Map<String, String> = emptyMap() + ) +} + +internal class DefaultGetThirdPartyUserTask @Inject constructor( + private val thirdPartyAPI: ThirdPartyAPI, + private val globalErrorReceiver: GlobalErrorReceiver +) : GetThirdPartyUserTask { + + override suspend fun execute(params: GetThirdPartyUserTask.Params): List<ThirdPartyUser> { + return executeRequest(globalErrorReceiver) { + apiCall = thirdPartyAPI.getThirdPartyUser(params.protocol, params.fields) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt new file mode 100644 index 0000000000..0c60a27341 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.thirdparty + +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.QueryMap + +internal interface ThirdPartyAPI { + + /** + * Get the third party server protocols. + * + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1.html#get-matrix-client-r0-thirdparty-protocols + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols") + fun thirdPartyProtocols(): Call<Map<String, ThirdPartyProtocol>> + + /** + * Retrieve a Matrix User ID linked to a user on the third party service, given a set of user parameters. + * + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-thirdparty-user-protocol + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols/user/{protocol}") + fun getThirdPartyUser(@Path("protocol") protocol: String, @QueryMap params: Map<String, String>?): Call<List<ThirdPartyUser>> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyModule.kt new file mode 100644 index 0000000000..d3acd7a9f3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyModule.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.thirdparty + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class ThirdPartyModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesThirdPartyAPI(retrofit: Retrofit): ThirdPartyAPI { + return retrofit.create(ThirdPartyAPI::class.java) + } + } + + @Binds + abstract fun bindThirdPartyService(service: DefaultThirdPartyService): ThirdPartyService + + @Binds + abstract fun bindGetThirdPartyProtocolsTask(task: DefaultGetThirdPartyProtocolsTask): GetThirdPartyProtocolsTask + + @Binds + abstract fun bindGetThirdPartyUserTask(task: DefaultGetThirdPartyUserTask): GetThirdPartyUserTask +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt index 3e4e430e3b..9f5a9360ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt @@ -50,6 +50,10 @@ internal class DefaultWidgetService @Inject constructor(private val widgetManage return widgetManager.getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes) } + override fun getWidgetComputedUrl(widget: Widget, isLightTheme: Boolean): String? { + return widgetManager.getWidgetComputedUrl(widget, isLightTheme) + } + override fun getRoomWidgetsLive( roomId: String, widgetId: QueryStringValue, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt index 329903f15b..f841a2a245 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt @@ -104,6 +104,10 @@ internal class WidgetManager @Inject constructor(private val integrationManager: return widgetEvents.mapEventsToWidgets(widgetTypes, excludedTypes) } + fun getWidgetComputedUrl(widget: Widget, isLightTheme: Boolean): String? { + return widgetFactory.computeURL(widget, isLightTheme) + } + private fun List<Event>.mapEventsToWidgets(widgetTypes: Set<String>? = null, excludedTypes: Set<String>? = null): List<Widget> { val widgetEvents = this diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt index 702e424218..a469a9fe97 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.widgets.helper +import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.sender.SenderInfo @@ -31,6 +32,7 @@ import javax.inject.Inject internal class WidgetFactory @Inject constructor(private val userDataSource: UserDataSource, private val realmSessionProvider: RealmSessionProvider, + private val urlResolver: ContentUrlResolver, @UserId private val userId: String) { fun create(widgetEvent: Event): Widget? { @@ -53,30 +55,29 @@ internal class WidgetFactory @Inject constructor(private val userDataSource: Use } } val isAddedByMe = widgetEvent.senderId == userId - val computedUrl = widgetContent.computeURL(widgetEvent.roomId, widgetId) return Widget( widgetContent = widgetContent, event = widgetEvent, widgetId = widgetId, senderInfo = senderInfo, isAddedByMe = isAddedByMe, - computedUrl = computedUrl, type = WidgetType.fromString(type) ) } // Ref: https://github.com/matrix-org/matrix-widget-api/blob/master/src/templating/url-template.ts#L29-L33 - private fun WidgetContent.computeURL(roomId: String?, widgetId: String): String? { - var computedUrl = url ?: return null + fun computeURL(widget: Widget, isLightTheme: Boolean): String? { + var computedUrl = widget.widgetContent.url ?: return null val myUser = userDataSource.getUser(userId) - val keyValue = data.mapKeys { "\$${it.key}" }.toMutableMap() + val keyValue = widget.widgetContent.data.mapKeys { "\$${it.key}" }.toMutableMap() keyValue[WIDGET_PATTERN_MATRIX_USER_ID] = userId keyValue[WIDGET_PATTERN_MATRIX_DISPLAY_NAME] = myUser?.getBestName() ?: userId - keyValue[WIDGET_PATTERN_MATRIX_AVATAR_URL] = myUser?.avatarUrl ?: "" - keyValue[WIDGET_PATTERN_MATRIX_WIDGET_ID] = widgetId - keyValue[WIDGET_PATTERN_MATRIX_ROOM_ID] = roomId ?: "" + keyValue[WIDGET_PATTERN_MATRIX_AVATAR_URL] = urlResolver.resolveFullSize(myUser?.avatarUrl) ?: "" + keyValue[WIDGET_PATTERN_MATRIX_WIDGET_ID] = widget.widgetId + keyValue[WIDGET_PATTERN_MATRIX_ROOM_ID] = widget.event.roomId ?: "" + keyValue[WIDGET_PATTERN_THEME] = getTheme(isLightTheme) for ((key, value) in keyValue) { computedUrl = computedUrl.replace(key, URLEncoder.encode(value.toString(), "utf-8")) @@ -84,6 +85,10 @@ internal class WidgetFactory @Inject constructor(private val userDataSource: Use return computedUrl } + private fun getTheme(isLightTheme: Boolean): String { + return if (isLightTheme) "light" else "dark" + } + companion object { // Value to be replaced in URLS const val WIDGET_PATTERN_MATRIX_USER_ID = "\$matrix_user_id" @@ -91,5 +96,6 @@ internal class WidgetFactory @Inject constructor(private val userDataSource: Use const val WIDGET_PATTERN_MATRIX_AVATAR_URL = "\$matrix_avatar_url" const val WIDGET_PATTERN_MATRIX_WIDGET_ID = "\$matrix_widget_id" const val WIDGET_PATTERN_MATRIX_ROOM_ID = "\$matrix_room_id" + const val WIDGET_PATTERN_THEME = "\$theme" } } diff --git a/matrix-sdk-android/src/main/res/values-gl/strings.xml b/matrix-sdk-android/src/main/res/values-gl/strings.xml index 21eb8cb70c..6ae9b33b66 100644 --- a/matrix-sdk-android/src/main/res/values-gl/strings.xml +++ b/matrix-sdk-android/src/main/res/values-gl/strings.xml @@ -80,4 +80,22 @@ <string name="notice_room_invite_no_invitee_by_you">O teu convite</string> <string name="summary_you_sent_sticker">Enviaches un adhesivo.</string> <string name="summary_you_sent_image">Enviaches unha imaxe.</string> + <string name="notice_room_server_acl_set_ip_literals_not_allowed">• Servidores con literais IP están vetados.</string> + <string name="notice_room_server_acl_set_ip_literals_allowed">• Servidores con IP literais están permitidos.</string> + <string name="notice_room_server_acl_set_banned">• Servidores con %s están vetados.</string> + <string name="notice_room_server_acl_set_allowed">• Servidores con %s están permitidos.</string> + <string name="notice_room_server_acl_set_title_by_you">Estableceches os ACLs do servidor para esta sala.</string> + <string name="notice_room_server_acl_set_title">%s estableceu os ACLs do servidor para esta sala.</string> + <string name="notice_direct_room_update">%s actualizou aquí.</string> + <string name="notice_direct_room_update_by_you">Actualizaches aquí.</string> + <string name="notice_room_update_by_you">Actualizaches esta sala.</string> + <string name="notice_room_update">%s actualizou esta sala.</string> + <string name="notice_end_to_end_by_you">Activaches o cifrado extremo-a-extremo (%1$s)</string> + <string name="notice_made_future_direct_room_visibility_by_you">Fixeches visibles as mensaxes futuras para %1$s</string> + <string name="notice_made_future_direct_room_visibility">%1$s fixo visibles as mensaxes futuras para %2$s</string> + <string name="notice_made_future_room_visibility_by_you">Fixeches visible no futuro o historial da sala para %1$s</string> + <string name="notice_ended_call_by_you">Remataches a chamada.</string> + <string name="notice_answered_call_by_you">Respondeches á chamada.</string> + <string name="notice_call_candidates_by_you">Enviaches datos para configurar a chamada.</string> + <string name="notice_call_candidates">%s enviou datos para configurar a chamada.</string> </resources> \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-lv/strings.xml b/matrix-sdk-android/src/main/res/values-lv/strings.xml index ec107b47d6..815760f394 100644 --- a/matrix-sdk-android/src/main/res/values-lv/strings.xml +++ b/matrix-sdk-android/src/main/res/values-lv/strings.xml @@ -2,46 +2,46 @@ <resources> <string name="summary_message">%1$s: %2$s</string> <string name="summary_user_sent_image">%1$s nosūtīja attēlu.</string> - <string name="notice_room_invite_no_invitee">%s\'s uzaicinājums</string> + <string name="notice_room_invite_no_invitee">Uzaicinājums no %s</string> <string name="notice_room_invite">%1$s uzaicināja %2$s</string> - <string name="notice_room_invite_you">%1$s uzaicināja tevi</string> + <string name="notice_room_invite_you">%1$s uzaicināja jūs</string> <string name="notice_room_join">%1$s pievienojās</string> - <string name="notice_room_leave">%1$s atstāja</string> + <string name="notice_room_leave">%1$s pameta istabu</string> <string name="notice_room_reject">%1$s noraidīja uzaicinājumu</string> - <string name="notice_room_kick">%1$s \"izspēra\" ārā %2$s</string> - <string name="notice_room_unban">%1$s atbanoja (atcēla pieejas liegumu) %2$s</string> - <string name="notice_room_ban">%1$s liedza pieeju (banoja) %2$s</string> + <string name="notice_room_kick">%1$s padzina %2$s</string> + <string name="notice_room_unban">%1$s atcēla pieejas liegumu %2$s</string> + <string name="notice_room_ban">%1$s liedza pieeju %2$s</string> <string name="notice_room_withdraw">%1$s atsauca %2$s uzaicinājumu</string> - <string name="notice_avatar_url_changed">%1$s nomainīja profila attēlu</string> - <string name="notice_display_name_set">%1$s uzstādīja redzamo vārdu uz %2$s</string> - <string name="notice_display_name_changed_from">%1$s nomainīja redzamo vārdu no %2$s uz %3$s</string> - <string name="notice_display_name_removed">%1$s dzēsa savu redzamo vārdu (%2$s)</string> - <string name="notice_room_topic_changed">%1$s nomainīja tēmas nosaukumu uz: %2$s</string> - <string name="notice_room_name_changed">%1$s nomainīja istabas nosaukumu uz: %2$s</string> + <string name="notice_avatar_url_changed">%1$s nomainīja avataru</string> + <string name="notice_display_name_set">%1$s uzstādīja parādāmo vārdu uz %2$s</string> + <string name="notice_display_name_changed_from">%1$s nomainīja parādāmo vārdu no %2$s uz %3$s</string> + <string name="notice_display_name_removed">%1$s dzēsa savu parādāmo vārdu (iepriekš %2$s)</string> + <string name="notice_room_topic_changed">%1$s nomainīja tematu uz %2$s</string> + <string name="notice_room_name_changed">%1$s nomainīja istabas nosaukumu uz %2$s</string> <string name="notice_placed_video_call">%s veica video zvanu.</string> <string name="notice_placed_voice_call">%s veica audio zvanu.</string> - <string name="notice_answered_call">%s atbildēja zvanam.</string> + <string name="notice_answered_call">%s atbildēja uz zvanu.</string> <string name="notice_ended_call">%s beidza zvanu.</string> - <string name="notice_made_future_room_visibility">%1$s padarīja istabas nākamo ziņu vēsturi redzamu %2$s</string> + <string name="notice_made_future_room_visibility">%1$s padarīja istabas turpmāko ziņu vēsturi redzamu %2$s</string> <string name="notice_room_visibility_invited">visi istabas biedri no brīža, kad tika uzaicināti.</string> <string name="notice_room_visibility_joined">visi istabas biedri no brīža, kad tika pievienojušies.</string> <string name="notice_room_visibility_shared">visi istabas biedri.</string> <string name="notice_room_visibility_world_readable">ikviens.</string> <string name="notice_room_visibility_unknown">nezināms (%s).</string> - <string name="notice_end_to_end">%1$s ieslēdza ierīce-ierīce šifrēšanu (%2$s)</string> - <string name="notice_requested_voip_conference">%1$s vēlas VoIP konferenci</string> - <string name="notice_voip_started">VoIP konference sākusies</string> - <string name="notice_voip_finished">VoIP konference ir beigusies</string> - <string name="notice_avatar_changed_too">(arī profila attēls mainījās)</string> + <string name="notice_end_to_end">%1$s ieslēdza pilnīgu šifrēšanu (%2$s)</string> + <string name="notice_requested_voip_conference">%1$s pieprasīja VoIP konferenci</string> + <string name="notice_voip_started">VoIP konference sākās</string> + <string name="notice_voip_finished">VoIP konference beidzās</string> + <string name="notice_avatar_changed_too">(arī avatars tika nomainīts)</string> <string name="notice_room_name_removed">%1$s dzēsa istabas nosaukumu</string> - <string name="notice_room_topic_removed">%1$s dzēsa istabas tēmas nosaukumu</string> - <string name="notice_profile_change_redacted">%1$s atjaunoja profila informāciju %2$s</string> - <string name="notice_room_third_party_invite">%1$s nosūtīja uzaicinājumu %2$s pievienoties istabai</string> - <string name="notice_room_third_party_registered_invite">%1$s apstiprināja uzaicinājumu priekš %2$s</string> - <string name="notice_crypto_unable_to_decrypt">** Nav iespējams atkodēt: %s **</string> - <string name="notice_crypto_error_unkwown_inbound_session_id">Sūtītāja ierīce mums nenosūtīja atslēgas priekš šīs ziņas.</string> + <string name="notice_room_topic_removed">%1$s izdzēsa istabas tematu</string> + <string name="notice_profile_change_redacted">%1$s atjaunoja savu profilu %2$s</string> + <string name="notice_room_third_party_invite">%1$s nosūtīja %2$s uzaicinājumu pievienoties istabai</string> + <string name="notice_room_third_party_registered_invite">%1$s pieņēma uzaicinājumu %2$s</string> + <string name="notice_crypto_unable_to_decrypt">** Neizdodas atšifrēt: %s **</string> + <string name="notice_crypto_error_unkwown_inbound_session_id">Sūtītāja ierīce mums nav nenosūtījusi atslēgas priekš šīs ziņas.</string> <string name="could_not_redact">Nevarēja rediģēt</string> - <string name="unable_to_send_message">Nav iespējams nosūtīt ziņu</string> + <string name="unable_to_send_message">Neizdodas nosūtīt ziņu</string> <string name="message_failed_to_upload">Neizdevās augšuplādēt attēlu</string> <string name="network_error">Tīkla kļūda</string> <string name="matrix_error">Matrix kļūda</string> @@ -58,27 +58,188 @@ <item quantity="one">%1$s un %2$d citi</item> <item quantity="other">%1$s un %2$d citu</item> </plurals> - <string name="notice_display_name_changed_from_by_you">Tu nomainīji savu attēlojamo vārdu no %1$s uz %2$s</string> - <string name="notice_display_name_set_by_you">Tu nomainījis savu attēlojamo vārdu uz %1$s</string> - <string name="notice_avatar_url_changed_by_you">Tu nomainīji savu avataru</string> - <string name="notice_room_withdraw_by_you">Tu atsauci %1$s uzaicinājumu</string> - <string name="notice_room_ban_by_you">Tu nobanoji %1$s</string> - <string name="notice_room_unban_by_you">Tu atbanoji %1$s</string> - <string name="notice_room_kick_by_you">Tu izspēri %1$s</string> - <string name="notice_room_reject_by_you">Tu noraidīji uzaicinājumu</string> - <string name="notice_direct_room_leave_by_you">Tu pameti telpu</string> - <string name="notice_direct_room_leave">%1$s atstāja telpu</string> - <string name="notice_room_leave_by_you">Tu atstāji telpu</string> - <string name="notice_direct_room_join_by_you">Tu pievienojies</string> - <string name="notice_direct_room_join">%1$s pievienojās telpai</string> - <string name="notice_room_join_by_you">Tu pievienojies telpai</string> - <string name="notice_room_invite_by_you">Tu uzaicināji %1$s</string> - <string name="notice_direct_room_created_by_you">Tu izveidoji apspriedi (diskusiju)</string> - <string name="notice_direct_room_created">%1$s izveidoja apspriedi (diskusiju)</string> - <string name="notice_room_created_by_you">Tu izveidoji istabu</string> - <string name="notice_room_created">%1$s izveidoja telpu</string> - <string name="notice_room_invite_no_invitee_by_you">Tavs uzaicinājums</string> - <string name="summary_you_sent_sticker">Tu nosūtīji uzlīmi/lipekli.</string> - <string name="summary_user_sent_sticker">%1$s nosūtīja uzlīmi/lipekli.</string> - <string name="summary_you_sent_image">Tu nosūtīji attēlu.</string> + <string name="notice_display_name_changed_from_by_you">Jūs nomainījāt savu parādāmo vārdu no %1$s uz %2$s</string> + <string name="notice_display_name_set_by_you">Jūs nomainījāt savu parādāmo vārdu uz %1$s</string> + <string name="notice_avatar_url_changed_by_you">Jūs nomainījāt savu avataru</string> + <string name="notice_room_withdraw_by_you">Jūs atsaucāt %1$s uzaicinājumu</string> + <string name="notice_room_ban_by_you">Jūs liedzāt pieeju %1$s</string> + <string name="notice_room_unban_by_you">Jūs atcēlāt pieejas liegumu %1$s</string> + <string name="notice_room_kick_by_you">Jūs padzināt %1$s</string> + <string name="notice_room_reject_by_you">Jūs noraidījāt uzaicinājumu</string> + <string name="notice_direct_room_leave_by_you">Jūs pametāt istabu</string> + <string name="notice_direct_room_leave">%1$s pameta istabu</string> + <string name="notice_room_leave_by_you">Jūs pametāt istabu</string> + <string name="notice_direct_room_join_by_you">Jūs pievienojāties</string> + <string name="notice_direct_room_join">%1$s pievienojās istabai</string> + <string name="notice_room_join_by_you">Jūs pievienojāties istabai</string> + <string name="notice_room_invite_by_you">Jūs uzaicinājāt %1$s</string> + <string name="notice_direct_room_created_by_you">Jūs izveidojāt diskusiju</string> + <string name="notice_direct_room_created">%1$s izveidoja diskusiju</string> + <string name="notice_room_created_by_you">Jūs izveidojāt istabu</string> + <string name="notice_room_created">%1$s izveidoja istabu</string> + <string name="notice_room_invite_no_invitee_by_you">Jūsu uzaicinājums</string> + <string name="summary_you_sent_sticker">Jūs nosūtījāt uzlīmi.</string> + <string name="summary_user_sent_sticker">%1$s nosūtīja uzlīmi.</string> + <string name="summary_you_sent_image">Jūs nosūtījāt attēlu.</string> + <string name="key_verification_request_fallback_message">%s pieprasa verificēt jūsu atslēgu, taču jūsu klients neatbalsta tērzēšanas atslēgas verifikāciju. Lai verificētu atslēgas, jums būs jāizmanto atslēgu verifikācija novecojušā veidā.</string> + <string name="notice_end_to_end_unknown_algorithm_by_you">Jūs ieslēdzāt pilnīgu šifrēšanu (neatpazīts algoritms %1$s).</string> + <string name="notice_end_to_end_unknown_algorithm">%1$s ieslēdza pilnīgu šifrēšanu (neatpazīts algoritms %2$s).</string> + <string name="notice_end_to_end_ok_by_you">Jūs ieslēdzāt pilnīgu šifrēšanu.</string> + <string name="notice_end_to_end_ok">%1$s ieslēdza pilnīgu šifrēšanu.</string> + <string name="notice_direct_room_guest_access_forbidden_by_you">Jūs esat novērsis iespēju viesiem pievienoties istabai.</string> + <string name="notice_direct_room_guest_access_forbidden">%1$s ir novērsis iespēju viesiem pievienoties istabai.</string> + <string name="notice_room_guest_access_forbidden_by_you">Jūs esat novērsis iespēju viesiem pievienoties istabai.</string> + <string name="notice_room_guest_access_forbidden">%1$s ir novērsis iespēju viesiem pievienoties istabai.</string> + <string name="notice_direct_room_guest_access_can_join_by_you">Jūs esat atļāvis viesiem pievienoties istabai.</string> + <string name="notice_direct_room_guest_access_can_join">%1$s ir atļāvis viesiem pievienoties istabai.</string> + <string name="notice_room_guest_access_can_join_by_you">Jūs esat atļāvis viesiem pievienoties istabai.</string> + <string name="notice_room_guest_access_can_join">%1$s ir atļāvis viesiem pievienoties istabai.</string> + <string name="notice_room_canonical_alias_no_change_by_you">Jūs nomainījāt adreses šai istabai.</string> + <string name="notice_room_canonical_alias_no_change">%1$s nomainīja adreses šai istabai.</string> + <string name="notice_room_canonical_alias_main_and_alternative_changed_by_you">Jūs nomainījāt galveno un alternatīvās adreses šai istabai.</string> + <string name="notice_room_canonical_alias_main_and_alternative_changed">%1$s nomainīja galveno un alternatīvās adreses šai istabai.</string> + <string name="notice_room_canonical_alias_alternative_changed_by_you">Jūs nomainījāt alternatīvās adreses šai istabai.</string> + <string name="notice_room_canonical_alias_alternative_changed">%1$s nomainīja alternatīvās adreses šai istabai.</string> + <plurals name="notice_room_canonical_alias_alternative_removed_by_you"> + <item quantity="zero">Jūs izdzēsāt šīs istabas alternatīvo adresi %1$s.</item> + <item quantity="one">Jūs izdzēsāt šīs istabas alternatīvās adreses %1$s.</item> + <item quantity="other">Jūs izdzēsāt šīs istabas alternatīvās adreses %1$s.</item> + </plurals> + <plurals name="notice_room_canonical_alias_alternative_removed"> + <item quantity="zero">%1$s izdzēsa šīs istabas alternatīvo adresi %2$s.</item> + <item quantity="one">%1$s izdzēsa šīs istabas alternatīvās adreses %2$s.</item> + <item quantity="other">%1$s izdzēsa šīs istabas alternatīvās adreses %2$s.</item> + </plurals> + <plurals name="notice_room_canonical_alias_alternative_added_by_you"> + <item quantity="zero">Jūs pievienojāt šīs istabas alternatīvo adresi %1$s.</item> + <item quantity="one">Jūs pievienojāt šīs istabas alternatīvās adreses %1$s.</item> + <item quantity="other">Jūs pievienojāt šīs istabas alternatīvās adreses %1$s.</item> + </plurals> + <plurals name="notice_room_canonical_alias_alternative_added"> + <item quantity="zero">%1$s pievienoja šīs istabas alternatīvo adresi %2$s.</item> + <item quantity="one">%1$s pievienoja šīs istabas alternatīvās adreses %2$s.</item> + <item quantity="other">%1$s pievienoja šīs istabas alternatīvās adreses %2$s.</item> + </plurals> + <string name="notice_room_canonical_alias_unset_by_you">Jūs izdzēsāt šis istabas galveno adresi.</string> + <string name="notice_room_canonical_alias_unset">%1$s izdzēsa šis istabas galveno adresi.</string> + <string name="notice_room_canonical_alias_set_by_you">Jūs iestatījāt %1$s kā šis istabas galveno adresi.</string> + <string name="notice_room_canonical_alias_set">%1$s iestatīja %2$s kā šis istabas galveno adresi.</string> + <string name="notice_room_aliases_added_and_removed_by_you">Jūs pievienojāt %1$s un izdzēsāt %2$s kā šīs istabas adreses.</string> + <string name="notice_room_aliases_added_and_removed">%1$s pievienoja %2$s un izdzēsa %3$s kā šīs istabas adreses.</string> + <plurals name="notice_room_aliases_removed_by_you"> + <item quantity="zero">Jūs izdzēsāt %1$s kā šīs istabas adresi.</item> + <item quantity="one">Jūs izdzēsāt %1$s kā šīs istabas adreses.</item> + <item quantity="other">Jūs izdzēsāt %1$s kā šīs istabas adreses.</item> + </plurals> + <plurals name="notice_room_aliases_removed"> + <item quantity="zero">%1$s izdzēsa %2$s kā šīs istabas adresi.</item> + <item quantity="one">%1$s izdzēsa %2$s kā šīs istabas adreses.</item> + <item quantity="other">%1$s izdzēsa %2$s kā šīs istabas adreses.</item> + </plurals> + <plurals name="notice_room_aliases_added_by_you"> + <item quantity="zero">Jūs pievienojāt %1$s kā šīs istabas adresi.</item> + <item quantity="one">Jūs pievienojāt %1$s kā šīs istabas adreses.</item> + <item quantity="other">Jūs pievienojāt %1$s kā šīs istabas adreses.</item> + </plurals> + <plurals name="notice_room_aliases_added"> + <item quantity="zero">%1$s pievienoja %2$s kā šīs istabas adresi.</item> + <item quantity="one">%1$s pievienoja %2$s kā šis istabas adreses.</item> + <item quantity="other">%1$s pievienoja %2$s kā šīs istabas adreses.</item> + </plurals> + <string name="notice_room_withdraw_with_reason_by_you">Jūs atsaucāt %1$s uzaicinājumu. Iemesls: %2$s</string> + <string name="notice_room_withdraw_with_reason">%1$s atsauca uzaicinājumu %2$s. Iemesls: %3$s</string> + <string name="notice_room_third_party_registered_invite_with_reason_by_you">Jūs pieņēmāt uzaicinājumu %1$s. Iemesls: %2$s</string> + <string name="notice_room_third_party_registered_invite_with_reason">%1$s pieņēma uzaicinājumu %2$s. Iemesls: %3$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason_by_you">Jūs atsaucāt uzaicinājumu %1$s pievienoties istabai. Iemesls: %2$s</string> + <string name="notice_room_third_party_revoked_invite_with_reason">%1$s atsauca uzaicinājumu %2$s pievienoties istabai. Iemesls: %3$s</string> + <string name="notice_room_third_party_invite_with_reason_by_you">Jūs nosūtījāt uzaicinājumu %1$s pievienoties istabai. Iemesls: %2$s</string> + <string name="notice_room_third_party_invite_with_reason">%1$s nosūtīja uzaicinājumu %2$s pievienoties istabai. Iemesls: %3$s</string> + <string name="notice_room_ban_with_reason_by_you">Jūs liedzāt pieeju %1$s. Iemesls: %2$s</string> + <string name="notice_room_ban_with_reason">%1$s liedza pieeju %2$s. Iemesls: %3$s</string> + <string name="notice_room_unban_with_reason_by_you">Jūs atcēlāt pieejas liegumu %1$s. Iemesls: %2$s</string> + <string name="notice_room_unban_with_reason">%1$s atcēla %2$s pieejas liegumu. Iemesls: %3$s</string> + <string name="notice_room_kick_with_reason_by_you">Jūs padzināt %1$s. Iemesls: %2$s</string> + <string name="notice_room_kick_with_reason">%1$s padzina %2$s. Iemesls: %3$s</string> + <string name="notice_room_reject_with_reason_by_you">Jūs noraidījāt uzaicinājumu. Iemesls: %1$s</string> + <string name="notice_room_reject_with_reason">%1$s noraidīja uzaicinājumu. Iemesls: %2$s</string> + <string name="notice_direct_room_leave_with_reason_by_you">Jūs izgājāt. Iemesls: %1$s</string> + <string name="notice_direct_room_leave_with_reason">%1$s izgāja. Iemels: %2$s</string> + <string name="notice_room_leave_with_reason_by_you">Jūs pametāt istabu. Iemesls: %1$s</string> + <string name="notice_room_leave_with_reason">%1$s pameta istabu. Iemesls: %2$s</string> + <string name="notice_direct_room_join_with_reason_by_you">Jūs pievienojāties. Iemesls: %1$s</string> + <string name="notice_direct_room_join_with_reason">%1$s pievienojās. Iemesls: %2$s</string> + <string name="notice_room_join_with_reason_by_you">Jūs pievienojāties istabai. Iemesls: %1$s</string> + <string name="notice_room_join_with_reason">%1$s pievienojās istabai. Iemesls: %2$s</string> + <string name="notice_room_invite_you_with_reason">%1$s uzaicināja jūs. Iemesls: %2$s</string> + <string name="notice_room_invite_with_reason_by_you">Jūs uzaicinājāt %1$s. Iemesls: %2$s</string> + <string name="notice_room_invite_with_reason">%1$s uzaicināja %2$s. Iemesls: %3$s</string> + <string name="notice_room_invite_no_invitee_with_reason_by_you">Jūsu uzaicinājums. Iemesls: %1$s</string> + <string name="notice_room_invite_no_invitee_with_reason">%1$s uzaicinājums. Iemesls: %2$s</string> + <string name="clear_timeline_send_queue">Notīrīt sūtīšanas rindu</string> + <string name="event_status_sending_message">Sūta ziņu…</string> + <string name="initial_sync_start_importing_account_data">Sākotnējā sinhronizācija: +\nImportē konta datus</string> + <string name="initial_sync_start_importing_account_groups">Sākotnējā sinhronizācija: +\nImportē kopienas</string> + <string name="initial_sync_start_importing_account_left_rooms">Sākotnējā sinhronizācija: +\nImportē pamestās istabas</string> + <string name="initial_sync_start_importing_account_invited_rooms">Sākotnējā sinhronizācija: +\nImportē istabas, uz kurām uzaicināts</string> + <string name="initial_sync_start_importing_account_joined_rooms">Sākotnējā sinhronizācija: +\nImportē istabas, kurās ieiets</string> + <string name="initial_sync_start_importing_account_rooms">Sākotnējā sinhronizācija: +\nImportē istabas</string> + <string name="initial_sync_start_importing_account_crypto">Sākotnējā sinhronizācija: +\nImportē kriptogrāfiju</string> + <string name="initial_sync_start_importing_account">Sākotnējā sinhronizācija: +\nImportē kontu…</string> + <string name="room_displayname_empty_room_was">Tukša istaba (bija %s)</string> + <plurals name="room_displayname_four_and_more_members"> + <item quantity="zero">%1$s, %2$s, %3$s un %4$d citi</item> + <item quantity="one">%1$s, %2$s, %3$s un %4$d cits</item> + <item quantity="other">%1$s, %2$s, %3$s un %4$d citi</item> + </plurals> + <string name="room_displayname_4_members">%1$s, %2$s, %3$s un %4$s</string> + <string name="room_displayname_3_members">%1$s, %2$s un %3$s</string> + <string name="notice_power_level_diff">%1$s no %2$s uz %3$s</string> + <string name="notice_power_level_changed">%1$s nomainīja %2$s pieejas līmeni.</string> + <string name="notice_power_level_changed_by_you">Jūs nomainījāt %1$s pieejas līmeni.</string> + <string name="power_level_custom_no_value">Pielāgots</string> + <string name="power_level_custom">Pielāgots (%1$d)</string> + <string name="power_level_default">Noklusējuma</string> + <string name="power_level_moderator">Moderators</string> + <string name="power_level_admin">Administrators</string> + <string name="notice_room_third_party_registered_invite_by_you">Jūs pieņēmāt uzaicinājumu %1$s</string> + <string name="notice_direct_room_third_party_revoked_invite_by_you">Jūs atsaucāt uzaicinājumu %1$s</string> + <string name="notice_direct_room_third_party_revoked_invite">%1$s atsauca uzaicinājumu %2$s</string> + <string name="notice_room_third_party_revoked_invite_by_you">Jūs atsaucāt uzaicinājumu %1$s pievienoties istabai</string> + <string name="notice_room_third_party_revoked_invite">%1$s atsauca uzaicinājumu %2$s pievienoties istabai</string> + <string name="notice_direct_room_third_party_invite_by_you">Jūs uzaicinājāt %1$s</string> + <string name="notice_direct_room_third_party_invite">%1$s uzaicināja %2$s</string> + <string name="notice_room_third_party_invite_by_you">Jūs nosūtījāt %1$s uzaicinājumu pievienoties istabai</string> + <string name="notice_profile_change_redacted_by_you">Jūs atjaunojāt savu profilu %1$s</string> + <string name="notice_event_redacted_by_with_reason">%1$s izdzēsa ziņu [iemesls: %2$s]</string> + <string name="notice_event_redacted_with_reason">Ziņa izdzēsta [iemesls: %1$s]</string> + <string name="notice_event_redacted_by">%1$s izdzēsa ziņu</string> + <string name="notice_event_redacted">Ziņa izdzēsta</string> + <string name="notice_room_avatar_removed_by_you">Jūs izdzēsāt istabas avataru</string> + <string name="notice_room_avatar_removed">%1$s izdzēsa istabas avataru</string> + <string name="notice_room_topic_removed_by_you">Jūs izdzēsāt istabas tematu</string> + <string name="notice_room_name_removed_by_you">Jūs dzēsāt istabas nosaukumu</string> + <string name="notice_requested_voip_conference_by_you">Jūs pieprasījāt VoIP konferenci</string> + <string name="notice_end_to_end_by_you">Jūs ieslēdzāt pilnīgu šifrēšanu (%1$s)</string> + <string name="notice_made_future_direct_room_visibility_by_you">Jūs padarījāt turpmākās ziņas redzamas %1$s</string> + <string name="notice_made_future_direct_room_visibility">%1$s padarīja turpmākās ziņas redzamas %2$s</string> + <string name="notice_made_future_room_visibility_by_you">Jūs padarījāt istabas turpmāko ziņu vēsturi redzamu %1$s</string> + <string name="notice_ended_call_by_you">Jūs beidzāt zvanu.</string> + <string name="notice_answered_call_by_you">Jūs atbildējāt uz zvanu.</string> + <string name="notice_call_candidates_by_you">Jūs nosūtījāt datus zvana uzsākšanai.</string> + <string name="notice_call_candidates">%s nosūtīja datus zvana uzsākšanai.</string> + <string name="notice_placed_voice_call_by_you">Jūs veicāt balss zvanu.</string> + <string name="notice_placed_video_call_by_you">Jūs veicāt video zvanu.</string> + <string name="notice_room_name_changed_by_you">Jūs nomainījāt istabas nosaukumu uz %1$s</string> + <string name="notice_room_avatar_changed_by_you">Jūs nomainījāt istabas avataru</string> + <string name="notice_room_avatar_changed">%1$s nomainīja istabas avataru</string> + <string name="notice_room_topic_changed_by_you">Jūs nomainījāt tematu uz %1$s</string> + <string name="notice_display_name_removed_by_you">Jūs dzēsāt savu parādāmo vārdu (iepriekš %1$s)</string> </resources> \ No newline at end of file diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 2306eaed8b..e9b29d99ba 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===85 +enum class===88 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/tools/jitsi/build_jisti_libs.sh b/tools/jitsi/build_jisti_libs.sh index 1341a87d10..34051d463c 100755 --- a/tools/jitsi/build_jisti_libs.sh +++ b/tools/jitsi/build_jisti_libs.sh @@ -25,8 +25,8 @@ cd jitsi-meet # This is commit after version 2.2.2, which does not compile # git checkout 5a934c071a5cbe64de275a25d0ed62d8193cdd03 -# Version android-sdk-2.9.3, commit abcbbbea12e3ef88012b14723bb8cd42dbefc988 -git checkout android-sdk-2.9.3 +# Version android-sdk-3.1.0, commit 7a64bf006ea027b77564d8847570e1ac46ff0ec0 +git checkout android-sdk-3.1.0 echo echo "##################################################" diff --git a/vector/build.gradle b/vector/build.gradle index 838d13a0dd..cc4efb4590 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -114,6 +114,9 @@ android { targetSdkVersion 30 multiDexEnabled true + renderscriptTargetApi 24 + renderscriptSupportModeEnabled true + // `develop` branch will have version code from timestamp, to ensure each build from CI has a incremented versionCode. // Other branches (master, features, etc.) will have version code based on application version. versionCode project.getVersionCode() @@ -232,7 +235,7 @@ android { productFlavors { gplay { dimension "store" - + isDefault = true versionName "${versionMajor}.${versionMinor}.${versionPatch}${getGplayVersionSuffix()}" resValue "bool", "isGplay", "true" @@ -319,6 +322,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation "androidx.sharetarget:sharetarget:1.0.0" implementation 'androidx.core:core-ktx:1.3.2' + implementation "androidx.media:media:1.2.1" implementation "org.threeten:threetenbp:1.4.0:no-tzdb" implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0" @@ -373,6 +377,7 @@ dependencies { implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'com.google.android:flexbox:1.1.1' implementation "androidx.autofill:autofill:$autofill_version" + implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' // Custom Tab @@ -430,7 +435,10 @@ dependencies { // WebRTC // org.webrtc:google-webrtc is for development purposes only // implementation 'org.webrtc:google-webrtc:1.0.+' - implementation('org.jitsi.react:jitsi-meet-sdk:2.9.3') { transitive = true } + implementation('com.facebook.react:react-native-webrtc:1.87.3-jitsi-6624067@aar') + + // Jitsi + implementation('org.jitsi.react:jitsi-meet-sdk:3.1.0') // QR-code // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 @@ -441,6 +449,8 @@ dependencies { implementation 'com.vanniktech:emoji-material:0.7.0' implementation 'com.vanniktech:emoji-google:0.7.0' + implementation 'im.dlg:android-dialer:1.2.5' + // TESTS testImplementation 'junit:junit:4.13' testImplementation "org.amshove.kluent:kluent-android:$kluent_version" diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt index 2d0077fc55..338d57fea8 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -277,8 +277,23 @@ class UiAllScreensSanityTest { assertDisplayed(R.id.roomProfileAvatarView) - // Leave + // Room addresses clickListItem(R.id.matrixProfileRecyclerView, 13) + onView(isRoot()).perform(waitForView(withText(R.string.room_alias_published_alias_title))) + pressBack() + + // Room permissions + clickListItem(R.id.matrixProfileRecyclerView, 15) + onView(isRoot()).perform(waitForView(withText(R.string.room_permissions_title))) + clickOn(R.string.room_permissions_change_room_avatar) + clickDialogNegativeButton() + // Toggle + clickOn(R.string.show_advanced) + clickOn(R.string.hide_advanced) + pressBack() + + // Leave + clickListItem(R.id.matrixProfileRecyclerView, 17) clickDialogNegativeButton() // Menu share @@ -289,27 +304,12 @@ class UiAllScreensSanityTest { } private fun navigateToRoomParameters() { - // Room addresses - clickListItem(R.id.roomSettingsRecyclerView, 4) - onView(isRoot()).perform(waitForView(withText(R.string.room_alias_published_alias_title))) - pressBack() - - // Room permissions - clickListItem(R.id.roomSettingsRecyclerView, 6) - onView(isRoot()).perform(waitForView(withText(R.string.room_permissions_title))) - clickOn(R.string.room_permissions_change_room_avatar) - clickDialogNegativeButton() - // Toggle - clickOn(R.string.show_advanced) - clickOn(R.string.hide_advanced) - pressBack() - // Room history readability - clickListItem(R.id.roomSettingsRecyclerView, 8) + clickListItem(R.id.roomSettingsRecyclerView, 4) pressBack() // Room access - clickListItem(R.id.roomSettingsRecyclerView, 10) + clickListItem(R.id.roomSettingsRecyclerView, 6) pressBack() } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 6f2ff4e8ca..52932920d4 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -36,6 +36,9 @@ android:name="android.permission.WRITE_CALENDAR" tools:node="remove" /> + <!-- Jitsi SDK is now API23+ --> + <uses-sdk tools:overrideLibrary="org.jitsi.meet.sdk,com.oney.WebRTCModule,com.learnium.RNDeviceInfo,com.reactnativecommunity.asyncstorage,com.ocetnik.timer,com.calendarevents,com.reactnativecommunity.netinfo,com.kevinresol.react_native_default_preference,com.rnimmersive,com.corbt.keepawake,com.BV.LinearGradient,com.horcrux.svg"/> + <!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore --> <!-- Tell that the Camera is not mandatory to install the application --> <uses-feature @@ -230,7 +233,8 @@ <activity android:name=".features.attachments.preview.AttachmentsPreviewActivity" android:theme="@style/AppTheme.AttachmentsPreview" /> - <activity android:name=".features.call.VectorCallActivity" /> + <activity android:name=".features.call.VectorCallActivity" + android:excludeFromRecents="true"/> <activity android:name=".features.call.conference.VectorJitsiActivity" android:configChanges="orientation|screenSize" /> @@ -240,6 +244,7 @@ <activity android:name=".features.pin.PinActivity" /> <activity android:name=".features.home.room.detail.search.SearchActivity" /> <activity android:name=".features.usercode.UserCodeActivity" /> + <activity android:name=".features.call.transfer.CallTransferActivity" /> <!-- Single instance is very important for the custom scheme callback--> <activity android:name=".features.auth.ReAuthActivity" diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 4ce4e9a210..3c6a9c48a4 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -385,6 +385,11 @@ SOFTWARE. <br/> Copyright 2016 JetRadar </li> + <li> + <b>dialogs / android-dialer</b> + <br/> + Copyright (c) 2017-present, dialog LLC <info@dlg.im> + </li> </ul> <pre> Apache License diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index 921e8c0780..1a7fe35745 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -44,7 +44,7 @@ import im.vector.app.core.di.HasVectorInjector import im.vector.app.core.di.VectorComponent import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.rx.RxConfig -import im.vector.app.features.call.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks @@ -92,7 +92,7 @@ class VectorApplication : @Inject lateinit var rxConfig: RxConfig @Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var pinLocker: PinLocker - @Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager + @Inject lateinit var callManager: WebRtcCallManager lateinit var vectorComponent: VectorComponent @@ -177,7 +177,7 @@ class VectorApplication : }) ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler) ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker) - ProcessLifecycleOwner.get().lifecycle.addObserver(webRtcPeerConnectionManager) + ProcessLifecycleOwner.get().lifecycle.addObserver(callManager) // This should be done as early as possible // initKnownEmojiHashSet(appContext) diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index 1c47b38fdc..77ca68fcf1 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -18,7 +18,7 @@ package im.vector.app.core.di import arrow.core.Option import im.vector.app.ActiveSessionDataSource -import im.vector.app.features.call.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.crypto.keysrequest.KeyRequestHandler import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler import im.vector.app.features.notifications.PushRuleTriggerListener @@ -35,7 +35,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService: private val sessionObservableStore: ActiveSessionDataSource, private val keyRequestHandler: KeyRequestHandler, private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler, - private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, + private val callManager: WebRtcCallManager, private val pushRuleTriggerListener: PushRuleTriggerListener, private val sessionListener: SessionListener, private val imageManager: ImageManager @@ -52,7 +52,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService: incomingVerificationRequestHandler.start(session) session.addListener(sessionListener) pushRuleTriggerListener.startWithSession(session) - session.callSignalingService().addCallListener(webRtcPeerConnectionManager) + session.callSignalingService().addCallListener(callManager) imageManager.onSessionStarted(session) } @@ -60,7 +60,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService: // Do some cleanup first getSafeActiveSession()?.let { Timber.w("clearActiveSession of ${it.myUserId}") - it.callSignalingService().removeCallListener(webRtcPeerConnectionManager) + it.callSignalingService().removeCallListener(callManager) it.removeListener(sessionListener) } diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index 1e8e9b0503..a1de892c4e 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -29,6 +29,7 @@ import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.call.CallControlsBottomSheet import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.conference.VectorJitsiActivity +import im.vector.app.features.call.transfer.CallTransferActivity import im.vector.app.features.createdirect.CreateDirectRoomActivity import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.app.features.crypto.quads.SharedSecureStorageActivity @@ -147,6 +148,7 @@ interface ScreenComponent { fun inject(activity: VectorJitsiActivity) fun inject(activity: SearchActivity) fun inject(activity: UserCodeActivity) + fun inject(activity: CallTransferActivity) fun inject(activity: ReAuthActivity) fun inject(activity: RoomDevToolActivity) diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index 273a142ff1..23d6b618fe 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -29,7 +29,7 @@ import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.pushers.PushersManager import im.vector.app.core.utils.AssetReader import im.vector.app.core.utils.DimensionConverter -import im.vector.app.features.call.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.crypto.keysrequest.KeyRequestHandler import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler @@ -38,6 +38,7 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.HomeRoomListDataSource import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider +import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.login.ReAuthHelper @@ -156,7 +157,9 @@ interface VectorComponent { fun pinLocker(): PinLocker - fun webRtcPeerConnectionManager(): WebRtcPeerConnectionManager + fun webRtcCallManager(): WebRtcCallManager + + fun roomSummaryHolder(): RoomSummariesHolder @Component.Factory interface Factory { diff --git a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt index bed2e0b850..8409021845 100644 --- a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt @@ -22,7 +22,7 @@ import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap import im.vector.app.core.platform.ConfigurationViewModel -import im.vector.app.features.call.SharedActiveCallViewModel +import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel @@ -85,8 +85,8 @@ interface ViewModelModule { @Binds @IntoMap - @ViewModelKey(SharedActiveCallViewModel::class) - fun bindSharedActiveCallViewModel(viewModel: SharedActiveCallViewModel): ViewModel + @ViewModelKey(SharedKnownCallsViewModel::class) + fun bindSharedActiveCallViewModel(viewModel: SharedKnownCallsViewModel): ViewModel @Binds @IntoMap diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt index 8258370fe0..d7f003574c 100644 --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt @@ -18,6 +18,7 @@ package im.vector.app.core.error import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.features.call.dialpad.DialPadLookup import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.isInvalidPassword @@ -112,7 +113,9 @@ class DefaultErrorFormatter @Inject constructor( throwable.localizedMessage } } - else -> throwable.localizedMessage + is DialPadLookup.Failure -> + stringProvider.getString(R.string.call_dial_pad_lookup_error) + else -> throwable.localizedMessage } ?: stringProvider.getString(R.string.unknown_error) } diff --git a/vector/src/main/java/im/vector/app/core/extensions/Set.kt b/vector/src/main/java/im/vector/app/core/extensions/Set.kt index a78fb85a1d..e1787076b9 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Set.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Set.kt @@ -17,10 +17,18 @@ package im.vector.app.core.extensions // Create a new Set including the provided element if not already present, or removing the element if already present -fun <T> Set<T>.toggle(element: T): Set<T> { +fun <T> Set<T>.toggle(element: T, singleElement: Boolean = false): Set<T> { return if (contains(element)) { - minus(element) + if (singleElement) { + emptySet() + } else { + minus(element) + } } else { - plus(element) + if (singleElement) { + setOf(element) + } else { + plus(element) + } } } diff --git a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt index 44b85df93a..b7f97dc6f7 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TextView.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TextView.kt @@ -18,11 +18,19 @@ package im.vector.app.core.extensions import android.text.Spannable import android.text.SpannableString +import android.text.TextPaint +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan import android.text.style.UnderlineSpan +import android.view.View import android.widget.TextView import androidx.annotation.AttrRes +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.isVisible import com.google.android.material.snackbar.Snackbar import im.vector.app.R @@ -48,11 +56,13 @@ fun TextView.setTextOrHide(newText: CharSequence?, hideWhenBlank: Boolean = true * @param coloredTextRes the resource id of the colored part of the text * @param colorAttribute attribute of the color. Default to colorAccent * @param underline true to also underline the text. Default to false + * @param onClick attributes to handle click on the colored part if needed */ fun TextView.setTextWithColoredPart(@StringRes fullTextRes: Int, @StringRes coloredTextRes: Int, @AttrRes colorAttribute: Int = R.attr.colorAccent, - underline: Boolean = false) { + underline: Boolean = false, + onClick: (() -> Unit)?) { val coloredPart = resources.getString(coloredTextRes) // Insert colored part into the full text val fullText = resources.getString(fullTextRes, coloredPart) @@ -65,12 +75,38 @@ fun TextView.setTextWithColoredPart(@StringRes fullTextRes: Int, text = SpannableString(fullText) .apply { setSpan(foregroundSpan, index, index + coloredPart.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + if (onClick != null) { + val clickableSpan = object : ClickableSpan() { + override fun onClick(widget: View) { + onClick() + } + + override fun updateDrawState(ds: TextPaint) { + ds.color = color + ds.isUnderlineText = !underline + } + } + setSpan(clickableSpan, index, index + coloredPart.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + movementMethod = LinkMovementMethod.getInstance() + } if (underline) { setSpan(UnderlineSpan(), index, index + coloredPart.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } } } +fun TextView.setLeftDrawable(@DrawableRes iconRes: Int, @ColorRes tintColor: Int? = null) { + val icon = if (tintColor != null) { + val tint = ContextCompat.getColor(context, tintColor) + ContextCompat.getDrawable(context, iconRes)?.also { + DrawableCompat.setTint(it.mutate(), tint) + } + } else { + ContextCompat.getDrawable(context, iconRes) + } + setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null) +} + /** * Set long click listener to copy the current text of the TextView to the clipboard and show a Snackbar */ diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index d8b61f3cba..5747f5158d 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -45,6 +45,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.viewbinding.ViewBinding import com.bumptech.glide.util.Util import com.google.android.material.snackbar.Snackbar +import com.jakewharton.rxbinding3.view.clicks import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder @@ -87,6 +88,7 @@ import io.reactivex.disposables.Disposable import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.GlobalError import timber.log.Timber +import java.util.concurrent.TimeUnit import kotlin.system.measureTimeMillis abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScreenInjector { @@ -116,6 +118,18 @@ abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScr .disposeOnDestroy() } + /* ========================================================================================== + * Views + * ========================================================================================== */ + + protected fun View.debouncedClicks(onClicked: () -> Unit) { + clicks() + .throttleFirst(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { onClicked() } + .disposeOnDestroy() + } + /* ========================================================================================== * DATA * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt b/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt index d5d8bb14dd..f725742711 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt @@ -16,33 +16,76 @@ package im.vector.app.core.services +import android.app.NotificationChannel import android.content.Context -import android.media.Ringtone -import android.media.RingtoneManager import android.media.AudioAttributes import android.media.AudioManager import android.media.MediaPlayer +import android.media.Ringtone +import android.media.RingtoneManager import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator import androidx.core.content.getSystemService import im.vector.app.R +import im.vector.app.features.notifications.NotificationUtils +import org.matrix.android.sdk.api.extensions.orFalse import timber.log.Timber class CallRingPlayerIncoming( - context: Context + context: Context, + private val notificationUtils: NotificationUtils ) { private val applicationContext = context.applicationContext - private var r: Ringtone? = null + private var ringtone: Ringtone? = null + private var vibrator: Vibrator? = null - fun start() { - val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) - r = RingtoneManager.getRingtone(applicationContext, notification) - Timber.v("## VOIP Starting ringing incomming") - r?.play() + private val VIBRATE_PATTERN = longArrayOf(0, 400, 600) + + fun start(fromBg: Boolean) { + val audioManager = applicationContext.getSystemService<AudioManager>() + val incomingCallChannel = notificationUtils.getChannelForIncomingCall(fromBg) + val ringerMode = audioManager?.ringerMode + if (ringerMode == AudioManager.RINGER_MODE_NORMAL) { + playRingtoneIfNeeded(incomingCallChannel) + } else if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) { + vibrateIfNeeded(incomingCallChannel) + } + } + + private fun playRingtoneIfNeeded(incomingCallChannel: NotificationChannel?) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && incomingCallChannel?.sound != null) { + Timber.v("Ringtone already configured by notification channel") + return + } + val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + ringtone = RingtoneManager.getRingtone(applicationContext, ringtoneUri) + Timber.v("Play ringtone for incoming call") + ringtone?.play() + } + + private fun vibrateIfNeeded(incomingCallChannel: NotificationChannel?) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && incomingCallChannel?.shouldVibrate().orFalse()) { + Timber.v("## Vibration already configured by notification channel") + return + } + vibrator = applicationContext.getSystemService() + Timber.v("Vibrate for incoming call") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val vibrationEffect = VibrationEffect.createWaveform(VIBRATE_PATTERN, 0) + vibrator?.vibrate(vibrationEffect) + } else { + @Suppress("DEPRECATION") + vibrator?.vibrate(VIBRATE_PATTERN, 0) + } } fun stop() { - r?.stop() + ringtone?.stop() + ringtone = null + vibrator?.cancel() + vibrator = null } } @@ -55,12 +98,12 @@ class CallRingPlayerOutgoing( private var player: MediaPlayer? = null fun start() { - val audioManager = applicationContext.getSystemService<AudioManager>()!! + val audioManager: AudioManager? = applicationContext.getSystemService() player?.release() player = createPlayer() // Check if sound is enabled - val ringerMode = audioManager.ringerMode + val ringerMode = audioManager?.ringerMode if (player != null && ringerMode == AudioManager.RINGER_MODE_NORMAL) { try { if (player?.isPlaying == false) { @@ -89,14 +132,14 @@ class CallRingPlayerOutgoing( mediaPlayer.setOnErrorListener(MediaPlayerErrorListener()) mediaPlayer.isLooping = true - if (Build.VERSION.SDK_INT <= 21) { - @Suppress("DEPRECATION") - mediaPlayer.setAudioStreamType(AudioManager.STREAM_RING) - } else { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { mediaPlayer.setAudioAttributes(AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) .build()) + } else { + @Suppress("DEPRECATION") + mediaPlayer.setAudioStreamType(AudioManager.STREAM_RING) } return mediaPlayer } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/core/services/CallService.kt b/vector/src/main/java/im/vector/app/core/services/CallService.kt index 075b237be2..e9e855e760 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallService.kt @@ -22,30 +22,41 @@ import android.content.Intent import android.os.Binder import android.support.v4.media.session.MediaSessionCompat import android.view.KeyEvent +import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.media.session.MediaButtonReceiver +import com.airbnb.mvrx.MvRx import im.vector.app.core.extensions.vectorComponent -import im.vector.app.features.call.WebRtcPeerConnectionManager +import im.vector.app.features.call.CallArgs +import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.telecom.CallConnection +import im.vector.app.features.call.webrtc.WebRtcCall +import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.notifications.NotificationUtils +import im.vector.app.features.popup.IncomingCallAlert +import im.vector.app.features.popup.PopupAlertManager +import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.api.util.toMatrixItem import timber.log.Timber /** * Foreground service to manage calls */ -class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListener, BluetoothHeadsetReceiver.EventListener { +class CallService : VectorService() { private val connections = mutableMapOf<String, CallConnection>() + private val knownCalls = mutableSetOf<String>() + private lateinit var notificationManager: NotificationManagerCompat private lateinit var notificationUtils: NotificationUtils - private lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager + private lateinit var callManager: WebRtcCallManager + private lateinit var avatarRenderer: AvatarRenderer + private lateinit var alertManager: PopupAlertManager private var callRingPlayerIncoming: CallRingPlayerIncoming? = null private var callRingPlayerOutgoing: CallRingPlayerOutgoing? = null - private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null - private var bluetoothHeadsetStateReceiver: BluetoothHeadsetReceiver? = null - // A media button receiver receives and helps translate hardware media playback buttons, // such as those found on wired and wireless headsets, into the appropriate callbacks in your app private var mediaSession: MediaSessionCompat? = null @@ -53,7 +64,7 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean { val keyEvent = mediaButtonEvent?.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT) ?: return false if (keyEvent.keyCode == KeyEvent.KEYCODE_HEADSETHOOK) { - webRtcPeerConnectionManager.headSetButtonTapped() + callManager.headSetButtonTapped() return true } return false @@ -62,22 +73,19 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe override fun onCreate() { super.onCreate() + notificationManager = NotificationManagerCompat.from(this) notificationUtils = vectorComponent().notificationUtils() - webRtcPeerConnectionManager = vectorComponent().webRtcPeerConnectionManager() - callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext) + callManager = vectorComponent().webRtcCallManager() + avatarRenderer = vectorComponent().avatarRenderer() + alertManager = vectorComponent().alertManager() + callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext, notificationUtils) callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext) - wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this) - bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(this, this) } override fun onDestroy() { super.onDestroy() callRingPlayerIncoming?.stop() callRingPlayerOutgoing?.stop() - wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(this, it) } - wiredHeadsetStateReceiver = null - bluetoothHeadsetStateReceiver?.let { BluetoothHeadsetReceiver.unRegister(this, it) } - bluetoothHeadsetStateReceiver = null mediaSession?.release() mediaSession = null } @@ -89,21 +97,17 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe setCallback(mediaSessionButtonCallback) } } - if (intent == null) { - // Service started again by the system. - // TODO What do we do here? - return START_STICKY - } mediaSession?.let { // This ensures that the correct callbacks to MediaSessionCompat.Callback // will be triggered based on the incoming KeyEvent. MediaButtonReceiver.handleIntent(it, intent) } - when (intent.action) { + when (intent?.action) { ACTION_INCOMING_RINGING_CALL -> { mediaSession?.isActive = true - callRingPlayerIncoming?.start() + val fromBg = intent.getBooleanExtra(EXTRA_IS_IN_BG, false) + callRingPlayerIncoming?.start(fromBg) displayIncomingCallNotification(intent) } ACTION_OUTGOING_RINGING_CALL -> { @@ -111,33 +115,28 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe callRingPlayerOutgoing?.start() displayOutgoingRingingCallNotification(intent) } - ACTION_ONGOING_CALL -> { + ACTION_ONGOING_CALL -> { callRingPlayerIncoming?.stop() callRingPlayerOutgoing?.stop() displayCallInProgressNotification(intent) } - ACTION_NO_ACTIVE_CALL -> hideCallNotifications() - ACTION_CALL_CONNECTING -> { + ACTION_CALL_CONNECTING -> { // lower notification priority displayCallInProgressNotification(intent) // stop ringing callRingPlayerIncoming?.stop() callRingPlayerOutgoing?.stop() } - ACTION_ONGOING_CALL_BG -> { - // there is an ongoing call but call activity is in background - displayCallOnGoingInBackground(intent) + ACTION_CALL_TERMINATED -> { + handleCallTerminated(intent) } else -> { - // Should not happen - callRingPlayerIncoming?.stop() - callRingPlayerOutgoing?.stop() - myStopSelf() + handleUnexpectedState(null) } } // We want the system to restore the service if killed - return START_STICKY + return START_REDELIVER_INTENT } // ================================================================================ @@ -147,64 +146,90 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe /** * Display a permanent notification when there is an incoming call. * - * @param session the session - * @param isVideo true if this is a video call, false for voice call - * @param room the room - * @param callId the callId */ private fun displayIncomingCallNotification(intent: Intent) { Timber.v("## VOIP displayIncomingCallNotification $intent") - - // the incoming call in progress is already displayed -// if (!TextUtils.isEmpty(mIncomingCallId)) { -// Timber.v("displayIncomingCallNotification : the incoming call in progress is already displayed") -// } else if (!TextUtils.isEmpty(mCallIdInProgress)) { -// Timber.v("displayIncomingCallNotification : a 'call in progress' notification is displayed") -// } else -// // if (null == webRtcPeerConnectionManager.currentCall) -// { - val callId = intent.getStringExtra(EXTRA_CALL_ID) - + val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" + val call = callManager.getCallById(callId) ?: return Unit.also { + handleUnexpectedState(callId) + } + val isVideoCall = call.mxCall.isVideoCall + val fromBg = intent.getBooleanExtra(EXTRA_IS_IN_BG, false) + val opponentMatrixItem = getOpponentMatrixItem(call) Timber.v("displayIncomingCallNotification : display the dedicated notification") + val incomingCallAlert = IncomingCallAlert(callId, + shouldBeDisplayedIn = { activity -> + if (activity is VectorCallActivity) { + activity.intent.getParcelableExtra<CallArgs>(MvRx.KEY_ARG)?.callId != call.callId + } else true + } + ).apply { + viewBinder = IncomingCallAlert.ViewBinder( + matrixItem = opponentMatrixItem, + avatarRenderer = avatarRenderer, + isVideoCall = isVideoCall, + onAccept = { showCallScreen(call, VectorCallActivity.INCOMING_ACCEPT) }, + onReject = { call.endCall() } + ) + dismissedAction = Runnable { call.endCall() } + contentAction = Runnable { showCallScreen(call, VectorCallActivity.INCOMING_RINGING) } + } + alertManager.postVectorAlert(incomingCallAlert) val notification = notificationUtils.buildIncomingCallNotification( - intent.getBooleanExtra(EXTRA_IS_VIDEO, false), - intent.getStringExtra(EXTRA_ROOM_NAME) ?: "", - intent.getStringExtra(EXTRA_ROOM_ID) ?: "", - callId ?: "") - startForeground(NOTIFICATION_ID, notification) + mxCall = call.mxCall, + title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId, + fromBg = fromBg + ) + if (knownCalls.isEmpty()) { + startForeground(callId.hashCode(), notification) + } else { + notificationManager.notify(callId.hashCode(), notification) + } + knownCalls.add(callId) + } -// mIncomingCallId = callId + private fun handleCallTerminated(intent: Intent) { + val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" + alertManager.cancelAlert(callId) + if (!knownCalls.remove(callId)) { + Timber.v("Call terminated for unknown call $callId$") + handleUnexpectedState(callId) + return + } + val notification = notificationUtils.buildCallEndedNotification() + notificationManager.notify(callId.hashCode(), notification) + if (knownCalls.isEmpty()) { + mediaSession?.isActive = false + myStopSelf() + } + } - // turn the screen on for 3 seconds -// if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) { -// try { -// val pm = getSystemService<PowerManager>()!! -// val wl = pm.newWakeLock( -// WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP, -// CallService::class.java.simpleName) -// wl.acquire(3000) -// wl.release() -// } catch (re: RuntimeException) { -// Timber.e(re, "displayIncomingCallNotification : failed to turn screen on ") -// } -// -// } -// } -// else { -// Timber.i("displayIncomingCallNotification : do not display the incoming call notification because there is a pending call") -// } + private fun showCallScreen(call: WebRtcCall, mode: String) { + val intent = VectorCallActivity.newIntent( + context = this, + mxCall = call.mxCall, + mode = mode + ) + startActivity(intent) } private fun displayOutgoingRingingCallNotification(intent: Intent) { - val callId = intent.getStringExtra(EXTRA_CALL_ID) - + val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" + val call = callManager.getCallById(callId) ?: return Unit.also { + handleUnexpectedState(callId) + } + val opponentMatrixItem = getOpponentMatrixItem(call) Timber.v("displayOutgoingCallNotification : display the dedicated notification") val notification = notificationUtils.buildOutgoingRingingCallNotification( - intent.getBooleanExtra(EXTRA_IS_VIDEO, false), - intent.getStringExtra(EXTRA_ROOM_NAME) ?: "", - intent.getStringExtra(EXTRA_ROOM_ID) ?: "", - callId ?: "") - startForeground(NOTIFICATION_ID, notification) + mxCall = call.mxCall, + title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId + ) + if (knownCalls.isEmpty()) { + startForeground(callId.hashCode(), notification) + } else { + notificationManager.notify(callId.hashCode(), notification) + } + knownCalls.add(callId) } /** @@ -213,125 +238,78 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe private fun displayCallInProgressNotification(intent: Intent) { Timber.v("## VOIP displayCallInProgressNotification") val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" - + val call = callManager.getCallById(callId) ?: return Unit.also { + handleUnexpectedState(callId) + } + val opponentMatrixItem = getOpponentMatrixItem(call) + alertManager.cancelAlert(callId) val notification = notificationUtils.buildPendingCallNotification( - intent.getBooleanExtra(EXTRA_IS_VIDEO, false), - intent.getStringExtra(EXTRA_ROOM_NAME) ?: "", - intent.getStringExtra(EXTRA_ROOM_ID) ?: "", - intent.getStringExtra(EXTRA_MATRIX_ID) ?: "", - callId) - - startForeground(NOTIFICATION_ID, notification) - - // mCallIdInProgress = callId + mxCall = call.mxCall, + title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId + ) + if (knownCalls.isEmpty()) { + startForeground(callId.hashCode(), notification) + } else { + notificationManager.notify(callId.hashCode(), notification) + } + knownCalls.add(callId) } - /** - * Display a call in progress notification. - */ - private fun displayCallOnGoingInBackground(intent: Intent) { - Timber.v("## VOIP displayCallInProgressNotification") - val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" - - val notification = notificationUtils.buildPendingCallNotification( - isVideo = intent.getBooleanExtra(EXTRA_IS_VIDEO, false), - roomName = intent.getStringExtra(EXTRA_ROOM_NAME) ?: "", - roomId = intent.getStringExtra(EXTRA_ROOM_ID) ?: "", - matrixId = intent.getStringExtra(EXTRA_MATRIX_ID) ?: "", - callId = callId, - fromBg = true) - - startForeground(NOTIFICATION_ID, notification) - - // mCallIdInProgress = callId - } - - /** - * Hide the permanent call notifications - */ - private fun hideCallNotifications() { + private fun handleUnexpectedState(callId: String?) { + Timber.v("Fallback to clear everything") + callRingPlayerIncoming?.stop() + callRingPlayerOutgoing?.stop() + if (callId != null) { + notificationManager.cancel(callId.hashCode()) + } val notification = notificationUtils.buildCallEndedNotification() - - mediaSession?.isActive = false - // It's mandatory to startForeground to avoid crash - startForeground(NOTIFICATION_ID, notification) - - myStopSelf() + startForeground(DEFAULT_NOTIFICATION_ID, notification) + if (knownCalls.isEmpty()) { + mediaSession?.isActive = false + myStopSelf() + } } fun addConnection(callConnection: CallConnection) { connections[callConnection.callId] = callConnection } + private fun getOpponentMatrixItem(call: WebRtcCall): MatrixItem? { + return vectorComponent().currentSession().getUser(call.mxCall.opponentUserId)?.toMatrixItem() + } + companion object { - private const val NOTIFICATION_ID = 6480 + private const val DEFAULT_NOTIFICATION_ID = 6480 private const val ACTION_INCOMING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_INCOMING_RINGING_CALL" private const val ACTION_OUTGOING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_OUTGOING_RINGING_CALL" private const val ACTION_CALL_CONNECTING = "im.vector.app.core.services.CallService.ACTION_CALL_CONNECTING" private const val ACTION_ONGOING_CALL = "im.vector.app.core.services.CallService.ACTION_ONGOING_CALL" - private const val ACTION_ONGOING_CALL_BG = "im.vector.app.core.services.CallService.ACTION_ONGOING_CALL_BG" + private const val ACTION_CALL_TERMINATED = "im.vector.app.core.services.CallService.ACTION_CALL_TERMINATED" private const val ACTION_NO_ACTIVE_CALL = "im.vector.app.core.services.CallService.NO_ACTIVE_CALL" // private const val ACTION_ACTIVITY_VISIBLE = "im.vector.app.core.services.CallService.ACTION_ACTIVITY_VISIBLE" // private const val ACTION_STOP_RINGING = "im.vector.app.core.services.CallService.ACTION_STOP_RINGING" - private const val EXTRA_IS_VIDEO = "EXTRA_IS_VIDEO" - private const val EXTRA_ROOM_NAME = "EXTRA_ROOM_NAME" - private const val EXTRA_ROOM_ID = "EXTRA_ROOM_ID" - private const val EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID" private const val EXTRA_CALL_ID = "EXTRA_CALL_ID" + private const val EXTRA_IS_IN_BG = "EXTRA_IS_IN_BG" fun onIncomingCallRinging(context: Context, - isVideo: Boolean, - roomName: String, - roomId: String, - matrixId: String, - callId: String) { + callId: String, + isInBackground: Boolean) { val intent = Intent(context, CallService::class.java) .apply { action = ACTION_INCOMING_RINGING_CALL - putExtra(EXTRA_IS_VIDEO, isVideo) - putExtra(EXTRA_ROOM_NAME, roomName) - putExtra(EXTRA_ROOM_ID, roomId) - putExtra(EXTRA_MATRIX_ID, matrixId) putExtra(EXTRA_CALL_ID, callId) + putExtra(EXTRA_IS_IN_BG, isInBackground) } - - ContextCompat.startForegroundService(context, intent) - } - - fun onOnGoingCallBackground(context: Context, - isVideo: Boolean, - roomName: String, - roomId: String, - matrixId: String, - callId: String) { - val intent = Intent(context, CallService::class.java) - .apply { - action = ACTION_ONGOING_CALL_BG - putExtra(EXTRA_IS_VIDEO, isVideo) - putExtra(EXTRA_ROOM_NAME, roomName) - putExtra(EXTRA_ROOM_ID, roomId) - putExtra(EXTRA_MATRIX_ID, matrixId) - putExtra(EXTRA_CALL_ID, callId) - } - ContextCompat.startForegroundService(context, intent) } fun onOutgoingCallRinging(context: Context, - isVideo: Boolean, - roomName: String, - roomId: String, - matrixId: String, callId: String) { val intent = Intent(context, CallService::class.java) .apply { action = ACTION_OUTGOING_RINGING_CALL - putExtra(EXTRA_IS_VIDEO, isVideo) - putExtra(EXTRA_ROOM_NAME, roomName) - putExtra(EXTRA_ROOM_ID, roomId) - putExtra(EXTRA_MATRIX_ID, matrixId) putExtra(EXTRA_CALL_ID, callId) } @@ -339,30 +317,22 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe } fun onPendingCall(context: Context, - isVideo: Boolean, - roomName: String, - roomId: String, - matrixId: String, callId: String) { val intent = Intent(context, CallService::class.java) .apply { action = ACTION_ONGOING_CALL - putExtra(EXTRA_IS_VIDEO, isVideo) - putExtra(EXTRA_ROOM_NAME, roomName) - putExtra(EXTRA_ROOM_ID, roomId) - putExtra(EXTRA_MATRIX_ID, matrixId) putExtra(EXTRA_CALL_ID, callId) } ContextCompat.startForegroundService(context, intent) } - fun onNoActiveCall(context: Context) { + fun onCallTerminated(context: Context, callId: String) { val intent = Intent(context, CallService::class.java) .apply { - action = ACTION_NO_ACTIVE_CALL + action = ACTION_CALL_TERMINATED + putExtra(EXTRA_CALL_ID, callId) } - ContextCompat.startForegroundService(context, intent) } } @@ -372,14 +342,4 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe return this@CallService } } - - override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) { - Timber.v("## VOIP: onHeadsetEvent $event") - webRtcPeerConnectionManager.onWiredDeviceEvent(event) - } - - override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) { - Timber.v("## VOIP: onBTHeadsetEvent $event") - webRtcPeerConnectionManager.onWirelessDeviceEvent(event) - } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt index f86825750a..59418147d7 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt @@ -41,13 +41,13 @@ class BottomSheetActionButton @JvmOverloads constructor( var title: String? = null set(value) { field = value - views.itemVerificationActionTitle.setTextOrHide(value) + views.bottomSheetActionTitle.setTextOrHide(value) } var subTitle: String? = null set(value) { field = value - views.itemVerificationActionSubTitle.setTextOrHide(value) + views.bottomSheetActionSubTitle.setTextOrHide(value) } var forceStartPadding: Boolean? = null @@ -55,9 +55,9 @@ class BottomSheetActionButton @JvmOverloads constructor( field = value if (leftIcon == null) { if (forceStartPadding == true) { - views.itemVerificationLeftIcon.isInvisible = true + views.bottomSheetActionLeftIcon.isInvisible = true } else { - views.itemVerificationLeftIcon.isGone = true + views.bottomSheetActionLeftIcon.isGone = true } } } @@ -67,33 +67,33 @@ class BottomSheetActionButton @JvmOverloads constructor( field = value if (value == null) { if (forceStartPadding == true) { - views.itemVerificationLeftIcon.isInvisible = true + views.bottomSheetActionLeftIcon.isInvisible = true } else { - views.itemVerificationLeftIcon.isGone = true + views.bottomSheetActionLeftIcon.isGone = true } - views.itemVerificationLeftIcon.setImageDrawable(null) + views.bottomSheetActionLeftIcon.setImageDrawable(null) } else { - views.itemVerificationLeftIcon.isVisible = true - views.itemVerificationLeftIcon.setImageDrawable(value) + views.bottomSheetActionLeftIcon.isVisible = true + views.bottomSheetActionLeftIcon.setImageDrawable(value) } } var rightIcon: Drawable? = null set(value) { field = value - views.itemVerificationActionIcon.setImageDrawable(value) + views.bottomSheetActionIcon.setImageDrawable(value) } var tint: Int? = null set(value) { field = value - views.itemVerificationLeftIcon.imageTintList = value?.let { ColorStateList.valueOf(value) } + views.bottomSheetActionLeftIcon.imageTintList = value?.let { ColorStateList.valueOf(value) } } var titleTextColor: Int? = null set(value) { field = value - value?.let { views.itemVerificationActionTitle.setTextColor(it) } + value?.let { views.bottomSheetActionTitle.setTextColor(it) } } init { diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallView.kt b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt similarity index 50% rename from vector/src/main/java/im/vector/app/core/ui/views/ActiveCallView.kt rename to vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt index 19d1fbb6f6..d1332f18dc 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt @@ -20,9 +20,12 @@ import android.content.Context import android.util.AttributeSet import android.widget.RelativeLayout import im.vector.app.R +import im.vector.app.databinding.ViewCurrentCallsBinding +import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.themes.ThemeUtils +import org.matrix.android.sdk.api.session.call.CallState -class ActiveCallView @JvmOverloads constructor( +class CurrentCallsView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 @@ -32,15 +35,32 @@ class ActiveCallView @JvmOverloads constructor( fun onTapToReturnToCall() } + val views: ViewCurrentCallsBinding var callback: Callback? = null init { - setupView() - } - - private fun setupView() { - inflate(context, R.layout.view_active_call_view, this) + inflate(context, R.layout.view_current_calls, this) + views = ViewCurrentCallsBinding.bind(this) setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary)) setOnClickListener { callback?.onTapToReturnToCall() } } + + fun render(calls: List<WebRtcCall>, formattedDuration: String) { + val connectedCalls = calls.filter { + it.mxCall.state is CallState.Connected + } + val heldCalls = connectedCalls.filter { + it.isLocalOnHold || it.remoteOnHold + } + if (connectedCalls.isEmpty()) return + views.currentCallsInfo.text = if (connectedCalls.size == heldCalls.size) { + resources.getQuantityString(R.plurals.call_only_paused, heldCalls.size, heldCalls.size) + } else { + if (heldCalls.isEmpty()) { + resources.getString(R.string.call_only_active, formattedDuration) + } else { + resources.getQuantityString(R.plurals.call_one_active_and_other_paused, heldCalls.size, formattedDuration, heldCalls.size) + } + } + } } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt b/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt similarity index 58% rename from vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt rename to vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt index 43346e583e..3bd9ce713d 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt @@ -16,42 +16,56 @@ package im.vector.app.core.ui.views -import android.view.View import androidx.cardview.widget.CardView import androidx.core.view.isVisible import im.vector.app.core.utils.DebouncedClickListener -import im.vector.app.features.call.WebRtcPeerConnectionManager import org.matrix.android.sdk.api.session.call.CallState -import org.matrix.android.sdk.api.session.call.EglUtils -import org.matrix.android.sdk.api.session.call.MxCall +import im.vector.app.features.call.utils.EglUtils +import im.vector.app.features.call.webrtc.WebRtcCall import org.webrtc.RendererCommon import org.webrtc.SurfaceViewRenderer -class ActiveCallViewHolder { +class KnownCallsViewHolder { private var activeCallPiP: SurfaceViewRenderer? = null - private var activeCallView: ActiveCallView? = null + private var currentCallsView: CurrentCallsView? = null private var pipWrapper: CardView? = null + private var currentCall: WebRtcCall? = null + private var calls: List<WebRtcCall> = emptyList() private var activeCallPipInitialized = false - fun updateCall(activeCall: MxCall?, webRtcPeerConnectionManager: WebRtcPeerConnectionManager) { - val hasActiveCall = activeCall?.state is CallState.Connected + private val tickListener = object : WebRtcCall.Listener { + override fun onTick(formattedDuration: String) { + currentCallsView?.render(calls, formattedDuration) + } + } + + fun updateCall(currentCall: WebRtcCall?, calls: List<WebRtcCall>) { + activeCallPiP?.let { + this.currentCall?.detachRenderers(listOf(it)) + } + this.currentCall?.removeListener(tickListener) + this.currentCall = currentCall + this.currentCall?.addListener(tickListener) + this.calls = calls + val hasActiveCall = currentCall?.mxCall?.state is CallState.Connected if (hasActiveCall) { - val isVideoCall = activeCall?.isVideoCall == true + val isVideoCall = currentCall?.mxCall?.isVideoCall == true if (isVideoCall) initIfNeeded() - activeCallView?.isVisible = !isVideoCall + currentCallsView?.isVisible = !isVideoCall + currentCallsView?.render(calls, currentCall?.formattedDuration() ?: "") pipWrapper?.isVisible = isVideoCall activeCallPiP?.isVisible = isVideoCall activeCallPiP?.let { - webRtcPeerConnectionManager.attachViewRenderers(null, it, null) + currentCall?.attachViewRenderers(null, it, null) } } else { - activeCallView?.isVisible = false + currentCallsView?.isVisible = false activeCallPiP?.isVisible = false pipWrapper?.isVisible = false activeCallPiP?.let { - webRtcPeerConnectionManager.detachRenderers(listOf(it)) + currentCall?.detachRenderers(listOf(it)) } } } @@ -69,30 +83,31 @@ class ActiveCallViewHolder { } } - fun bind(activeCallPiP: SurfaceViewRenderer, activeCallView: ActiveCallView, pipWrapper: CardView, interactionListener: ActiveCallView.Callback) { + fun bind(activeCallPiP: SurfaceViewRenderer, activeCallView: CurrentCallsView, pipWrapper: CardView, interactionListener: CurrentCallsView.Callback) { this.activeCallPiP = activeCallPiP - this.activeCallView = activeCallView + this.currentCallsView = activeCallView this.pipWrapper = pipWrapper - - this.activeCallView?.callback = interactionListener + this.currentCallsView?.callback = interactionListener pipWrapper.setOnClickListener( - DebouncedClickListener(View.OnClickListener { _ -> + DebouncedClickListener({ _ -> interactionListener.onTapToReturnToCall() }) ) + this.currentCall?.addListener(tickListener) } - fun unBind(webRtcPeerConnectionManager: WebRtcPeerConnectionManager) { + fun unBind() { activeCallPiP?.let { - webRtcPeerConnectionManager.detachRenderers(listOf(it)) + currentCall?.detachRenderers(listOf(it)) } if (activeCallPipInitialized) { activeCallPiP?.release() } - this.activeCallView?.callback = null + this.currentCallsView?.callback = null + this.currentCall?.removeListener(tickListener) pipWrapper?.setOnClickListener(null) activeCallPiP = null - activeCallView = null + currentCallsView = null pipWrapper = null } } diff --git a/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt b/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt new file mode 100644 index 0000000000..2a9482765c --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/CountUpTimer.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2020 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.utils + +import io.reactivex.Observable +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong + +class CountUpTimer(private val intervalInMs: Long) { + + private val elapsedTime: AtomicLong = AtomicLong() + private val resumed: AtomicBoolean = AtomicBoolean(false) + + private val disposable = Observable.interval(intervalInMs, TimeUnit.MILLISECONDS) + .filter { _ -> resumed.get() } + .doOnNext { _ -> elapsedTime.addAndGet(intervalInMs) } + .subscribe { + tickListener?.onTick(elapsedTime.get()) + } + + var tickListener: TickListener? = null + + fun elapsedTime(): Long { + return elapsedTime.get() + } + + fun pause() { + resumed.set(false) + } + + fun resume() { + resumed.set(true) + } + + fun stop() { + disposable.dispose() + } + + interface TickListener { + fun onTick(milliseconds: Long) + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt b/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt deleted file mode 100644 index 82bbaf1d54..0000000000 --- a/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt +++ /dev/null @@ -1,318 +0,0 @@ -/* - * Copyright (c) 2020 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.call - -import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothManager -import android.bluetooth.BluetoothProfile -import android.content.Context -import android.content.pm.PackageManager -import android.media.AudioManager -import androidx.core.content.getSystemService -import im.vector.app.core.services.WiredHeadsetStateReceiver -import org.matrix.android.sdk.api.session.call.CallState -import org.matrix.android.sdk.api.session.call.MxCall -import timber.log.Timber -import java.util.concurrent.Executors - -class CallAudioManager( - val applicationContext: Context, - val configChange: (() -> Unit)? -) { - - enum class SoundDevice { - PHONE, - SPEAKER, - HEADSET, - WIRELESS_HEADSET - } - - // if all calls to audio manager not in the same thread it's not working well. - private val executor = Executors.newSingleThreadExecutor() - - private var audioManager: AudioManager? = null - - private var savedIsSpeakerPhoneOn = false - private var savedIsMicrophoneMute = false - private var savedAudioMode = AudioManager.MODE_NORMAL - - private var connectedBlueToothHeadset: BluetoothProfile? = null - private var wantsBluetoothConnection = false - - private var bluetoothAdapter: BluetoothAdapter? = null - - init { - executor.execute { - audioManager = applicationContext.getSystemService() - } - val bm = applicationContext.getSystemService<BluetoothManager>() - val adapter = bm?.adapter - Timber.d("## VOIP Bluetooth adapter $adapter") - bluetoothAdapter = adapter - adapter?.getProfileProxy(applicationContext, object : BluetoothProfile.ServiceListener { - override fun onServiceDisconnected(profile: Int) { - Timber.d("## VOIP onServiceDisconnected $profile") - if (profile == BluetoothProfile.HEADSET) { - connectedBlueToothHeadset = null - configChange?.invoke() - } - } - - override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) { - Timber.d("## VOIP onServiceConnected $profile , proxy:$proxy") - if (profile == BluetoothProfile.HEADSET) { - connectedBlueToothHeadset = proxy - configChange?.invoke() - } - } - }, BluetoothProfile.HEADSET) - } - - private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> - - // Called on the listener to notify if the audio focus for this listener has been changed. - // The |focusChange| value indicates whether the focus was gained, whether the focus was lost, - // and whether that loss is transient, or whether the new focus holder will hold it for an - // unknown amount of time. - Timber.v("## VOIP: Audio focus change $focusChange") - } - - fun startForCall(mxCall: MxCall) { - Timber.v("## VOIP: AudioManager startForCall ${mxCall.callId}") - } - - private fun setupAudioManager(mxCall: MxCall) { - Timber.v("## VOIP: AudioManager setupAudioManager ${mxCall.callId}") - val audioManager = audioManager ?: return - savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn - savedIsMicrophoneMute = audioManager.isMicrophoneMute - savedAudioMode = audioManager.mode - - // Request audio playout focus (without ducking) and install listener for changes in focus. - - // Remove the deprecation forces us to use 2 different method depending on API level - @Suppress("DEPRECATION") val result = audioManager.requestAudioFocus(audioFocusChangeListener, - AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) - if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - Timber.d("## VOIP Audio focus request granted for VOICE_CALL streams") - } else { - Timber.d("## VOIP Audio focus request failed") - } - - // Start by setting MODE_IN_COMMUNICATION as default audio mode. It is - // required to be in this mode when playout and/or recording starts for - // best possible VoIP performance. - audioManager.mode = AudioManager.MODE_IN_COMMUNICATION - - // Always disable microphone mute during a WebRTC call. - setMicrophoneMute(false) - - adjustCurrentSoundDevice(mxCall) - } - - private fun adjustCurrentSoundDevice(mxCall: MxCall) { - val audioManager = audioManager ?: return - executor.execute { - if (mxCall.state == CallState.LocalRinging && !isHeadsetOn()) { - // Always use speaker if incoming call is in ringing state and a headset is not connected - Timber.v("##VOIP: AudioManager default to SPEAKER (it is ringing)") - setCurrentSoundDevice(SoundDevice.SPEAKER) - } else if (mxCall.isVideoCall && !isHeadsetOn()) { - // If there are no headset, start video output in speaker - // (you can't watch the video and have the phone close to your ear) - Timber.v("##VOIP: AudioManager default to speaker ") - setCurrentSoundDevice(SoundDevice.SPEAKER) - } else { - // if a wired headset is plugged, sound will be directed to it - // (can't really force earpiece when headset is plugged) - if (isBluetoothHeadsetConnected(audioManager)) { - Timber.v("##VOIP: AudioManager default to WIRELESS_HEADSET ") - setCurrentSoundDevice(SoundDevice.WIRELESS_HEADSET) - // try now in case already connected? - audioManager.isBluetoothScoOn = true - } else { - Timber.v("##VOIP: AudioManager default to PHONE/HEADSET ") - setCurrentSoundDevice(if (isWiredHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE) - } - } - } - } - - fun onCallConnected(mxCall: MxCall) { - Timber.v("##VOIP: AudioManager call answered, adjusting current sound device") - setupAudioManager(mxCall) - } - - fun getAvailableSoundDevices(): List<SoundDevice> { - return ArrayList<SoundDevice>().apply { - if (isBluetoothHeadsetOn()) add(SoundDevice.WIRELESS_HEADSET) - add(if (isWiredHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE) - add(SoundDevice.SPEAKER) - } - } - - fun stop() { - Timber.v("## VOIP: AudioManager stopCall") - executor.execute { - // Restore previously stored audio states. - setSpeakerphoneOn(savedIsSpeakerPhoneOn) - setMicrophoneMute(savedIsMicrophoneMute) - audioManager?.mode = savedAudioMode - - connectedBlueToothHeadset?.let { - if (audioManager != null && isBluetoothHeadsetConnected(audioManager!!)) { - audioManager?.stopBluetoothSco() - audioManager?.isBluetoothScoOn = false - audioManager?.isSpeakerphoneOn = false - } - bluetoothAdapter?.closeProfileProxy(BluetoothProfile.HEADSET, it) - } - - audioManager?.mode = AudioManager.MODE_NORMAL - - @Suppress("DEPRECATION") - audioManager?.abandonAudioFocus(audioFocusChangeListener) - } - } - - fun getCurrentSoundDevice(): SoundDevice { - val audioManager = audioManager ?: return SoundDevice.PHONE - if (audioManager.isSpeakerphoneOn) { - return SoundDevice.SPEAKER - } else { - if (isBluetoothHeadsetConnected(audioManager)) return SoundDevice.WIRELESS_HEADSET - return if (isHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE - } - } - - private fun isBluetoothHeadsetConnected(audioManager: AudioManager) = - isBluetoothHeadsetOn() - && !connectedBlueToothHeadset?.connectedDevices.isNullOrEmpty() - && (wantsBluetoothConnection || audioManager.isBluetoothScoOn) - - fun setCurrentSoundDevice(device: SoundDevice) { - executor.execute { - Timber.v("## VOIP setCurrentSoundDevice $device") - when (device) { - SoundDevice.HEADSET, - SoundDevice.PHONE -> { - wantsBluetoothConnection = false - if (isBluetoothHeadsetOn()) { - audioManager?.stopBluetoothSco() - audioManager?.isBluetoothScoOn = false - } - setSpeakerphoneOn(false) - } - SoundDevice.SPEAKER -> { - setSpeakerphoneOn(true) - wantsBluetoothConnection = false - audioManager?.stopBluetoothSco() - audioManager?.isBluetoothScoOn = false - } - SoundDevice.WIRELESS_HEADSET -> { - setSpeakerphoneOn(false) - // I cannot directly do it, i have to start then wait that it's connected - // to route to bt - audioManager?.startBluetoothSco() - wantsBluetoothConnection = true - } - } - - configChange?.invoke() - } - } - - fun bluetoothStateChange(plugged: Boolean) { - executor.execute { - if (plugged && wantsBluetoothConnection) { - audioManager?.isBluetoothScoOn = true - } else if (!plugged && !wantsBluetoothConnection) { - audioManager?.stopBluetoothSco() - } - - configChange?.invoke() - } - } - - fun wiredStateChange(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) { - executor.execute { - // if it's plugged and speaker is on we should route to headset - if (event.plugged && getCurrentSoundDevice() == SoundDevice.SPEAKER) { - setCurrentSoundDevice(CallAudioManager.SoundDevice.HEADSET) - } else if (!event.plugged) { - // if it's unplugged ? always route to speaker? - // this is questionable? - if (!wantsBluetoothConnection) { - setCurrentSoundDevice(SoundDevice.SPEAKER) - } - } - configChange?.invoke() - } - } - - private fun isHeadsetOn(): Boolean { - return isWiredHeadsetOn() || (audioManager?.let { isBluetoothHeadsetConnected(it) } ?: false) - } - - private fun isWiredHeadsetOn(): Boolean { - @Suppress("DEPRECATION") - return audioManager?.isWiredHeadsetOn ?: false - } - - private fun isBluetoothHeadsetOn(): Boolean { - Timber.v("## VOIP: AudioManager isBluetoothHeadsetOn") - try { - if (connectedBlueToothHeadset == null) return false.also { - Timber.v("## VOIP: AudioManager no connected bluetooth headset") - } - if (audioManager?.isBluetoothScoAvailableOffCall == false) return false.also { - Timber.v("## VOIP: AudioManager isBluetoothScoAvailableOffCall false") - } - return true - } catch (failure: Throwable) { - Timber.e("## VOIP: AudioManager isBluetoothHeadsetOn failure ${failure.localizedMessage}") - return false - } - } - - /** Sets the speaker phone mode. */ - private fun setSpeakerphoneOn(on: Boolean) { - Timber.v("## VOIP: AudioManager setSpeakerphoneOn $on") - val wasOn = audioManager?.isSpeakerphoneOn ?: false - if (wasOn == on) { - return - } - audioManager?.isSpeakerphoneOn = on - } - - /** Sets the microphone mute state. */ - private fun setMicrophoneMute(on: Boolean) { - Timber.v("## VOIP: AudioManager setMicrophoneMute $on") - val wasMuted = audioManager?.isMicrophoneMute ?: false - if (wasMuted == on) { - return - } - audioManager?.isMicrophoneMute = on - } - - /** true if the device has a telephony radio with data - * communication support. */ - private fun isThisPhone(): Boolean { - return applicationContext.packageManager.hasSystemFeature( - PackageManager.FEATURE_TELEPHONY) - } -} diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt index 75b1033098..84658a830c 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt @@ -27,6 +27,7 @@ import com.airbnb.mvrx.activityViewModel import im.vector.app.R import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.databinding.BottomSheetCallControlsBinding +import im.vector.app.features.call.audio.CallAudioManager import me.gujun.android.span.span @@ -44,20 +45,34 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC renderState(it) } - views.callControlsSoundDevice.views.itemVerificationClickableZone.debouncedClicks { + views.callControlsSoundDevice.views.bottomSheetActionClickableZone.debouncedClicks { callViewModel.handle(VectorCallViewActions.SwitchSoundDevice) } - views.callControlsSwitchCamera.views.itemVerificationClickableZone.debouncedClicks { + views.callControlsSwitchCamera.views.bottomSheetActionClickableZone.debouncedClicks { callViewModel.handle(VectorCallViewActions.ToggleCamera) dismiss() } - views.callControlsToggleSDHD.views.itemVerificationClickableZone.debouncedClicks { + views.callControlsToggleSDHD.views.bottomSheetActionClickableZone.debouncedClicks { callViewModel.handle(VectorCallViewActions.ToggleHDSD) dismiss() } + views.callControlsToggleHoldResume.views.bottomSheetActionClickableZone.debouncedClicks { + callViewModel.handle(VectorCallViewActions.ToggleHoldResume) + dismiss() + } + + views.callControlsOpenDialPad.views.bottomSheetActionClickableZone.debouncedClicks { + callViewModel.handle(VectorCallViewActions.OpenDialPad) + } + + views.callControlsTransfer.views.bottomSheetActionClickableZone.debouncedClicks { + callViewModel.handle(VectorCallViewActions.InitiateCallTransfer) + dismiss() + } + callViewModel.observeViewEvents { when (it) { is VectorCallViewEvents.ShowSoundDeviceChooser -> { @@ -69,22 +84,22 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC } } - private fun showSoundDeviceChooser(available: List<CallAudioManager.SoundDevice>, current: CallAudioManager.SoundDevice) { + private fun showSoundDeviceChooser(available: Set<CallAudioManager.Device>, current: CallAudioManager.Device) { val soundDevices = available.map { when (it) { - CallAudioManager.SoundDevice.WIRELESS_HEADSET -> span { + CallAudioManager.Device.WIRELESS_HEADSET -> span { text = getString(R.string.sound_device_wireless_headset) textStyle = if (current == it) "bold" else "normal" } - CallAudioManager.SoundDevice.PHONE -> span { + CallAudioManager.Device.PHONE -> span { text = getString(R.string.sound_device_phone) textStyle = if (current == it) "bold" else "normal" } - CallAudioManager.SoundDevice.SPEAKER -> span { + CallAudioManager.Device.SPEAKER -> span { text = getString(R.string.sound_device_speaker) textStyle = if (current == it) "bold" else "normal" } - CallAudioManager.SoundDevice.HEADSET -> span { + CallAudioManager.Device.HEADSET -> span { text = getString(R.string.sound_device_headset) textStyle = if (current == it) "bold" else "normal" } @@ -95,17 +110,17 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC d.cancel() when (soundDevices[n].toString()) { // TODO Make an adapter and handle multiple Bluetooth headsets. Also do not use translations. - getString(R.string.sound_device_phone) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.PHONE)) + getString(R.string.sound_device_phone) -> { + callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.PHONE)) } - getString(R.string.sound_device_speaker) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.SPEAKER)) + getString(R.string.sound_device_speaker) -> { + callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.SPEAKER)) } - getString(R.string.sound_device_headset) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.HEADSET)) + getString(R.string.sound_device_headset) -> { + callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.HEADSET)) } getString(R.string.sound_device_wireless_headset) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.WIRELESS_HEADSET)) + callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.WIRELESS_HEADSET)) } } } @@ -115,11 +130,11 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC private fun renderState(state: VectorCallViewState) { views.callControlsSoundDevice.title = getString(R.string.call_select_sound_device) - views.callControlsSoundDevice.subTitle = when (state.soundDevice) { - CallAudioManager.SoundDevice.PHONE -> getString(R.string.sound_device_phone) - CallAudioManager.SoundDevice.SPEAKER -> getString(R.string.sound_device_speaker) - CallAudioManager.SoundDevice.HEADSET -> getString(R.string.sound_device_headset) - CallAudioManager.SoundDevice.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset) + views.callControlsSoundDevice.subTitle = when (state.device) { + CallAudioManager.Device.PHONE -> getString(R.string.sound_device_phone) + CallAudioManager.Device.SPEAKER -> getString(R.string.sound_device_speaker) + CallAudioManager.Device.HEADSET -> getString(R.string.sound_device_headset) + CallAudioManager.Device.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset) } views.callControlsSwitchCamera.isVisible = state.isVideoCall && state.canSwitchCamera @@ -139,5 +154,15 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC } else { views.callControlsToggleSDHD.isVisible = false } + if (state.isRemoteOnHold) { + views.callControlsToggleHoldResume.title = getString(R.string.call_resume_action) + views.callControlsToggleHoldResume.subTitle = null + views.callControlsToggleHoldResume.leftIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_call_resume_action) + } else { + views.callControlsToggleHoldResume.title = getString(R.string.call_hold_action) + views.callControlsToggleHoldResume.subTitle = null + views.callControlsToggleHoldResume.leftIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_call_hold_action) + } + views.callControlsTransfer.isVisible = state.canOpponentBeTransferred } } diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt index 9aa6ccd298..1a54551072 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt @@ -22,9 +22,8 @@ import android.widget.FrameLayout import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.databinding.ViewCallControlsBinding - import org.matrix.android.sdk.api.session.call.CallState -import org.webrtc.PeerConnection +import org.matrix.android.sdk.api.session.call.MxPeerConnectionState class CallControlsView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 @@ -36,16 +35,15 @@ class CallControlsView @JvmOverloads constructor( init { inflate(context, R.layout.view_call_controls, this) - // layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) views = ViewCallControlsBinding.bind(this) views.ringingControlAccept.setOnClickListener { acceptIncomingCall() } views.ringingControlDecline.setOnClickListener { declineIncomingCall() } - views.ivEndCall.setOnClickListener { endOngoingCall() } + views.endCallIcon.setOnClickListener { endOngoingCall() } views.muteIcon.setOnClickListener { toggleMute() } views.videoToggleIcon.setOnClickListener { toggleVideo() } - views.ivLeftMiniControl.setOnClickListener { returnToChat() } - views.ivMore.setOnClickListener { moreControlOption() } + views.openChatIcon.setOnClickListener { returnToChat() } + views.moreIcon.setOnClickListener { moreControlOption() } } private fun acceptIncomingCall() { @@ -109,7 +107,7 @@ class CallControlsView @JvmOverloads constructor( views.connectedControls.isVisible = false } is CallState.Connected -> { - if (callState.iceConnectionState == PeerConnection.PeerConnectionState.CONNECTED) { + if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { views.ringingControls.isVisible = false views.connectedControls.isVisible = true views.videoToggleIcon.isVisible = state.isVideoCall diff --git a/vector/src/main/java/im/vector/app/features/call/DialerChoiceBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/DialerChoiceBottomSheet.kt new file mode 100644 index 0000000000..401b3e23d7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/DialerChoiceBottomSheet.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 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.call + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.databinding.BottomSheetCallDialerChoiceBinding + +class DialerChoiceBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCallDialerChoiceBinding>() { + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetCallDialerChoiceBinding { + return BottomSheetCallDialerChoiceBinding.inflate(inflater, container, false) + } + + var onDialPadClicked: (() -> Unit)? = null + var onVoiceCallClicked: (() -> Unit)? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + views.dialerChoiceDialPad.views.bottomSheetActionClickableZone.debouncedClicks { + onDialPadClicked?.invoke() + dismiss() + } + + views.dialerChoiceVoiceCall.views.bottomSheetActionClickableZone.debouncedClicks { + onVoiceCallClicked?.invoke() + dismiss() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt deleted file mode 100644 index f2414f0a22..0000000000 --- a/vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2020 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.call - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.matrix.android.sdk.api.session.call.MxCall -import javax.inject.Inject - -class SharedActiveCallViewModel @Inject constructor( - private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager -) : ViewModel() { - - val activeCall: MutableLiveData<MxCall?> = MutableLiveData() - - val callStateListener = object : MxCall.StateListener { - - override fun onStateUpdate(call: MxCall) { - if (activeCall.value?.callId == call.callId) { - activeCall.postValue(call) - } - } - } - - private val listener = object : WebRtcPeerConnectionManager.CurrentCallListener { - override fun onCurrentCallChange(call: MxCall?) { - activeCall.value?.removeListener(callStateListener) - activeCall.postValue(call) - call?.addListener(callStateListener) - } - } - - init { - activeCall.postValue(webRtcPeerConnectionManager.currentCall?.mxCall) - webRtcPeerConnectionManager.addCurrentCallListener(listener) - } - - override fun onCleared() { - activeCall.value?.removeListener(callStateListener) - webRtcPeerConnectionManager.removeCurrentCallListener(listener) - super.onCleared() - } -} diff --git a/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt b/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt new file mode 100644 index 0000000000..b33edd09e0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020 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.call + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import im.vector.app.features.call.webrtc.WebRtcCall +import im.vector.app.features.call.webrtc.WebRtcCallManager +import org.matrix.android.sdk.api.session.call.MxCall +import javax.inject.Inject + +class SharedKnownCallsViewModel @Inject constructor( + private val callManager: WebRtcCallManager +) : ViewModel() { + + val liveKnownCalls: MutableLiveData<List<WebRtcCall>> = MutableLiveData() + + val callListener = object : WebRtcCall.Listener { + + override fun onStateUpdate(call: MxCall) { + // post it-self + liveKnownCalls.postValue(liveKnownCalls.value) + } + + override fun onHoldUnhold() { + super.onHoldUnhold() + // post it-self + liveKnownCalls.postValue(liveKnownCalls.value) + } + } + + private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { + override fun onCurrentCallChange(call: WebRtcCall?) { + val knownCalls = callManager.getCalls() + liveKnownCalls.postValue(knownCalls) + knownCalls.forEach { + it.removeListener(callListener) + it.addListener(callListener) + } + } + } + + init { + val knownCalls = callManager.getCalls() + liveKnownCalls.postValue(knownCalls) + callManager.addCurrentCallListener(currentCallListener) + knownCalls.forEach { + it.addListener(callListener) + } + } + + override fun onCleared() { + callManager.getCalls().forEach { + it.removeListener(callListener) + } + callManager.removeCurrentCallListener(currentCallListener) + super.onCleared() + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 6c49d4d3e2..ab3769d1f3 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -20,54 +20,52 @@ import android.app.KeyguardManager import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP +import android.graphics.Color import android.os.Build import android.os.Bundle import android.os.Parcelable import android.view.View -import android.view.Window -import android.view.WindowInsets -import android.view.WindowInsetsController import android.view.WindowManager import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.content.getSystemService -import androidx.core.view.ViewCompat import androidx.core.view.isInvisible import androidx.core.view.isVisible -import androidx.core.view.updatePadding import com.airbnb.mvrx.Fail import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.viewModel -import com.jakewharton.rxbinding3.view.clicks +import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.di.ScreenComponent import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.core.services.CallService import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL import im.vector.app.core.utils.allGranted import im.vector.app.core.utils.checkPermissions import im.vector.app.databinding.ActivityCallBinding +import im.vector.app.features.call.dialpad.CallDialPadBottomSheet +import im.vector.app.features.call.dialpad.DialPadFragment +import im.vector.app.features.call.utils.EglUtils +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.call.CallState -import org.matrix.android.sdk.api.session.call.EglUtils import org.matrix.android.sdk.api.session.call.MxCallDetail +import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.matrix.android.sdk.api.session.call.TurnServerResponse import org.webrtc.EglBase -import org.webrtc.PeerConnection import org.webrtc.RendererCommon import timber.log.Timber -import java.util.concurrent.TimeUnit import javax.inject.Inject @Parcelize data class CallArgs( val roomId: String, - val callId: String?, + val callId: String, val participantUserId: String, val isIncomingCall: Boolean, val isVideoCall: Boolean @@ -87,101 +85,36 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro private val callViewModel: VectorCallViewModel by viewModel() private lateinit var callArgs: CallArgs - @Inject lateinit var peerConnectionManager: WebRtcPeerConnectionManager - + @Inject lateinit var callManager: WebRtcCallManager @Inject lateinit var viewModelFactory: VectorCallViewModel.Factory - private var rootEglBase: EglBase? = null + private val dialPadCallback = object : DialPadFragment.Callback { + override fun onDigitAppended(digit: String) { + callViewModel.handle(VectorCallViewActions.SendDtmfDigit(digit)) + } + } - var systemUiVisibility = false + private var rootEglBase: EglBase? = null var surfaceRenderersAreInitialized = false override fun doBeforeSetContentView() { - // Set window styles for fullscreen-window size. Needs to be done before adding content. - requestWindowFeature(Window.FEATURE_NO_TITLE) - - hideSystemUI() setContentView(R.layout.activity_call) } - @Suppress("DEPRECATION") - private fun hideSystemUI() { - systemUiVisibility = false - // Enables regular immersive mode. - // For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE. - // Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - window.setDecorFitsSystemWindows(false) - // New API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION - window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars()) - // New API instead of SYSTEM_UI_FLAG_IMMERSIVE - window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE - // New API instead of FLAG_TRANSLUCENT_STATUS - window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) - // New API instead of FLAG_TRANSLUCENT_NAVIGATION - window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar) - } else { - window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE - // Set the content to appear under the system bars so that the - // content doesn't resize when the system bars hide and show. - or View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - // Hide the nav bar and status bar - or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_FULLSCREEN) - } - } - - // Shows the system bars by removing all the flags -// except for the ones that make the content appear under the system bars. - @Suppress("DEPRECATION") - private fun showSystemUI() { - systemUiVisibility = true - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - window.setDecorFitsSystemWindows(false) - } else { - window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE - or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) - } - } - - private fun toggleUiSystemVisibility() { - if (systemUiVisibility) { - hideSystemUI() - } else { - showSystemUI() - } - } - - override fun onWindowFocusChanged(hasFocus: Boolean) { - super.onWindowFocusChanged(hasFocus) - // Rehide when bottom sheet is dismissed - if (hasFocus) { - hideSystemUI() - } - } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // This will need to be refined - ViewCompat.setOnApplyWindowInsetsListener(views.constraintLayout) { v, insets -> - v.updatePadding(bottom = if (systemUiVisibility) insets.systemWindowInsetBottom else 0) - insets - } - + @Suppress("DEPRECATION") + window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + window.statusBarColor = Color.TRANSPARENT + window.navigationBarColor = Color.BLACK + super.onCreate(savedInstanceState) if (intent.hasExtra(MvRx.KEY_ARG)) { callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!! } else { Timber.e("## VOIP missing callArgs for VectorCall Activity") - CallService.onNoActiveCall(this) finish() } @@ -189,13 +122,9 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro if (intent.getStringExtra(EXTRA_MODE) == INCOMING_RINGING) { turnScreenOnAndKeyguardOff() } - - views.constraintLayout.clicks() - .throttleFirst(300, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { toggleUiSystemVisibility() } - .disposeOnDestroy() - + if (savedInstanceState != null) { + (supportFragmentManager.findFragmentByTag(FRAGMENT_DIAL_PAD_TAG) as? CallDialPadBottomSheet)?.callback = dialPadCallback + } configureCallViews() callViewModel.subscribe(this) { @@ -222,7 +151,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro } override fun onDestroy() { - peerConnectionManager.detachRenderers(listOf(views.pipRenderer, views.fullscreenRenderer)) + callManager.getCallById(callArgs.callId)?.detachRenderers(listOf(views.pipRenderer, views.fullscreenRenderer)) if (surfaceRenderersAreInitialized) { views.pipRenderer.release() views.fullscreenRenderer.release() @@ -234,8 +163,6 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro private fun renderState(state: VectorCallViewState) { Timber.v("## VOIP renderState call $state") if (state.callState is Fail) { - // be sure to clear notification - CallService.onNoActiveCall(this) finish() return } @@ -243,9 +170,13 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro views.callControlsView.updateForState(state) val callState = state.callState.invoke() views.callConnectingProgress.isVisible = false + views.callActionText.setOnClickListener(null) + views.callActionText.isVisible = false + views.smallIsHeldIcon.isVisible = false when (callState) { is CallState.Idle, - is CallState.Dialing -> { + is CallState.CreateOffer, + is CallState.Dialing -> { views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true views.callStatusText.setText(R.string.call_ring) @@ -259,24 +190,42 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro configureCallInfo(state) } - is CallState.Answering -> { + is CallState.Answering -> { views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true views.callStatusText.setText(R.string.call_connecting) views.callConnectingProgress.isVisible = true configureCallInfo(state) } - is CallState.Connected -> { - if (callState.iceConnectionState == PeerConnection.PeerConnectionState.CONNECTED) { - if (callArgs.isVideoCall) { - views.callVideoGroup.isVisible = true - views.callInfoGroup.isVisible = false - views.pipRenderer.isVisible = !state.isVideoCaptureInError - } else { + is CallState.Connected -> { + if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { + if (state.isLocalOnHold || state.isRemoteOnHold) { + views.smallIsHeldIcon.isVisible = true views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true + configureCallInfo(state, blurAvatar = true) + if (state.isRemoteOnHold) { + views.callActionText.setText(R.string.call_resume_action) + views.callActionText.isVisible = true + views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.ToggleHoldResume) } + views.callStatusText.setText(R.string.call_held_by_you) + } else { + views.callActionText.isInvisible = true + state.callInfo.otherUserItem?.let { + views.callStatusText.text = getString(R.string.call_held_by_user, it.getBestName()) + } + } + } else { + views.callStatusText.text = state.formattedDuration configureCallInfo(state) - views.callStatusText.text = null + if (callArgs.isVideoCall) { + views.callVideoGroup.isVisible = true + views.callInfoGroup.isVisible = false + views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null + } else { + views.callVideoGroup.isInvisible = true + views.callInfoGroup.isVisible = true + } } } else { // This state is not final, if you change network, new candidates will be sent @@ -286,27 +235,52 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro views.callStatusText.setText(R.string.call_connecting) views.callConnectingProgress.isVisible = true } - // ensure all attached? - peerConnectionManager.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer, null) } - is CallState.Terminated -> { + is CallState.Terminated -> { finish() } - null -> { + null -> { } } } - private fun configureCallInfo(state: VectorCallViewState) { - state.otherUserMatrixItem.invoke()?.let { - avatarRenderer.render(it, views.otherMemberAvatar) + private fun configureCallInfo(state: VectorCallViewState, blurAvatar: Boolean = false) { + state.callInfo.otherUserItem?.let { + val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) + avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter) views.participantNameText.text = it.getBestName() - views.callTypeText.setText(if (state.isVideoCall) R.string.action_video_call else R.string.action_voice_call) + if (blurAvatar) { + avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter) + } else { + avatarRenderer.render(it, views.otherMemberAvatar) + } + } + if (state.otherKnownCallInfo?.otherUserItem == null) { + views.otherKnownCallLayout.isVisible = false + } else { + val otherCall = callManager.getCallById(state.otherKnownCallInfo.callId) + val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) + avatarRenderer.renderBlur( + matrixItem = state.otherKnownCallInfo.otherUserItem, + imageView = views.otherKnownCallAvatarView, + sampling = 20, + rounded = false, + colorFilter = colorFilter + ) + views.otherKnownCallLayout.isVisible = true + views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.remoteOnHold }.orFalse() } } private fun configureCallViews() { views.callControlsView.interactionListener = this + views.otherKnownCallAvatarView.setOnClickListener { + withState(callViewModel) { + val otherCall = callManager.getCallById(it.otherKnownCallInfo?.callId ?: "") ?: return@withState + startActivity(newIntent(this, otherCall.mxCall, null)) + finish() + } + } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { @@ -331,17 +305,14 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro // Init Full Screen renderer views.fullscreenRenderer.init(rootEglBase!!.eglBaseContext, null) - views.fullscreenRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) + views.fullscreenRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL) views.pipRenderer.setZOrderMediaOverlay(true) views.pipRenderer.setEnableHardwareScaler(true /* enabled */) views.fullscreenRenderer.setEnableHardwareScaler(true /* enabled */) - peerConnectionManager.attachViewRenderers( - views.pipRenderer, - views.fullscreenRenderer, - intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() } - ) + callManager.getCallById(callArgs.callId)?.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer, + intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() }) views.pipRenderer.setOnClickListener { callViewModel.handle(VectorCallViewActions.ToggleCamera) @@ -352,14 +323,21 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro private fun handleViewEvents(event: VectorCallViewEvents?) { Timber.v("## VOIP handleViewEvents $event") when (event) { - VectorCallViewEvents.DismissNoCall -> { - CallService.onNoActiveCall(this) + VectorCallViewEvents.DismissNoCall -> { finish() } is VectorCallViewEvents.ConnectionTimeout -> { onErrorTimoutConnect(event.turn) } - null -> { + is VectorCallViewEvents.ShowDialPad -> { + CallDialPadBottomSheet.newInstance(false).apply { + callback = dialPadCallback + }.show(supportFragmentManager, FRAGMENT_DIAL_PAD_TAG) + } + is VectorCallViewEvents.ShowCallTransferScreen -> { + navigator.openCallTransfer(this, callArgs.callId) + } + null -> { } } } @@ -381,22 +359,23 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro private const val CAPTURE_PERMISSION_REQUEST_CODE = 1 private const val EXTRA_MODE = "EXTRA_MODE" + private const val FRAGMENT_DIAL_PAD_TAG = "FRAGMENT_DIAL_PAD_TAG" const val OUTGOING_CREATED = "OUTGOING_CREATED" const val INCOMING_RINGING = "INCOMING_RINGING" const val INCOMING_ACCEPT = "INCOMING_ACCEPT" - fun newIntent(context: Context, mxCall: MxCallDetail): Intent { + fun newIntent(context: Context, mxCall: MxCallDetail, mode: String?): Intent { return Intent(context, VectorCallActivity::class.java).apply { // what could be the best flags? flags = Intent.FLAG_ACTIVITY_NEW_TASK - putExtra(MvRx.KEY_ARG, CallArgs(mxCall.roomId, mxCall.callId, mxCall.otherUserId, !mxCall.isOutgoing, mxCall.isVideoCall)) - putExtra(EXTRA_MODE, OUTGOING_CREATED) + putExtra(MvRx.KEY_ARG, CallArgs(mxCall.roomId, mxCall.callId, mxCall.opponentUserId, !mxCall.isOutgoing, mxCall.isVideoCall)) + putExtra(EXTRA_MODE, mode) } } fun newIntent(context: Context, - callId: String?, + callId: String, roomId: String, otherUserId: String, isIncomingCall: Boolean, diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt index 4ca21a0f1d..7addabf724 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt @@ -17,6 +17,7 @@ package im.vector.app.features.call import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.call.audio.CallAudioManager sealed class VectorCallViewActions : VectorViewModelAction { object EndCall : VectorCallViewActions() @@ -24,9 +25,13 @@ sealed class VectorCallViewActions : VectorViewModelAction { object DeclineCall : VectorCallViewActions() object ToggleMute : VectorCallViewActions() object ToggleVideo : VectorCallViewActions() - data class ChangeAudioDevice(val device: CallAudioManager.SoundDevice) : VectorCallViewActions() + object ToggleHoldResume: VectorCallViewActions() + data class ChangeAudioDevice(val device: CallAudioManager.Device) : VectorCallViewActions() + object OpenDialPad: VectorCallViewActions() + data class SendDtmfDigit(val digit: String) : VectorCallViewActions() object SwitchSoundDevice : VectorCallViewActions() object HeadSetButtonPressed : VectorCallViewActions() object ToggleCamera : VectorCallViewActions() object ToggleHDSD : VectorCallViewActions() + object InitiateCallTransfer : VectorCallViewActions() } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt index b79cd5d772..91c3154d0a 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt @@ -17,6 +17,7 @@ package im.vector.app.features.call import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.features.call.audio.CallAudioManager import org.matrix.android.sdk.api.session.call.TurnServerResponse sealed class VectorCallViewEvents : VectorViewEvents { @@ -24,9 +25,11 @@ sealed class VectorCallViewEvents : VectorViewEvents { object DismissNoCall : VectorCallViewEvents() data class ConnectionTimeout(val turn: TurnServerResponse?) : VectorCallViewEvents() data class ShowSoundDeviceChooser( - val available: List<CallAudioManager.SoundDevice>, - val current: CallAudioManager.SoundDevice + val available: Set<CallAudioManager.Device>, + val current: CallAudioManager.Device ) : VectorCallViewEvents() + object ShowDialPad: VectorCallViewEvents() + object ShowCallTransferScreen: VectorCallViewEvents() // data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents() // data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents() // object CallAccepted : VectorCallViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index fd735de085..25b2a80a85 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -20,41 +20,77 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.call.audio.CallAudioManager +import im.vector.app.features.call.webrtc.WebRtcCall +import im.vector.app.features.call.webrtc.WebRtcCallManager import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.matrix.android.sdk.api.session.call.TurnServerResponse +import org.matrix.android.sdk.api.session.room.model.call.supportCallTransfer import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem -import org.webrtc.PeerConnection import java.util.Timer import java.util.TimerTask class VectorCallViewModel @AssistedInject constructor( @Assisted initialState: VectorCallViewState, - @Assisted val args: CallArgs, val session: Session, - val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, + val callManager: WebRtcCallManager, val proximityManager: CallProximityManager ) : VectorViewModel<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(initialState) { - private var call: MxCall? = null + private var call: WebRtcCall? = null private var connectionTimeoutTimer: Timer? = null private var hasBeenConnectedOnce = false - private val callStateListener = object : MxCall.StateListener { + private val callListener = object : WebRtcCall.Listener { + + override fun onHoldUnhold() { + setState { + copy( + isLocalOnHold = call?.isLocalOnHold ?: false, + isRemoteOnHold = call?.remoteOnHold ?: false + ) + } + } + + override fun onCaptureStateChanged() { + setState { + copy( + isVideoCaptureInError = call?.videoCapturerIsInError ?: false, + isHD = call?.currentCaptureFormat() is CaptureFormat.HD + ) + } + } + + override fun onCameraChanged() { + setState { + copy( + canSwitchCamera = call?.canSwitchCamera() ?: false, + isFrontCamera = call?.currentCameraType() == CameraType.FRONT + ) + } + } + + override fun onTick(formattedDuration: String) { + setState { + copy(formattedDuration = formattedDuration) + } + } + override fun onStateUpdate(call: MxCall) { val callState = call.state - if (callState is CallState.Connected && callState.iceConnectionState == PeerConnection.PeerConnectionState.CONNECTED) { + if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { hasBeenConnectedOnce = true connectionTimeoutTimer?.cancel() connectionTimeoutTimer = null @@ -81,189 +117,182 @@ class VectorCallViewModel @AssistedInject constructor( } setState { copy( - callState = Success(callState) + callState = Success(callState), + canOpponentBeTransferred = call.capabilities.supportCallTransfer() ) } } } - private val currentCallListener = object : WebRtcPeerConnectionManager.CurrentCallListener { - override fun onCurrentCallChange(call: MxCall?) { - // we need to check the state - if (call == null) { - // we should dismiss, e.g handled by other session? - _viewEvents.post(VectorCallViewEvents.DismissNoCall) - } - } + private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { - override fun onCaptureStateChanged() { - setState { - copy( - isVideoCaptureInError = webRtcPeerConnectionManager.capturerIsInError, - isHD = webRtcPeerConnectionManager.currentCaptureFormat() is CaptureFormat.HD - ) + override fun onCurrentCallChange(call: WebRtcCall?) { + if (call == null) { + _viewEvents.post(VectorCallViewEvents.DismissNoCall) + } else { + updateOtherKnownCall(call) } } override fun onAudioDevicesChange() { - val currentSoundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice() - if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) { + val currentSoundDevice = callManager.audioManager.selectedDevice ?: return + if (currentSoundDevice == CallAudioManager.Device.PHONE) { proximityManager.start() } else { proximityManager.stop() } - setState { copy( - availableSoundDevices = webRtcPeerConnectionManager.callAudioManager.getAvailableSoundDevices(), - soundDevice = currentSoundDevice + availableDevices = callManager.audioManager.availableDevices, + device = currentSoundDevice ) } } + } - override fun onCameraChange() { - setState { - copy( - canSwitchCamera = webRtcPeerConnectionManager.canSwitchCamera(), - isFrontCamera = webRtcPeerConnectionManager.currentCameraType() == CameraType.FRONT - ) + private fun updateOtherKnownCall(currentCall: WebRtcCall) { + val otherCall = callManager.getCalls().firstOrNull { + it.callId != currentCall.callId && it.mxCall.state is CallState.Connected + } + setState { + if (otherCall == null) { + copy(otherKnownCallInfo = null) + } else { + val otherUserItem: MatrixItem? = session.getUser(otherCall.mxCall.opponentUserId)?.toMatrixItem() + copy(otherKnownCallInfo = VectorCallViewState.CallInfo(otherCall.callId, otherUserItem)) } } } init { - initialState.callId?.let { - webRtcPeerConnectionManager.addCurrentCallListener(currentCallListener) - - session.callSignalingService().getCallWithId(it)?.let { mxCall -> - this.call = mxCall - mxCall.otherUserId - val item: MatrixItem? = session.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem() - - mxCall.addListener(callStateListener) - - val currentSoundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice() - if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) { - proximityManager.start() - } - - setState { - copy( - isVideoCall = mxCall.isVideoCall, - callState = Success(mxCall.state), - otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized, - soundDevice = currentSoundDevice, - availableSoundDevices = webRtcPeerConnectionManager.callAudioManager.getAvailableSoundDevices(), - isFrontCamera = webRtcPeerConnectionManager.currentCameraType() == CameraType.FRONT, - canSwitchCamera = webRtcPeerConnectionManager.canSwitchCamera(), - isHD = mxCall.isVideoCall && webRtcPeerConnectionManager.currentCaptureFormat() is CaptureFormat.HD - ) - } - } ?: run { - setState { - copy( - callState = Fail(IllegalArgumentException("No call")) - ) - } + val webRtcCall = callManager.getCallById(initialState.callId) + if (webRtcCall == null) { + setState { + copy(callState = Fail(IllegalArgumentException("No call"))) } + } else { + call = webRtcCall + callManager.addCurrentCallListener(currentCallListener) + val item: MatrixItem? = session.getUser(webRtcCall.mxCall.opponentUserId)?.toMatrixItem() + webRtcCall.addListener(callListener) + val currentSoundDevice = callManager.audioManager.selectedDevice + if (currentSoundDevice == CallAudioManager.Device.PHONE) { + proximityManager.start() + } + setState { + copy( + isVideoCall = webRtcCall.mxCall.isVideoCall, + callState = Success(webRtcCall.mxCall.state), + callInfo = VectorCallViewState.CallInfo(callId, item), + device = currentSoundDevice ?: CallAudioManager.Device.PHONE, + isLocalOnHold = webRtcCall.isLocalOnHold, + isRemoteOnHold = webRtcCall.remoteOnHold, + availableDevices = callManager.audioManager.availableDevices, + isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT, + canSwitchCamera = webRtcCall.canSwitchCamera(), + formattedDuration = webRtcCall.formattedDuration(), + isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD, + canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer() + ) + } + updateOtherKnownCall(webRtcCall) } } override fun onCleared() { - // session.callService().removeCallListener(callServiceListener) - webRtcPeerConnectionManager.removeCurrentCallListener(currentCallListener) - this.call?.removeListener(callStateListener) + callManager.removeCurrentCallListener(currentCallListener) + call?.removeListener(callListener) proximityManager.stop() super.onCleared() } override fun handle(action: VectorCallViewActions) = withState { state -> when (action) { - VectorCallViewActions.EndCall -> webRtcPeerConnectionManager.endCall() - VectorCallViewActions.AcceptCall -> { + VectorCallViewActions.EndCall -> call?.endCall() + VectorCallViewActions.AcceptCall -> { setState { copy(callState = Loading()) } - webRtcPeerConnectionManager.acceptIncomingCall() + call?.acceptIncomingCall() } - VectorCallViewActions.DeclineCall -> { + VectorCallViewActions.DeclineCall -> { setState { copy(callState = Loading()) } - webRtcPeerConnectionManager.endCall() + call?.endCall() } - VectorCallViewActions.ToggleMute -> { + VectorCallViewActions.ToggleMute -> { val muted = state.isAudioMuted - webRtcPeerConnectionManager.muteCall(!muted) + call?.muteCall(!muted) setState { copy(isAudioMuted = !muted) } } - VectorCallViewActions.ToggleVideo -> { + VectorCallViewActions.ToggleVideo -> { if (state.isVideoCall) { val videoEnabled = state.isVideoEnabled - webRtcPeerConnectionManager.enableVideo(!videoEnabled) + call?.enableVideo(!videoEnabled) setState { copy(isVideoEnabled = !videoEnabled) } } Unit } - is VectorCallViewActions.ChangeAudioDevice -> { - webRtcPeerConnectionManager.callAudioManager.setCurrentSoundDevice(action.device) - setState { - copy( - soundDevice = webRtcPeerConnectionManager.callAudioManager.getCurrentSoundDevice() - ) - } + VectorCallViewActions.ToggleHoldResume -> { + val isRemoteOnHold = state.isRemoteOnHold + call?.updateRemoteOnHold(!isRemoteOnHold) } - VectorCallViewActions.SwitchSoundDevice -> { + is VectorCallViewActions.ChangeAudioDevice -> { + callManager.audioManager.setAudioDevice(action.device) + } + VectorCallViewActions.SwitchSoundDevice -> { _viewEvents.post( - VectorCallViewEvents.ShowSoundDeviceChooser(state.availableSoundDevices, state.soundDevice) + VectorCallViewEvents.ShowSoundDeviceChooser(state.availableDevices, state.device) ) } VectorCallViewActions.HeadSetButtonPressed -> { if (state.callState.invoke() is CallState.LocalRinging) { // accept call - webRtcPeerConnectionManager.acceptIncomingCall() + call?.acceptIncomingCall() } if (state.callState.invoke() is CallState.Connected) { // end call? - webRtcPeerConnectionManager.endCall() + call?.endCall() } Unit } - VectorCallViewActions.ToggleCamera -> { - webRtcPeerConnectionManager.switchCamera() + VectorCallViewActions.ToggleCamera -> { + call?.switchCamera() } - VectorCallViewActions.ToggleHDSD -> { + VectorCallViewActions.ToggleHDSD -> { if (!state.isVideoCall) return@withState - webRtcPeerConnectionManager.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD) + call?.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD) + } + VectorCallViewActions.OpenDialPad -> { + _viewEvents.post(VectorCallViewEvents.ShowDialPad) + } + is VectorCallViewActions.SendDtmfDigit -> { + call?.sendDtmfDigit(action.digit) + } + VectorCallViewActions.InitiateCallTransfer -> { + _viewEvents.post( + VectorCallViewEvents.ShowCallTransferScreen + ) } }.exhaustive } @AssistedFactory interface Factory { - fun create(initialState: VectorCallViewState, args: CallArgs): VectorCallViewModel + fun create(initialState: VectorCallViewState): VectorCallViewModel } companion object : MvRxViewModelFactory<VectorCallViewModel, VectorCallViewState> { @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: VectorCallViewState): VectorCallViewModel? { + override fun create(viewModelContext: ViewModelContext, state: VectorCallViewState): VectorCallViewModel { val callActivity: VectorCallActivity = viewModelContext.activity() - val callArgs: CallArgs = viewModelContext.args() - return callActivity.viewModelFactory.create(state, callArgs) - } - - override fun initialState(viewModelContext: ViewModelContext): VectorCallViewState? { - val args: CallArgs = viewModelContext.args() - return VectorCallViewState( - callId = args.callId, - roomId = args.roomId, - isVideoCall = args.isVideoCall - ) + return callActivity.viewModelFactory.create(state) } } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt index f24e810400..cdd002114a 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt @@ -19,21 +19,39 @@ package im.vector.app.features.call import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.call.audio.CallAudioManager import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.util.MatrixItem data class VectorCallViewState( - val callId: String? = null, - val roomId: String = "", + val callId: String, + val roomId: String, val isVideoCall: Boolean, + val isRemoteOnHold: Boolean = false, + val isLocalOnHold: Boolean = false, val isAudioMuted: Boolean = false, val isVideoEnabled: Boolean = true, val isVideoCaptureInError: Boolean = false, val isHD: Boolean = false, val isFrontCamera: Boolean = true, val canSwitchCamera: Boolean = true, - val soundDevice: CallAudioManager.SoundDevice = CallAudioManager.SoundDevice.PHONE, - val availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(), - val otherUserMatrixItem: Async<MatrixItem> = Uninitialized, - val callState: Async<CallState> = Uninitialized -) : MvRxState + val device: CallAudioManager.Device = CallAudioManager.Device.PHONE, + val availableDevices: Set<CallAudioManager.Device> = emptySet(), + val callState: Async<CallState> = Uninitialized, + val otherKnownCallInfo: CallInfo? = null, + val callInfo: CallInfo = CallInfo(callId), + val formattedDuration: String = "", + val canOpponentBeTransferred: Boolean = false +) : MvRxState { + + data class CallInfo( + val callId: String, + val otherUserItem: MatrixItem? = null + ) + + constructor(callArgs: CallArgs): this( + callId = callArgs.callId, + roomId = callArgs.roomId, + isVideoCall = callArgs.isVideoCall + ) +} diff --git a/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt b/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt deleted file mode 100644 index 5bc87ed1d1..0000000000 --- a/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt +++ /dev/null @@ -1,1088 +0,0 @@ -/* - * Copyright (c) 2020 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.call - -import android.content.Context -import android.hardware.camera2.CameraManager -import androidx.core.content.getSystemService -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.OnLifecycleEvent -import im.vector.app.ActiveSessionDataSource -import im.vector.app.core.services.BluetoothHeadsetReceiver -import im.vector.app.core.services.CallService -import im.vector.app.core.services.WiredHeadsetStateReceiver -import im.vector.app.push.fcm.FcmHelper -import io.reactivex.disposables.Disposable -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.ReplaySubject -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.call.CallState -import org.matrix.android.sdk.api.session.call.CallsListener -import org.matrix.android.sdk.api.session.call.EglUtils -import org.matrix.android.sdk.api.session.call.MxCall -import org.matrix.android.sdk.api.session.call.TurnServerResponse -import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent -import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent -import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent -import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent -import org.matrix.android.sdk.api.util.toMatrixItem -import org.webrtc.AudioSource -import org.webrtc.AudioTrack -import org.webrtc.Camera1Enumerator -import org.webrtc.Camera2Enumerator -import org.webrtc.CameraVideoCapturer -import org.webrtc.DataChannel -import org.webrtc.DefaultVideoDecoderFactory -import org.webrtc.DefaultVideoEncoderFactory -import org.webrtc.IceCandidate -import org.webrtc.MediaConstraints -import org.webrtc.MediaStream -import org.webrtc.PeerConnection -import org.webrtc.PeerConnectionFactory -import org.webrtc.RtpReceiver -import org.webrtc.SessionDescription -import org.webrtc.SurfaceTextureHelper -import org.webrtc.SurfaceViewRenderer -import org.webrtc.VideoSource -import org.webrtc.VideoTrack -import timber.log.Timber -import java.lang.ref.WeakReference -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Manage peerConnectionFactory & Peer connections outside of activity lifecycle to resist configuration changes - * Use app context - */ -@Singleton -class WebRtcPeerConnectionManager @Inject constructor( - private val context: Context, - private val activeSessionDataSource: ActiveSessionDataSource -) : CallsListener, LifecycleObserver { - - private val currentSession: Session? - get() = activeSessionDataSource.currentValue?.orNull() - - interface CurrentCallListener { - fun onCurrentCallChange(call: MxCall?) - fun onCaptureStateChanged() {} - fun onAudioDevicesChange() {} - fun onCameraChange() {} - } - - private val currentCallsListeners = emptyList<CurrentCallListener>().toMutableList() - fun addCurrentCallListener(listener: CurrentCallListener) { - currentCallsListeners.add(listener) - } - - fun removeCurrentCallListener(listener: CurrentCallListener) { - currentCallsListeners.remove(listener) - } - - val callAudioManager = CallAudioManager(context.applicationContext) { - currentCallsListeners.forEach { - tryOrNull { it.onAudioDevicesChange() } - } - } - - data class CallContext( - val mxCall: MxCall, - - var peerConnection: PeerConnection? = null, - - var localMediaStream: MediaStream? = null, - var remoteMediaStream: MediaStream? = null, - - var localAudioSource: AudioSource? = null, - var localAudioTrack: AudioTrack? = null, - - var localVideoSource: VideoSource? = null, - var localVideoTrack: VideoTrack? = null, - - var remoteVideoTrack: VideoTrack? = null - ) { - - var offerSdp: CallInviteContent.Offer? = null - - val iceCandidateSource: PublishSubject<IceCandidate> = PublishSubject.create() - private val iceCandidateDisposable = iceCandidateSource - .buffer(300, TimeUnit.MILLISECONDS) - .subscribe { - // omit empty :/ - if (it.isNotEmpty()) { - Timber.v("## Sending local ice candidates to call") - // it.forEach { peerConnection?.addIceCandidate(it) } - mxCall.sendLocalIceCandidates(it) - } - } - - var remoteCandidateSource: ReplaySubject<IceCandidate>? = null - var remoteIceCandidateDisposable: Disposable? = null - - // We register an availability callback if we loose access to camera - var cameraAvailabilityCallback: CameraRestarter? = null - - fun release() { - remoteIceCandidateDisposable?.dispose() - iceCandidateDisposable?.dispose() - - peerConnection?.close() - peerConnection?.dispose() - - localAudioSource?.dispose() - localVideoSource?.dispose() - - localAudioSource = null - localAudioTrack = null - localVideoSource = null - localVideoTrack = null - localMediaStream = null - remoteMediaStream = null - } - } - -// var localMediaStream: MediaStream? = null - - private val executor = Executors.newSingleThreadExecutor() - - private val rootEglBase by lazy { EglUtils.rootEglBase } - - private var peerConnectionFactory: PeerConnectionFactory? = null - - private var videoCapturer: CameraVideoCapturer? = null - - private val availableCamera = ArrayList<CameraProxy>() - private var cameraInUse: CameraProxy? = null - - private var currentCaptureMode: CaptureFormat = CaptureFormat.HD - - private var isInBackground: Boolean = true - - var capturerIsInError = false - set(value) { - field = value - currentCallsListeners.forEach { - tryOrNull { it.onCaptureStateChanged() } - } - } - - var localSurfaceRenderer: MutableList<WeakReference<SurfaceViewRenderer>> = ArrayList() - var remoteSurfaceRenderer: MutableList<WeakReference<SurfaceViewRenderer>> = ArrayList() - - fun addIfNeeded(renderer: SurfaceViewRenderer?, list: MutableList<WeakReference<SurfaceViewRenderer>>) { - if (renderer == null) return - val exists = list.firstOrNull { - it.get() == renderer - } != null - if (!exists) { - list.add(WeakReference(renderer)) - } - } - - fun removeIfNeeded(renderer: SurfaceViewRenderer?, list: MutableList<WeakReference<SurfaceViewRenderer>>) { - if (renderer == null) return - val exists = list.indexOfFirst { - it.get() == renderer - } - if (exists != -1) { - list.add(WeakReference(renderer)) - } - } - - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) - fun entersForeground() { - isInBackground = false - } - - @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) - fun entersBackground() { - isInBackground = true - } - - var currentCall: CallContext? = null - set(value) { - field = value - currentCallsListeners.forEach { - tryOrNull { it.onCurrentCallChange(value?.mxCall) } - } - } - - fun headSetButtonTapped() { - Timber.v("## VOIP headSetButtonTapped") - val call = currentCall?.mxCall ?: return - if (call.state is CallState.LocalRinging) { - // accept call - acceptIncomingCall() - } - if (call.state is CallState.Connected) { - // end call? - endCall() - } - } - - private fun createPeerConnectionFactory() { - if (peerConnectionFactory != null) return - Timber.v("## VOIP createPeerConnectionFactory") - val eglBaseContext = rootEglBase?.eglBaseContext ?: return Unit.also { - Timber.e("## VOIP No EGL BASE") - } - - Timber.v("## VOIP PeerConnectionFactory.initialize") - PeerConnectionFactory.initialize(PeerConnectionFactory - .InitializationOptions.builder(context.applicationContext) - .createInitializationOptions() - ) - - val options = PeerConnectionFactory.Options() - val defaultVideoEncoderFactory = DefaultVideoEncoderFactory( - eglBaseContext, - /* enableIntelVp8Encoder */ - true, - /* enableH264HighProfile */ - true) - val defaultVideoDecoderFactory = DefaultVideoDecoderFactory(eglBaseContext) - Timber.v("## VOIP PeerConnectionFactory.createPeerConnectionFactory ...") - peerConnectionFactory = PeerConnectionFactory.builder() - .setOptions(options) - .setVideoEncoderFactory(defaultVideoEncoderFactory) - .setVideoDecoderFactory(defaultVideoDecoderFactory) - .createPeerConnectionFactory() - - // attachViewRenderersInternal() - } - - private fun createPeerConnection(callContext: CallContext, turnServerResponse: TurnServerResponse?) { - val iceServers = mutableListOf<PeerConnection.IceServer>().apply { - turnServerResponse?.let { server -> - server.uris?.forEach { uri -> - add( - PeerConnection - .IceServer - .builder(uri) - .setUsername(server.username) - .setPassword(server.password) - .createIceServer() - ) - } - } - } - Timber.v("## VOIP creating peer connection...with iceServers $iceServers ") - callContext.peerConnection = peerConnectionFactory?.createPeerConnection(iceServers, StreamObserver(callContext)) - } - - private fun sendSdpOffer(callContext: CallContext) { - val constraints = MediaConstraints() - // These are deprecated options -// constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")) -// constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", if (currentCall?.mxCall?.isVideoCall == true) "true" else "false")) - - Timber.v("## VOIP creating offer...") - callContext.peerConnection?.createOffer(object : SdpObserverAdapter() { - override fun onCreateSuccess(p0: SessionDescription?) { - if (p0 == null) return -// localSdp = p0 - callContext.peerConnection?.setLocalDescription(object : SdpObserverAdapter() {}, p0) - // send offer to peer - currentCall?.mxCall?.offerSdp(p0) - } - }, constraints) - } - - private fun getTurnServer(callback: ((TurnServerResponse?) -> Unit)) { - currentSession?.callSignalingService() - ?.getTurnServer(object : MatrixCallback<TurnServerResponse?> { - override fun onSuccess(data: TurnServerResponse?) { - callback(data) - } - - override fun onFailure(failure: Throwable) { - callback(null) - } - }) - } - - fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) { - Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer") -// this.localSurfaceRenderer = WeakReference(localViewRenderer) -// this.remoteSurfaceRenderer = WeakReference(remoteViewRenderer) - addIfNeeded(localViewRenderer, this.localSurfaceRenderer) - addIfNeeded(remoteViewRenderer, this.remoteSurfaceRenderer) - - // The call is going to resume from background, we can reduce notif - currentCall?.mxCall - ?.takeIf { it.state is CallState.Connected } - ?.let { mxCall -> - val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName() - ?: mxCall.otherUserId - // Start background service with notification - CallService.onPendingCall( - context = context, - isVideo = mxCall.isVideoCall, - roomName = name, - roomId = mxCall.roomId, - matrixId = currentSession?.myUserId ?: "", - callId = mxCall.callId) - } - - getTurnServer { turnServer -> - val call = currentCall ?: return@getTurnServer - when (mode) { - VectorCallActivity.INCOMING_ACCEPT -> { - internalAcceptIncomingCall(call, turnServer) - } - VectorCallActivity.INCOMING_RINGING -> { - // wait until accepted to create peer connection - // TODO eventually we could already display local stream in PIP? - } - VectorCallActivity.OUTGOING_CREATED -> { - executor.execute { - // 1. Create RTCPeerConnection - createPeerConnection(call, turnServer) - - // 2. Access camera (if video call) + microphone, create local stream - createLocalStream(call) - - // 3. add local stream - call.localMediaStream?.let { call.peerConnection?.addStream(it) } - attachViewRenderersInternal() - - // create an offer, set local description and send via signaling - sendSdpOffer(call) - - Timber.v("## VOIP remoteCandidateSource ${call.remoteCandidateSource}") - call.remoteIceCandidateDisposable = call.remoteCandidateSource?.subscribe({ - Timber.v("## VOIP adding remote ice candidate $it") - call.peerConnection?.addIceCandidate(it) - }, { - Timber.v("## VOIP failed to add remote ice candidate $it") - }) - } - } - else -> { - // sink existing tracks (configuration change, e.g screen rotation) - attachViewRenderersInternal() - } - } - } - } - - private fun internalAcceptIncomingCall(callContext: CallContext, turnServerResponse: TurnServerResponse?) { - val mxCall = callContext.mxCall - // Update service state - - val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName() - ?: mxCall.roomId - CallService.onPendingCall( - context = context, - isVideo = mxCall.isVideoCall, - roomName = name, - roomId = mxCall.roomId, - matrixId = currentSession?.myUserId ?: "", - callId = mxCall.callId - ) - executor.execute { - // 1) create peer connection - createPeerConnection(callContext, turnServerResponse) - - // create sdp using offer, and set remote description - // the offer has beed stored when invite was received - callContext.offerSdp?.sdp?.let { - SessionDescription(SessionDescription.Type.OFFER, it) - }?.let { - callContext.peerConnection?.setRemoteDescription(SdpObserverAdapter(), it) - } - // 2) Access camera + microphone, create local stream - createLocalStream(callContext) - - // 2) add local stream - currentCall?.localMediaStream?.let { callContext.peerConnection?.addStream(it) } - attachViewRenderersInternal() - - // create a answer, set local description and send via signaling - createAnswer() - - Timber.v("## VOIP remoteCandidateSource ${callContext.remoteCandidateSource}") - callContext.remoteIceCandidateDisposable = callContext.remoteCandidateSource?.subscribe({ - Timber.v("## VOIP adding remote ice candidate $it") - callContext.peerConnection?.addIceCandidate(it) - }, { - Timber.v("## VOIP failed to add remote ice candidate $it") - }) - } - } - - private fun createLocalStream(callContext: CallContext) { - if (callContext.localMediaStream != null) { - Timber.e("## VOIP localMediaStream already created") - return - } - if (peerConnectionFactory == null) { - Timber.e("## VOIP peerConnectionFactory is null") - return - } - val audioSource = peerConnectionFactory!!.createAudioSource(DEFAULT_AUDIO_CONSTRAINTS) - val localAudioTrack = peerConnectionFactory!!.createAudioTrack(AUDIO_TRACK_ID, audioSource) - localAudioTrack?.setEnabled(true) - - callContext.localAudioSource = audioSource - callContext.localAudioTrack = localAudioTrack - - val localMediaStream = peerConnectionFactory!!.createLocalMediaStream("ARDAMS") // magic value? - - // Add audio track - localMediaStream?.addTrack(localAudioTrack) - - callContext.localMediaStream = localMediaStream - - // add video track if needed - if (callContext.mxCall.isVideoCall) { - availableCamera.clear() - - val cameraIterator = if (Camera2Enumerator.isSupported(context)) Camera2Enumerator(context) else Camera1Enumerator(false) - - // I don't realy know how that works if there are 2 front or 2 back cameras - val frontCamera = cameraIterator.deviceNames - ?.firstOrNull { cameraIterator.isFrontFacing(it) } - ?.let { - CameraProxy(it, CameraType.FRONT).also { availableCamera.add(it) } - } - - val backCamera = cameraIterator.deviceNames - ?.firstOrNull { cameraIterator.isBackFacing(it) } - ?.let { - CameraProxy(it, CameraType.BACK).also { availableCamera.add(it) } - } - - val camera = frontCamera?.also { cameraInUse = frontCamera } - ?: backCamera?.also { cameraInUse = backCamera } - ?: null.also { cameraInUse = null } - - if (camera != null) { - val videoCapturer = cameraIterator.createCapturer(camera.name, object : CameraEventsHandlerAdapter() { - override fun onFirstFrameAvailable() { - super.onFirstFrameAvailable() - capturerIsInError = false - } - - override fun onCameraClosed() { - // This could happen if you open the camera app in chat - // We then register in order to restart capture as soon as the camera is available again - Timber.v("## VOIP onCameraClosed") - this@WebRtcPeerConnectionManager.capturerIsInError = true - val restarter = CameraRestarter(cameraInUse?.name ?: "", callContext.mxCall.callId) - callContext.cameraAvailabilityCallback = restarter - val cameraManager = context.getSystemService<CameraManager>()!! - cameraManager.registerAvailabilityCallback(restarter, null) - } - }) - - val videoSource = peerConnectionFactory!!.createVideoSource(videoCapturer.isScreencast) - val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase!!.eglBaseContext) - Timber.v("## VOIP Local video source created") - - videoCapturer.initialize(surfaceTextureHelper, context.applicationContext, videoSource!!.capturerObserver) - // HD - videoCapturer.startCapture(currentCaptureMode.width, currentCaptureMode.height, currentCaptureMode.fps) - this.videoCapturer = videoCapturer - - val localVideoTrack = peerConnectionFactory!!.createVideoTrack("ARDAMSv0", videoSource) - Timber.v("## VOIP Local video track created") - localVideoTrack?.setEnabled(true) - - callContext.localVideoSource = videoSource - callContext.localVideoTrack = localVideoTrack - - localMediaStream?.addTrack(localVideoTrack) - } - } - } - - private fun attachViewRenderersInternal() { - // render local video in pip view - localSurfaceRenderer.forEach { - it.get()?.let { pipSurface -> - pipSurface.setMirror(this.cameraInUse?.type == CameraType.FRONT) - // no need to check if already added, addSink is checking that - currentCall?.localVideoTrack?.addSink(pipSurface) - } - } - - // If remote track exists, then sink it to surface - remoteSurfaceRenderer.forEach { - it.get()?.let { participantSurface -> - currentCall?.remoteVideoTrack?.let { - // no need to check if already added, addSink is checking that - it.addSink(participantSurface) - } - } - } - } - - fun acceptIncomingCall() { - Timber.v("## VOIP acceptIncomingCall from state ${currentCall?.mxCall?.state}") - val mxCall = currentCall?.mxCall - if (mxCall?.state == CallState.LocalRinging) { - getTurnServer { turnServer -> - internalAcceptIncomingCall(currentCall!!, turnServer) - } - } - } - - fun detachRenderers(renderes: List<SurfaceViewRenderer>?) { - Timber.v("## VOIP detachRenderers") - // currentCall?.localMediaStream?.let { currentCall?.peerConnection?.removeStream(it) } - if (renderes.isNullOrEmpty()) { - // remove all sinks - localSurfaceRenderer.forEach { - if (it.get() != null) currentCall?.localVideoTrack?.removeSink(it.get()) - } - remoteSurfaceRenderer.forEach { - if (it.get() != null) currentCall?.remoteVideoTrack?.removeSink(it.get()) - } - localSurfaceRenderer.clear() - remoteSurfaceRenderer.clear() - } else { - renderes.forEach { - removeIfNeeded(it, localSurfaceRenderer) - removeIfNeeded(it, remoteSurfaceRenderer) - // no need to check if it's in the track, removeSink is doing it - currentCall?.localVideoTrack?.removeSink(it) - currentCall?.remoteVideoTrack?.removeSink(it) - } - } - - if (remoteSurfaceRenderer.isEmpty()) { - // The call is going to continue in background, so ensure notification is visible - currentCall?.mxCall - ?.takeIf { it.state is CallState.Connected } - ?.let { mxCall -> - // Start background service with notification - - val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName() - ?: mxCall.otherUserId - CallService.onOnGoingCallBackground( - context = context, - isVideo = mxCall.isVideoCall, - roomName = name, - roomId = mxCall.roomId, - matrixId = currentSession?.myUserId ?: "", - callId = mxCall.callId - ) - } - } - } - - fun close() { - Timber.v("## VOIP WebRtcPeerConnectionManager close() >") - CallService.onNoActiveCall(context) - callAudioManager.stop() - val callToEnd = currentCall - currentCall = null - // This must be done in this thread - videoCapturer?.stopCapture() - videoCapturer?.dispose() - videoCapturer = null - executor.execute { - callToEnd?.release() - - if (currentCall == null) { - Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one") - peerConnectionFactory?.dispose() - peerConnectionFactory = null - } - - Timber.v("## VOIP WebRtcPeerConnectionManager close() executor done") - } - } - - companion object { - - private const val AUDIO_TRACK_ID = "ARDAMSa0" - - private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints().apply { - // add all existing audio filters to avoid having echos -// mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation", "true")) -// mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation2", "true")) -// mandatory.add(MediaConstraints.KeyValuePair("googDAEchoCancellation", "true")) -// -// mandatory.add(MediaConstraints.KeyValuePair("googTypingNoiseDetection", "true")) -// -// mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl", "true")) -// mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl2", "true")) -// -// mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression", "true")) -// mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression2", "true")) -// -// mandatory.add(MediaConstraints.KeyValuePair("googAudioMirroring", "false")) -// mandatory.add(MediaConstraints.KeyValuePair("googHighpassFilter", "true")) - } - } - - fun startOutgoingCall(signalingRoomId: String, otherUserId: String, isVideoCall: Boolean) { - executor.execute { - if (peerConnectionFactory == null) { - createPeerConnectionFactory() - } - } - - Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall") - val createdCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return - val callContext = CallContext(createdCall) - - callAudioManager.startForCall(createdCall) - currentCall = callContext - - val name = currentSession?.getRoomMember(createdCall.otherUserId, createdCall.roomId)?.toMatrixItem()?.getBestName() - ?: createdCall.otherUserId - CallService.onOutgoingCallRinging( - context = context.applicationContext, - isVideo = createdCall.isVideoCall, - roomName = name, - roomId = createdCall.roomId, - matrixId = currentSession?.myUserId ?: "", - callId = createdCall.callId) - - executor.execute { - callContext.remoteCandidateSource = ReplaySubject.create() - } - - // start the activity now - context.applicationContext.startActivity(VectorCallActivity.newIntent(context, createdCall)) - } - - override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) { - Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId}") - if (currentCall?.mxCall?.callId != mxCall.callId) return Unit.also { - Timber.w("## VOIP ignore ice candidates from other call") - } - val callContext = currentCall ?: return - - executor.execute { - iceCandidatesContent.candidates.forEach { - Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId} sdp: ${it.candidate}") - val iceCandidate = IceCandidate(it.sdpMid, it.sdpMLineIndex, it.candidate) - callContext.remoteCandidateSource?.onNext(iceCandidate) - } - } - } - - override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) { - Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}") - // to simplify we only treat one call at a time, and ignore others - if (currentCall != null) { - Timber.w("## VOIP receiving incoming call while already in call?") - // Just ignore, maybe we could answer from other session? - return - } - executor.execute { - if (peerConnectionFactory == null) { - createPeerConnectionFactory() - } - } - - val callContext = CallContext(mxCall) - currentCall = callContext - callAudioManager.startForCall(mxCall) - executor.execute { - callContext.remoteCandidateSource = ReplaySubject.create() - } - - // Start background service with notification - val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName() - ?: mxCall.otherUserId - CallService.onIncomingCallRinging( - context = context, - isVideo = mxCall.isVideoCall, - roomName = name, - roomId = mxCall.roomId, - matrixId = currentSession?.myUserId ?: "", - callId = mxCall.callId - ) - - callContext.offerSdp = callInviteContent.offer - - // If this is received while in background, the app will not sync, - // and thus won't be able to received events. For example if the call is - // accepted on an other session this device will continue ringing - if (isInBackground) { - if (FcmHelper.isPushSupported()) { - // only for push version as fdroid version is already doing it? - currentSession?.startAutomaticBackgroundSync(30, 0) - } else { - // Maybe increase sync freq? but how to set back to default values? - } - } - } - - private fun createAnswer() { - Timber.w("## VOIP createAnswer") - val call = currentCall ?: return - val constraints = MediaConstraints().apply { - mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")) - mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", if (call.mxCall.isVideoCall) "true" else "false")) - } - executor.execute { - call.peerConnection?.createAnswer(object : SdpObserverAdapter() { - override fun onCreateSuccess(p0: SessionDescription?) { - if (p0 == null) return - call.peerConnection?.setLocalDescription(object : SdpObserverAdapter() {}, p0) - // Now need to send it - call.mxCall.accept(p0) - } - }, constraints) - } - } - - fun muteCall(muted: Boolean) { - currentCall?.localAudioTrack?.setEnabled(!muted) - } - - fun enableVideo(enabled: Boolean) { - currentCall?.localVideoTrack?.setEnabled(enabled) - } - - fun switchCamera() { - Timber.v("## VOIP switchCamera") - if (!canSwitchCamera()) return - if (currentCall != null && currentCall?.mxCall?.state is CallState.Connected && currentCall?.mxCall?.isVideoCall == true) { - videoCapturer?.switchCamera(object : CameraVideoCapturer.CameraSwitchHandler { - // Invoked on success. |isFrontCamera| is true if the new camera is front facing. - override fun onCameraSwitchDone(isFrontCamera: Boolean) { - Timber.v("## VOIP onCameraSwitchDone isFront $isFrontCamera") - cameraInUse = availableCamera.first { if (isFrontCamera) it.type == CameraType.FRONT else it.type == CameraType.BACK } - localSurfaceRenderer.forEach { - it.get()?.setMirror(isFrontCamera) - } - - currentCallsListeners.forEach { - tryOrNull { it.onCameraChange() } - } - } - - override fun onCameraSwitchError(errorDescription: String?) { - Timber.v("## VOIP onCameraSwitchError isFront $errorDescription") - } - }) - } - } - - fun canSwitchCamera(): Boolean { - return availableCamera.size > 0 - } - - fun currentCameraType(): CameraType? { - return cameraInUse?.type - } - - fun setCaptureFormat(format: CaptureFormat) { - Timber.v("## VOIP setCaptureFormat $format") - currentCall ?: return - executor.execute { - // videoCapturer?.stopCapture() - videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps) - currentCaptureMode = format - currentCallsListeners.forEach { tryOrNull { it.onCaptureStateChanged() } } - } - } - - fun currentCaptureFormat(): CaptureFormat { - return currentCaptureMode - } - - fun endCall(originatedByMe: Boolean = true) { - // Update service state - CallService.onNoActiveCall(context) - // close tracks ASAP - currentCall?.localVideoTrack?.setEnabled(false) - currentCall?.localVideoTrack?.setEnabled(false) - - currentCall?.cameraAvailabilityCallback?.let { cameraAvailabilityCallback -> - val cameraManager = context.getSystemService<CameraManager>()!! - cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) - } - - if (originatedByMe) { - // send hang up event - currentCall?.mxCall?.hangUp() - } - close() - } - - fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) { - Timber.v("## VOIP onWiredDeviceEvent $event") - currentCall ?: return - // sometimes we received un-wanted unplugged... - callAudioManager.wiredStateChange(event) - } - - fun onWirelessDeviceEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) { - Timber.v("## VOIP onWirelessDeviceEvent $event") - callAudioManager.bluetoothStateChange(event.plugged) - } - - override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) { - val call = currentCall ?: return - if (call.mxCall.callId != callAnswerContent.callId) return Unit.also { - Timber.w("onCallAnswerReceived for non active call? ${callAnswerContent.callId}") - } - val mxCall = call.mxCall - // Update service state - val name = currentSession?.getRoomMember(mxCall.otherUserId, mxCall.roomId)?.toMatrixItem()?.getBestName() - ?: mxCall.otherUserId - CallService.onPendingCall( - context = context, - isVideo = mxCall.isVideoCall, - roomName = name, - roomId = mxCall.roomId, - matrixId = currentSession?.myUserId ?: "", - callId = mxCall.callId - ) - executor.execute { - Timber.v("## VOIP onCallAnswerReceived ${callAnswerContent.callId}") - val sdp = SessionDescription(SessionDescription.Type.ANSWER, callAnswerContent.answer.sdp) - call.peerConnection?.setRemoteDescription(object : SdpObserverAdapter() { - }, sdp) - } - } - - override fun onCallHangupReceived(callHangupContent: CallHangupContent) { - val call = currentCall ?: return - // Remote echos are filtered, so it's only remote hangups that i will get here - if (call.mxCall.callId != callHangupContent.callId) return Unit.also { - Timber.w("onCallHangupReceived for non active call? ${callHangupContent.callId}") - } - call.mxCall.state = CallState.Terminated - endCall(false) - } - - override fun onCallManagedByOtherSession(callId: String) { - Timber.v("## VOIP onCallManagedByOtherSession: $callId") - currentCall = null - CallService.onNoActiveCall(context) - - // did we start background sync? so we should stop it - if (isInBackground) { - if (FcmHelper.isPushSupported()) { - currentSession?.stopAnyBackgroundSync() - } else { - // for fdroid we should not stop, it should continue syncing - // maybe we should restore default timeout/delay though? - } - } - } - - private inner class StreamObserver(val callContext: CallContext) : PeerConnection.Observer { - - override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) { - Timber.v("## VOIP StreamObserver onConnectionChange: $newState") - when (newState) { - /** - * Every ICE transport used by the connection is either in use (state "connected" or "completed") - * or is closed (state "closed"); in addition, at least one transport is either "connected" or "completed" - */ - PeerConnection.PeerConnectionState.CONNECTED -> { - callContext.mxCall.state = CallState.Connected(newState) - callAudioManager.onCallConnected(callContext.mxCall) - } - /** - * One or more of the ICE transports on the connection is in the "failed" state. - */ - PeerConnection.PeerConnectionState.FAILED -> { - // This can be temporary, e.g when other ice not yet received... - // callContext.mxCall.state = CallState.ERROR - callContext.mxCall.state = CallState.Connected(newState) - } - /** - * At least one of the connection's ICE transports (RTCIceTransports or RTCDtlsTransports) are in the "new" state, - * and none of them are in one of the following states: "connecting", "checking", "failed", or "disconnected", - * or all of the connection's transports are in the "closed" state. - */ - PeerConnection.PeerConnectionState.NEW, - - /** - * One or more of the ICE transports are currently in the process of establishing a connection; - * that is, their RTCIceConnectionState is either "checking" or "connected", and no transports are in the "failed" state - */ - PeerConnection.PeerConnectionState.CONNECTING -> { - callContext.mxCall.state = CallState.Connected(PeerConnection.PeerConnectionState.CONNECTING) - } - /** - * The RTCPeerConnection is closed. - * This value was in the RTCSignalingState enum (and therefore found by reading the value of the signalingState) - * property until the May 13, 2016 draft of the specification. - */ - PeerConnection.PeerConnectionState.CLOSED, - /** - * At least one of the ICE transports for the connection is in the "disconnected" state and none of - * the other transports are in the state "failed", "connecting", or "checking". - */ - PeerConnection.PeerConnectionState.DISCONNECTED -> { - callContext.mxCall.state = CallState.Connected(newState) - } - null -> { - } - } - } - - override fun onIceCandidate(iceCandidate: IceCandidate) { - Timber.v("## VOIP StreamObserver onIceCandidate: $iceCandidate") - callContext.iceCandidateSource.onNext(iceCandidate) - } - - override fun onDataChannel(dc: DataChannel) { - Timber.v("## VOIP StreamObserver onDataChannel: ${dc.state()}") - } - - override fun onIceConnectionReceivingChange(receiving: Boolean) { - Timber.v("## VOIP StreamObserver onIceConnectionReceivingChange: $receiving") - } - - override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState) { - Timber.v("## VOIP StreamObserver onIceConnectionChange IceConnectionState:$newState") - when (newState) { - /** - * the ICE agent is gathering addresses or is waiting to be given remote candidates through - * calls to RTCPeerConnection.addIceCandidate() (or both). - */ - PeerConnection.IceConnectionState.NEW -> { - } - /** - * The ICE agent has been given one or more remote candidates and is checking pairs of local and remote candidates - * against one another to try to find a compatible match, but has not yet found a pair which will allow - * the peer connection to be made. It's possible that gathering of candidates is also still underway. - */ - PeerConnection.IceConnectionState.CHECKING -> { - } - - /** - * A usable pairing of local and remote candidates has been found for all components of the connection, - * and the connection has been established. - * It's possible that gathering is still underway, and it's also possible that the ICE agent is still checking - * candidates against one another looking for a better connection to use. - */ - PeerConnection.IceConnectionState.CONNECTED -> { - } - /** - * Checks to ensure that components are still connected failed for at least one component of the RTCPeerConnection. - * This is a less stringent test than "failed" and may trigger intermittently and resolve just as spontaneously on less reliable networks, - * or during temporary disconnections. When the problem resolves, the connection may return to the "connected" state. - */ - PeerConnection.IceConnectionState.DISCONNECTED -> { - } - /** - * The ICE candidate has checked all candidates pairs against one another and has failed to find - * compatible matches for all components of the connection. - * It is, however, possible that the ICE agent did find compatible connections for some components. - */ - PeerConnection.IceConnectionState.FAILED -> { - // I should not hangup here.. - // because new candidates could arrive - // callContext.mxCall.hangUp() - } - /** - * The ICE agent has finished gathering candidates, has checked all pairs against one another, and has found a connection for all components. - */ - PeerConnection.IceConnectionState.COMPLETED -> { - } - /** - * The ICE agent for this RTCPeerConnection has shut down and is no longer handling requests. - */ - PeerConnection.IceConnectionState.CLOSED -> { - } - } - } - - override fun onAddStream(stream: MediaStream) { - Timber.v("## VOIP StreamObserver onAddStream: $stream") - executor.execute { - // reportError("Weird-looking stream: " + stream); - if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) { - Timber.e("## VOIP StreamObserver weird looking stream: $stream") - // TODO maybe do something more?? - callContext.mxCall.hangUp() - return@execute - } - - if (stream.videoTracks.size == 1) { - val remoteVideoTrack = stream.videoTracks.first() - remoteVideoTrack.setEnabled(true) - callContext.remoteVideoTrack = remoteVideoTrack - // sink to renderer if attached - remoteSurfaceRenderer.forEach { it.get()?.let { remoteVideoTrack.addSink(it) } } - } - } - } - - override fun onRemoveStream(stream: MediaStream) { - Timber.v("## VOIP StreamObserver onRemoveStream") - executor.execute { - // remoteSurfaceRenderer?.get()?.let { -// callContext.remoteVideoTrack?.removeSink(it) -// } - remoteSurfaceRenderer - .mapNotNull { it.get() } - .forEach { callContext.remoteVideoTrack?.removeSink(it) } - callContext.remoteVideoTrack = null - } - } - - override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState) { - Timber.v("## VOIP StreamObserver onIceGatheringChange: $newState") - } - - override fun onSignalingChange(newState: PeerConnection.SignalingState) { - Timber.v("## VOIP StreamObserver onSignalingChange: $newState") - } - - override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>) { - Timber.v("## VOIP StreamObserver onIceCandidatesRemoved: ${candidates.contentToString()}") - } - - override fun onRenegotiationNeeded() { - Timber.v("## VOIP StreamObserver onRenegotiationNeeded") - // Should not do anything, for now we follow a pre-agreed-upon - // signaling/negotiation protocol. - } - - /** - * This happens when a new track of any kind is added to the media stream. - * This event is fired when the browser adds a track to the stream - * (such as when a RTCPeerConnection is renegotiated or a stream being captured using HTMLMediaElement.captureStream() - * gets a new set of tracks because the media element being captured loaded a new source. - */ - override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) { - Timber.v("## VOIP StreamObserver onAddTrack") - } - } - - inner class CameraRestarter(val cameraId: String, val callId: String) : CameraManager.AvailabilityCallback() { - - override fun onCameraAvailable(cameraId: String) { - if (this.cameraId == cameraId && currentCall?.mxCall?.callId == callId) { - // re-start the capture - // TODO notify that video is enabled - videoCapturer?.startCapture(currentCaptureMode.width, currentCaptureMode.height, currentCaptureMode.fps) - context.getSystemService<CameraManager>()?.unregisterAvailabilityCallback(this) - } - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt b/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt new file mode 100644 index 0000000000..32b243aa2b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2021 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. + */ +@file:Suppress("DEPRECATION") + +package im.vector.app.features.call.audio + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothManager +import android.bluetooth.BluetoothProfile +import android.content.Context +import android.media.AudioManager +import androidx.core.content.getSystemService +import im.vector.app.core.services.BluetoothHeadsetReceiver +import im.vector.app.core.services.WiredHeadsetStateReceiver +import timber.log.Timber +import java.util.HashSet + +internal class API21AudioDeviceDetector(private val context: Context, + private val audioManager: AudioManager, + private val callAudioManager: CallAudioManager +) : CallAudioManager.AudioDeviceDetector, WiredHeadsetStateReceiver.HeadsetEventListener, BluetoothHeadsetReceiver.EventListener { + + private var bluetoothAdapter: BluetoothAdapter? = null + private var connectedBlueToothHeadset: BluetoothProfile? = null + private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null + private var bluetoothHeadsetStateReceiver: BluetoothHeadsetReceiver? = null + + private val onAudioDeviceChangeRunner = Runnable { + val devices = getAvailableSoundDevices() + callAudioManager.replaceDevices(devices) + Timber.i(" Available audio devices: $devices") + callAudioManager.updateAudioRoute() + } + + private fun getAvailableSoundDevices(): Set<CallAudioManager.Device> { + return HashSet<CallAudioManager.Device>().apply { + if (isBluetoothHeadsetOn()) add(CallAudioManager.Device.WIRELESS_HEADSET) + if (isWiredHeadsetOn()) { + add(CallAudioManager.Device.HEADSET) + } else { + add(CallAudioManager.Device.PHONE) + } + add(CallAudioManager.Device.SPEAKER) + } + } + + private fun isWiredHeadsetOn(): Boolean { + return audioManager.isWiredHeadsetOn + } + + private fun isBluetoothHeadsetOn(): Boolean { + Timber.v("## VOIP: AudioManager isBluetoothHeadsetOn") + try { + if (connectedBlueToothHeadset == null) return false.also { + Timber.v("## VOIP: AudioManager no connected bluetooth headset") + } + if (!audioManager.isBluetoothScoAvailableOffCall) return false.also { + Timber.v("## VOIP: AudioManager isBluetoothScoAvailableOffCall false") + } + return true + } catch (failure: Throwable) { + Timber.e("## VOIP: AudioManager isBluetoothHeadsetOn failure ${failure.localizedMessage}") + return false + } + } + + /** + * Helper method to trigger an audio route update when devices change. It + * makes sure the operation is performed on the audio thread. + */ + private fun onAudioDeviceChange() { + callAudioManager.runInAudioThread(onAudioDeviceChangeRunner) + } + + override fun start() { + Timber.i("Start using $this as the audio device handler") + wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(context, this) + bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(context, this) + val bm: BluetoothManager? = context.getSystemService() + val adapter = bm?.adapter + Timber.d("## VOIP Bluetooth adapter $adapter") + bluetoothAdapter = adapter + adapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener { + override fun onServiceDisconnected(profile: Int) { + Timber.d("## VOIP onServiceDisconnected $profile") + if (profile == BluetoothProfile.HEADSET) { + connectedBlueToothHeadset = null + onAudioDeviceChange() + } + } + + override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) { + Timber.d("## VOIP onServiceConnected $profile , proxy:$proxy") + if (profile == BluetoothProfile.HEADSET) { + connectedBlueToothHeadset = proxy + onAudioDeviceChange() + } + } + }, BluetoothProfile.HEADSET) + onAudioDeviceChange() + } + + override fun stop() { + Timber.i("Stop using $this as the audio device handler") + wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(context, it) } + wiredHeadsetStateReceiver = null + bluetoothHeadsetStateReceiver?.let { BluetoothHeadsetReceiver.unRegister(context, it) } + bluetoothHeadsetStateReceiver = null + } + + override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) { + Timber.v("onHeadsetEvent $event") + onAudioDeviceChange() + } + + override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) { + Timber.v("onBTHeadsetEvent $event") + onAudioDeviceChange() + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt b/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt new file mode 100644 index 0000000000..7174554d5f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2021 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.call.audio + +import android.media.AudioDeviceCallback +import android.media.AudioDeviceInfo +import android.media.AudioManager +import android.os.Build +import androidx.annotation.RequiresApi +import timber.log.Timber +import java.util.HashSet + +@RequiresApi(Build.VERSION_CODES.M) +internal class API23AudioDeviceDetector(private val audioManager: AudioManager, + private val callAudioManager: CallAudioManager +) : CallAudioManager.AudioDeviceDetector { + + private val onAudioDeviceChangeRunner = Runnable { + val devices: MutableSet<CallAudioManager.Device> = HashSet() + val deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) + for (info in deviceInfos) { + when (info.type) { + AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> devices.add(CallAudioManager.Device.WIRELESS_HEADSET) + AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> devices.add(CallAudioManager.Device.PHONE) + AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> devices.add(CallAudioManager.Device.SPEAKER) + AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_USB_HEADSET -> devices.add(CallAudioManager.Device.HEADSET) + } + } + callAudioManager.replaceDevices(devices) + Timber.i(" Available audio devices: $devices") + callAudioManager.updateAudioRoute() + } + private val audioDeviceCallback: AudioDeviceCallback = object : AudioDeviceCallback() { + override fun onAudioDevicesAdded( + addedDevices: Array<AudioDeviceInfo>) { + Timber.d(" Audio devices added") + onAudioDeviceChange() + } + + override fun onAudioDevicesRemoved( + removedDevices: Array<AudioDeviceInfo>) { + Timber.d(" Audio devices removed") + onAudioDeviceChange() + } + } + + /** + * Helper method to trigger an audio route update when devices change. It + * makes sure the operation is performed on the audio thread. + */ + private fun onAudioDeviceChange() { + callAudioManager.runInAudioThread(onAudioDeviceChangeRunner) + } + + override fun start() { + Timber.i("Using $this as the audio device handler") + audioManager.registerAudioDeviceCallback(audioDeviceCallback, null) + onAudioDeviceChange() + } + + override fun stop() { + audioManager.unregisterAudioDeviceCallback(audioDeviceCallback) + } + + companion object { + /** + * Constant defining a USB headset. Only available on API level >= 26. + * The value of: AudioDeviceInfo.TYPE_USB_HEADSET + */ + private const val TYPE_USB_HEADSET = 22 + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt b/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt new file mode 100644 index 0000000000..66370763e1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2021 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.call.audio + +import android.content.Context +import android.media.AudioManager +import android.os.Build +import androidx.core.content.getSystemService +import org.matrix.android.sdk.api.extensions.orFalse +import timber.log.Timber +import java.util.HashSet +import java.util.concurrent.Executors + +class CallAudioManager(private val context: Context, val configChange: (() -> Unit)?) { + + private val audioManager: AudioManager? = context.getSystemService() + private var audioDeviceDetector: AudioDeviceDetector? = null + private var audioDeviceRouter: AudioDeviceRouter? = null + + enum class Device { + PHONE, + SPEAKER, + HEADSET, + WIRELESS_HEADSET + } + + enum class Mode { + DEFAULT, + AUDIO_CALL, + VIDEO_CALL + } + + private var mode = Mode.DEFAULT + private var _availableDevices: MutableSet<Device> = HashSet() + val availableDevices: Set<Device> + get() = _availableDevices + + var selectedDevice: Device? = null + private set + private var userSelectedDevice: Device? = null + + init { + runInAudioThread { setup() } + } + + private fun setup() { + if (audioManager == null) { + return + } + audioDeviceDetector?.stop() + audioDeviceDetector = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + API23AudioDeviceDetector(audioManager, this) + } else { + API21AudioDeviceDetector(context, audioManager, this) + } + audioDeviceDetector?.start() + audioDeviceRouter = DefaultAudioDeviceRouter(audioManager, this) + } + + fun runInAudioThread(runnable: Runnable) { + executor.execute(runnable) + } + + /** + * Sets the user selected audio device as the active audio device. + * + * @param device the desired device which will become active. + */ + fun setAudioDevice(device: Device) { + runInAudioThread(Runnable { + if (!_availableDevices.contains(device)) { + Timber.w(" Audio device not available: $device") + userSelectedDevice = null + return@Runnable + } + if (mode != Mode.DEFAULT) { + Timber.i(" User selected device set to: $device") + userSelectedDevice = device + updateAudioRoute(mode, false) + } + }) + } + + /** + * Public method to set the current audio mode. + * + * @param mode the desired audio mode. + * could be updated successfully, and it will be rejected otherwise. + */ + fun setMode(mode: Mode) { + runInAudioThread { + var success: Boolean + try { + success = updateAudioRoute(mode, false) + } catch (e: Throwable) { + success = false + Timber.e(e, " Failed to update audio route for mode: " + mode) + } + if (success) { + this@CallAudioManager.mode = mode + } + } + } + + /** + * Updates the audio route for the given mode. + * + * @param mode the audio mode to be used when computing the audio route. + * @return `true` if the audio route was updated successfully; + * `false`, otherwise. + */ + private fun updateAudioRoute(mode: Mode, force: Boolean): Boolean { + Timber.i(" Update audio route for mode: " + mode) + if (!audioDeviceRouter?.setMode(mode).orFalse()) { + return false + } + if (mode == Mode.DEFAULT) { + selectedDevice = null + userSelectedDevice = null + return true + } + val bluetoothAvailable = _availableDevices.contains(Device.WIRELESS_HEADSET) + val headsetAvailable = _availableDevices.contains(Device.HEADSET) + + // Pick the desired device based on what's available and the mode. + var audioDevice: Device + audioDevice = if (bluetoothAvailable) { + Device.WIRELESS_HEADSET + } else if (headsetAvailable) { + Device.HEADSET + } else if (mode == Mode.VIDEO_CALL) { + Device.SPEAKER + } else { + Device.PHONE + } + // Consider the user's selection + if (userSelectedDevice != null && _availableDevices.contains(userSelectedDevice)) { + audioDevice = userSelectedDevice!! + } + + // If the previously selected device and the current default one + // match, do nothing. + if (!force && selectedDevice != null && selectedDevice == audioDevice) { + return true + } + selectedDevice = audioDevice + Timber.i(" Selected audio device: " + audioDevice) + audioDeviceRouter?.setAudioRoute(audioDevice) + configChange?.invoke() + return true + } + + /** + * Resets the current device selection. + */ + fun resetSelectedDevice() { + selectedDevice = null + userSelectedDevice = null + } + + /** + * Adds a new device to the list of available devices. + * + * @param device The new device. + */ + fun addDevice(device: Device) { + _availableDevices.add(device) + resetSelectedDevice() + } + + /** + * Removes a device from the list of available devices. + * + * @param device The old device to the removed. + */ + fun removeDevice(device: Device) { + _availableDevices.remove(device) + resetSelectedDevice() + } + + /** + * Replaces the current list of available devices with a new one. + * + * @param devices The new devices list. + */ + fun replaceDevices(devices: Set<Device>) { + _availableDevices.clear() + _availableDevices.addAll(devices) + resetSelectedDevice() + } + + /** + * Re-sets the current audio route. Needed when devices changes have happened. + */ + fun updateAudioRoute() { + if (mode != Mode.DEFAULT) { + updateAudioRoute(mode, false) + } + } + + /** + * Re-sets the current audio route. Needed when focus is lost and regained. + */ + fun resetAudioRoute() { + if (mode != Mode.DEFAULT) { + updateAudioRoute(mode, true) + } + } + + /** + * Interface for the modules implementing the actual audio device management. + */ + interface AudioDeviceDetector { + /** + * Start detecting audio device changes. + */ + fun start() + + /** + * Stop audio device detection. + */ + fun stop() + } + + interface AudioDeviceRouter { + /** + * Set the appropriate route for the given audio device. + * + * @param device Audio device for which the route must be set. + */ + fun setAudioRoute(device: Device) + + /** + * Set the given audio mode. + * + * @param mode The new audio mode to be used. + * @return Whether the operation was successful or not. + */ + fun setMode(mode: Mode): Boolean + } + + companion object { + // Every audio operations should be launched on single thread + private val executor = Executors.newSingleThreadExecutor() + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/audio/DefaultAudioDeviceRouter.kt b/vector/src/main/java/im/vector/app/features/call/audio/DefaultAudioDeviceRouter.kt new file mode 100644 index 0000000000..c252cc9f89 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/audio/DefaultAudioDeviceRouter.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2021 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.call.audio + +import android.media.AudioManager +import androidx.media.AudioAttributesCompat +import androidx.media.AudioFocusRequestCompat +import androidx.media.AudioManagerCompat +import timber.log.Timber + +class DefaultAudioDeviceRouter(private val audioManager: AudioManager, + private val callAudioManager: CallAudioManager +) : CallAudioManager.AudioDeviceRouter, AudioManager.OnAudioFocusChangeListener { + + private var audioFocusLost = false + + private var focusRequestCompat: AudioFocusRequestCompat? = null + + override fun setAudioRoute(device: CallAudioManager.Device) { + audioManager.isSpeakerphoneOn = device === CallAudioManager.Device.SPEAKER + setBluetoothAudioRoute(device === CallAudioManager.Device.WIRELESS_HEADSET) + } + + override fun setMode(mode: CallAudioManager.Mode): Boolean { + if (mode === CallAudioManager.Mode.DEFAULT) { + audioFocusLost = false + audioManager.mode = AudioManager.MODE_NORMAL + focusRequestCompat?.also { + AudioManagerCompat.abandonAudioFocusRequest(audioManager, it) + } + focusRequestCompat = null + audioManager.isSpeakerphoneOn = false + setBluetoothAudioRoute(false) + return true + } + audioManager.mode = AudioManager.MODE_IN_COMMUNICATION + audioManager.isMicrophoneMute = false + + val audioFocusRequest = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) + .setAudioAttributes( + AudioAttributesCompat.Builder() + .setUsage(AudioAttributesCompat.USAGE_VOICE_COMMUNICATION) + .setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH) + .build() + ) + .setOnAudioFocusChangeListener(this) + .build() + .also { + focusRequestCompat = it + } + + val gotFocus = AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest) + if (gotFocus == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { + Timber.w(" Audio focus request failed") + return false + } + return true + } + + /** + * Helper method to set the output route to a Bluetooth device. + * + * @param enabled true if Bluetooth should use used, false otherwise. + */ + private fun setBluetoothAudioRoute(enabled: Boolean) { + if (enabled) { + audioManager.startBluetoothSco() + audioManager.isBluetoothScoOn = true + } else { + audioManager.isBluetoothScoOn = false + audioManager.stopBluetoothSco() + } + } + + /** + * [AudioManager.OnAudioFocusChangeListener] interface method. Called + * when the audio focus of the system is updated. + * + * @param focusChange - The type of focus change. + */ + override fun onAudioFocusChange(focusChange: Int) { + callAudioManager.runInAudioThread { + when (focusChange) { + AudioManager.AUDIOFOCUS_GAIN -> { + Timber.d(" Audio focus gained") + if (audioFocusLost) { + callAudioManager.resetAudioRoute() + } + audioFocusLost = false + } + AudioManager.AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + Timber.d(" Audio focus lost") + audioFocusLost = true + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt index 5a323aeb85..1525ca6dfe 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiCallViewModel.kt @@ -24,7 +24,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory import im.vector.app.core.platform.VectorViewModel -import im.vector.app.core.resources.StringProvider +import im.vector.app.features.themes.ThemeProvider import org.jitsi.meet.sdk.JitsiMeetUserInfo import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session @@ -37,7 +37,8 @@ class JitsiCallViewModel @AssistedInject constructor( @Assisted initialState: JitsiCallViewState, @Assisted val args: VectorJitsiActivity.Args, private val session: Session, - private val stringProvider: StringProvider + private val jitsiMeetPropertiesFactory: JitsiWidgetPropertiesFactory, + private val themeProvider: ThemeProvider ) : VectorViewModel<JitsiCallViewState, JitsiCallViewActions, JitsiCallViewEvents>(initialState) { @AssistedFactory @@ -45,6 +46,8 @@ class JitsiCallViewModel @AssistedInject constructor( fun create(initialState: JitsiCallViewState, args: VectorJitsiActivity.Args): JitsiCallViewModel } + private val widgetService = session.widgetService() + init { val me = session.getRoomMember(session.myUserId, args.roomId)?.toMatrixItem() val userInfo = JitsiMeetUserInfo().apply { @@ -57,13 +60,14 @@ class JitsiCallViewModel @AssistedInject constructor( copy(userInfo = userInfo) } - session.widgetService().getRoomWidgetsLive(args.roomId, QueryStringValue.Equals(args.widgetId), WidgetType.Jitsi.values()) + widgetService.getRoomWidgetsLive(args.roomId, QueryStringValue.Equals(args.widgetId), WidgetType.Jitsi.values()) .asObservable() .distinctUntilChanged() .subscribe { val jitsiWidget = it.firstOrNull() if (jitsiWidget != null) { - val ppt = jitsiWidget.computedUrl?.let { url -> JitsiWidgetProperties(url, stringProvider) } + val ppt = widgetService.getWidgetComputedUrl(jitsiWidget, themeProvider.isLightTheme()) + ?.let { url -> jitsiMeetPropertiesFactory.create(url) } setState { copy( widget = Success(jitsiWidget), diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetProperties.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetProperties.kt index 46e2e68dd6..ed63f723c8 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetProperties.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetProperties.kt @@ -16,25 +16,9 @@ package im.vector.app.features.call.conference -import android.net.Uri -import im.vector.app.R -import im.vector.app.core.resources.StringProvider -import java.net.URLDecoder - -class JitsiWidgetProperties(private val uriString: String, val stringProvider: StringProvider) { - val domain: String by lazy { configs["conferenceDomain"] ?: stringProvider.getString(R.string.preferred_jitsi_domain) } - val confId: String? by lazy { configs["conferenceId"] } - val displayName: String? by lazy { configs["displayName"] } - val avatarUrl: String? by lazy { configs["avatarUrl"] } - - private val configString: String? by lazy { Uri.parse(uriString).fragment } - - private val configs: Map<String, String?> by lazy { - configString?.split("&") - ?.map { it.split("=") } - ?.filter { it.size == 2 } - ?.map { (key, value) -> key to URLDecoder.decode(value, "UTF-8") } - ?.toMap() - .orEmpty() - } -} +data class JitsiWidgetProperties( + val domain: String, + val confId: String?, + val displayName: String?, + val avatarUrl: String? +) diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt new file mode 100644 index 0000000000..8014e01fb2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiWidgetPropertiesFactory.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 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.call.conference + +import android.net.Uri +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import org.matrix.android.sdk.api.extensions.tryOrNull +import java.net.URLDecoder +import javax.inject.Inject + +class JitsiWidgetPropertiesFactory @Inject constructor( + private val stringProvider: StringProvider +) { + fun create(url: String): JitsiWidgetProperties { + val configString = tryOrNull { Uri.parse(url) }?.fragment + + val configs = configString?.split("&") + ?.map { it.split("=") } + ?.filter { it.size == 2 } + ?.map { (key, value) -> key to URLDecoder.decode(value, "UTF-8") } + ?.toMap() + .orEmpty() + + return JitsiWidgetProperties( + domain = configs["conferenceDomain"] ?: stringProvider.getString(R.string.preferred_jitsi_domain), + confId = configs["conferenceId"], + displayName = configs["displayName"], + avatarUrl = configs["avatarUrl"] + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt index 4851afaed0..caece858aa 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt @@ -16,12 +16,15 @@ package im.vector.app.features.call.conference +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.os.Bundle import android.os.Parcelable import android.widget.FrameLayout import androidx.core.view.isVisible +import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.airbnb.mvrx.Fail import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.Success @@ -31,17 +34,17 @@ import im.vector.app.core.di.ScreenComponent import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityJitsiBinding import kotlinx.parcelize.Parcelize +import org.jitsi.meet.sdk.BroadcastEvent import org.jitsi.meet.sdk.JitsiMeetActivityDelegate import org.jitsi.meet.sdk.JitsiMeetActivityInterface import org.jitsi.meet.sdk.JitsiMeetConferenceOptions import org.jitsi.meet.sdk.JitsiMeetView -import org.jitsi.meet.sdk.JitsiMeetViewListener import org.matrix.android.sdk.api.extensions.tryOrNull import timber.log.Timber import java.net.URL import javax.inject.Inject -class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMeetActivityInterface, JitsiMeetViewListener { +class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMeetActivityInterface { @Parcelize data class Args( @@ -63,12 +66,21 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee injector.inject(this) } + // See https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk#listening-for-broadcasted-events + private val broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + intent?.let { onBroadcastReceived(it) } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) jitsiViewModel.subscribe(this) { renderState(it) } + + registerForBroadcastMessages() } override fun initUiAndData() { @@ -76,7 +88,6 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee jitsiMeetView = JitsiMeetView(this) val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT) views.jitsiLayout.addView(jitsiMeetView, params) - jitsiMeetView?.listener = this } private fun renderState(viewState: JitsiCallViewState) { @@ -132,6 +143,7 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee override fun onDestroy() { JitsiMeetActivityDelegate.onHostDestroy(this) + unregisterForBroadcastMessages() super.onDestroy() } @@ -154,20 +166,37 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults) } - override fun onConferenceTerminated(p0: MutableMap<String, Any>?) { - Timber.v("JitsiMeetViewListener.onConferenceTerminated()") - // Do not finish if there is an error - if (p0?.get("error") == null) { - finish() + private fun registerForBroadcastMessages() { + val intentFilter = IntentFilter() + for (type in BroadcastEvent.Type.values()) { + intentFilter.addAction(type.action) + } + tryOrNull("Unable to register receiver") { + LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, intentFilter) } } - override fun onConferenceJoined(p0: MutableMap<String, Any>?) { - Timber.v("JitsiMeetViewListener.onConferenceJoined()") + private fun unregisterForBroadcastMessages() { + tryOrNull("Unable to unregister receiver") { + LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver) + } } - override fun onConferenceWillJoin(p0: MutableMap<String, Any>?) { - Timber.v("JitsiMeetViewListener.onConferenceWillJoin()") + private fun onBroadcastReceived(intent: Intent) { + val event = BroadcastEvent(intent) + Timber.v("Broadcast received: ${event.type}") + when (event.type) { + BroadcastEvent.Type.CONFERENCE_TERMINATED -> onConferenceTerminated(event.data) + else -> Unit + } + } + + private fun onConferenceTerminated(data: Map<String, Any>) { + Timber.v("JitsiMeetViewListener.onConferenceTerminated()") + // Do not finish if there is an error + if (data["error"] == null) { + finish() + } } companion object { diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt new file mode 100644 index 0000000000..06b4dbfe7b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2021 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.call.dialpad + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import im.vector.app.R +import im.vector.app.core.extensions.addChildFragment +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.databinding.BottomSheetCallDialPadBinding +import im.vector.app.features.settings.VectorLocale + +class CallDialPadBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCallDialPadBinding>() { + + companion object { + + private const val EXTRA_SHOW_ACTIONS = "EXTRA_SHOW_ACTIONS" + + fun newInstance(showActions: Boolean): CallDialPadBottomSheet { + return CallDialPadBottomSheet().apply { + arguments = Bundle().apply { + putBoolean(EXTRA_SHOW_ACTIONS, showActions) + } + } + } + } + + override val showExpanded = true + + var callback: DialPadFragment.Callback? = null + set(value) { + field = value + setCallbackToFragment(callback) + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetCallDialPadBinding { + return BottomSheetCallDialPadBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (savedInstanceState == null) { + val showActions = arguments?.getBoolean(EXTRA_SHOW_ACTIONS, false) ?: false + DialPadFragment().apply { + arguments = Bundle().apply { + putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, showActions) + putBoolean(DialPadFragment.EXTRA_ENABLE_OK, showActions) + putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country) + } + callback = DialPadFragmentCallbackWrapper(this@CallDialPadBottomSheet.callback) + }.also { + addChildFragment(R.id.callDialPadFragmentContainer, it) + } + } else { + setCallbackToFragment(callback) + } + views.callDialPadClose.setOnClickListener { + dismiss() + } + } + + override fun onDestroyView() { + setCallbackToFragment(null) + super.onDestroyView() + } + + private fun setCallbackToFragment(callback: DialPadFragment.Callback?) { + if (!isAdded) return + val dialPadFragment = childFragmentManager.findFragmentById(R.id.callDialPadFragmentContainer) as? DialPadFragment + dialPadFragment?.callback = DialPadFragmentCallbackWrapper(callback) + } + + private inner class DialPadFragmentCallbackWrapper(val callback: DialPadFragment.Callback?): DialPadFragment.Callback { + + override fun onDigitAppended(digit: String) { + callback?.onDigitAppended(digit) + } + + override fun onOkClicked(formatted: String?, raw: String?) { + callback?.onOkClicked(formatted, raw) + dismiss() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt new file mode 100644 index 0000000000..b488a1af0e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2021 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.call.dialpad + +import android.content.res.ColorStateList +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.core.widget.ImageViewCompat +import androidx.fragment.app.Fragment +import com.android.dialer.dialpadview.DialpadView +import com.android.dialer.dialpadview.DigitsEditText +import com.android.dialer.dialpadview.R +import com.google.i18n.phonenumbers.AsYouTypeFormatter +import com.google.i18n.phonenumbers.PhoneNumberUtil +import im.vector.app.features.themes.ThemeUtils + +class DialPadFragment : Fragment() { + + var callback: Callback? = null + + private var digits: DigitsEditText? = null + private var formatter: AsYouTypeFormatter? = null + private var input = "" + private var regionCode: String = DEFAULT_REGION_CODE + private var formatAsYouType = true + private var enableStar = true + private var enablePound = true + private var enablePlus = true + private var cursorVisible = false + private var enableDelete = true + private var enableFabOk = true + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?): View { + initArgs(savedInstanceState) + val view = inflater.inflate(R.layout.dialpad_fragment, container, false) + val dialpadView = view.findViewById<View>(R.id.dialpad_view) as DialpadView + dialpadView.findViewById<View>(R.id.dialpad_key_voicemail).isVisible = false + digits = dialpadView.digits as? DigitsEditText + digits?.isCursorVisible = cursorVisible + digits?.setTextColor(ThemeUtils.getColor(requireContext(), im.vector.app.R.attr.riotx_text_primary)) + dialpadView.findViewById<View>(R.id.zero).setOnClickListener { append('0') } + if (enablePlus) { + dialpadView.findViewById<View>(R.id.zero).setOnLongClickListener { + append('+') + true + } + } + dialpadView.findViewById<View>(R.id.one).setOnClickListener { append('1') } + dialpadView.findViewById<View>(R.id.two).setOnClickListener { append('2') } + dialpadView.findViewById<View>(R.id.three).setOnClickListener { append('3') } + dialpadView.findViewById<View>(R.id.four).setOnClickListener { append('4') } + dialpadView.findViewById<View>(R.id.four).setOnClickListener { append('4') } + dialpadView.findViewById<View>(R.id.five).setOnClickListener { append('5') } + dialpadView.findViewById<View>(R.id.six).setOnClickListener { append('6') } + dialpadView.findViewById<View>(R.id.seven).setOnClickListener { append('7') } + dialpadView.findViewById<View>(R.id.eight).setOnClickListener { append('8') } + dialpadView.findViewById<View>(R.id.nine).setOnClickListener { append('9') } + if (enableStar) { + dialpadView.findViewById<View>(R.id.star).setOnClickListener { append('*') } + } else { + dialpadView.findViewById<View>(R.id.star).isVisible = false + } + if (enablePound) { + dialpadView.findViewById<View>(R.id.pound).setOnClickListener { append('#') } + } else { + dialpadView.findViewById<View>(R.id.pound).isVisible = false + } + if (enableDelete) { + dialpadView.deleteButton.setOnClickListener { poll() } + dialpadView.deleteButton.setOnLongClickListener { + clear() + true + } + val tintColor = ThemeUtils.getColor(requireContext(), im.vector.app.R.attr.riotx_text_secondary) + ImageViewCompat.setImageTintList(dialpadView.deleteButton, ColorStateList.valueOf(tintColor)) + } else { + dialpadView.deleteButton.isVisible = false + } + + // if region code is null, no formatting is performed + formatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(if (formatAsYouType) regionCode else "") + + val fabOk = view.findViewById<View>(R.id.fab_ok) + if (enableFabOk) { + fabOk.setOnClickListener { + callback?.onOkClicked(digits?.text.toString(), input) + } + } else { + fabOk.isVisible = false + } + + digits?.setOnTextContextMenuClickListener { + val string = digits?.text.toString() + clear() + for (element in string) { + append(element) + } + } + return view + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(EXTRA_REGION_CODE, regionCode) + outState.putBoolean(EXTRA_FORMAT_AS_YOU_TYPE, formatAsYouType) + outState.putBoolean(EXTRA_ENABLE_STAR, enableStar) + outState.putBoolean(EXTRA_ENABLE_POUND, enablePound) + outState.putBoolean(EXTRA_ENABLE_PLUS, enablePlus) + outState.putBoolean(EXTRA_ENABLE_OK, enableFabOk) + outState.putBoolean(EXTRA_ENABLE_DELETE, enableDelete) + outState.putBoolean(EXTRA_CURSOR_VISIBLE, cursorVisible) + } + + private fun initArgs(savedInstanceState: Bundle?) { + val args = savedInstanceState ?: arguments + if (args != null) { + regionCode = args.getString(EXTRA_REGION_CODE, DEFAULT_REGION_CODE) + formatAsYouType = args.getBoolean(EXTRA_FORMAT_AS_YOU_TYPE, formatAsYouType) + enableStar = args.getBoolean(EXTRA_ENABLE_STAR, enableStar) + enablePound = args.getBoolean(EXTRA_ENABLE_POUND, enablePound) + enablePlus = args.getBoolean(EXTRA_ENABLE_PLUS, enablePlus) + enableDelete = args.getBoolean(EXTRA_ENABLE_DELETE, enableDelete) + enableFabOk = args.getBoolean(EXTRA_ENABLE_OK, enableFabOk) + cursorVisible = args.getBoolean(EXTRA_CURSOR_VISIBLE, cursorVisible) + } + } + + private fun poll() { + if (!input.isEmpty()) { + input = input.substring(0, input.length - 1) + formatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(regionCode) + if (formatAsYouType) { + digits?.setText("") + for (c in input.toCharArray()) { + digits?.setText(formatter?.inputDigit(c)) + } + } else { + digits?.setText(input) + } + } + } + + private fun clear() { + formatter?.clear() + digits?.setText("") + input = "" + } + + private fun append(c: Char) { + callback?.onDigitAppended(c.toString()) + input += c + if (formatAsYouType) { + digits?.setText(formatter?.inputDigit(c)) + } else { + digits?.setText(input) + } + } + + interface Callback { + fun onOkClicked(formatted: String?, raw: String?) = Unit + fun onDigitAppended(digit: String) = Unit + } + + companion object { + const val EXTRA_REGION_CODE = "EXTRA_REGION_CODE" + const val EXTRA_FORMAT_AS_YOU_TYPE = "EXTRA_FORMAT_AS_YOU_TYPE" + const val EXTRA_ENABLE_STAR = "EXTRA_ENABLE_STAR" + const val EXTRA_ENABLE_POUND = "EXTRA_ENABLE_POUND" + const val EXTRA_ENABLE_PLUS = "EXTRA_ENABLE_PLUS" + const val EXTRA_ENABLE_DELETE = "EXTRA_ENABLE_DELETE" + const val EXTRA_ENABLE_OK = "EXTRA_ENABLE_OK" + const val EXTRA_CURSOR_VISIBLE = "EXTRA_CURSOR_VISIBLE" + + private const val DEFAULT_REGION_CODE = "US" + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt new file mode 100644 index 0000000000..1c5caee2cd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 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.call.dialpad + +import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.features.createdirect.DirectRoomHelper +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.Session +import javax.inject.Inject + +class DialPadLookup @Inject constructor(val session: Session, + val directRoomHelper: DirectRoomHelper, + val callManager: WebRtcCallManager +) { + + class Failure : Throwable() + data class Result(val userId: String, val roomId: String) + + suspend fun lookupPhoneNumber(phoneNumber: String): Result { + val supportedProtocolKey = callManager.supportedPSTNProtocol ?: throw Failure() + val thirdPartyUser = tryOrNull { + session.thirdPartyService().getThirdPartyUser(supportedProtocolKey, fields = mapOf( + "m.id.phone" to phoneNumber + )).firstOrNull() + } ?: throw Failure() + + val roomId = directRoomHelper.ensureDMExists(thirdPartyUser.userId) + return Result(userId = thirdPartyUser.userId, roomId = roomId) + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt b/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt index 04e7401e6c..5a1d8cd396 100644 --- a/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt @@ -20,24 +20,28 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import im.vector.app.core.di.HasVectorInjector -import im.vector.app.features.call.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import timber.log.Timber class CallHeadsUpActionReceiver : BroadcastReceiver() { companion object { const val EXTRA_CALL_ACTION_KEY = "EXTRA_CALL_ACTION_KEY" + const val EXTRA_CALL_ID = "EXTRA_CALL_ID" const val CALL_ACTION_REJECT = 0 } override fun onReceive(context: Context, intent: Intent?) { - val peerConnectionManager = (context.applicationContext as? HasVectorInjector) + val webRtcCallManager = (context.applicationContext as? HasVectorInjector) ?.injector() - ?.webRtcPeerConnectionManager() + ?.webRtcCallManager() ?: return when (intent?.getIntExtra(EXTRA_CALL_ACTION_KEY, 0)) { - CALL_ACTION_REJECT -> onCallRejectClicked(peerConnectionManager) + CALL_ACTION_REJECT -> { + val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return + onCallRejectClicked(webRtcCallManager, callId) + } } // Not sure why this should be needed @@ -48,9 +52,9 @@ class CallHeadsUpActionReceiver : BroadcastReceiver() { // context.stopService(Intent(context, CallHeadsUpService::class.java)) } - private fun onCallRejectClicked(peerConnectionManager: WebRtcPeerConnectionManager) { + private fun onCallRejectClicked(callManager: WebRtcCallManager, callId: String) { Timber.d("onCallRejectClicked") - peerConnectionManager.endCall() + callManager.getCallById(callId)?.endCall() } // private fun onCallAnswerClicked(context: Context) { diff --git a/vector/src/main/java/im/vector/app/features/call/telecom/CallConnection.kt b/vector/src/main/java/im/vector/app/features/call/telecom/CallConnection.kt index 0a9a164993..b298eebb23 100644 --- a/vector/src/main/java/im/vector/app/features/call/telecom/CallConnection.kt +++ b/vector/src/main/java/im/vector/app/features/call/telecom/CallConnection.kt @@ -21,7 +21,7 @@ import android.os.Build import android.telecom.Connection import android.telecom.DisconnectCause import androidx.annotation.RequiresApi -import im.vector.app.features.call.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import timber.log.Timber import javax.inject.Inject @@ -31,7 +31,7 @@ import javax.inject.Inject val callId: String ) : Connection() { - @Inject lateinit var peerConnectionManager: WebRtcPeerConnectionManager + @Inject lateinit var callManager: WebRtcCallManager init { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/vector/src/main/java/im/vector/app/features/call/telecom/CallConnectionService.java b/vector/src/main/java/im/vector/app/features/call/telecom/CallConnectionService.java new file mode 100644 index 0000000000..f3fbfc9ac4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/telecom/CallConnectionService.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 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.call.telecom; + +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import org.jitsi.meet.sdk.ConnectionService; + +@RequiresApi(api = Build.VERSION_CODES.O) +public class CallConnectionService extends ConnectionService { +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/call/telecom/VectorConnectionService.kt b/vector/src/main/java/im/vector/app/features/call/telecom/VectorConnectionService.kt index 410a4621e8..e289537177 100644 --- a/vector/src/main/java/im/vector/app/features/call/telecom/VectorConnectionService.kt +++ b/vector/src/main/java/im/vector/app/features/call/telecom/VectorConnectionService.kt @@ -71,7 +71,7 @@ import im.vector.app.core.services.CallService bindService(Intent(applicationContext, CallService::class.java), CallServiceConnection(connection), 0) connection.setInitializing() - return CallConnection(applicationContext, roomId, callId) + return connection } inner class CallServiceConnection(private val callConnection: CallConnection) : ServiceConnection { diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferAction.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferAction.kt new file mode 100644 index 0000000000..bd694ad14e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferAction.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 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.call.transfer + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class CallTransferAction : VectorViewModelAction { + data class ConnectWithUserId(val consultFirst: Boolean, val selectedUserId: String) : CallTransferAction() + data class ConnectWithPhoneNumber(val consultFirst: Boolean, val phoneNumber: String) : CallTransferAction() +} diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt new file mode 100644 index 0000000000..c5b4dda135 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2020 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.call.transfer + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.viewModel +import com.google.android.material.tabs.TabLayoutMediator +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivityCallTransferBinding +import im.vector.app.features.contactsbook.ContactsBookViewModel +import im.vector.app.features.contactsbook.ContactsBookViewState +import im.vector.app.features.userdirectory.UserListViewModel +import im.vector.app.features.userdirectory.UserListViewState +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +@Parcelize +data class CallTransferArgs(val callId: String) : Parcelable + +private const val USER_LIST_FRAGMENT_TAG = "USER_LIST_FRAGMENT_TAG" + +class CallTransferActivity : VectorBaseActivity<ActivityCallTransferBinding>(), + CallTransferViewModel.Factory, + UserListViewModel.Factory, + ContactsBookViewModel.Factory { + + @Inject lateinit var userListViewModelFactory: UserListViewModel.Factory + @Inject lateinit var callTransferViewModelFactory: CallTransferViewModel.Factory + @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory + @Inject lateinit var errorFormatter: ErrorFormatter + + private lateinit var sectionsPagerAdapter: CallTransferPagerAdapter + + private val callTransferViewModel: CallTransferViewModel by viewModel() + + override fun getBinding() = ActivityCallTransferBinding.inflate(layoutInflater) + + override fun getCoordinatorLayout() = views.vectorCoordinatorLayout + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + + override fun create(initialState: UserListViewState): UserListViewModel { + return userListViewModelFactory.create(initialState) + } + + override fun create(initialState: CallTransferViewState): CallTransferViewModel { + return callTransferViewModelFactory.create(initialState) + } + + override fun create(initialState: ContactsBookViewState): ContactsBookViewModel { + return contactsBookViewModelFactory.create(initialState) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + waitingView = views.waitingView.waitingView + + callTransferViewModel.observeViewEvents { + when (it) { + is CallTransferViewEvents.Dismiss -> finish() + CallTransferViewEvents.Loading -> showWaitingView() + is CallTransferViewEvents.FailToTransfer -> showSnackbar(getString(R.string.call_transfer_failure)) + } + } + + sectionsPagerAdapter = CallTransferPagerAdapter(this).register() + views.callTransferViewPager.adapter = sectionsPagerAdapter + sectionsPagerAdapter.onDialPadOkClicked = { phoneNumber -> + val action = CallTransferAction.ConnectWithPhoneNumber(views.callTransferConsultCheckBox.isChecked, phoneNumber) + callTransferViewModel.handle(action) + } + + TabLayoutMediator(views.callTransferTabLayout, views.callTransferViewPager) { tab, position -> + when (position) { + 0 -> tab.text = getString(R.string.call_transfer_users_tab_title) + 1 -> tab.text = getString(R.string.call_dial_pad_title) + } + }.attach() + configureToolbar(views.callTransferToolbar) + views.callTransferToolbar.title = getString(R.string.call_transfer_title) + setupConnectAction() + } + + private fun setupConnectAction() { + views.callTransferConnectAction.debouncedClicks { + val selectedUser = sectionsPagerAdapter.userListFragment?.getCurrentState()?.getSelectedMatrixId()?.firstOrNull() + if (selectedUser != null) { + val action = CallTransferAction.ConnectWithUserId(views.callTransferConsultCheckBox.isChecked, selectedUser) + callTransferViewModel.handle(action) + } + } + } + + companion object { + + fun newIntent(context: Context, callId: String): Intent { + return Intent(context, CallTransferActivity::class.java).also { + it.putExtra(MvRx.KEY_ARG, CallTransferArgs(callId)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferPagerAdapter.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferPagerAdapter.kt new file mode 100644 index 0000000000..1f2d3070dd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferPagerAdapter.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 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.call.transfer + +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import im.vector.app.core.extensions.toMvRxBundle +import im.vector.app.core.platform.Restorable +import im.vector.app.features.call.dialpad.DialPadFragment +import im.vector.app.features.settings.VectorLocale +import im.vector.app.features.userdirectory.UserListFragment +import im.vector.app.features.userdirectory.UserListFragmentArgs + +class CallTransferPagerAdapter( + private val fragmentActivity: FragmentActivity +) : FragmentStateAdapter(fragmentActivity), Restorable { + + val userListFragment: UserListFragment? + get() = findFragmentAtPosition(0) as? UserListFragment + val dialPadFragment: DialPadFragment? + get() = findFragmentAtPosition(1) as? DialPadFragment + + var onDialPadOkClicked: ((String) -> Unit)? = null + + override fun getItemCount() = 2 + + override fun createFragment(position: Int): Fragment { + val fragment: Fragment + if (position == 0) { + fragment = fragmentActivity.supportFragmentManager.fragmentFactory.instantiate(fragmentActivity.classLoader, UserListFragment::class.java.name) + fragment.arguments = UserListFragmentArgs( + title = "", + menuResId = -1, + singleSelection = true, + showInviteActions = false, + showToolbar = false, + showContactBookAction = false + ).toMvRxBundle() + } else { + fragment = fragmentActivity.supportFragmentManager.fragmentFactory.instantiate(fragmentActivity.classLoader, DialPadFragment::class.java.name) + (fragment as DialPadFragment).apply { + arguments = Bundle().apply { + putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true) + putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true) + putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country) + } + applyCallback() + } + } + return fragment + } + + private fun findFragmentAtPosition(position: Int): Fragment? { + return fragmentActivity.supportFragmentManager.findFragmentByTag("f$position") + } + + override fun onSaveInstanceState(outState: Bundle) = Unit + + override fun onRestoreInstanceState(savedInstanceState: Bundle?) { + dialPadFragment?.applyCallback() + } + + private fun DialPadFragment.applyCallback(): DialPadFragment { + callback = object : DialPadFragment.Callback { + override fun onOkClicked(formatted: String?, raw: String?) { + if (raw.isNullOrEmpty()) return + onDialPadOkClicked?.invoke(raw) + } + } + return this + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewEvents.kt new file mode 100644 index 0000000000..b110164d1e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewEvents.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 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.call.transfer + +import im.vector.app.core.platform.VectorViewEvents + +sealed class CallTransferViewEvents : VectorViewEvents { + object Dismiss : CallTransferViewEvents() + object Loading: CallTransferViewEvents() + object FailToTransfer : CallTransferViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt new file mode 100644 index 0000000000..5f661faf80 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2020 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.call.transfer + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.call.dialpad.DialPadLookup +import im.vector.app.features.call.webrtc.WebRtcCall +import im.vector.app.features.call.webrtc.WebRtcCallManager +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.call.CallState +import org.matrix.android.sdk.api.session.call.MxCall + +class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState, + private val dialPadLookup: DialPadLookup, + callManager: WebRtcCallManager) + : VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) { + + @AssistedFactory + interface Factory { + fun create(initialState: CallTransferViewState): CallTransferViewModel + } + + companion object : MvRxViewModelFactory<CallTransferViewModel, CallTransferViewState> { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: CallTransferViewState): CallTransferViewModel? { + val activity: CallTransferActivity = (viewModelContext as ActivityViewModelContext).activity() + return activity.callTransferViewModelFactory.create(state) + } + } + + private val call = callManager.getCallById(initialState.callId) + private val callListener = object : WebRtcCall.Listener { + override fun onStateUpdate(call: MxCall) { + if (call.state == CallState.Terminated) { + _viewEvents.post(CallTransferViewEvents.Dismiss) + } + } + } + + init { + if (call == null) { + _viewEvents.post(CallTransferViewEvents.Dismiss) + } else { + call.addListener(callListener) + } + } + + override fun onCleared() { + super.onCleared() + call?.removeListener(callListener) + } + + override fun handle(action: CallTransferAction) { + when (action) { + is CallTransferAction.ConnectWithUserId -> connectWithUserId(action) + is CallTransferAction.ConnectWithPhoneNumber -> connectWithPhoneNumber(action) + }.exhaustive + } + + private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) { + viewModelScope.launch { + try { + _viewEvents.post(CallTransferViewEvents.Loading) + call?.mxCall?.transfer(action.selectedUserId, null) + _viewEvents.post(CallTransferViewEvents.Dismiss) + } catch (failure: Throwable) { + _viewEvents.post(CallTransferViewEvents.FailToTransfer) + } + } + } + + private fun connectWithPhoneNumber(action: CallTransferAction.ConnectWithPhoneNumber) { + viewModelScope.launch { + try { + _viewEvents.post(CallTransferViewEvents.Loading) + val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber) + call?.mxCall?.transfer(result.userId, result.roomId) + _viewEvents.post(CallTransferViewEvents.Dismiss) + } catch (failure: Throwable) { + _viewEvents.post(CallTransferViewEvents.FailToTransfer) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewState.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewState.kt new file mode 100644 index 0000000000..2b29d9f6f2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 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.call.transfer + +import com.airbnb.mvrx.MvRxState + +data class CallTransferViewState( + val callId: String +) : MvRxState { + + constructor(args: CallTransferArgs) : this(callId = args.callId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/EglUtils.kt b/vector/src/main/java/im/vector/app/features/call/utils/EglUtils.kt similarity index 94% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/EglUtils.kt rename to vector/src/main/java/im/vector/app/features/call/utils/EglUtils.kt index 131779a4dc..045124a900 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/EglUtils.kt +++ b/vector/src/main/java/im/vector/app/features/call/utils/EglUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2020 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.matrix.android.sdk.api.session.call +package im.vector.app.features.call.utils import org.webrtc.EglBase import timber.log.Timber diff --git a/vector/src/main/java/im/vector/app/features/call/utils/PeerConnectionExt.kt b/vector/src/main/java/im/vector/app/features/call/utils/PeerConnectionExt.kt new file mode 100644 index 0000000000..978b984dce --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/utils/PeerConnectionExt.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020 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.call.utils + +import im.vector.app.features.call.webrtc.SdpObserverAdapter +import org.webrtc.MediaConstraints +import org.webrtc.PeerConnection +import org.webrtc.SessionDescription +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +suspend fun PeerConnection.awaitCreateOffer(mediaConstraints: MediaConstraints): SessionDescription? = suspendCoroutine { cont -> + createOffer(object : SdpObserverAdapter() { + override fun onCreateSuccess(p0: SessionDescription?) { + super.onCreateSuccess(p0) + cont.resume(p0) + } + + override fun onCreateFailure(p0: String?) { + super.onCreateFailure(p0) + cont.resumeWithException(IllegalStateException(p0)) + } + }, mediaConstraints) +} + +suspend fun PeerConnection.awaitCreateAnswer(mediaConstraints: MediaConstraints): SessionDescription? = suspendCoroutine { cont -> + createAnswer(object : SdpObserverAdapter() { + override fun onCreateSuccess(p0: SessionDescription?) { + super.onCreateSuccess(p0) + cont.resume(p0) + } + + override fun onCreateFailure(p0: String?) { + super.onCreateFailure(p0) + cont.resumeWithException(IllegalStateException(p0)) + } + }, mediaConstraints) +} + +suspend fun PeerConnection.awaitSetLocalDescription(sessionDescription: SessionDescription): Unit = suspendCoroutine { cont -> + setLocalDescription(object : SdpObserverAdapter() { + override fun onSetFailure(p0: String?) { + super.onSetFailure(p0) + cont.resumeWithException(IllegalStateException(p0)) + } + + override fun onSetSuccess() { + super.onSetSuccess() + cont.resume(Unit) + } + }, sessionDescription) +} + +suspend fun PeerConnection.awaitSetRemoteDescription(sessionDescription: SessionDescription): Unit = suspendCoroutine { cont -> + setRemoteDescription(object : SdpObserverAdapter() { + override fun onSetFailure(p0: String?) { + super.onSetFailure(p0) + cont.resumeWithException(IllegalStateException(p0)) + } + + override fun onSetSuccess() { + super.onSetSuccess() + cont.resume(Unit) + } + }, sessionDescription) +} diff --git a/vector/src/main/java/im/vector/app/features/call/utils/WebRtcMapping.kt b/vector/src/main/java/im/vector/app/features/call/utils/WebRtcMapping.kt new file mode 100644 index 0000000000..2b0d5281d2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/utils/WebRtcMapping.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 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.call.utils + +import org.matrix.android.sdk.api.session.room.model.call.CallCandidate +import org.matrix.android.sdk.api.session.room.model.call.SdpType +import org.webrtc.IceCandidate +import org.webrtc.SessionDescription + +fun List<IceCandidate>.mapToCallCandidate() = map { + CallCandidate( + sdpMid = it.sdpMid, + sdpMLineIndex = it.sdpMLineIndex, + candidate = it.sdp + ) +} + +fun SdpType.asWebRTC(): SessionDescription.Type { + return if (this == SdpType.OFFER) { + SessionDescription.Type.OFFER + } else { + SessionDescription.Type.ANSWER + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/PSTNProtocol.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/PSTNProtocol.kt new file mode 100644 index 0000000000..3e6d2df690 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/PSTNProtocol.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 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.call.webrtc + +import kotlinx.coroutines.delay +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol + +private const val PSTN_VECTOR_KEY = "im.vector.protocol.pstn" +private const val PSTN_MATRIX_KEY = "m.protocol.pstn" + +suspend fun Session.getSupportedPSTN(maxTries: Int): String? { + val thirdPartyProtocols: Map<String, ThirdPartyProtocol> = try { + thirdPartyService().getThirdPartyProtocols() + } catch (failure: Throwable) { + if (maxTries == 1) { + return null + } else { + // Wait for 10s before trying again + delay(10_000L) + return getSupportedPSTN(maxTries - 1) + } + } + return when { + thirdPartyProtocols.containsKey(PSTN_VECTOR_KEY) -> PSTN_VECTOR_KEY + thirdPartyProtocols.containsKey(PSTN_MATRIX_KEY) -> PSTN_MATRIX_KEY + else -> null + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt new file mode 100644 index 0000000000..f14bb2f849 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2020 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.call.webrtc + +import org.matrix.android.sdk.api.session.call.CallState +import org.matrix.android.sdk.api.session.call.MxPeerConnectionState +import org.webrtc.DataChannel +import org.webrtc.IceCandidate +import org.webrtc.MediaStream +import org.webrtc.PeerConnection +import org.webrtc.RtpReceiver +import timber.log.Timber + +class PeerConnectionObserver(private val webRtcCall: WebRtcCall) : PeerConnection.Observer { + + override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) { + Timber.v("## VOIP StreamObserver onConnectionChange: $newState") + when (newState) { + /** + * Every ICE transport used by the connection is either in use (state "connected" or "completed") + * or is closed (state "closed"); in addition, at least one transport is either "connected" or "completed" + */ + PeerConnection.PeerConnectionState.CONNECTED -> { + webRtcCall.mxCall.state = CallState.Connected(MxPeerConnectionState.CONNECTED) + } + /** + * One or more of the ICE transports on the connection is in the "failed" state. + */ + PeerConnection.PeerConnectionState.FAILED -> { + // This can be temporary, e.g when other ice not yet received... + // webRtcCall.mxCall.state = CallState.ERROR + webRtcCall.mxCall.state = CallState.Connected(MxPeerConnectionState.FAILED) + } + /** + * At least one of the connection's ICE transports (RTCIceTransports or RTCDtlsTransports) are in the "new" state, + * and none of them are in one of the following states: "connecting", "checking", "failed", or "disconnected", + * or all of the connection's transports are in the "closed" state. + */ + PeerConnection.PeerConnectionState.NEW, + /** + * One or more of the ICE transports are currently in the process of establishing a connection; + * that is, their RTCIceConnectionState is either "checking" or "connected", and no transports are in the "failed" state + */ + PeerConnection.PeerConnectionState.CONNECTING -> { + webRtcCall.mxCall.state = CallState.Connected(MxPeerConnectionState.CONNECTING) + } + /** + * The RTCPeerConnection is closed. + * This value was in the RTCSignalingState enum (and therefore found by reading the value of the signalingState) + * property until the May 13, 2016 draft of the specification. + */ + PeerConnection.PeerConnectionState.CLOSED -> { + webRtcCall.mxCall.state = CallState.Connected(MxPeerConnectionState.CLOSED) + } + /** + * At least one of the ICE transports for the connection is in the "disconnected" state and none of + * the other transports are in the state "failed", "connecting", or "checking". + */ + PeerConnection.PeerConnectionState.DISCONNECTED -> { + webRtcCall.mxCall.state = CallState.Connected(MxPeerConnectionState.DISCONNECTED) + } + null -> { + } + } + } + + override fun onIceCandidate(iceCandidate: IceCandidate) { + Timber.v("## VOIP StreamObserver onIceCandidate: $iceCandidate") + webRtcCall.onIceCandidate(iceCandidate) + } + + override fun onDataChannel(dc: DataChannel) { + Timber.v("## VOIP StreamObserver onDataChannel: ${dc.state()}") + } + + override fun onIceConnectionReceivingChange(receiving: Boolean) { + Timber.v("## VOIP StreamObserver onIceConnectionReceivingChange: $receiving") + } + + override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState) { + Timber.v("## VOIP StreamObserver onIceConnectionChange IceConnectionState:$newState") + when (newState) { + /** + * the ICE agent is gathering addresses or is waiting to be given remote candidates through + * calls to RTCPeerConnection.addIceCandidate() (or both). + */ + PeerConnection.IceConnectionState.NEW -> { + } + /** + * The ICE agent has been given one or more remote candidates and is checking pairs of local and remote candidates + * against one another to try to find a compatible match, but has not yet found a pair which will allow + * the peer connection to be made. It's possible that gathering of candidates is also still underway. + */ + PeerConnection.IceConnectionState.CHECKING -> { + } + + /** + * A usable pairing of local and remote candidates has been found for all components of the connection, + * and the connection has been established. + * It's possible that gathering is still underway, and it's also possible that the ICE agent is still checking + * candidates against one another looking for a better connection to use. + */ + PeerConnection.IceConnectionState.CONNECTED -> { + } + /** + * Checks to ensure that components are still connected failed for at least one component of the RTCPeerConnection. + * This is a less stringent test than "failed" and may trigger intermittently and resolve just as spontaneously on less reliable networks, + * or during temporary disconnections. When the problem resolves, the connection may return to the "connected" state. + */ + PeerConnection.IceConnectionState.DISCONNECTED -> { + } + /** + * The ICE candidate has checked all candidates pairs against one another and has failed to find + * compatible matches for all components of the connection. + * It is, however, possible that the ICE agent did find compatible connections for some components. + */ + PeerConnection.IceConnectionState.FAILED -> { + webRtcCall.onRenegotiationNeeded(restartIce = true) + } + /** + * The ICE agent has finished gathering candidates, has checked all pairs against one another, and has found a connection for all components. + */ + PeerConnection.IceConnectionState.COMPLETED -> { + } + /** + * The ICE agent for this RTCPeerConnection has shut down and is no longer handling requests. + */ + PeerConnection.IceConnectionState.CLOSED -> { + } + } + } + + override fun onAddStream(stream: MediaStream) { + Timber.v("## VOIP StreamObserver onAddStream: $stream") + webRtcCall.onAddStream(stream) + } + + override fun onRemoveStream(stream: MediaStream) { + Timber.v("## VOIP StreamObserver onRemoveStream") + webRtcCall.onRemoveStream() + } + + override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState) { + Timber.v("## VOIP StreamObserver onIceGatheringChange: $newState") + } + + override fun onSignalingChange(newState: PeerConnection.SignalingState) { + Timber.v("## VOIP StreamObserver onSignalingChange: $newState") + } + + override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>) { + Timber.v("## VOIP StreamObserver onIceCandidatesRemoved: ${candidates.contentToString()}") + } + + override fun onRenegotiationNeeded() { + Timber.v("## VOIP StreamObserver onRenegotiationNeeded") + webRtcCall.onRenegotiationNeeded(restartIce = false) + } + + /** + * This happens when a new track of any kind is added to the media stream. + * This event is fired when the browser adds a track to the stream + * (such as when a RTCPeerConnection is renegotiated or a stream being captured using HTMLMediaElement.captureStream() + * gets a new set of tracks because the media element being captured loaded a new source. + */ + override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) { + Timber.v("## VOIP StreamObserver onAddTrack") + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/PeerConnectionObserverAdapter.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserverAdapter.kt similarity index 98% rename from vector/src/main/java/im/vector/app/features/call/PeerConnectionObserverAdapter.kt rename to vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserverAdapter.kt index 32e30c5345..3d31f0e705 100644 --- a/vector/src/main/java/im/vector/app/features/call/PeerConnectionObserverAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserverAdapter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.call +package im.vector.app.features.call.webrtc import org.webrtc.DataChannel import org.webrtc.IceCandidate diff --git a/vector/src/main/java/im/vector/app/features/call/SdpObserverAdapter.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/SdpObserverAdapter.kt similarity index 87% rename from vector/src/main/java/im/vector/app/features/call/SdpObserverAdapter.kt rename to vector/src/main/java/im/vector/app/features/call/webrtc/SdpObserverAdapter.kt index 0685928d1c..24d0e7b1f8 100644 --- a/vector/src/main/java/im/vector/app/features/call/SdpObserverAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/SdpObserverAdapter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.app.features.call +package im.vector.app.features.call.webrtc import org.webrtc.SdpObserver import org.webrtc.SessionDescription @@ -30,10 +30,10 @@ open class SdpObserverAdapter : SdpObserver { } override fun onCreateSuccess(p0: SessionDescription?) { - Timber.e("## SdpObserver: onSetFailure $p0") + Timber.v("## SdpObserver: onCreateSuccess $p0") } override fun onCreateFailure(p0: String?) { - Timber.e("## SdpObserver: onSetFailure $p0") + Timber.e("## SdpObserver: onCreateFailure $p0") } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt new file mode 100644 index 0000000000..c72d7c8a76 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -0,0 +1,876 @@ +/* + * Copyright (c) 2020 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.call.webrtc + +import android.content.Context +import android.hardware.camera2.CameraManager +import androidx.core.content.getSystemService +import im.vector.app.core.services.CallService +import im.vector.app.core.utils.CountUpTimer +import im.vector.app.features.call.CameraEventsHandlerAdapter +import im.vector.app.features.call.CameraProxy +import im.vector.app.features.call.CameraType +import im.vector.app.features.call.CaptureFormat +import im.vector.app.features.call.VectorCallActivity +import im.vector.app.features.call.utils.asWebRTC +import im.vector.app.features.call.utils.awaitCreateAnswer +import im.vector.app.features.call.utils.awaitCreateOffer +import im.vector.app.features.call.utils.awaitSetLocalDescription +import im.vector.app.features.call.utils.awaitSetRemoteDescription +import im.vector.app.features.call.utils.mapToCallCandidate +import io.reactivex.disposables.Disposable +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.ReplaySubject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.call.CallState +import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.api.session.call.MxPeerConnectionState +import org.matrix.android.sdk.api.session.call.TurnServerResponse +import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent +import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent +import org.matrix.android.sdk.api.session.room.model.call.SdpType +import org.matrix.android.sdk.internal.util.awaitCallback +import org.threeten.bp.Duration +import org.webrtc.AudioSource +import org.webrtc.AudioTrack +import org.webrtc.Camera1Enumerator +import org.webrtc.Camera2Enumerator +import org.webrtc.CameraVideoCapturer +import org.webrtc.EglBase +import org.webrtc.IceCandidate +import org.webrtc.MediaConstraints +import org.webrtc.MediaStream +import org.webrtc.PeerConnection +import org.webrtc.PeerConnectionFactory +import org.webrtc.RtpTransceiver +import org.webrtc.SessionDescription +import org.webrtc.SurfaceTextureHelper +import org.webrtc.SurfaceViewRenderer +import org.webrtc.VideoSource +import org.webrtc.VideoTrack +import timber.log.Timber +import java.lang.ref.WeakReference +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.TimeUnit +import javax.inject.Provider +import kotlin.coroutines.CoroutineContext + +private const val STREAM_ID = "ARDAMS" +private const val AUDIO_TRACK_ID = "ARDAMSa0" +private const val VIDEO_TRACK_ID = "ARDAMSv0" +private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints() + +class WebRtcCall(val mxCall: MxCall, + private val rootEglBase: EglBase?, + private val context: Context, + private val dispatcher: CoroutineContext, + private val sessionProvider: Provider<Session?>, + private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>, + private val onCallBecomeActive: (WebRtcCall) -> Unit, + private val onCallEnded: (String) -> Unit) : MxCall.StateListener { + + interface Listener : MxCall.StateListener { + fun onCaptureStateChanged() {} + fun onCameraChanged() {} + fun onHoldUnhold() {} + fun onTick(formattedDuration: String) {} + override fun onStateUpdate(call: MxCall) {} + } + + private val listeners = CopyOnWriteArrayList<Listener>() + + fun addListener(listener: Listener) { + listeners.add(listener) + } + + fun removeListener(listener: Listener) { + listeners.remove(listener) + } + + val callId = mxCall.callId + val roomId = mxCall.roomId + + private var peerConnection: PeerConnection? = null + private var localAudioSource: AudioSource? = null + private var localAudioTrack: AudioTrack? = null + private var localVideoSource: VideoSource? = null + private var localVideoTrack: VideoTrack? = null + private var remoteAudioTrack: AudioTrack? = null + private var remoteVideoTrack: VideoTrack? = null + + // Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example + private var makingOffer: Boolean = false + private var ignoreOffer: Boolean = false + + private var videoCapturer: CameraVideoCapturer? = null + + private val availableCamera = ArrayList<CameraProxy>() + private var cameraInUse: CameraProxy? = null + private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD + private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null + + private val timer = CountUpTimer(Duration.ofSeconds(1).toMillis()).apply { + tickListener = object : CountUpTimer.TickListener { + override fun onTick(milliseconds: Long) { + val formattedDuration = formatDuration(Duration.ofMillis(milliseconds)) + listeners.forEach { + tryOrNull { it.onTick(formattedDuration) } + } + } + } + } + + // Mute status + var micMuted = false + private set + var videoMuted = false + private set + var remoteOnHold = false + private set + var isLocalOnHold = false + private set + + // This value is used to track localOnHold when changing remoteOnHold value + private var wasLocalOnHold = false + + var offerSdp: CallInviteContent.Offer? = null + + var videoCapturerIsInError = false + set(value) { + field = value + listeners.forEach { + tryOrNull { it.onCaptureStateChanged() } + } + } + private var localSurfaceRenderers: MutableList<WeakReference<SurfaceViewRenderer>> = ArrayList() + private var remoteSurfaceRenderers: MutableList<WeakReference<SurfaceViewRenderer>> = ArrayList() + + private val iceCandidateSource: PublishSubject<IceCandidate> = PublishSubject.create() + private val iceCandidateDisposable = iceCandidateSource + .buffer(300, TimeUnit.MILLISECONDS) + .subscribe { + // omit empty :/ + if (it.isNotEmpty()) { + Timber.v("## Sending local ice candidates to call") + // it.forEach { peerConnection?.addIceCandidate(it) } + mxCall.sendLocalCallCandidates(it.mapToCallCandidate()) + } + } + + private val remoteCandidateSource: ReplaySubject<IceCandidate> = ReplaySubject.create() + private var remoteIceCandidateDisposable: Disposable? = null + + init { + mxCall.addListener(this) + } + + fun onIceCandidate(iceCandidate: IceCandidate) = iceCandidateSource.onNext(iceCandidate) + + fun onRenegotiationNeeded(restartIce: Boolean) { + GlobalScope.launch(dispatcher) { + if (mxCall.state != CallState.CreateOffer && mxCall.opponentVersion == 0) { + Timber.v("Opponent does not support renegotiation: ignoring onRenegotiationNeeded event") + return@launch + } + val constraints = MediaConstraints() + if (restartIce) { + constraints.mandatory.add(MediaConstraints.KeyValuePair("IceRestart", "true")) + } + val peerConnection = peerConnection ?: return@launch + Timber.v("## VOIP creating offer...") + makingOffer = true + try { + val sessionDescription = peerConnection.awaitCreateOffer(constraints) ?: return@launch + peerConnection.awaitSetLocalDescription(sessionDescription) + if (peerConnection.iceGatheringState() == PeerConnection.IceGatheringState.GATHERING) { + // Allow a short time for initial candidates to be gathered + delay(200) + } + if (mxCall.state == CallState.Terminated) { + return@launch + } + if (mxCall.state == CallState.CreateOffer) { + // send offer to peer + mxCall.offerSdp(sessionDescription.description) + } else { + mxCall.negotiate(sessionDescription.description, SdpType.OFFER) + } + } catch (failure: Throwable) { + // Need to handle error properly. + Timber.v("Failure while creating offer") + } finally { + makingOffer = false + } + } + } + + fun formattedDuration(): String { + return formatDuration( + Duration.ofMillis(timer.elapsedTime()) + ) + } + + private fun createPeerConnection(turnServerResponse: TurnServerResponse?) { + val peerConnectionFactory = peerConnectionFactoryProvider.get() ?: return + val iceServers = mutableListOf<PeerConnection.IceServer>().apply { + turnServerResponse?.let { server -> + server.uris?.forEach { uri -> + add( + PeerConnection + .IceServer + .builder(uri) + .setUsername(server.username) + .setPassword(server.password) + .createIceServer() + ) + } + } + } + Timber.v("## VOIP creating peer connection...with iceServers $iceServers ") + val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply { + sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN + } + peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, PeerConnectionObserver(this)) + } + + fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) { + Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer") + localSurfaceRenderers.addIfNeeded(localViewRenderer) + remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer) + + GlobalScope.launch(dispatcher) { + when (mode) { + VectorCallActivity.INCOMING_ACCEPT -> { + internalAcceptIncomingCall() + } + VectorCallActivity.INCOMING_RINGING -> { + // wait until accepted to create peer connection + // TODO eventually we could already display local stream in PIP? + } + VectorCallActivity.OUTGOING_CREATED -> { + setupOutgoingCall() + } + else -> { + // sink existing tracks (configuration change, e.g screen rotation) + attachViewRenderersInternal() + } + } + } + } + + fun acceptIncomingCall() { + GlobalScope.launch { + Timber.v("## VOIP acceptIncomingCall from state ${mxCall.state}") + if (mxCall.state == CallState.LocalRinging) { + internalAcceptIncomingCall() + } + } + } + + /** + * Sends a DTMF digit to the other party + * @param digit The digit (nb. string - '#' and '*' are dtmf too) + */ + fun sendDtmfDigit(digit: String) { + for (sender in peerConnection?.senders.orEmpty()) { + if (sender.track()?.kind() == "audio" && sender.dtmf()?.canInsertDtmf() == true) { + try { + sender.dtmf()?.insertDtmf(digit, 100, 70) + return + } catch (failure: Throwable) { + Timber.v("Fail to send Dtmf digit") + } + } + } + } + + fun detachRenderers(renderers: List<SurfaceViewRenderer>?) { + Timber.v("## VOIP detachRenderers") + if (renderers.isNullOrEmpty()) { + // remove all sinks + localSurfaceRenderers.forEach { + if (it.get() != null) localVideoTrack?.removeSink(it.get()) + } + remoteSurfaceRenderers.forEach { + if (it.get() != null) remoteVideoTrack?.removeSink(it.get()) + } + localSurfaceRenderers.clear() + remoteSurfaceRenderers.clear() + } else { + renderers.forEach { + localSurfaceRenderers.removeIfNeeded(it) + remoteSurfaceRenderers.removeIfNeeded(it) + // no need to check if it's in the track, removeSink is doing it + localVideoTrack?.removeSink(it) + remoteVideoTrack?.removeSink(it) + } + } + } + + private suspend fun setupOutgoingCall() = withContext(dispatcher) { + tryOrNull { + onCallBecomeActive(this@WebRtcCall) + } + val turnServer = getTurnServer() + mxCall.state = CallState.CreateOffer + // 1. Create RTCPeerConnection + createPeerConnection(turnServer) + // 2. Access camera (if video call) + microphone, create local stream + createLocalStream() + attachViewRenderersInternal() + Timber.v("## VOIP remoteCandidateSource $remoteCandidateSource") + remoteIceCandidateDisposable = remoteCandidateSource.subscribe({ + Timber.v("## VOIP adding remote ice candidate $it") + peerConnection?.addIceCandidate(it) + }, { + Timber.v("## VOIP failed to add remote ice candidate $it") + }) + // Now we wait for negotiation callback + } + + private suspend fun internalAcceptIncomingCall() = withContext(dispatcher) { + tryOrNull { + onCallBecomeActive(this@WebRtcCall) + } + val turnServerResponse = getTurnServer() + // Update service state + withContext(Dispatchers.Main) { + CallService.onPendingCall( + context = context, + callId = mxCall.callId + ) + } + // 1) create peer connection + createPeerConnection(turnServerResponse) + + // create sdp using offer, and set remote description + // the offer has beed stored when invite was received + val offerSdp = offerSdp?.sdp?.let { + SessionDescription(SessionDescription.Type.OFFER, it) + } + if (offerSdp == null) { + Timber.v("We don't have any offer to process") + return@withContext + } + Timber.v("Offer sdp for invite: ${offerSdp.description}") + try { + peerConnection?.awaitSetRemoteDescription(offerSdp) + } catch (failure: Throwable) { + Timber.v("Failure putting remote description") + return@withContext + } + // 2) Access camera + microphone, create local stream + createLocalStream() + attachViewRenderersInternal() + + // create a answer, set local description and send via signaling + createAnswer()?.also { + mxCall.accept(it.description) + } + Timber.v("## VOIP remoteCandidateSource $remoteCandidateSource") + remoteIceCandidateDisposable = remoteCandidateSource.subscribe({ + Timber.v("## VOIP adding remote ice candidate $it") + peerConnection?.addIceCandidate(it) + }, { + Timber.v("## VOIP failed to add remote ice candidate $it") + }) + } + + private fun attachViewRenderersInternal() { + // render local video in pip view + localSurfaceRenderers.forEach { renderer -> + renderer.get()?.let { pipSurface -> + pipSurface.setMirror(this.cameraInUse?.type == CameraType.FRONT) + // no need to check if already added, addSink is checking that + localVideoTrack?.addSink(pipSurface) + } + } + + // If remote track exists, then sink it to surface + remoteSurfaceRenderers.forEach { renderer -> + renderer.get()?.let { participantSurface -> + remoteVideoTrack?.addSink(participantSurface) + } + } + } + + private suspend fun getTurnServer(): TurnServerResponse? { + return tryOrNull { + awaitCallback { + sessionProvider.get()?.callSignalingService()?.getTurnServer(it) + } + } + } + + private fun createLocalStream() { + val peerConnectionFactory = peerConnectionFactoryProvider.get() ?: return + Timber.v("Create local stream for call ${mxCall.callId}") + configureAudioTrack(peerConnectionFactory) + // add video track if needed + if (mxCall.isVideoCall) { + configureVideoTrack(peerConnectionFactory) + } + updateMuteStatus() + } + + private fun configureAudioTrack(peerConnectionFactory: PeerConnectionFactory) { + val audioSource = peerConnectionFactory.createAudioSource(DEFAULT_AUDIO_CONSTRAINTS) + val audioTrack = peerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource) + audioTrack.setEnabled(true) + Timber.v("Add audio track $AUDIO_TRACK_ID to call ${mxCall.callId}") + peerConnection?.addTrack(audioTrack, listOf(STREAM_ID)) + localAudioSource = audioSource + localAudioTrack = audioTrack + } + + private fun configureVideoTrack(peerConnectionFactory: PeerConnectionFactory) { + val cameraIterator = if (Camera2Enumerator.isSupported(context)) { + Camera2Enumerator(context) + } else { + Camera1Enumerator(false) + } + // I don't realy know how that works if there are 2 front or 2 back cameras + val frontCamera = cameraIterator.deviceNames + ?.firstOrNull { cameraIterator.isFrontFacing(it) } + ?.let { + CameraProxy(it, CameraType.FRONT).also { availableCamera.add(it) } + } + + val backCamera = cameraIterator.deviceNames + ?.firstOrNull { cameraIterator.isBackFacing(it) } + ?.let { + CameraProxy(it, CameraType.BACK).also { availableCamera.add(it) } + } + + val camera = frontCamera?.also { cameraInUse = frontCamera } + ?: backCamera?.also { cameraInUse = backCamera } + ?: null.also { cameraInUse = null } + + listeners.forEach { + tryOrNull { it.onCameraChanged() } + } + + if (camera != null) { + val videoCapturer = cameraIterator.createCapturer(camera.name, object : CameraEventsHandlerAdapter() { + override fun onFirstFrameAvailable() { + super.onFirstFrameAvailable() + videoCapturerIsInError = false + } + + override fun onCameraClosed() { + super.onCameraClosed() + Timber.v("onCameraClosed") + // This could happen if you open the camera app in chat + // We then register in order to restart capture as soon as the camera is available again + videoCapturerIsInError = true + val cameraManager = context.getSystemService<CameraManager>() + cameraAvailabilityCallback = object : CameraManager.AvailabilityCallback() { + override fun onCameraUnavailable(cameraId: String) { + super.onCameraUnavailable(cameraId) + Timber.v("On camera unavailable: $cameraId") + } + + override fun onCameraAccessPrioritiesChanged() { + super.onCameraAccessPrioritiesChanged() + Timber.v("onCameraAccessPrioritiesChanged") + } + + override fun onCameraAvailable(cameraId: String) { + Timber.v("On camera available: $cameraId") + if (cameraId == camera.name) { + videoCapturer?.startCapture(currentCaptureFormat.width, currentCaptureFormat.height, currentCaptureFormat.fps) + cameraManager?.unregisterAvailabilityCallback(this) + } + } + } + cameraManager?.registerAvailabilityCallback(cameraAvailabilityCallback!!, null) + } + }) + + val videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast) + val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase!!.eglBaseContext) + Timber.v("## VOIP Local video source created") + + videoCapturer.initialize(surfaceTextureHelper, context, videoSource.capturerObserver) + // HD + videoCapturer.startCapture(currentCaptureFormat.width, currentCaptureFormat.height, currentCaptureFormat.fps) + this.videoCapturer = videoCapturer + + val videoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource) + Timber.v("Add video track $VIDEO_TRACK_ID to call ${mxCall.callId}") + videoTrack.setEnabled(true) + peerConnection?.addTrack(videoTrack, listOf(STREAM_ID)) + localVideoSource = videoSource + localVideoTrack = videoTrack + } + } + + fun setCaptureFormat(format: CaptureFormat) { + Timber.v("## VOIP setCaptureFormat $format") + videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps) + currentCaptureFormat = format + } + + private fun updateMuteStatus() { + val micShouldBeMuted = micMuted || remoteOnHold + localAudioTrack?.setEnabled(!micShouldBeMuted) + remoteAudioTrack?.setEnabled(!remoteOnHold) + val vidShouldBeMuted = videoMuted || remoteOnHold + localVideoTrack?.setEnabled(!vidShouldBeMuted) + remoteVideoTrack?.setEnabled(!remoteOnHold) + } + + /** + * Indicates whether we are 'on hold' to the remote party (ie. if true, + * they cannot hear us). Note that this will return true when we put the + * remote on hold too due to the way hold is implemented (since we don't + * wish to play hold music when we put a call on hold, we use 'inactive' + * rather than 'sendonly') + * @returns true if the other party has put us on hold + */ + private fun computeIsLocalOnHold(): Boolean { + if (mxCall.state !is CallState.Connected) return false + var callOnHold = true + // We consider a call to be on hold only if *all* the tracks are on hold + // (is this the right thing to do?) + for (transceiver in peerConnection?.transceivers ?: emptyList()) { + val trackOnHold = transceiver.currentDirection == RtpTransceiver.RtpTransceiverDirection.INACTIVE + || transceiver.currentDirection == RtpTransceiver.RtpTransceiverDirection.RECV_ONLY + if (!trackOnHold) callOnHold = false + } + return callOnHold + } + + fun updateRemoteOnHold(onHold: Boolean) { + GlobalScope.launch(dispatcher) { + if (remoteOnHold == onHold) return@launch + val direction: RtpTransceiver.RtpTransceiverDirection + if (onHold) { + wasLocalOnHold = isLocalOnHold + remoteOnHold = true + isLocalOnHold = true + direction = RtpTransceiver.RtpTransceiverDirection.INACTIVE + timer.pause() + } else { + remoteOnHold = false + isLocalOnHold = wasLocalOnHold + onCallBecomeActive(this@WebRtcCall) + direction = RtpTransceiver.RtpTransceiverDirection.SEND_RECV + if (!isLocalOnHold) { + timer.resume() + } + } + for (transceiver in peerConnection?.transceivers ?: emptyList()) { + transceiver.direction = direction + } + updateMuteStatus() + listeners.forEach { + tryOrNull { it.onHoldUnhold() } + } + } + } + + fun muteCall(muted: Boolean) { + micMuted = muted + updateMuteStatus() + } + + fun enableVideo(enabled: Boolean) { + videoMuted = !enabled + updateMuteStatus() + } + + fun canSwitchCamera(): Boolean { + return availableCamera.size > 1 + } + + private fun getOppositeCameraIfAny(): CameraProxy? { + val currentCamera = cameraInUse ?: return null + return if (currentCamera.type == CameraType.FRONT) { + availableCamera.firstOrNull { it.type == CameraType.BACK } + } else { + availableCamera.firstOrNull { it.type == CameraType.FRONT } + } + } + + fun switchCamera() { + Timber.v("## VOIP switchCamera") + if (mxCall.state is CallState.Connected && mxCall.isVideoCall) { + val oppositeCamera = getOppositeCameraIfAny() ?: return + videoCapturer?.switchCamera( + object : CameraVideoCapturer.CameraSwitchHandler { + // Invoked on success. |isFrontCamera| is true if the new camera is front facing. + override fun onCameraSwitchDone(isFrontCamera: Boolean) { + Timber.v("## VOIP onCameraSwitchDone isFront $isFrontCamera") + cameraInUse = oppositeCamera + localSurfaceRenderers.forEach { + it.get()?.setMirror(isFrontCamera) + } + listeners.forEach { + tryOrNull { it.onCameraChanged() } + } + } + + override fun onCameraSwitchError(errorDescription: String?) { + Timber.v("## VOIP onCameraSwitchError isFront $errorDescription") + } + }, oppositeCamera.name + ) + } + } + + private suspend fun createAnswer(): SessionDescription? { + Timber.w("## VOIP createAnswer") + val peerConnection = peerConnection ?: return null + val constraints = MediaConstraints().apply { + mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")) + mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", if (mxCall.isVideoCall) "true" else "false")) + } + return try { + val localDescription = peerConnection.awaitCreateAnswer(constraints) ?: return null + peerConnection.awaitSetLocalDescription(localDescription) + localDescription + } catch (failure: Throwable) { + Timber.v("Fail to create answer") + null + } + } + + fun currentCameraType(): CameraType? { + return cameraInUse?.type + } + + fun currentCaptureFormat(): CaptureFormat { + return currentCaptureFormat + } + + private fun release() { + listeners.clear() + mxCall.removeListener(this) + timer.stop() + timer.tickListener = null + videoCapturer?.stopCapture() + videoCapturer?.dispose() + videoCapturer = null + remoteIceCandidateDisposable?.dispose() + iceCandidateDisposable?.dispose() + peerConnection?.close() + peerConnection?.dispose() + localAudioSource?.dispose() + localVideoSource?.dispose() + localAudioSource = null + localAudioTrack = null + localVideoSource = null + localVideoTrack = null + cameraAvailabilityCallback = null + } + + fun onAddStream(stream: MediaStream) { + GlobalScope.launch(dispatcher) { + // reportError("Weird-looking stream: " + stream); + if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) { + Timber.e("## VOIP StreamObserver weird looking stream: $stream") + // TODO maybe do something more?? + mxCall.hangUp() + return@launch + } + if (stream.audioTracks.size == 1) { + val remoteAudioTrack = stream.audioTracks.first() + remoteAudioTrack.setEnabled(true) + this@WebRtcCall.remoteAudioTrack = remoteAudioTrack + } + if (stream.videoTracks.size == 1) { + val remoteVideoTrack = stream.videoTracks.first() + remoteVideoTrack.setEnabled(true) + this@WebRtcCall.remoteVideoTrack = remoteVideoTrack + // sink to renderer if attached + remoteSurfaceRenderers.forEach { it.get()?.let { remoteVideoTrack.addSink(it) } } + } + } + } + + fun onRemoveStream() { + GlobalScope.launch(dispatcher) { + remoteSurfaceRenderers + .mapNotNull { it.get() } + .forEach { remoteVideoTrack?.removeSink(it) } + remoteVideoTrack = null + remoteAudioTrack = null + } + } + + fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) { + if (mxCall.state == CallState.Terminated) { + return + } + // Close tracks ASAP + localVideoTrack?.setEnabled(false) + localVideoTrack?.setEnabled(false) + cameraAvailabilityCallback?.let { cameraAvailabilityCallback -> + val cameraManager = context.getSystemService<CameraManager>()!! + cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) + } + val wasRinging = mxCall.state is CallState.LocalRinging + mxCall.state = CallState.Terminated + GlobalScope.launch(dispatcher) { + release() + } + onCallEnded(callId) + if (originatedByMe) { + if (wasRinging) { + mxCall.reject() + } else { + mxCall.hangUp(reason) + } + } + } + + // Call listener + + fun onCallIceCandidateReceived(iceCandidatesContent: CallCandidatesContent) { + GlobalScope.launch(dispatcher) { + iceCandidatesContent.candidates.forEach { + if (it.sdpMid.isNullOrEmpty() || it.candidate.isNullOrEmpty()) { + return@forEach + } + Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId} sdp: ${it.candidate}") + val iceCandidate = IceCandidate(it.sdpMid, it.sdpMLineIndex, it.candidate) + remoteCandidateSource.onNext(iceCandidate) + } + } + } + + fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) { + GlobalScope.launch(dispatcher) { + Timber.v("## VOIP onCallAnswerReceived ${callAnswerContent.callId}") + val sdp = SessionDescription(SessionDescription.Type.ANSWER, callAnswerContent.answer.sdp) + try { + peerConnection?.awaitSetRemoteDescription(sdp) + } catch (failure: Throwable) { + endCall(true, CallHangupContent.Reason.UNKWOWN_ERROR) + return@launch + } + if (mxCall.opponentPartyId?.hasValue().orFalse()) { + mxCall.selectAnswer() + } + } + } + + fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) { + GlobalScope.launch(dispatcher) { + val description = callNegotiateContent.description + val type = description?.type + val sdpText = description?.sdp + if (type == null || sdpText == null) { + Timber.i("Ignoring invalid m.call.negotiate event") + return@launch + } + val peerConnection = peerConnection ?: return@launch + // Politeness always follows the direction of the call: in a glare situation, + // we pick either the inbound or outbound call, so one side will always be + // inbound and one outbound + val polite = !mxCall.isOutgoing + // Here we follow the perfect negotiation logic from + // https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation + val offerCollision = description.type == SdpType.OFFER + && (makingOffer || peerConnection.signalingState() != PeerConnection.SignalingState.STABLE) + + ignoreOffer = !polite && offerCollision + if (ignoreOffer) { + Timber.i("Ignoring colliding negotiate event because we're impolite") + return@launch + } + val prevOnHold = computeIsLocalOnHold() + try { + val sdp = SessionDescription(type.asWebRTC(), sdpText) + peerConnection.awaitSetRemoteDescription(sdp) + if (type == SdpType.OFFER) { + createAnswer()?.also { + mxCall.negotiate(it.description, SdpType.ANSWER) + } + } + } catch (failure: Throwable) { + Timber.e(failure, "Failed to complete negotiation") + } + val nowOnHold = computeIsLocalOnHold() + wasLocalOnHold = nowOnHold + if (prevOnHold != nowOnHold) { + isLocalOnHold = nowOnHold + if (nowOnHold) { + timer.pause() + } else { + timer.resume() + } + listeners.forEach { + tryOrNull { it.onHoldUnhold() } + } + } + } + } + + private fun formatDuration(duration: Duration): String { + val hours = duration.seconds / 3600 + val minutes = (duration.seconds % 3600) / 60 + val seconds = duration.seconds % 60 + return if (hours > 0) { + String.format("%d:%02d:%02d", hours, minutes, seconds) + } else { + String.format("%02d:%02d", minutes, seconds) + } + } + + // MxCall.StateListener + + override fun onStateUpdate(call: MxCall) { + val state = call.state + if (state is CallState.Connected && state.iceConnectionState == MxPeerConnectionState.CONNECTED) { + timer.resume() + } else { + timer.pause() + } + listeners.forEach { + tryOrNull { it.onStateUpdate(call) } + } + } +} + +private fun MutableList<WeakReference<SurfaceViewRenderer>>.addIfNeeded(renderer: SurfaceViewRenderer?) { + if (renderer == null) return + val exists = any { + it.get() == renderer + } + if (!exists) { + add(WeakReference(renderer)) + } +} + +private fun MutableList<WeakReference<SurfaceViewRenderer>>.removeIfNeeded(renderer: SurfaceViewRenderer?) { + if (renderer == null) return + removeAll { + it.get() == renderer + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt new file mode 100644 index 0000000000..95728e0a97 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -0,0 +1,410 @@ +/* + * Copyright (c) 2020 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.call.webrtc + +import android.content.Context +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import im.vector.app.ActiveSessionDataSource +import im.vector.app.core.services.CallService +import im.vector.app.features.call.VectorCallActivity +import im.vector.app.features.call.audio.CallAudioManager +import im.vector.app.features.call.utils.EglUtils +import im.vector.app.push.fcm.FcmHelper +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.call.CallListener +import org.matrix.android.sdk.api.session.call.CallState +import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent +import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent +import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent +import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent +import org.webrtc.DefaultVideoDecoderFactory +import org.webrtc.DefaultVideoEncoderFactory +import org.webrtc.PeerConnectionFactory +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicReference +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Manage peerConnectionFactory & Peer connections outside of activity lifecycle to resist configuration changes + * Use app context + */ +@Singleton +class WebRtcCallManager @Inject constructor( + private val context: Context, + private val activeSessionDataSource: ActiveSessionDataSource +) : CallListener, LifecycleObserver { + + private val currentSession: Session? + get() = activeSessionDataSource.currentValue?.orNull() + + interface CurrentCallListener { + fun onCurrentCallChange(call: WebRtcCall?) {} + fun onAudioDevicesChange() {} + } + + interface PSTNSupportListener { + fun onPSTNSupportUpdated() + } + + private val pstnSupportListeners = emptyList<PSTNSupportListener>().toMutableList() + fun addPstnSupportListener(listener: PSTNSupportListener) { + pstnSupportListeners.add(listener) + } + + fun removePstnSupportListener(listener: PSTNSupportListener) { + pstnSupportListeners.remove(listener) + } + + private val currentCallsListeners = CopyOnWriteArrayList<CurrentCallListener>() + + fun addCurrentCallListener(listener: CurrentCallListener) { + currentCallsListeners.add(listener) + } + + fun removeCurrentCallListener(listener: CurrentCallListener) { + currentCallsListeners.remove(listener) + } + + val audioManager = CallAudioManager(context) { + currentCallsListeners.forEach { + tryOrNull { it.onAudioDevicesChange() } + } + }.apply { + setMode(CallAudioManager.Mode.DEFAULT) + } + + private var peerConnectionFactory: PeerConnectionFactory? = null + private val executor = Executors.newSingleThreadExecutor() + private val dispatcher = executor.asCoroutineDispatcher() + var supportedPSTNProtocol: String? = null + private set + + val supportsPSTNProtocol: Boolean + get() = supportedPSTNProtocol != null + + private val rootEglBase by lazy { EglUtils.rootEglBase } + + private var isInBackground: Boolean = true + + init { + GlobalScope.launch { + supportedPSTNProtocol = currentSession?.getSupportedPSTN(3) + if (supportedPSTNProtocol != null) { + pstnSupportListeners.forEach { + tryOrNull { it.onPSTNSupportUpdated() } + } + } + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun entersForeground() { + isInBackground = false + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + fun entersBackground() { + isInBackground = true + } + + /** + * The current call is the call we interacted with whatever his state (connected,resumed, held...) + * As soon as we interact with an other call, it replaces this one and put it on held if not already. + */ + var currentCall: AtomicReference<WebRtcCall?> = AtomicReference(null) + private fun AtomicReference<WebRtcCall?>.setAndNotify(newValue: WebRtcCall?) { + set(newValue) + currentCallsListeners.forEach { + tryOrNull { it.onCurrentCallChange(newValue) } + } + } + + private val advertisedCalls = HashSet<String>() + private val callsByCallId = ConcurrentHashMap<String, WebRtcCall>() + private val callsByRoomId = ConcurrentHashMap<String, MutableList<WebRtcCall>>() + + fun getCallById(callId: String): WebRtcCall? { + return callsByCallId[callId] + } + + fun getCallsByRoomId(roomId: String): List<WebRtcCall> { + return callsByRoomId[roomId] ?: emptyList() + } + + fun getCurrentCall(): WebRtcCall? { + return currentCall.get() + } + + fun getCalls(): List<WebRtcCall> { + return callsByCallId.values.toList() + } + + /** + * @return a set of all advertised call during the lifetime of the app. + */ + fun getAdvertisedCalls() = advertisedCalls + + fun headSetButtonTapped() { + Timber.v("## VOIP headSetButtonTapped") + val call = getCurrentCall() ?: return + if (call.mxCall.state is CallState.LocalRinging) { + // accept call + call.acceptIncomingCall() + } + if (call.mxCall.state is CallState.Connected) { + // end call? + call.endCall() + } + } + + private fun createPeerConnectionFactoryIfNeeded() { + if (peerConnectionFactory != null) return + Timber.v("## VOIP createPeerConnectionFactory") + val eglBaseContext = rootEglBase?.eglBaseContext ?: return Unit.also { + Timber.e("## VOIP No EGL BASE") + } + + Timber.v("## VOIP PeerConnectionFactory.initialize") + PeerConnectionFactory.initialize(PeerConnectionFactory + .InitializationOptions.builder(context.applicationContext) + .createInitializationOptions() + ) + + val options = PeerConnectionFactory.Options() + val defaultVideoEncoderFactory = DefaultVideoEncoderFactory( + eglBaseContext, + /* enableIntelVp8Encoder */ + true, + /* enableH264HighProfile */ + true) + val defaultVideoDecoderFactory = DefaultVideoDecoderFactory(eglBaseContext) + Timber.v("## VOIP PeerConnectionFactory.createPeerConnectionFactory ...") + peerConnectionFactory = PeerConnectionFactory.builder() + .setOptions(options) + .setVideoEncoderFactory(defaultVideoEncoderFactory) + .setVideoDecoderFactory(defaultVideoDecoderFactory) + .createPeerConnectionFactory() + } + + private fun onCallActive(call: WebRtcCall) { + Timber.v("## VOIP WebRtcPeerConnectionManager onCall active: ${call.mxCall.callId}") + val currentCall = getCurrentCall().takeIf { it != call } + currentCall?.updateRemoteOnHold(onHold = true) + audioManager.setMode(if (call.mxCall.isVideoCall) CallAudioManager.Mode.VIDEO_CALL else CallAudioManager.Mode.AUDIO_CALL) + this.currentCall.setAndNotify(call) + } + + private fun onCallEnded(callId: String) { + Timber.v("## VOIP WebRtcPeerConnectionManager onCall ended: $callId") + val webRtcCall = callsByCallId.remove(callId) ?: return Unit.also { + Timber.v("On call ended for unknown call $callId") + } + CallService.onCallTerminated(context, callId) + callsByRoomId[webRtcCall.roomId]?.remove(webRtcCall) + if (getCurrentCall()?.callId == callId) { + val otherCall = getCalls().lastOrNull() + currentCall.setAndNotify(otherCall) + } + // This must be done in this thread + executor.execute { + // There is no active calls + if (getCurrentCall() == null) { + Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one") + peerConnectionFactory?.dispose() + peerConnectionFactory = null + audioManager.setMode(CallAudioManager.Mode.DEFAULT) + // did we start background sync? so we should stop it + if (isInBackground) { + if (FcmHelper.isPushSupported()) { + currentSession?.stopAnyBackgroundSync() + } else { + // for fdroid we should not stop, it should continue syncing + // maybe we should restore default timeout/delay though? + } + } + } + Timber.v("## VOIP WebRtcPeerConnectionManager close() executor done") + } + } + + fun startOutgoingCall(signalingRoomId: String, otherUserId: String, isVideoCall: Boolean) { + Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall") + if (getCallsByRoomId(signalingRoomId).isNotEmpty()) { + Timber.w("## VOIP you already have a call in this room") + return + } + if (getCurrentCall() != null && getCurrentCall()?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) { + Timber.w("## VOIP cannot start outgoing call") + // Just ignore, maybe we could answer from other session? + return + } + executor.execute { + createPeerConnectionFactoryIfNeeded() + } + getCurrentCall()?.updateRemoteOnHold(onHold = true) + val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return + val webRtcCall = createWebRtcCall(mxCall) + currentCall.setAndNotify(webRtcCall) + + CallService.onOutgoingCallRinging( + context = context.applicationContext, + callId = mxCall.callId) + + // start the activity now + context.startActivity(VectorCallActivity.newIntent(context, mxCall, VectorCallActivity.OUTGOING_CREATED)) + } + + override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) { + Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId}") + val call = callsByCallId[iceCandidatesContent.callId] + ?: return Unit.also { + Timber.w("onCallIceCandidateReceived for non active call? ${iceCandidatesContent.callId}") + } + call.onCallIceCandidateReceived(iceCandidatesContent) + } + + private fun createWebRtcCall(mxCall: MxCall): WebRtcCall { + val webRtcCall = WebRtcCall( + mxCall = mxCall, + rootEglBase = rootEglBase, + context = context, + dispatcher = dispatcher, + peerConnectionFactoryProvider = { + createPeerConnectionFactoryIfNeeded() + peerConnectionFactory + }, + sessionProvider = { currentSession }, + onCallBecomeActive = this::onCallActive, + onCallEnded = this::onCallEnded + ) + advertisedCalls.add(mxCall.callId) + callsByCallId[mxCall.callId] = webRtcCall + callsByRoomId.getOrPut(mxCall.roomId) { ArrayList(1) } + .add(webRtcCall) + if (getCurrentCall() == null) { + currentCall.setAndNotify(webRtcCall) + } + return webRtcCall + } + + fun endCallForRoom(roomId: String, originatedByMe: Boolean = true) { + callsByRoomId[roomId]?.forEach { it.endCall(originatedByMe) } + } + + override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) { + Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}") + if (getCallsByRoomId(mxCall.roomId).isNotEmpty()) { + Timber.w("## VOIP you already have a call in this room") + return + } + if ((getCurrentCall() != null && getCurrentCall()?.mxCall?.state !is CallState.Connected) || getCalls().size >= 2) { + Timber.w("## VOIP receiving incoming call but cannot handle it") + // Just ignore, maybe we could answer from other session? + return + } + createWebRtcCall(mxCall).apply { + offerSdp = callInviteContent.offer + } + // Start background service with notification + CallService.onIncomingCallRinging( + context = context, + callId = mxCall.callId, + isInBackground = isInBackground + ) + // If this is received while in background, the app will not sync, + // and thus won't be able to received events. For example if the call is + // accepted on an other session this device will continue ringing + if (isInBackground) { + if (FcmHelper.isPushSupported()) { + // only for push version as fdroid version is already doing it? + currentSession?.startAutomaticBackgroundSync(30, 0) + } else { + // Maybe increase sync freq? but how to set back to default values? + } + } + } + + override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) { + val call = callsByCallId[callAnswerContent.callId] + ?: return Unit.also { + Timber.w("onCallAnswerReceived for non active call? ${callAnswerContent.callId}") + } + val mxCall = call.mxCall + // Update service state + CallService.onPendingCall( + context = context, + callId = mxCall.callId + ) + call.onCallAnswerReceived(callAnswerContent) + } + + override fun onCallHangupReceived(callHangupContent: CallHangupContent) { + val call = callsByCallId[callHangupContent.callId] + ?: return Unit.also { + Timber.w("onCallHangupReceived for non active call? ${callHangupContent.callId}") + } + call.endCall(false) + } + + override fun onCallRejectReceived(callRejectContent: CallRejectContent) { + val call = callsByCallId[callRejectContent.callId] + ?: return Unit.also { + Timber.w("onCallRejectReceived for non active call? ${callRejectContent.callId}") + } + call.endCall(false) + } + + override fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) { + val call = callsByCallId[callSelectAnswerContent.callId] + ?: return Unit.also { + Timber.w("onCallSelectAnswerReceived for non active call? ${callSelectAnswerContent.callId}") + } + val selectedPartyId = callSelectAnswerContent.selectedPartyId + if (selectedPartyId != call.mxCall.ourPartyId) { + Timber.i("Got select_answer for party ID $selectedPartyId: we are party ID ${call.mxCall.ourPartyId}.") + // The other party has picked somebody else's answer + call.endCall(false) + } + } + + override fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) { + val call = callsByCallId[callNegotiateContent.callId] + ?: return Unit.also { + Timber.w("onCallNegotiateReceived for non active call? ${callNegotiateContent.callId}") + } + call.onCallNegotiateReceived(callNegotiateContent) + } + + override fun onCallManagedByOtherSession(callId: String) { + Timber.v("## VOIP onCallManagedByOtherSession: $callId") + onCallEnded(callId) + } +} diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt index 6aaa69fbc0..68e169b8c5 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookFragment.kt @@ -32,7 +32,7 @@ import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentContactsBookBinding -import im.vector.app.features.userdirectory.PendingInvitee +import im.vector.app.features.userdirectory.PendingSelection import im.vector.app.features.userdirectory.UserListAction import im.vector.app.features.userdirectory.UserListSharedAction import im.vector.app.features.userdirectory.UserListSharedActionViewModel @@ -44,9 +44,9 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject class ContactsBookFragment @Inject constructor( - val contactsBookViewModelFactory: ContactsBookViewModel.Factory, + private val contactsBookViewModelFactory: ContactsBookViewModel.Factory, private val contactsBookController: ContactsBookController -) : VectorBaseFragment<FragmentContactsBookBinding>(), ContactsBookController.Callback { +) : VectorBaseFragment<FragmentContactsBookBinding>(), ContactsBookController.Callback, ContactsBookViewModel.Factory { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentContactsBookBinding { return FragmentContactsBookBinding.inflate(inflater, container, false) @@ -59,6 +59,10 @@ class ContactsBookFragment @Inject constructor( private lateinit var sharedActionViewModel: UserListSharedActionViewModel + override fun create(initialState: ContactsBookViewState): ContactsBookViewModel { + return contactsBookViewModelFactory.create(initialState) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java) @@ -128,13 +132,13 @@ class ContactsBookFragment @Inject constructor( override fun onMatrixIdClick(matrixId: String) { view?.hideKeyboard() - viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId)))) + viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.UserPendingSelection(User(matrixId)))) sharedActionViewModel.post(UserListSharedAction.GoBack) } override fun onThreePidClick(threePid: ThreePid) { view?.hideKeyboard() - viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid))) + viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.ThreePidPendingSelection(threePid))) sharedActionViewModel.post(UserListSharedAction.GoBack) } } diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt index d73794f8d8..05af63d7ba 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewModel.kt @@ -16,7 +16,6 @@ package im.vector.app.features.contactsbook -import androidx.fragment.app.FragmentActivity import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.FragmentViewModelContext @@ -32,8 +31,6 @@ import im.vector.app.core.contacts.MappedContact import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel -import im.vector.app.features.createdirect.CreateDirectRoomActivity -import im.vector.app.features.invite.InviteUsersToRoomActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback @@ -57,17 +54,11 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted companion object : MvRxViewModelFactory<ContactsBookViewModel, ContactsBookViewState> { override fun create(viewModelContext: ViewModelContext, state: ContactsBookViewState): ContactsBookViewModel? { - return when (viewModelContext) { - is FragmentViewModelContext -> (viewModelContext.fragment() as ContactsBookFragment).contactsBookViewModelFactory.create(state) - is ActivityViewModelContext -> { - when (viewModelContext.activity<FragmentActivity>()) { - is CreateDirectRoomActivity -> viewModelContext.activity<CreateDirectRoomActivity>().contactsBookViewModelFactory.create(state) - is InviteUsersToRoomActivity -> viewModelContext.activity<InviteUsersToRoomActivity>().contactsBookViewModelFactory.create(state) - else -> error("Wrong activity or fragment") - } - } - else -> error("Wrong activity or fragment") + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") } } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt index ce91761fdd..ffc25210e9 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomAction.kt @@ -17,11 +17,11 @@ package im.vector.app.features.createdirect import im.vector.app.core.platform.VectorViewModelAction -import im.vector.app.features.userdirectory.PendingInvitee +import im.vector.app.features.userdirectory.PendingSelection sealed class CreateDirectRoomAction : VectorViewModelAction { data class CreateRoomAndInviteSelectedUsers( - val invitees: Set<PendingInvitee>, + val selections: Set<PendingSelection>, val existingDmRoomId: String? ) : CreateDirectRoomAction() } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt index beb7931fd4..4f81841b73 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomActivity.kt @@ -45,6 +45,7 @@ import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.onPermissionDeniedSnackbar import im.vector.app.features.contactsbook.ContactsBookFragment import im.vector.app.features.contactsbook.ContactsBookViewModel +import im.vector.app.features.contactsbook.ContactsBookViewState import im.vector.app.features.userdirectory.UserListFragment import im.vector.app.features.userdirectory.UserListFragmentArgs import im.vector.app.features.userdirectory.UserListSharedAction @@ -57,7 +58,7 @@ import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import java.net.HttpURLConnection import javax.inject.Inject -class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory { +class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory, CreateDirectRoomViewModel.Factory, ContactsBookViewModel.Factory { private val viewModel: CreateDirectRoomViewModel by viewModel() private lateinit var sharedActionViewModel: UserListSharedActionViewModel @@ -71,9 +72,11 @@ class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fac injector.inject(this) } - override fun create(initialState: UserListViewState): UserListViewModel { - return userListViewModelFactory.create(initialState) - } + override fun create(initialState: UserListViewState) = userListViewModelFactory.create(initialState) + + override fun create(initialState: CreateDirectRoomViewState) = createDirectRoomViewModelFactory.create(initialState) + + override fun create(initialState: ContactsBookViewState) = contactsBookViewModelFactory.create(initialState) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -143,7 +146,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fac private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) { if (action.itemId == R.id.action_create_direct_room) { viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers( - action.invitees, + action.selections, null )) } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt index 94578ed5c7..92a03c5483 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomByQrCodeFragment.kt @@ -29,8 +29,7 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.FragmentQrCodeScannerBinding -import im.vector.app.features.userdirectory.PendingInvitee - +import im.vector.app.features.userdirectory.PendingSelection import me.dm7.barcodescanner.zxing.ZXingScannerView import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkParser @@ -107,7 +106,7 @@ class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragmen val qrInvitee = if (viewModel.session.getUser(mxid) != null) viewModel.session.getUser(mxid)!! else User(mxid, null, null) viewModel.handle( - CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingInvitee.UserPendingInvitee(qrInvitee)), existingDm) + CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingSelection.UserPendingSelection(qrInvitee)), existingDm) ) } } diff --git a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt index 30bbedf7ec..cbe363aa0e 100644 --- a/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/createdirect/CreateDirectRoomViewModel.kt @@ -18,6 +18,7 @@ package im.vector.app.features.createdirect import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext @@ -28,7 +29,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault -import im.vector.app.features.userdirectory.PendingInvitee +import im.vector.app.features.userdirectory.PendingSelection import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.raw.RawService @@ -51,8 +52,11 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted @JvmStatic override fun create(viewModelContext: ViewModelContext, state: CreateDirectRoomViewState): CreateDirectRoomViewModel? { - val activity: CreateDirectRoomActivity = (viewModelContext as ActivityViewModelContext).activity() - return activity.createDirectRoomViewModelFactory.create(state) + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") } } @@ -73,11 +77,11 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted } } else { // Create the DM - createRoomAndInviteSelectedUsers(action.invitees) + createRoomAndInviteSelectedUsers(action.selections) } } - private fun createRoomAndInviteSelectedUsers(invitees: Set<PendingInvitee>) { + private fun createRoomAndInviteSelectedUsers(selections: Set<PendingSelection>) { viewModelScope.launch(Dispatchers.IO) { val adminE2EByDefault = rawService.getElementWellknown(session.myUserId) ?.isE2EByDefault() @@ -85,10 +89,10 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted val roomParams = CreateRoomParams() .apply { - invitees.forEach { + selections.forEach { when (it) { - is PendingInvitee.UserPendingInvitee -> invitedUserIds.add(it.user.userId) - is PendingInvitee.ThreePidPendingInvitee -> invite3pids.add(it.threePid) + is PendingSelection.UserPendingSelection -> invitedUserIds.add(it.user.userId) + is PendingSelection.ThreePidPendingSelection -> invite3pids.add(it.threePid) }.exhaustive } setDirectMessage() diff --git a/vector/src/main/java/im/vector/app/features/createdirect/DirectRoomHelper.kt b/vector/src/main/java/im/vector/app/features/createdirect/DirectRoomHelper.kt new file mode 100644 index 0000000000..171970ec1e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/createdirect/DirectRoomHelper.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021 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.createdirect + +import im.vector.app.features.raw.wellknown.getElementWellknown +import im.vector.app.features.raw.wellknown.isE2EByDefault +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.raw.RawService +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams +import org.matrix.android.sdk.internal.util.awaitCallback +import javax.inject.Inject + +class DirectRoomHelper @Inject constructor( + private val rawService: RawService, + private val session: Session +) { + + suspend fun ensureDMExists(userId: String): String { + val existingRoomId = tryOrNull { session.getExistingDirectRoomWithUser(userId) } + val roomId: String + if (existingRoomId != null) { + roomId = existingRoomId + } else { + val adminE2EByDefault = rawService.getElementWellknown(session.myUserId) + ?.isE2EByDefault() + ?: true + + val roomParams = CreateRoomParams().apply { + invitedUserIds.add(userId) + setDirectMessage() + enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault + } + roomId = awaitCallback { + session.createRoom(roomParams, it) + } + } + return roomId + } +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt index 0ce473caaa..feff326cfd 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt @@ -68,7 +68,7 @@ class SharedSecuredStorageKeyFragment @Inject constructor() : VectorBaseFragment views.ssssKeyUseFile.debouncedClicks { startImportTextFromFileIntent(requireContext(), importFileStartForActivityResult) } - views.ssssKeyReset.views.itemVerificationClickableZone.debouncedClicks { + views.ssssKeyReset.views.bottomSheetActionClickableZone.debouncedClicks { sharedViewModel.handle(SharedSecureStorageAction.ForgotResetAll) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt index 8d9157c953..24cb7294d4 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt @@ -79,7 +79,7 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor( } .disposeOnDestroyView() - views.ssssPassphraseReset.views.itemVerificationClickableZone.debouncedClicks { + views.ssssPassphraseReset.views.bottomSheetActionClickableZone.debouncedClicks { sharedViewModel.handle(SharedSecureStorageAction.ForgotResetAll) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConclusionFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConclusionFragment.kt index f0a7811666..6c5c259755 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConclusionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapConclusionFragment.kt @@ -44,7 +44,7 @@ class BootstrapConclusionFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - views.bootstrapConclusionContinue.views.itemVerificationClickableZone.debouncedClicks { sharedViewModel.handle(BootstrapActions.Completed) } + views.bootstrapConclusionContinue.views.bottomSheetActionClickableZone.debouncedClicks { sharedViewModel.handle(BootstrapActions.Completed) } } override fun invalidate() = withState(sharedViewModel) { state -> diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt index 82ac15a069..f587f5e58f 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt @@ -52,9 +52,9 @@ class BootstrapSaveRecoveryKeyFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - views.recoverySave.views.itemVerificationClickableZone.debouncedClicks { downloadRecoveryKey() } - views.recoveryCopy.views.itemVerificationClickableZone.debouncedClicks { shareRecoveryKey() } - views.recoveryContinue.views.itemVerificationClickableZone.debouncedClicks { + views.recoverySave.views.bottomSheetActionClickableZone.debouncedClicks { downloadRecoveryKey() } + views.recoveryCopy.views.bottomSheetActionClickableZone.debouncedClicks { shareRecoveryKey() } + views.recoveryContinue.views.bottomSheetActionClickableZone.debouncedClicks { // We do not display the final Fragment anymore // TODO Do some cleanup // sharedViewModel.handle(BootstrapActions.GoToCompleted) diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt index 9131ab3c36..8676f1fb6b 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSetupRecoveryKeyFragment.kt @@ -42,15 +42,15 @@ class BootstrapSetupRecoveryKeyFragment @Inject constructor() super.onViewCreated(view, savedInstanceState) // Actions when a key backup exist - views.bootstrapSetupSecureSubmit.views.itemVerificationClickableZone.debouncedClicks { + views.bootstrapSetupSecureSubmit.views.bottomSheetActionClickableZone.debouncedClicks { sharedViewModel.handle(BootstrapActions.StartKeyBackupMigration) } // Actions when there is no key backup - views.bootstrapSetupSecureUseSecurityKey.views.itemVerificationClickableZone.debouncedClicks { + views.bootstrapSetupSecureUseSecurityKey.views.bottomSheetActionClickableZone.debouncedClicks { sharedViewModel.handle(BootstrapActions.Start(userWantsToEnterPassphrase = false)) } - views.bootstrapSetupSecureUseSecurityPassphrase.views.itemVerificationClickableZone.debouncedClicks { + views.bootstrapSetupSecureUseSecurityPassphrase.views.bottomSheetActionClickableZone.debouncedClicks { sharedViewModel.handle(BootstrapActions.Start(userWantsToEnterPassphrase = true)) } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt index 51aa6d063b..b7f967e592 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -18,11 +18,11 @@ package im.vector.app.features.crypto.verification import android.content.Context import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.VerificationVectorAlert -import im.vector.app.features.themes.ThemeUtils import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest import org.matrix.android.sdk.api.session.crypto.verification.VerificationService @@ -31,6 +31,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxStat import org.matrix.android.sdk.api.util.toMatrixItem import timber.log.Timber import javax.inject.Inject +import javax.inject.Provider import javax.inject.Singleton /** @@ -39,6 +40,7 @@ import javax.inject.Singleton @Singleton class IncomingVerificationRequestHandler @Inject constructor( private val context: Context, + private var avatarRenderer: Provider<AvatarRenderer>, private val popupAlertManager: PopupAlertManager) : VerificationService.Listener { private var session: Session? = null @@ -60,9 +62,8 @@ class IncomingVerificationRequestHandler @Inject constructor( when (tx.state) { is VerificationTxState.OnStarted -> { // Add a notification for every incoming request - val name = session?.getUser(tx.otherUserId)?.displayName - ?: tx.otherUserId - + val user = session?.getUser(tx.otherUserId) + val name = user?.getBestName() ?: tx.otherUserId val alert = VerificationVectorAlert( uid, context.getString(R.string.sas_incoming_request_notif_title), @@ -77,10 +78,10 @@ class IncomingVerificationRequestHandler @Inject constructor( } } ?: true } else true - }, - matrixItem = session?.getUser(tx.otherUserId)?.toMatrixItem() + } ) .apply { + viewBinder = VerificationVectorAlert.ViewBinder(user?.toMatrixItem(), avatarRenderer.get()) contentAction = Runnable { (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { it.navigator.performDeviceVerification(it, tx.otherUserId, tx.transactionId) @@ -120,8 +121,8 @@ class IncomingVerificationRequestHandler @Inject constructor( Timber.v("## SAS verificationRequestCreated ${pr.transactionId}") // For incoming request we should prompt (if not in activity where this request apply) if (pr.isIncoming) { - val name = session?.getUser(pr.otherUserId)?.displayName - ?: pr.otherUserId + val user = session?.getUser(pr.otherUserId) + val name = user?.getBestName() ?: pr.otherUserId val alert = VerificationVectorAlert( uniqueIdForVerificationRequest(pr), @@ -134,10 +135,10 @@ class IncomingVerificationRequestHandler @Inject constructor( it.roomId != pr.roomId } ?: true } else true - }, - matrixItem = session?.getUser(pr.otherUserId)?.toMatrixItem() + } ) .apply { + viewBinder = VerificationVectorAlert.ViewBinder(user?.toMatrixItem(), avatarRenderer.get()) contentAction = Runnable { (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { val roomId = pr.roomId @@ -154,7 +155,7 @@ class IncomingVerificationRequestHandler @Inject constructor( pr.roomId ?: "" ) } - colorInt = ThemeUtils.getColor(context, R.attr.vctr_notice_secondary) + colorAttribute = R.attr.vctr_notice_secondary // 5mn expiration expirationTimestamp = System.currentTimeMillis() + (5 * 60 * 1000L) } diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index 08f18a00ba..1d673a2a07 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -20,9 +20,13 @@ import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.widget.ImageView import androidx.annotation.AnyThread +import androidx.annotation.ColorInt import androidx.annotation.UiThread import androidx.core.graphics.drawable.toBitmap import com.amulyakhare.textdrawable.TextDrawable +import com.bumptech.glide.load.MultiTransformation +import com.bumptech.glide.load.Transformation +import com.bumptech.glide.load.resource.bitmap.CircleCrop import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.DrawableImageViewTarget import com.bumptech.glide.request.target.Target @@ -32,6 +36,8 @@ import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideRequest import im.vector.app.core.glide.GlideRequests import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider +import jp.wasabeef.glide.transformations.BlurTransformation +import jp.wasabeef.glide.transformations.ColorFilterTransformation import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.util.MatrixItem @@ -90,6 +96,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active target: Target<Drawable>) { val placeholder = getPlaceholderDrawable(matrixItem) buildGlideRequest(glideRequests, matrixItem.avatarUrl) + .apply(RequestOptions.circleCropTransform()) .placeholder(placeholder) .into(target) } @@ -117,10 +124,27 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active .get() } + @UiThread + fun renderBlur(matrixItem: MatrixItem, imageView: ImageView, sampling: Int, rounded: Boolean, @ColorInt colorFilter: Int? = null) { + val transformations = mutableListOf<Transformation<Bitmap>>( + BlurTransformation(20, sampling) + ) + if (colorFilter != null) { + transformations.add(ColorFilterTransformation(colorFilter)) + } + if (rounded) { + transformations.add(CircleCrop()) + } + buildGlideRequest(GlideApp.with(imageView), matrixItem.avatarUrl) + .apply(RequestOptions.bitmapTransform(MultiTransformation(transformations))) + .into(imageView) + } + @AnyThread fun getCachedDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem): Drawable { return buildGlideRequest(glideRequests, matrixItem.avatarUrl) .onlyRetrieveFromCache(true) + .apply(RequestOptions.circleCropTransform()) .submit() .get() } @@ -139,9 +163,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active private fun buildGlideRequest(glideRequests: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> { val resolvedUrl = resolvedUrl(avatarUrl) - return glideRequests - .load(resolvedUrl) - .apply(RequestOptions.circleCropTransform()) + return glideRequests.load(resolvedUrl) } private fun resolvedUrl(avatarUrl: String?): String? { 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 108e0512a7..15e982492b 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 @@ -93,6 +93,7 @@ class HomeActivity : @Inject lateinit var shortcutsHandler: ShortcutsHandler @Inject lateinit var unknownDeviceViewModelFactory: UnknownDeviceDetectorSharedViewModel.Factory @Inject lateinit var permalinkHandler: PermalinkHandler + @Inject lateinit var avatarRenderer: AvatarRenderer private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { override fun onDrawerStateChanged(newState: Int) { @@ -128,9 +129,9 @@ class HomeActivity : .observe() .subscribe { sharedAction -> when (sharedAction) { - is HomeActivitySharedAction.OpenDrawer -> views.drawerLayout.openDrawer(GravityCompat.START) + is HomeActivitySharedAction.OpenDrawer -> views.drawerLayout.openDrawer(GravityCompat.START) is HomeActivitySharedAction.CloseDrawer -> views.drawerLayout.closeDrawer(GravityCompat.START) - is HomeActivitySharedAction.OpenGroup -> { + is HomeActivitySharedAction.OpenGroup -> { views.drawerLayout.closeDrawer(GravityCompat.START) replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java, allowStateLoss = true) } @@ -147,9 +148,9 @@ class HomeActivity : homeActivityViewModel.observeViewEvents { when (it) { is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it) - is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it) - HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush() - is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) + is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it) + HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush() + is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) }.exhaustive } homeActivityViewModel.subscribe(this) { renderState(it) } @@ -202,7 +203,7 @@ class HomeActivity : private fun renderState(state: HomeActivityViewState) { when (val status = state.initialSyncProgressServiceStatus) { - is InitialSyncProgressService.Status.Idle -> { + is InitialSyncProgressService.Status.Idle -> { views.waitingView.root.isVisible = false } is InitialSyncProgressService.Status.Progressing -> { @@ -304,9 +305,9 @@ class HomeActivity : uid = "upgradeSecurity", title = getString(titleRes), description = getString(descRes), - iconId = R.drawable.ic_shield_warning, - matrixItem = userItem + iconId = R.drawable.ic_shield_warning ).apply { + viewBinder = VerificationVectorAlert.ViewBinder(userItem, avatarRenderer) colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent) contentAction = Runnable { (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { @@ -367,11 +368,11 @@ class HomeActivity : bugReporter.openBugReportScreen(this, false) return true } - R.id.menu_home_filter -> { + R.id.menu_home_filter -> { navigator.openRoomsFiltering(this) return true } - R.id.menu_home_setting -> { + R.id.menu_home_setting -> { navigator.openSettings(this) return true } 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 afaa290190..44e02fea0b 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 @@ -32,20 +32,20 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor +import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +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.pushrules.RuleIds import org.matrix.android.sdk.api.session.InitialSyncProgressService import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.util.toMatrixItem -import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse -import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.asObservable import org.matrix.android.sdk.rx.rx @@ -211,8 +211,8 @@ class HomeActivityViewModel @AssistedInject constructor( } else { // Try to initialize cross signing in background if possible Timber.d("Initialize cross signing...") - awaitCallback<Unit> { - try { + try { + awaitCallback<Unit> { session.cryptoService().crossSigningService().initializeCrossSigning( object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { @@ -235,9 +235,9 @@ class HomeActivityViewModel @AssistedInject constructor( callback = it ) Timber.d("Initialize cross signing SUCCESS") - } catch (failure: Throwable) { - Timber.e(failure, "Failed to initialize cross signing") } + } catch (failure: Throwable) { + Timber.e(failure, "Failed to initialize cross signing") } } } 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 db9fb61cb3..4c7b7aa991 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 @@ -21,7 +21,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat -import androidx.lifecycle.Observer import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -33,13 +32,13 @@ import im.vector.app.core.glide.GlideApp import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.core.ui.views.ActiveCallView -import im.vector.app.core.ui.views.ActiveCallViewHolder +import im.vector.app.core.ui.views.CurrentCallsView +import im.vector.app.core.ui.views.KnownCallsViewHolder import im.vector.app.core.ui.views.KeysBackupBanner import im.vector.app.databinding.FragmentHomeDetailBinding -import im.vector.app.features.call.SharedActiveCallViewModel +import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.VectorCallActivity -import im.vector.app.features.call.WebRtcPeerConnectionManager +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.room.list.RoomListFragment import im.vector.app.features.home.room.list.RoomListParams import im.vector.app.features.popup.PopupAlertManager @@ -66,11 +65,11 @@ class HomeDetailFragment @Inject constructor( private val serverBackupStatusViewModelFactory: ServerBackupStatusViewModel.Factory, private val avatarRenderer: AvatarRenderer, private val alertManager: PopupAlertManager, - private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, + private val callManager: WebRtcCallManager, private val vectorPreferences: VectorPreferences ) : VectorBaseFragment<FragmentHomeDetailBinding>(), KeysBackupBanner.Delegate, - ActiveCallView.Callback, + CurrentCallsView.Callback, ServerBackupStatusViewModel.Factory { private val viewModel: HomeDetailViewModel by fragmentViewModel() @@ -78,18 +77,18 @@ class HomeDetailFragment @Inject constructor( private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel() private lateinit var sharedActionViewModel: HomeSharedActionViewModel - private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel + private lateinit var sharedCallActionViewModel: SharedKnownCallsViewModel override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeDetailBinding { return FragmentHomeDetailBinding.inflate(inflater, container, false) } - private val activeCallViewHolder = ActiveCallViewHolder() + private val activeCallViewHolder = KnownCallsViewHolder() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java) - sharedCallActionViewModel = activityViewModelProvider.get(SharedActiveCallViewModel::class.java) + sharedCallActionViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java) setupBottomNavigationView() setupToolbar() @@ -127,9 +126,9 @@ class HomeDetailFragment @Inject constructor( } sharedCallActionViewModel - .activeCall - .observe(viewLifecycleOwner, Observer { - activeCallViewHolder.updateCall(it, webRtcPeerConnectionManager) + .liveKnownCalls + .observe(viewLifecycleOwner, { + activeCallViewHolder.updateCall(callManager.getCurrentCall(), callManager.getCalls()) invalidateOptionsMenu() }) } @@ -160,9 +159,9 @@ class HomeDetailFragment @Inject constructor( uid = uid, title = getString(R.string.new_session), description = getString(R.string.verify_this_session, newest.displayName ?: newest.deviceId ?: ""), - iconId = R.drawable.ic_shield_warning, - matrixItem = user + iconId = R.drawable.ic_shield_warning ).apply { + viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer) colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent) contentAction = Runnable { (weakCurrentActivity?.get() as? VectorBaseActivity<*>) @@ -188,9 +187,9 @@ class HomeDetailFragment @Inject constructor( uid = uid, title = getString(R.string.review_logins), description = getString(R.string.verify_other_sessions), - iconId = R.drawable.ic_shield_warning, - matrixItem = user + iconId = R.drawable.ic_shield_warning ).apply { + viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer) colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent) contentAction = Runnable { (weakCurrentActivity?.get() as? VectorBaseActivity<*>)?.let { @@ -336,14 +335,14 @@ class HomeDetailFragment @Inject constructor( } override fun onTapToReturnToCall() { - sharedCallActionViewModel.activeCall.value?.let { call -> + callManager.getCurrentCall()?.let { call -> VectorCallActivity.newIntent( context = requireContext(), callId = call.callId, - roomId = call.roomId, - otherUserId = call.otherUserId, - isIncomingCall = !call.isOutgoing, - isVideoCall = call.isVideoCall, + roomId = call.mxCall.roomId, + otherUserId = call.mxCall.opponentUserId, + isIncomingCall = !call.mxCall.isOutgoing, + isVideoCall = call.mxCall.isVideoCall, mode = null ).let { startActivity(it) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index e034e373f3..98ad6c454c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -72,7 +72,10 @@ sealed class RoomDetailAction : VectorViewModelAction { data class IgnoreUser(val userId: String?) : RoomDetailAction() object ResendAll : RoomDetailAction() + + data class StartCallWithPhoneNumber(val phoneNumber: String, val videoCall: Boolean): RoomDetailAction() data class StartCall(val isVideo: Boolean) : RoomDetailAction() + data class AcceptCall(val callId: String): RoomDetailAction() object EndCall : RoomDetailAction() data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt index c5484b2724..906d81bc25 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailActivity.kt @@ -74,7 +74,7 @@ class RoomDetailActivity : } // Simple filter - private var currentRoomId: String? = null + var currentRoomId: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index fc380193d1..8a219d0cba 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -89,15 +89,13 @@ import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.ui.views.ActiveCallView -import im.vector.app.core.ui.views.ActiveCallViewHolder +import im.vector.app.core.ui.views.CurrentCallsView +import im.vector.app.core.ui.views.KnownCallsViewHolder import im.vector.app.core.ui.views.ActiveConferenceView import im.vector.app.core.ui.views.JumpToReadMarkerView import im.vector.app.core.ui.views.NotificationAreaView import im.vector.app.core.utils.Debouncer import im.vector.app.core.utils.KeyboardStateUtils -import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL -import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES import im.vector.app.core.utils.TextUtils import im.vector.app.core.utils.checkPermissions @@ -120,10 +118,10 @@ import im.vector.app.features.attachments.ContactAttachment import im.vector.app.features.attachments.preview.AttachmentsPreviewActivity import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs import im.vector.app.features.attachments.toGroupedContentAttachmentData -import im.vector.app.features.call.SharedActiveCallViewModel +import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.VectorCallActivity -import im.vector.app.features.call.WebRtcPeerConnectionManager import im.vector.app.features.call.conference.JitsiCallViewModel +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.command.Command import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.util.toImageRes @@ -224,11 +222,11 @@ class RoomDetailFragment @Inject constructor( private val vectorPreferences: VectorPreferences, private val colorProvider: ColorProvider, private val notificationUtils: NotificationUtils, - private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, private val matrixItemColorProvider: MatrixItemColorProvider, private val imageContentRenderer: ImageContentRenderer, private val roomDetailPendingActionStore: RoomDetailPendingActionStore, - private val pillsPostProcessorFactory: PillsPostProcessor.Factory + private val pillsPostProcessorFactory: PillsPostProcessor.Factory, + private val callManager: WebRtcCallManager ) : VectorBaseFragment<FragmentRoomDetailBinding>(), TimelineEventController.Callback, @@ -237,7 +235,7 @@ class RoomDetailFragment @Inject constructor( AttachmentTypeSelectorView.Callback, AttachmentsHelper.Callback, GalleryOrCameraDialogHelper.Listener, - ActiveCallView.Callback { + CurrentCallsView.Callback { companion object { /** @@ -283,7 +281,7 @@ class RoomDetailFragment @Inject constructor( override fun getMenuRes() = R.menu.menu_timeline private lateinit var sharedActionViewModel: MessageSharedActionViewModel - private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel + private lateinit var knownCallsViewModel: SharedKnownCallsViewModel private lateinit var layoutManager: LinearLayoutManager private lateinit var jumpToBottomViewVisibilityManager: JumpToBottomViewVisibilityManager @@ -291,19 +289,30 @@ class RoomDetailFragment @Inject constructor( private lateinit var attachmentsHelper: AttachmentsHelper private lateinit var keyboardStateUtils: KeyboardStateUtils + private lateinit var callActionsHandler : StartCallActionsHandler private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView private var lockSendButton = false - private val activeCallViewHolder = ActiveCallViewHolder() + private val knownCallsViewHolder = KnownCallsViewHolder() private lateinit var emojiPopup: EmojiPopup override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) - sharedCallActionViewModel = activityViewModelProvider.get(SharedActiveCallViewModel::class.java) + knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java) attachmentsHelper = AttachmentsHelper(requireContext(), this).register() + callActionsHandler = StartCallActionsHandler( + roomId = roomDetailArgs.roomId, + fragment = this, + vectorPreferences = vectorPreferences, + roomDetailViewModel = roomDetailViewModel, + callManager = callManager, + startCallActivityResultLauncher = startCallActivityResultLauncher, + showDialogWithMessage = ::showDialogWithMessage, + onTapToReturnToCall = ::onTapToReturnToCall + ).register() keyboardStateUtils = KeyboardStateUtils(requireActivity()) setupToolbar(views.roomToolbar) setupRecyclerView() @@ -327,10 +336,10 @@ class RoomDetailFragment @Inject constructor( } .disposeOnDestroyView() - sharedCallActionViewModel - .activeCall + knownCallsViewModel + .liveKnownCalls .observe(viewLifecycleOwner, { - activeCallViewHolder.updateCall(it, webRtcPeerConnectionManager) + knownCallsViewHolder.updateCall(callManager.getCurrentCall(), it) invalidateOptionsMenu() }) @@ -387,6 +396,7 @@ class RoomDetailFragment @Inject constructor( } is RoomDetailViewEvents.StartChatEffect -> handleChatEffect(it.type) RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() + is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) }.exhaustive } @@ -395,7 +405,16 @@ class RoomDetailFragment @Inject constructor( } } - private fun handleChatEffect(chatEffect: ChatEffect) { + private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) { + val intent = VectorCallActivity.newIntent( + context = vectorBaseActivity, + mxCall = event.call.mxCall, + mode = VectorCallActivity.INCOMING_ACCEPT + ) + startActivity(intent) + } + + private fun handleChatEffect(chatEffect: ChatEffect) { when (chatEffect) { ChatEffect.CONFETTI -> { views.viewKonfetti.isVisible = true @@ -599,7 +618,7 @@ class RoomDetailFragment @Inject constructor( } override fun onDestroy() { - activeCallViewHolder.unBind(webRtcPeerConnectionManager) + knownCallsViewHolder.unBind() roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) super.onDestroy() } @@ -630,7 +649,7 @@ class RoomDetailFragment @Inject constructor( } private fun setupActiveCallView() { - activeCallViewHolder.bind( + knownCallsViewHolder.bind( views.activeCallPiP, views.activeCallView, views.activeCallPiPWrap, @@ -754,9 +773,12 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations) true } - R.id.voice_call, + R.id.voice_call -> { + callActionsHandler.onVoiceCallClicked() + true + } R.id.video_call -> { - handleCallRequest(item) + callActionsHandler.onVideoCallClicked() true } R.id.hangup_call -> { @@ -783,76 +805,6 @@ class RoomDetailFragment @Inject constructor( } } - private fun handleCallRequest(item: MenuItem) = withState(roomDetailViewModel) { state -> - val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState - val isVideoCall = item.itemId == R.id.video_call - when (roomSummary.joinedMembersCount) { - 1 -> { - val pendingInvite = roomSummary.invitedMembersCount ?: 0 > 0 - if (pendingInvite) { - // wait for other to join - showDialogWithMessage(getString(R.string.cannot_call_yourself_with_invite)) - } else { - // You cannot place a call with yourself. - showDialogWithMessage(getString(R.string.cannot_call_yourself)) - } - } - 2 -> { - val activeCall = sharedCallActionViewModel.activeCall.value - if (activeCall != null) { - // resume existing if same room, if not prompt to kill and then restart new call? - if (activeCall.roomId == roomDetailArgs.roomId) { - onTapToReturnToCall() - } - // else { - // TODO might not work well, and should prompt - // webRtcPeerConnectionManager.endCall() - // safeStartCall(it, isVideoCall) - // } - } else if (!state.isAllowedToStartWebRTCCall) { - showDialogWithMessage(getString( - if (state.isDm()) { - R.string.no_permissions_to_start_webrtc_call_in_direct_room - } else { - R.string.no_permissions_to_start_webrtc_call - }) - ) - } else { - safeStartCall(isVideoCall) - } - } - else -> { - // it's jitsi call - // can you add widgets?? - if (!state.isAllowedToManageWidgets) { - // You do not have permission to start a conference call in this room - showDialogWithMessage(getString( - if (state.isDm()) { - R.string.no_permissions_to_start_conf_call_in_direct_room - } else { - R.string.no_permissions_to_start_conf_call - } - )) - } else { - if (state.activeRoomWidgets()?.filter { it.type == WidgetType.Jitsi }?.any() == true) { - // A conference is already in progress! - showDialogWithMessage(getString(R.string.conference_call_in_progress)) - } else { - AlertDialog.Builder(requireContext()) - .setTitle(if (isVideoCall) R.string.video_meeting else R.string.audio_meeting) - .setMessage(R.string.audio_video_meeting_description) - .setPositiveButton(getString(R.string.create)) { _, _ -> - // create the widget, then navigate to it.. - roomDetailViewModel.handle(RoomDetailAction.AddJitsiWidget(isVideoCall)) - } - .setNegativeButton(getString(R.string.cancel), null) - .show() - } - } - } - } - } - private fun displayDisabledIntegrationDialog() { AlertDialog.Builder(requireActivity()) .setTitle(R.string.disabled_integration_dialog_title) @@ -864,54 +816,6 @@ class RoomDetailFragment @Inject constructor( .show() } - private fun safeStartCall(isVideoCall: Boolean) { - if (vectorPreferences.preventAccidentalCall()) { - AlertDialog.Builder(requireActivity()) - .setMessage(if (isVideoCall) R.string.start_video_call_prompt_msg else R.string.start_voice_call_prompt_msg) - .setPositiveButton(if (isVideoCall) R.string.start_video_call else R.string.start_voice_call) { _, _ -> - safeStartCall2(isVideoCall) - } - .setNegativeButton(R.string.cancel, null) - .show() - } else { - safeStartCall2(isVideoCall) - } - } - - private val startCallActivityResultLauncher = registerForPermissionsResult { allGranted -> - if (allGranted) { - (roomDetailViewModel.pendingAction as? RoomDetailAction.StartCall)?.let { - roomDetailViewModel.pendingAction = null - roomDetailViewModel.handle(it) - } - } else { - context?.toast(R.string.permissions_action_not_performed_missing_permissions) - cleanUpAfterPermissionNotGranted() - } - } - - private fun safeStartCall2(isVideoCall: Boolean) { - val startCallAction = RoomDetailAction.StartCall(isVideoCall) - roomDetailViewModel.pendingAction = startCallAction - if (isVideoCall) { - if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, - requireActivity(), - startCallActivityResultLauncher, - R.string.permissions_rationale_msg_camera_and_audio)) { - roomDetailViewModel.pendingAction = null - roomDetailViewModel.handle(startCallAction) - } - } else { - if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, - requireActivity(), - startCallActivityResultLauncher, - R.string.permissions_rationale_msg_record_audio)) { - roomDetailViewModel.pendingAction = null - roomDetailViewModel.handle(startCallAction) - } - } - } - private fun renderRegularMode(text: String) { autoCompleter.exitSpecialMode() views.composerLayout.collapse() @@ -1051,6 +955,18 @@ class RoomDetailFragment @Inject constructor( } } + private val startCallActivityResultLauncher = registerForPermissionsResult { allGranted -> + if (allGranted) { + (roomDetailViewModel.pendingAction as? RoomDetailAction.StartCall)?.let { + roomDetailViewModel.pendingAction = null + roomDetailViewModel.handle(it) + } + } else { + context?.toast(R.string.permissions_action_not_performed_missing_permissions) + cleanUpAfterPermissionNotGranted() + } + } + // PRIVATE METHODS ***************************************************************************** private fun setupRecyclerView() { @@ -2019,14 +1935,14 @@ class RoomDetailFragment @Inject constructor( } override fun onTapToReturnToCall() { - sharedCallActionViewModel.activeCall.value?.let { call -> + callManager.getCurrentCall()?.let { call -> VectorCallActivity.newIntent( context = requireContext(), callId = call.callId, roomId = call.roomId, - otherUserId = call.otherUserId, - isIncomingCall = !call.isOutgoing, - isVideoCall = call.isVideoCall, + otherUserId = call.mxCall.opponentUserId, + isIncomingCall = !call.mxCall.isOutgoing, + isVideoCall = call.mxCall.isVideoCall, mode = null ).let { startActivity(it) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 81d3d622e7..9f801e7272 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -20,6 +20,7 @@ import android.net.Uri import android.view.View import androidx.annotation.StringRes import im.vector.app.core.platform.VectorViewEvents +import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.command.Command import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.util.MatrixItem @@ -73,6 +74,8 @@ sealed class RoomDetailViewEvents : VectorViewEvents { abstract class SendMessageResult : RoomDetailViewEvents() + data class DisplayAndAcceptCall(val call: WebRtcCall): RoomDetailViewEvents() + object DisplayPromptForIntegrationManager : RoomDetailViewEvents() object DisplayEnableIntegrationsWarning : RoomDetailViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index f7299363a3..9cced3d565 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -32,13 +32,15 @@ import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider -import im.vector.app.features.call.WebRtcPeerConnectionManager +import im.vector.app.features.call.dialpad.DialPadLookup +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.command.CommandParser import im.vector.app.features.command.ParsedCommand +import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder +import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsFactory import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.typing.TypingHelper @@ -112,12 +114,14 @@ class RoomDetailViewModel @AssistedInject constructor( private val rawService: RawService, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, private val stickerPickerActionHandler: StickerPickerActionHandler, - private val roomSummaryHolder: RoomSummaryHolder, + private val roomSummariesHolder: RoomSummariesHolder, private val typingHelper: TypingHelper, - private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, + private val callManager: WebRtcCallManager, private val chatEffectManager: ChatEffectManager, + private val directRoomHelper: DirectRoomHelper, timelineSettingsFactory: TimelineSettingsFactory -) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), Timeline.Listener, ChatEffectManager.Delegate { +) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), + Timeline.Listener, ChatEffectManager.Delegate, WebRtcCallManager.PSTNSupportListener { private val room = session.getRoom(initialState.roomId)!! private val eventId = initialState.eventId @@ -167,10 +171,12 @@ class RoomDetailViewModel @AssistedInject constructor( observeMyRoomMember() observeActiveRoomWidgets() observePowerLevel() + updateShowDialerOptionState() room.getRoomSummaryLive() room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, NoOpMatrixCallback()) // Inform the SDK that the room is displayed session.onRoomDisplayed(initialState.roomId) + callManager.addPstnSupportListener(this) chatEffectManager.delegate = this } @@ -264,7 +270,9 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() + is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action) is RoomDetailAction.StartCall -> handleStartCall(action) + is RoomDetailAction.AcceptCall -> handleAcceptCall(action) is RoomDetailAction.EndCall -> handleEndCall() is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) @@ -286,6 +294,23 @@ class RoomDetailViewModel @AssistedInject constructor( }.exhaustive } + private fun handleStartCallWithPhoneNumber(action: RoomDetailAction.StartCallWithPhoneNumber) { + viewModelScope.launch { + try { + val result = DialPadLookup(session, directRoomHelper, callManager).lookupPhoneNumber(action.phoneNumber) + callManager.startOutgoingCall(result.roomId, result.userId, action.videoCall) + } catch (failure: Throwable) { + _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure)) + } + } + } + + private fun handleAcceptCall(action: RoomDetailAction.AcceptCall) { + callManager.getCallById(action.callId)?.also { + _viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it)) + } + } + private fun handleDoNotShowPreviewUrlFor(action: RoomDetailAction.DoNotShowPreviewUrlFor) { previewUrlRetriever.doNotShowPreviewUrlFor(action.eventId, action.url) } @@ -310,18 +335,15 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleOpenOrCreateDm(action: RoomDetailAction.OpenOrCreateDm) { - val existingDmRoomId = session.getExistingDirectRoomWithUser(action.userId) - if (existingDmRoomId == null) { - // First create a direct room - viewModelScope.launch(Dispatchers.IO) { - val roomId = awaitCallback<String> { - session.createDirectRoom(action.userId, it) - } - _viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId)) + viewModelScope.launch { + val roomId = try { + directRoomHelper.ensureDMExists(action.userId) + } catch (failure: Throwable) { + _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure)) + return@launch } - } else { - if (existingDmRoomId != initialState.roomId) { - _viewEvents.post(RoomDetailViewEvents.OpenRoom(existingDmRoomId)) + if (roomId != initialState.roomId) { + _viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId = roomId)) } } } @@ -337,12 +359,12 @@ class RoomDetailViewModel @AssistedInject constructor( private fun handleStartCall(action: RoomDetailAction.StartCall) { room.roomSummary()?.otherMemberIds?.firstOrNull()?.let { - webRtcPeerConnectionManager.startOutgoingCall(room.roomId, it, action.isVideo) + callManager.startOutgoingCall(room.roomId, it, action.isVideo) } } private fun handleEndCall() { - webRtcPeerConnectionManager.endCall() + callManager.endCallForRoom(initialState.roomId) } private fun handleSelectStickerAttachment() { @@ -603,8 +625,8 @@ class RoomDetailViewModel @AssistedInject constructor( R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true R.id.open_matrix_apps -> true R.id.voice_call, - R.id.video_call -> true // always show for discoverability - R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null + R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty() + R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty() R.id.search -> true R.id.dev_tools -> vectorPreferences.developerMode() else -> false @@ -1322,6 +1344,7 @@ class RoomDetailViewModel @AssistedInject constructor( } } .subscribe { + Timber.v("Unread state: $it") setState { copy(unreadState = it) } } .disposeOnClear() @@ -1374,7 +1397,7 @@ class RoomDetailViewModel @AssistedInject constructor( private fun observeSummaryState() { asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary -> - roomSummaryHolder.set(summary) + roomSummariesHolder.set(summary) setState { val typingMessage = typingHelper.getTypingMessage(summary.typingUsers) copy(typingMessage = typingMessage) @@ -1418,8 +1441,18 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds)) } + override fun onPSTNSupportUpdated() { + updateShowDialerOptionState() + } + + private fun updateShowDialerOptionState() { + setState { + copy(showDialerOption = callManager.supportsPSTNProtocol) + } + } + override fun onCleared() { - roomSummaryHolder.clear() + roomSummariesHolder.remove(room.roomId) timeline.dispose() timeline.removeAllListeners() if (vectorPreferences.sendTypingNotifs()) { @@ -1427,6 +1460,7 @@ class RoomDetailViewModel @AssistedInject constructor( } chatEffectManager.delegate = null chatEffectManager.dispose() + callManager.removePstnSupportListener(this) super.onCleared() } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 38b93f9363..8c2b3ffe98 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -74,7 +74,8 @@ data class RoomDetailViewState( val canSendMessage: Boolean = true, val canInvite: Boolean = true, val isAllowedToManageWidgets: Boolean = false, - val isAllowedToStartWebRTCCall: Boolean = true + val isAllowedToStartWebRTCCall: Boolean = true, + val showDialerOption: Boolean = false ) : MvRxState { constructor(args: RoomDetailArgs) : this( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt new file mode 100644 index 0000000000..30f1ecdc6d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail + +import android.os.Bundle +import androidx.activity.result.ActivityResultLauncher +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.platform.Restorable +import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL +import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL +import im.vector.app.core.utils.checkPermissions +import im.vector.app.features.call.DialerChoiceBottomSheet +import im.vector.app.features.call.dialpad.CallDialPadBottomSheet +import im.vector.app.features.call.dialpad.DialPadFragment +import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.features.settings.VectorPreferences +import org.matrix.android.sdk.api.session.widgets.model.WidgetType + +private const val DIALER_OPTION_TAG = "DIALER_OPTION_TAG" +private const val DIAL_PAD_TAG = "DIAL_PAD_TAG" + +class StartCallActionsHandler( + private val roomId: String, + private val fragment: Fragment, + private val callManager: WebRtcCallManager, + private val vectorPreferences: VectorPreferences, + private val roomDetailViewModel: RoomDetailViewModel, + private val startCallActivityResultLauncher: ActivityResultLauncher<Array<String>>, + private val showDialogWithMessage: (String) -> Unit, + private val onTapToReturnToCall: () -> Unit): Restorable { + + fun onVideoCallClicked() { + handleCallRequest(true) + } + + fun onVoiceCallClicked() = withState(roomDetailViewModel) { + if (it.showDialerOption) { + displayDialerChoiceBottomSheet() + } else { + handleCallRequest(false) + } + } + + private fun DialerChoiceBottomSheet.applyListeners(): DialerChoiceBottomSheet { + onDialPadClicked = ::displayDialPadBottomSheet + onVoiceCallClicked = { handleCallRequest(false) } + return this + } + + private fun CallDialPadBottomSheet.applyCallback(): CallDialPadBottomSheet { + callback = object : DialPadFragment.Callback { + override fun onOkClicked(formatted: String?, raw: String?) { + if (raw.isNullOrEmpty()) return + roomDetailViewModel.handle(RoomDetailAction.StartCallWithPhoneNumber(raw, false)) + } + } + return this + } + + private fun displayDialerChoiceBottomSheet() { + DialerChoiceBottomSheet() + .applyListeners() + .show(fragment.parentFragmentManager, DIALER_OPTION_TAG) + } + + private fun displayDialPadBottomSheet() { + CallDialPadBottomSheet.newInstance(true) + .applyCallback() + .show(fragment.parentFragmentManager, DIAL_PAD_TAG) + } + + private fun handleCallRequest(isVideoCall: Boolean) = withState(roomDetailViewModel) { state -> + val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState + when (roomSummary.joinedMembersCount) { + 1 -> { + val pendingInvite = roomSummary.invitedMembersCount ?: 0 > 0 + if (pendingInvite) { + // wait for other to join + showDialogWithMessage(fragment.getString(R.string.cannot_call_yourself_with_invite)) + } else { + // You cannot place a call with yourself. + showDialogWithMessage(fragment.getString(R.string.cannot_call_yourself)) + } + } + 2 -> { + val currentCall = callManager.getCurrentCall() + if (currentCall != null) { + // resume existing if same room, if not prompt to kill and then restart new call? + if (currentCall.roomId == roomId) { + onTapToReturnToCall() + } + // else { + // TODO might not work well, and should prompt + // webRtcPeerConnectionManager.endCall() + // safeStartCall(it, isVideoCall) + // } + } else if (!state.isAllowedToStartWebRTCCall) { + showDialogWithMessage(fragment.getString( + if (state.isDm()) { + R.string.no_permissions_to_start_webrtc_call_in_direct_room + } else { + R.string.no_permissions_to_start_webrtc_call + }) + ) + } else { + safeStartCall(isVideoCall) + } + } + else -> { + // it's jitsi call + // can you add widgets?? + if (!state.isAllowedToManageWidgets) { + // You do not have permission to start a conference call in this room + showDialogWithMessage(fragment.getString( + if (state.isDm()) { + R.string.no_permissions_to_start_conf_call_in_direct_room + } else { + R.string.no_permissions_to_start_conf_call + } + )) + } else { + if (state.activeRoomWidgets()?.filter { it.type == WidgetType.Jitsi }?.any() == true) { + // A conference is already in progress! + showDialogWithMessage(fragment.getString(R.string.conference_call_in_progress)) + } else { + AlertDialog.Builder(fragment.requireContext()) + .setTitle(if (isVideoCall) R.string.video_meeting else R.string.audio_meeting) + .setMessage(R.string.audio_video_meeting_description) + .setPositiveButton(fragment.getString(R.string.create)) { _, _ -> + // create the widget, then navigate to it.. + roomDetailViewModel.handle(RoomDetailAction.AddJitsiWidget(isVideoCall)) + } + .setNegativeButton(fragment.getString(R.string.cancel), null) + .show() + } + } + } + } + } + + private fun safeStartCall(isVideoCall: Boolean) { + if (vectorPreferences.preventAccidentalCall()) { + AlertDialog.Builder(fragment.requireActivity()) + .setMessage(if (isVideoCall) R.string.start_video_call_prompt_msg else R.string.start_voice_call_prompt_msg) + .setPositiveButton(if (isVideoCall) R.string.start_video_call else R.string.start_voice_call) { _, _ -> + safeStartCall2(isVideoCall) + } + .setNegativeButton(R.string.cancel, null) + .show() + } else { + safeStartCall2(isVideoCall) + } + } + + private fun safeStartCall2(isVideoCall: Boolean) { + val startCallAction = RoomDetailAction.StartCall(isVideoCall) + roomDetailViewModel.pendingAction = startCallAction + if (isVideoCall) { + if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, + fragment.requireActivity(), + startCallActivityResultLauncher, + R.string.permissions_rationale_msg_camera_and_audio)) { + roomDetailViewModel.pendingAction = null + roomDetailViewModel.handle(startCallAction) + } + } else { + if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, + fragment.requireActivity(), + startCallActivityResultLauncher, + R.string.permissions_rationale_msg_record_audio)) { + roomDetailViewModel.pendingAction = null + roomDetailViewModel.handle(startCallAction) + } + } + } + + override fun onSaveInstanceState(outState: Bundle) = Unit + + override fun onRestoreInstanceState(savedInstanceState: Bundle?) { + if (savedInstanceState != null) { + (fragment.parentFragmentManager.findFragmentByTag(DIALER_OPTION_TAG) as? DialerChoiceBottomSheet)?.applyListeners() + (fragment.parentFragmentManager.findFragmentByTag(DIAL_PAD_TAG) as? CallDialPadBottomSheet)?.applyCallback() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/sticker/StickerPickerActionHandler.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/sticker/StickerPickerActionHandler.kt index d24b41ffb0..5039459c0c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/sticker/StickerPickerActionHandler.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/sticker/StickerPickerActionHandler.kt @@ -32,7 +32,7 @@ class StickerPickerActionHandler @Inject constructor(private val session: Sessio return@withContext RoomDetailViewEvents.DisplayEnableIntegrationsWarning } val stickerWidget = session.widgetService().getUserWidgets(WidgetType.StickerPicker.values()).firstOrNull { it.isActive } - if (stickerWidget == null || stickerWidget.computedUrl.isNullOrBlank()) { + if (stickerWidget == null || stickerWidget.widgetContent.url.isNullOrBlank()) { RoomDetailViewEvents.DisplayPromptForIntegrationManager } else { RoomDetailViewEvents.OpenStickerPicker( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 1e108a2062..29871cf307 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -30,6 +30,7 @@ import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.nextOrNull +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailViewState import im.vector.app.features.home.room.detail.UnreadState @@ -43,6 +44,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisi import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem +import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData @@ -73,6 +75,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val mergedHeaderItemFactory: MergedHeaderItemFactory, private val session: Session, + private val callManager: WebRtcCallManager, @TimelineEventControllerHandler private val backgroundHandler: Handler ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { @@ -99,6 +102,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // TODO move all callbacks to this? fun onTimelineItemAction(itemAction: RoomDetailAction) + // Introduce ViewModel scoped component (or Hilt?) fun getPreviewUrlRetriever(): PreviewUrlRetriever } @@ -199,10 +203,27 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec override fun intercept(models: MutableList<EpoxyModel<*>>) = synchronized(modelCache) { positionOfReadMarker = null adapterPositionMapping.clear() - models.forEachIndexed { index, epoxyModel -> + val callIds = mutableSetOf<String>() + val modelsIterator = models.listIterator() + val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents() + modelsIterator.withIndex().forEach { + val index = it.index + val epoxyModel = it.value + if (epoxyModel is CallTileTimelineItem) { + val callId = epoxyModel.attributes.callId + // We should remove the call tile if we already have one for this call or + // if this is an active call tile without an actual call (which can happen with permalink) + val shouldRemoveCallItem = callIds.contains(callId) + || (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive()) + if (shouldRemoveCallItem && !showHiddenEvents) { + modelsIterator.remove() + return@forEach + } + callIds.add(callId) + } if (epoxyModel is BaseEventItem) { - epoxyModel.getEventIds().forEach { - adapterPositionMapping[it] = index + epoxyModel.getEventIds().forEach { eventId -> + adapterPositionMapping[eventId] = index } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 5008f0e0aa..1697d9250e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -197,7 +197,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted EventType.CALL_CANDIDATES, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> { - noticeEventFormatter.format(timelineEvent, room?.roomSummary()) + noticeEventFormatter.format(timelineEvent) } else -> null } ?: "" diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt new file mode 100644 index 0000000000..d3dd94eae7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.app.features.home.room.detail.timeline.factory + +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.features.home.room.detail.timeline.MessageColorProvider +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider +import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory +import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory +import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder +import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem +import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_ +import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent +import org.matrix.android.sdk.api.session.room.model.call.CallSignallingContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class CallItemFactory @Inject constructor( + private val messageColorProvider: MessageColorProvider, + private val messageInformationDataFactory: MessageInformationDataFactory, + private val messageItemAttributesFactory: MessageItemAttributesFactory, + private val avatarSizeProvider: AvatarSizeProvider, + private val roomSummariesHolder: RoomSummariesHolder, + private val callManager: WebRtcCallManager +) { + + fun create(event: TimelineEvent, + highlight: Boolean, + callback: TimelineEventController.Callback? + ): VectorEpoxyModel<*>? { + if (event.root.eventId == null) return null + val roomId = event.roomId + val informationData = messageInformationDataFactory.create(event, null) + val callSignalingContent = event.getCallSignallingContent() ?: return null + val callId = callSignalingContent.callId ?: return null + val call = callManager.getCallById(callId) + val callKind = when { + call == null -> CallTileTimelineItem.CallKind.UNKNOWN + call.mxCall.isVideoCall -> CallTileTimelineItem.CallKind.VIDEO + else -> CallTileTimelineItem.CallKind.AUDIO + } + return when (event.root.getClearType()) { + EventType.CALL_ANSWER -> { + createCallTileTimelineItem( + roomId = roomId, + callId = callId, + callStatus = CallTileTimelineItem.CallStatus.IN_CALL, + callKind = callKind, + callback = callback, + highlight = highlight, + informationData = informationData, + isStillActive = call != null + ) + } + EventType.CALL_INVITE -> { + createCallTileTimelineItem( + roomId = roomId, + callId = callId, + callStatus = CallTileTimelineItem.CallStatus.INVITED, + callKind = callKind, + callback = callback, + highlight = highlight, + informationData = informationData, + isStillActive = call != null + ) + } + EventType.CALL_REJECT -> { + createCallTileTimelineItem( + roomId = roomId, + callId = callId, + callStatus = CallTileTimelineItem.CallStatus.REJECTED, + callKind = callKind, + callback = callback, + highlight = highlight, + informationData = informationData, + isStillActive = false + ) + } + EventType.CALL_HANGUP -> { + createCallTileTimelineItem( + roomId = roomId, + callId = callId, + callStatus = CallTileTimelineItem.CallStatus.ENDED, + callKind = callKind, + callback = callback, + highlight = highlight, + informationData = informationData, + isStillActive = false + ) + } + else -> null + } + } + + private fun TimelineEvent.getCallSignallingContent(): CallSignallingContent? { + return when (root.getClearType()) { + EventType.CALL_INVITE -> root.getClearContent().toModel<CallInviteContent>() + EventType.CALL_HANGUP -> root.getClearContent().toModel<CallHangupContent>() + EventType.CALL_REJECT -> root.getClearContent().toModel<CallRejectContent>() + EventType.CALL_ANSWER -> root.getClearContent().toModel<CallAnswerContent>() + else -> null + } + } + + private fun createCallTileTimelineItem( + roomId: String, + callId: String, + callKind: CallTileTimelineItem.CallKind, + callStatus: CallTileTimelineItem.CallStatus, + informationData: MessageInformationData, + highlight: Boolean, + isStillActive: Boolean, + callback: TimelineEventController.Callback? + ): CallTileTimelineItem? { + val userOfInterest = roomSummariesHolder.get(roomId)?.toMatrixItem() ?: return null + val attributes = messageItemAttributesFactory.create(null, informationData, callback).let { + CallTileTimelineItem.Attributes( + callId = callId, + callKind = callKind, + callStatus = callStatus, + informationData = informationData, + avatarRenderer = it.avatarRenderer, + messageColorProvider = messageColorProvider, + itemClickListener = it.itemClickListener, + itemLongClickListener = it.itemLongClickListener, + reactionPillCallback = it.reactionPillCallback, + readReceiptsCallback = it.readReceiptsCallback, + userOfInterest = userOfInterest, + callback = callback, + isStillActive = isStillActive + ) + } + return CallTileTimelineItem_() + .attributes(attributes) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 23bd041e95..2134645d8d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -22,7 +22,7 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder +import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration import im.vector.app.features.home.room.detail.timeline.helper.prevSameTypeEvents @@ -47,7 +47,7 @@ import javax.inject.Inject class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, private val avatarRenderer: AvatarRenderer, private val avatarSizeProvider: AvatarSizeProvider, - private val roomSummaryHolder: RoomSummaryHolder) { + private val roomSummariesHolder: RoomSummariesHolder) { private val collapsedEventIds = linkedSetOf<Long>() private val mergeItemCollapseStates = HashMap<Long, Boolean>() @@ -77,7 +77,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde } } - private fun isDirectRoom() = roomSummaryHolder.roomSummary?.isDirect.orFalse() + private fun isDirectRoom(roomId: String) = roomSummariesHolder.get(roomId)?.isDirect.orFalse() private fun buildMembershipEventsMergedSummary(currentPosition: Int, items: List<TimelineEvent>, @@ -102,7 +102,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde memberName = mergedEvent.senderInfo.disambiguatedDisplayName, localId = mergedEvent.localId, eventId = mergedEvent.root.eventId ?: "", - isDirectRoom = isDirectRoom() + isDirectRoom = isDirectRoom(event.roomId) ) mergedData.add(data) } @@ -174,7 +174,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde memberName = mergedEvent.senderInfo.disambiguatedDisplayName, localId = mergedEvent.localId, eventId = mergedEvent.root.eventId ?: "", - isDirectRoom = isDirectRoom() + isDirectRoom = isDirectRoom(event.roomId) ) mergedData.add(data) } @@ -191,8 +191,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde collapsedEventIds.removeAll(mergedEventIds) } val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } - val powerLevelsHelper = roomSummaryHolder.roomSummary?.roomId - ?.let { activeSessionHolder.getSafeActiveSession()?.getRoom(it) } + val powerLevelsHelper = activeSessionHolder.getSafeActiveSession()?.getRoom(event.roomId) ?.let { it.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)?.content?.toModel<PowerLevelsContent>() } ?.let { PowerLevelsHelper(it) } val currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: "" @@ -209,7 +208,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde readReceiptsCallback = callback, callback = callback, currentUserId = currentUserId, - roomSummary = roomSummaryHolder.roomSummary, + roomSummary = roomSummariesHolder.get(event.roomId), canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false, canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false, canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 4f52fcb54c..b3fa7c0eda 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -38,7 +38,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadSt import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.app.features.home.room.detail.timeline.item.MessageBlockCodeItem @@ -106,15 +105,17 @@ class MessageItemFactory @Inject constructor( private val messageItemAttributesFactory: MessageItemAttributesFactory, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder, - private val roomSummaryHolder: RoomSummaryHolder, private val defaultItemFactory: DefaultItemFactory, private val noticeItemFactory: NoticeItemFactory, private val avatarSizeProvider: AvatarSizeProvider, private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val session: Session) { + // TODO inject this properly? + private var roomId: String = "" + private val pillsPostProcessor by lazy { - pillsPostProcessorFactory.create(roomSummaryHolder.roomSummary?.roomId) + pillsPostProcessorFactory.create(roomId) } fun create(event: TimelineEvent, @@ -123,8 +124,8 @@ class MessageItemFactory @Inject constructor( callback: TimelineEventController.Callback? ): VectorEpoxyModel<*>? { event.root.eventId ?: return null + roomId = event.roomId val informationData = messageInformationDataFactory.create(event, nextEvent) - if (event.root.isRedacted()) { // message is redacted val attributes = messageItemAttributesFactory.create(null, informationData, callback) @@ -140,7 +141,7 @@ class MessageItemFactory @Inject constructor( || event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should display it when debugging as a notice event - return noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) + return noticeItemFactory.create(event, highlight, callback) } val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback) @@ -156,7 +157,7 @@ class MessageItemFactory @Inject constructor( is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) + is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, callback) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } } @@ -230,14 +231,13 @@ class MessageItemFactory @Inject constructor( attributes: AbsMessageItem.Attributes): VerificationRequestItem? { // If this request is not sent by me or sent to me, we should ignore it in timeline val myUserId = session.myUserId - val roomId = roomSummaryHolder.roomSummary?.roomId if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) { return null } val otherUserId = if (informationData.sentByMe) messageContent.toUserId else informationData.senderId val otherUserName = if (informationData.sentByMe) { - session.getRoomMember(messageContent.toUserId, roomId ?: "")?.displayName + session.getRoomMember(messageContent.toUserId, roomId)?.displayName } else { informationData.memberName } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index ec065543f5..cd8c682f39 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -24,7 +24,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvide import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.item.NoticeItem import im.vector.app.features.home.room.detail.timeline.item.NoticeItem_ -import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject @@ -35,9 +34,8 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv fun create(event: TimelineEvent, highlight: Boolean, - roomSummary: RoomSummary?, callback: TimelineEventController.Callback?): NoticeItem? { - val formattedText = eventFormatter.format(event, roomSummary) ?: return null + val formattedText = eventFormatter.format(event) ?: return null val informationData = informationDataFactory.create(event, null) val attributes = NoticeItem.Attributes( avatarRenderer = avatarRenderer, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt index 25b5fd718b..31adbdb8a6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt @@ -21,7 +21,6 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder import im.vector.app.features.home.room.detail.timeline.item.RoomCreateItem_ import me.gujun.android.span.span import org.matrix.android.sdk.api.session.Session @@ -33,7 +32,6 @@ import javax.inject.Inject class RoomCreateItemFactory @Inject constructor(private val stringProvider: StringProvider, private val userPreferencesProvider: UserPreferencesProvider, private val session: Session, - private val roomSummaryHolder: RoomSummaryHolder, private val noticeItemFactory: NoticeItemFactory) { fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { @@ -54,7 +52,7 @@ class RoomCreateItemFactory @Inject constructor(private val stringProvider: Stri private fun defaultRendering(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { return if (userPreferencesProvider.shouldShowHiddenEvents()) { - noticeItemFactory.create(event, false, roomSummaryHolder.roomSummary, callback) + noticeItemFactory.create(event, false, callback) } else { null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 943e78ae35..982ceb906c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -20,7 +20,6 @@ import im.vector.app.core.epoxy.EmptyItem_ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import timber.log.Timber @@ -33,8 +32,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me private val encryptionItemFactory: EncryptionItemFactory, private val roomCreateItemFactory: RoomCreateItemFactory, private val widgetItemFactory: WidgetItemFactory, - private val roomSummaryHolder: RoomSummaryHolder, private val verificationConclusionItemFactory: VerificationItemFactory, + private val callItemFactory: CallItemFactory, private val userPreferencesProvider: UserPreferencesProvider) { fun create(event: TimelineEvent, @@ -59,17 +58,19 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.STATE_ROOM_SERVER_ACL, EventType.STATE_ROOM_GUEST_ACCESS, - EventType.CALL_INVITE, - EventType.CALL_HANGUP, - EventType.CALL_ANSWER, EventType.STATE_ROOM_POWER_LEVELS, EventType.REACTION, - EventType.REDACTION -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) + EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) EventType.STATE_ROOM_WIDGET_LEGACY, EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(event, highlight, callback) EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback) // State room create EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) + // Calls + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.CALL_REJECT, + EventType.CALL_ANSWER -> callItemFactory.create(event, highlight, callback) // Crypto EventType.ENCRYPTED -> { if (event.root.isRedacted()) { @@ -85,11 +86,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_MAC, - EventType.CALL_CANDIDATES -> { + EventType.CALL_CANDIDATES, + EventType.CALL_REPLACES, + EventType.CALL_SELECT_ANSWER, + EventType.CALL_NEGOTIATE -> { // TODO These are not filtered out by timeline when encrypted // For now manually ignore if (userPreferencesProvider.shouldShowHiddenEvents()) { - noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) + noticeItemFactory.create(event, highlight, callback) } else { null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt index 59daf5a0a0..0b623d78f1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt @@ -24,7 +24,6 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem_ import org.matrix.android.sdk.api.session.Session @@ -51,7 +50,6 @@ class VerificationItemFactory @Inject constructor( private val avatarSizeProvider: AvatarSizeProvider, private val noticeItemFactory: NoticeItemFactory, private val userPreferencesProvider: UserPreferencesProvider, - private val roomSummaryHolder: RoomSummaryHolder, private val stringProvider: StringProvider, private val session: Session ) { @@ -153,7 +151,7 @@ class VerificationItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback? ): VectorEpoxyModel<*>? { - if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) + if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, callback) return null } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt index 8d8f42b2d1..260958b19e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt @@ -24,7 +24,6 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory -import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineItem import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineItem_ import org.matrix.android.sdk.api.extensions.orFalse @@ -37,7 +36,6 @@ import javax.inject.Inject class WidgetItemFactory @Inject constructor( private val sp: StringProvider, - private val roomSummaryHolder: RoomSummaryHolder, private val messageItemAttributesFactory: MessageItemAttributesFactory, private val informationDataFactory: MessageInformationDataFactory, private val noticeItemFactory: NoticeItemFactory, @@ -58,7 +56,7 @@ class WidgetItemFactory @Inject constructor( return when (WidgetType.fromString(widgetContent.type ?: previousWidgetContent?.type ?: "")) { WidgetType.Jitsi -> createJitsiItem(event, callback, widgetContent, previousWidgetContent) // There is lot of other widget types we could improve here - else -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback) + else -> noticeItemFactory.create(event, highlight, callback) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index f4632b0e10..499e27f838 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -23,7 +23,6 @@ import im.vector.app.core.resources.StringProvider import me.gujun.android.span.span import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS @@ -41,7 +40,7 @@ class DisplayableEventFormatter @Inject constructor( private val noticeEventFormatter: NoticeEventFormatter ) { - fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean, roomSummary: RoomSummary?): CharSequence { + fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean): CharSequence { if (timelineEvent.root.isRedacted()) { return noticeEventFormatter.formatRedactedEvent(timelineEvent.root) } @@ -131,7 +130,7 @@ class DisplayableEventFormatter @Inject constructor( } else -> { return span { - text = noticeEventFormatter.format(timelineEvent, roomSummary) ?: "" + text = noticeEventFormatter.format(timelineEvent) ?: "" textStyle = "italic" } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 8204ce39ec..1c86749abc 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.format import im.vector.app.ActiveSessionDataSource import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.extensions.appendNl import org.matrix.android.sdk.api.extensions.orFalse @@ -55,6 +56,7 @@ class NoticeEventFormatter @Inject constructor( private val activeSessionDataSource: ActiveSessionDataSource, private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter, private val vectorPreferences: VectorPreferences, + private val roomSummariesHolder: RoomSummariesHolder, private val sp: StringProvider ) { @@ -65,7 +67,8 @@ class NoticeEventFormatter @Inject constructor( private fun RoomSummary?.isDm() = this?.isDirect.orFalse() - fun format(timelineEvent: TimelineEvent, rs: RoomSummary?): CharSequence? { + fun format(timelineEvent: TimelineEvent): CharSequence? { + val rs = roomSummariesHolder.get(timelineEvent.roomId) return when (val type = timelineEvent.root.getClearType()) { EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs) EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, rs) @@ -88,7 +91,11 @@ class NoticeEventFormatter @Inject constructor( EventType.CALL_INVITE, EventType.CALL_CANDIDATES, EventType.CALL_HANGUP, + EventType.CALL_REJECT, EventType.CALL_ANSWER -> formatCallEvent(type, timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + EventType.CALL_NEGOTIATE, + EventType.CALL_SELECT_ANSWER, + EventType.CALL_REPLACES, EventType.MESSAGE, EventType.REACTION, EventType.KEY_VERIFICATION_START, @@ -176,6 +183,7 @@ class NoticeEventFormatter @Inject constructor( EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName, rs) EventType.CALL_INVITE, EventType.CALL_HANGUP, + EventType.CALL_REJECT, EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName) EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, rs) else -> { @@ -344,6 +352,12 @@ class NoticeEventFormatter @Inject constructor( } else { sp.getString(R.string.notice_call_candidates, senderName) } + EventType.CALL_REJECT -> + if (event.isSentByCurrentUser()) { + sp.getString(R.string.call_tile_you_declined, "") + } else { + sp.getString(R.string.call_tile_other_declined, senderName) + } else -> null } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 8a8bf364e1..802c177197 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -45,7 +45,7 @@ import javax.inject.Inject * This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline */ class MessageInformationDataFactory @Inject constructor(private val session: Session, - private val roomSummaryHolder: RoomSummaryHolder, + private val roomSummariesHolder: RoomSummariesHolder, private val dateFormatter: VectorDateFormatter, private val vectorPreferences: VectorPreferences) { @@ -116,7 +116,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses } private fun getE2EDecoration(event: TimelineEvent): E2EDecoration { - val roomSummary = roomSummaryHolder.roomSummary + val roomSummary = roomSummariesHolder.get(event.roomId) return if ( event.root.sendState == SendState.SYNCED && roomSummary?.isEncrypted.orFalse() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummaryHolder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummariesHolder.kt similarity index 63% rename from vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummaryHolder.kt rename to vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummariesHolder.kt index d3ae091733..ac953f91f7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummaryHolder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummariesHolder.kt @@ -16,25 +16,28 @@ package im.vector.app.features.home.room.detail.timeline.helper -import im.vector.app.core.di.ScreenScope import org.matrix.android.sdk.api.session.room.model.RoomSummary import javax.inject.Inject +import javax.inject.Singleton /* - This holds an instance of the current room summary. - You should use this in the context of the timeline. + You can use this to share room summary instances within the app. + You should probably use this only in the context of the timeline */ -@ScreenScope -class RoomSummaryHolder @Inject constructor() { +@Singleton +class RoomSummariesHolder @Inject constructor() { - var roomSummary: RoomSummary? = null - private set + private var roomSummaries = HashMap<String, RoomSummary>() fun set(roomSummary: RoomSummary) { - this.roomSummary = roomSummary + roomSummaries[roomSummary.roomId] = roomSummary } + fun get(roomId: String) = roomSummaries[roomId] + + fun remove(roomId: String) = roomSummaries.remove(roomId) + fun clear() { - roomSummary = null + roomSummaries.clear() } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 4fcac6c7f7..eb5b8081f9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -38,6 +38,7 @@ object TimelineDisplayableEvents { EventType.CALL_INVITE, EventType.CALL_HANGUP, EventType.CALL_ANSWER, + EventType.CALL_REJECT, EventType.ENCRYPTED, EventType.STATE_ROOM_ENCRYPTION, EventType.STATE_ROOM_GUEST_ACCESS, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt new file mode 100644 index 0000000000..2d40035df1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt @@ -0,0 +1,170 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.app.features.home.room.detail.timeline.item + +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.ImageView +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.extensions.setLeftDrawable +import im.vector.app.core.extensions.setTextWithColoredPart +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.RoomDetailAction +import im.vector.app.features.home.room.detail.timeline.MessageColorProvider +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import org.matrix.android.sdk.api.util.MatrixItem +import timber.log.Timber + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state) +abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Holder>() { + + override val baseAttributes: AbsBaseMessageItem.Attributes + get() = attributes + + @EpoxyAttribute + lateinit var attributes: Attributes + + override fun getViewType() = STUB_ID + + override fun bind(holder: Holder) { + super.bind(holder) + holder.endGuideline.updateLayoutParams<RelativeLayout.LayoutParams> { + this.marginEnd = leftGuideline + } + holder.creatorNameView.text = attributes.userOfInterest.getBestName() + attributes.avatarRenderer.render(attributes.userOfInterest, holder.creatorAvatarView) + if (attributes.callKind != CallKind.UNKNOWN) { + holder.callKindView.isVisible = true + holder.callKindView.setText(attributes.callKind.title) + holder.callKindView.setLeftDrawable(attributes.callKind.icon) + } else { + holder.callKindView.isVisible = false + } + if (attributes.callStatus == CallStatus.INVITED && !attributes.informationData.sentByMe && attributes.isStillActive) { + holder.acceptRejectViewGroup.isVisible = true + holder.acceptView.setOnClickListener { + attributes.callback?.onTimelineItemAction(RoomDetailAction.AcceptCall(callId = attributes.callId)) + } + holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.color.riotx_notice) + holder.rejectView.setOnClickListener { + attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall) + } + holder.statusView.isVisible = false + when (attributes.callKind) { + CallKind.CONFERENCE -> { + holder.rejectView.setText(R.string.ignore) + holder.acceptView.setText(R.string.join) + holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.color.riotx_accent) + } + CallKind.AUDIO -> { + holder.rejectView.setText(R.string.call_notification_reject) + holder.acceptView.setText(R.string.call_notification_answer) + holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.color.riotx_accent) + } + CallKind.VIDEO -> { + holder.rejectView.setText(R.string.call_notification_reject) + holder.acceptView.setText(R.string.call_notification_answer) + holder.acceptView.setLeftDrawable(R.drawable.ic_call_video_small, R.color.riotx_accent) + } + else -> { + Timber.w("Shouldn't be in that state") + } + } + } else { + holder.acceptRejectViewGroup.isVisible = false + holder.statusView.isVisible = true + } + holder.statusView.setCallStatus(attributes) + renderSendState(holder.view, null, holder.failedToSendIndicator) + } + + private fun TextView.setCallStatus(attributes: Attributes) { + when (attributes.callStatus) { + CallStatus.INVITED -> if (attributes.informationData.sentByMe) { + setText(R.string.call_tile_you_started_call) + } else { + text = context.getString(R.string.call_tile_other_started_call, attributes.userOfInterest.getBestName()) + } + CallStatus.IN_CALL -> setText(R.string.call_tile_in_call) + CallStatus.REJECTED -> if (attributes.informationData.sentByMe) { + setTextWithColoredPart(R.string.call_tile_you_declined, R.string.call_tile_call_back) { + val callbackAction = RoomDetailAction.StartCall(attributes.callKind == CallKind.VIDEO) + attributes.callback?.onTimelineItemAction(callbackAction) + } + } else { + text = context.getString(R.string.call_tile_other_declined, attributes.userOfInterest.getBestName()) + } + CallStatus.ENDED -> setText(R.string.call_tile_ended) + } + } + + class Holder : AbsBaseMessageItem.Holder(STUB_ID) { + val acceptView by bind<Button>(R.id.itemCallAcceptView) + val rejectView by bind<Button>(R.id.itemCallRejectView) + val acceptRejectViewGroup by bind<ViewGroup>(R.id.itemCallAcceptRejectViewGroup) + val callKindView by bind<TextView>(R.id.itemCallKindTextView) + val creatorAvatarView by bind<ImageView>(R.id.itemCallCreatorAvatar) + val creatorNameView by bind<TextView>(R.id.itemCallCreatorNameTextView) + val statusView by bind<TextView>(R.id.itemCallStatusTextView) + val endGuideline by bind<View>(R.id.messageEndGuideline) + val failedToSendIndicator by bind<ImageView>(R.id.messageFailToSendIndicator) + } + + companion object { + private const val STUB_ID = R.id.messageCallStub + } + + data class Attributes( + val callId: String, + val callKind: CallKind, + val callStatus: CallStatus, + val userOfInterest: MatrixItem, + val isStillActive: Boolean, + val callback: TimelineEventController.Callback? = null, + override val informationData: MessageInformationData, + override val avatarRenderer: AvatarRenderer, + override val messageColorProvider: MessageColorProvider, + override val itemLongClickListener: View.OnLongClickListener? = null, + override val itemClickListener: View.OnClickListener? = null, + override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null, + override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null + ) : AbsBaseMessageItem.Attributes + + enum class CallKind(@DrawableRes val icon: Int, @StringRes val title: Int) { + VIDEO(R.drawable.ic_call_video_small, R.string.action_video_call), + AUDIO(R.drawable.ic_call_audio_small, R.string.action_voice_call), + CONFERENCE(R.drawable.ic_call_conference_small, R.string.conference_call_in_progress), + UNKNOWN(0, 0) + } + + enum class CallStatus { + INVITED, + IN_CALL, + REJECTED, + ENDED; + + fun isActive() = this == INVITED || this == IN_CALL + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt index 33a6f627a1..662f11e7c2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt @@ -43,7 +43,7 @@ abstract class RoomWidgetItem : EpoxyModelWithHolder<RoomWidgetItem.Holder>() { override fun bind(holder: Holder) { super.bind(holder) holder.widgetName.text = widget.name - holder.widgetUrl.text = tryOrNull { URL(widget.computedUrl) }?.host ?: widget.computedUrl + holder.widgetUrl.text = tryOrNull { URL(widget.widgetContent.url) }?.host ?: widget.widgetContent.url if (iconRes != null) { holder.iconImage.isVisible = true holder.iconImage.setImageResource(iconRes!!) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index 7bb9940ec4..06cb0172d0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -86,7 +86,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor var latestEventTime: CharSequence = "" val latestEvent = roomSummary.latestPreviewableEvent if (latestEvent != null) { - latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect.not(), roomSummary) + latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect.not()) latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST) } val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers) diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomAction.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomAction.kt index cd9df8dc96..be9ad61868 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomAction.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomAction.kt @@ -17,8 +17,8 @@ package im.vector.app.features.invite import im.vector.app.core.platform.VectorViewModelAction -import im.vector.app.features.userdirectory.PendingInvitee +import im.vector.app.features.userdirectory.PendingSelection sealed class InviteUsersToRoomAction : VectorViewModelAction { - data class InviteSelectedUsers(val invitees: Set<PendingInvitee>) : InviteUsersToRoomAction() + data class InviteSelectedUsers(val selections: Set<PendingSelection>) : InviteUsersToRoomAction() } diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt index 9949255e32..f9f5b2b995 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomActivity.kt @@ -39,6 +39,7 @@ import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.toast import im.vector.app.features.contactsbook.ContactsBookFragment import im.vector.app.features.contactsbook.ContactsBookViewModel +import im.vector.app.features.contactsbook.ContactsBookViewState import im.vector.app.features.userdirectory.UserListFragment import im.vector.app.features.userdirectory.UserListFragmentArgs import im.vector.app.features.userdirectory.UserListSharedAction @@ -53,7 +54,7 @@ import javax.inject.Inject @Parcelize data class InviteUsersToRoomArgs(val roomId: String) : Parcelable -class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory { +class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory, ContactsBookViewModel.Factory, InviteUsersToRoomViewModel.Factory { private val viewModel: InviteUsersToRoomViewModel by viewModel() private lateinit var sharedActionViewModel: UserListSharedActionViewModel @@ -67,9 +68,11 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa injector.inject(this) } - override fun create(initialState: UserListViewState): UserListViewModel { - return userListViewModelFactory.create(initialState) - } + override fun create(initialState: UserListViewState) = userListViewModelFactory.create(initialState) + + override fun create(initialState: ContactsBookViewState) = contactsBookViewModelFactory.create(initialState) + + override fun create(initialState: InviteUsersToRoomViewState) = inviteUsersToRoomViewModelFactory.create(initialState) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -92,7 +95,6 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa } .disposeOnDestroy() if (isFirstCreation()) { - val args: InviteUsersToRoomArgs? = intent.extras?.getParcelable(MvRx.KEY_ARG) addFragment( R.id.container, UserListFragment::class.java, @@ -100,7 +102,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa title = getString(R.string.invite_users_to_room_title), menuResId = R.menu.vector_invite_users_to_room, excludedUserIds = viewModel.getUserIdsOfRoomMembers(), - existingRoomId = args?.roomId + showInviteActions = false ) ) } @@ -110,7 +112,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) { if (action.itemId == R.id.action_invite_users_to_room_invite) { - viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.invitees)) + viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.selections)) } } diff --git a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt index a694ee36ba..55730ff4ec 100644 --- a/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/invite/InviteUsersToRoomViewModel.kt @@ -17,6 +17,7 @@ package im.vector.app.features.invite import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted @@ -25,7 +26,7 @@ import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider -import im.vector.app.features.userdirectory.PendingInvitee +import im.vector.app.features.userdirectory.PendingSelection import io.reactivex.Observable import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.rx.rx @@ -47,37 +48,40 @@ class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted @JvmStatic override fun create(viewModelContext: ViewModelContext, state: InviteUsersToRoomViewState): InviteUsersToRoomViewModel? { - val activity: InviteUsersToRoomActivity = (viewModelContext as ActivityViewModelContext).activity() - return activity.inviteUsersToRoomViewModelFactory.create(state) + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") } } override fun handle(action: InviteUsersToRoomAction) { when (action) { - is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.invitees) + is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.selections) } } - private fun inviteUsersToRoom(invitees: Set<PendingInvitee>) { + private fun inviteUsersToRoom(selections: Set<PendingSelection>) { _viewEvents.post(InviteUsersToRoomViewEvents.Loading) - Observable.fromIterable(invitees).flatMapCompletable { user -> + Observable.fromIterable(selections).flatMapCompletable { user -> when (user) { - is PendingInvitee.UserPendingInvitee -> room.rx().invite(user.user.userId, null) - is PendingInvitee.ThreePidPendingInvitee -> room.rx().invite3pid(user.threePid) + is PendingSelection.UserPendingSelection -> room.rx().invite(user.user.userId, null) + is PendingSelection.ThreePidPendingSelection -> room.rx().invite3pid(user.threePid) } }.subscribe( { - val successMessage = when (invitees.size) { + val successMessage = when (selections.size) { 1 -> stringProvider.getString(R.string.invitation_sent_to_one_user, - invitees.first().getBestName()) + selections.first().getBestName()) 2 -> stringProvider.getString(R.string.invitations_sent_to_two_users, - invitees.first().getBestName(), - invitees.last().getBestName()) + selections.first().getBestName(), + selections.last().getBestName()) else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users, - invitees.size - 1, - invitees.first().getBestName(), - invitees.size - 1) + selections.size - 1, + selections.first().getBestName(), + selections.size - 1) } _viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage)) }, diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt index 207256c75a..e5bdc9bca8 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt @@ -31,8 +31,7 @@ import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider -import im.vector.app.features.raw.wellknown.getElementWellknown -import im.vector.app.features.raw.wellknown.isE2EByDefault +import im.vector.app.features.createdirect.DirectRoomHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull @@ -40,7 +39,6 @@ import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkParser -import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.internal.util.awaitCallback @@ -49,6 +47,7 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( @Assisted initialState: MatrixToBottomSheetState, private val session: Session, private val stringProvider: StringProvider, + private val directRoomHelper: DirectRoomHelper, private val rawService: RawService) : VectorViewModel<MatrixToBottomSheetState, MatrixToAction, MatrixToViewEvents>(initialState) { @AssistedFactory @@ -77,8 +76,8 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( return } - when (permalinkData) { - is PermalinkData.UserLink -> { + when (permalinkData) { + is PermalinkData.UserLink -> { val user = resolveUser(permalinkData.userId) setState { copy( @@ -87,11 +86,11 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( ) } } - is PermalinkData.RoomLink -> { + is PermalinkData.RoomLink -> { // not yet supported _viewEvents.post(MatrixToViewEvents.Dismiss) } - is PermalinkData.GroupLink -> { + is PermalinkData.GroupLink -> { // not yet supported _viewEvents.post(MatrixToViewEvents.Dismiss) } @@ -126,42 +125,23 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor( } private fun handleStartChatting(action: MatrixToAction.StartChattingWithUser) { - val mxId = action.matrixItem.id - val existing = session.getExistingDirectRoomWithUser(mxId) - if (existing != null) { - // navigate to this room - _viewEvents.post(MatrixToViewEvents.NavigateToRoom(existing)) - } else { + viewModelScope.launch { setState { copy(startChattingState = Loading()) } - // we should create the room then navigate - viewModelScope.launch(Dispatchers.IO) { - val adminE2EByDefault = rawService.getElementWellknown(session.myUserId) - ?.isE2EByDefault() - ?: true - - val roomParams = CreateRoomParams() - .apply { - invitedUserIds.add(mxId) - setDirectMessage() - enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault - } - - val roomId = try { - awaitCallback<String> { session.createRoom(roomParams, it) } - } catch (failure: Throwable) { - setState { - copy(startChattingState = Fail(Exception(stringProvider.getString(R.string.invite_users_to_room_failure)))) - } - return@launch - } + val roomId = try { + directRoomHelper.ensureDMExists(action.matrixItem.id) + } catch (failure: Throwable) { setState { - // we can hide this button has we will navigate out - copy(startChattingState = Uninitialized) + copy(startChattingState = Fail(Exception(stringProvider.getString(R.string.invite_users_to_room_failure)))) } - _viewEvents.post(MatrixToViewEvents.NavigateToRoom(roomId)) + return@launch } + setState { + // we can hide this button has we will navigate out + copy(startChattingState = Uninitialized) + } + _viewEvents.post(MatrixToViewEvents.NavigateToRoom(roomId)) } } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index c802a5f65c..76e4cad28f 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -19,9 +19,11 @@ package im.vector.app.features.navigation import android.app.Activity import android.content.Context import android.content.Intent +import android.os.Build import android.view.View import android.view.Window import androidx.activity.result.ActivityResultLauncher +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityOptionsCompat import androidx.core.app.TaskStackBuilder @@ -34,6 +36,7 @@ import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.toast import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.call.conference.VectorJitsiActivity +import im.vector.app.features.call.transfer.CallTransferActivity import im.vector.app.features.createdirect.CreateDirectRoomActivity import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity @@ -298,8 +301,17 @@ class DefaultNavigator @Inject constructor( override fun openRoomWidget(context: Context, roomId: String, widget: Widget, options: Map<String, Any>?) { if (widget.type is WidgetType.Jitsi) { - val enableVideo = options?.get(JitsiCallViewModel.ENABLE_VIDEO_OPTION) == true - context.startActivity(VectorJitsiActivity.newIntent(context, roomId = roomId, widgetId = widget.widgetId, enableVideo = enableVideo)) + // Jitsi SDK is now for API 23+ + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + AlertDialog.Builder(context) + .setTitle(R.string.dialog_title_error) + .setMessage(R.string.error_jitsi_not_supported_on_old_device) + .setPositiveButton(R.string.ok, null) + .show() + } else { + val enableVideo = options?.get(JitsiCallViewModel.ENABLE_VIDEO_OPTION) == true + context.startActivity(VectorJitsiActivity.newIntent(context, roomId = roomId, widgetId = widget.widgetId, enableVideo = enableVideo)) + } } else { val widgetArgs = widgetArgsBuilder.buildRoomWidgetArgs(roomId, widget) context.startActivity(WidgetActivity.newIntent(context, widgetArgs)) @@ -350,6 +362,11 @@ class DefaultNavigator @Inject constructor( context.startActivity(RoomDevToolActivity.intent(context, roomId)) } + override fun openCallTransfer(context: Context, callId: String) { + val intent = CallTransferActivity.newIntent(context, callId) + context.startActivity(intent) + } + private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) { if (buildTask) { val stackBuilder = TaskStackBuilder.create(context) diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 4d09bde93c..b4bd677b0c 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -119,4 +119,6 @@ interface Navigator { fun openSearch(context: Context, roomId: String) fun openDevTools(context: Context, roomId: String) + + fun openCallTransfer(context: Context, callId: String) } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index 9c2dc9b26d..c1bb1dde36 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -91,7 +91,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St if (room == null) { Timber.e("## Unable to resolve room for eventId [$event]") // Ok room is not known in store, but we can still display something - val body = displayableEventFormatter.format(event, false, null) + val body = displayableEventFormatter.format(event, false) val roomName = stringProvider.getString(R.string.notification_unknown_room_name) val senderDisplayName = event.senderInfo.disambiguatedDisplayName @@ -124,7 +124,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St } } - val body = displayableEventFormatter.format(event, false, room.roomSummary()).toString() + val body = displayableEventFormatter.format(event, false).toString() val roomName = room.roomSummary()?.displayName ?: "" val senderDisplayName = event.senderInfo.disambiguatedDisplayName diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 44eb278c64..e7cafc6a9b 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -30,6 +30,10 @@ import android.graphics.Bitmap import android.graphics.Canvas import android.net.Uri import android.os.Build +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.app.NotificationCompat @@ -52,6 +56,7 @@ import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.troubleshoot.TestNotificationReceiver +import org.matrix.android.sdk.api.session.call.MxCall import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -203,6 +208,10 @@ class NotificationUtils @Inject constructor(private val context: Context, }) } + fun getChannel(channelId: String): NotificationChannel? { + return notificationManager.getNotificationChannel(channelId) + } + /** * Build a polling thread listener notification * @@ -261,6 +270,11 @@ class NotificationUtils @Inject constructor(private val context: Context, return notification } + fun getChannelForIncomingCall(fromBg: Boolean): NotificationChannel? { + val notificationChannel = if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID + return getChannel(notificationChannel) + } + /** * Build an incoming call notification. * This notification starts the VectorHomeActivity which is in charge of centralizing the incoming call flow. @@ -269,19 +283,19 @@ class NotificationUtils @Inject constructor(private val context: Context, * @param roomName the room name in which the call is pending. * @param matrixId the matrix id * @param callId the call id. + * @param fromBg true if the app is in background when posting the notification * @return the call notification. */ @SuppressLint("NewApi") - fun buildIncomingCallNotification(isVideo: Boolean, - otherUserId: String, - roomId: String, - callId: String): Notification { + fun buildIncomingCallNotification(mxCall: MxCall, + title: String, + fromBg: Boolean): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) - - val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID) - .setContentTitle(ensureTitleNotEmpty(otherUserId)) + val notificationChannel = if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID + val builder = NotificationCompat.Builder(context, notificationChannel) + .setContentTitle(ensureTitleNotEmpty(title)) .apply { - if (isVideo) { + if (mxCall.isVideoCall) { setContentText(stringProvider.getString(R.string.incoming_video_call)) } else { setContentText(stringProvider.getString(R.string.incoming_voice_call)) @@ -292,24 +306,13 @@ class NotificationUtils @Inject constructor(private val context: Context, .setLights(accentColor, 500, 500) .setOngoing(true) - // Compat: Display the incoming call notification on the lock screen - builder.priority = NotificationCompat.PRIORITY_HIGH - - // - val requestId = Random.nextInt(1000) -// val pendingIntent = stackBuilder.getPendingIntent(requestId, PendingIntent.FLAG_UPDATE_CURRENT) - val contentIntent = VectorCallActivity.newIntent( context = context, - callId = callId, - roomId = roomId, - otherUserId = otherUserId, - isIncomingCall = true, - isVideoCall = isVideo, + mxCall = mxCall, mode = VectorCallActivity.INCOMING_RINGING ).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - data = Uri.parse("foobar://$callId") + data = Uri.parse("foobar://${mxCall.callId}") } val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0) @@ -317,56 +320,42 @@ class NotificationUtils @Inject constructor(private val context: Context, .addNextIntentWithParentStack(HomeActivity.newIntent(context)) .addNextIntent(VectorCallActivity.newIntent( context = context, - callId = callId, - roomId = roomId, - otherUserId = otherUserId, - isIncomingCall = true, - isVideoCall = isVideo, + mxCall = mxCall, mode = VectorCallActivity.INCOMING_ACCEPT) ) .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) - val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply { - putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ACTION_KEY, CallHeadsUpActionReceiver.CALL_ACTION_REJECT) - } - // val answerCallPendingIntent = PendingIntent.getBroadcast(context, requestId, answerCallActionReceiver, PendingIntent.FLAG_UPDATE_CURRENT) - val rejectCallPendingIntent = PendingIntent.getBroadcast( - context, - requestId + 1, - rejectCallActionReceiver, - PendingIntent.FLAG_UPDATE_CURRENT - ) + val rejectCallPendingIntent = buildRejectCallPendingIntent(mxCall.callId) builder.addAction( NotificationCompat.Action( - R.drawable.ic_call, - // IconCompat.createWithResource(applicationContext, R.drawable.ic_call) - // .setTint(ContextCompat.getColor(applicationContext, R.color.riotx_positive_accent)), - context.getString(R.string.call_notification_answer), - answerCallPendingIntent - ) - ) - - builder.addAction( - NotificationCompat.Action( - IconCompat.createWithResource(context, R.drawable.ic_call_end).setTint(ContextCompat.getColor(context, R.color.riotx_notice)), - context.getString(R.string.call_notification_reject), + IconCompat.createWithResource(context, R.drawable.ic_call_hangup).setTint(ContextCompat.getColor(context, R.color.riotx_notice)), + getActionText(R.string.call_notification_reject, R.color.riotx_notice), rejectCallPendingIntent) ) - builder.setFullScreenIntent(contentPendingIntent, true) - + builder.addAction( + NotificationCompat.Action( + R.drawable.ic_call_answer, + // IconCompat.createWithResource(applicationContext, R.drawable.ic_call) + // .setTint(ContextCompat.getColor(applicationContext, R.color.riotx_positive_accent)), + getActionText(R.string.call_notification_answer, R.color.riotx_positive_accent), + answerCallPendingIntent + ) + ) + if (fromBg) { + // Compat: Display the incoming call notification on the lock screen + builder.priority = NotificationCompat.PRIORITY_HIGH + builder.setFullScreenIntent(contentPendingIntent, true) + } return builder.build() } - fun buildOutgoingRingingCallNotification(isVideo: Boolean, - otherUserId: String, - roomId: String, - callId: String): Notification { + fun buildOutgoingRingingCallNotification(mxCall: MxCall, + title: String): Notification { val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) - val builder = NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID) - .setContentTitle(ensureTitleNotEmpty(otherUserId)) + .setContentTitle(ensureTitleNotEmpty(title)) .apply { setContentText(stringProvider.getString(R.string.call_ring)) } @@ -375,36 +364,21 @@ class NotificationUtils @Inject constructor(private val context: Context, .setLights(accentColor, 500, 500) .setOngoing(true) - val requestId = Random.nextInt(1000) - val contentIntent = VectorCallActivity.newIntent( context = context, - callId = callId, - roomId = roomId, - otherUserId = otherUserId, - isIncomingCall = true, - isVideoCall = isVideo, + mxCall = mxCall, mode = null).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP - data = Uri.parse("foobar://$callId") + data = Uri.parse("foobar://$mxCall.callId") } val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0) - val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply { - putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ACTION_KEY, CallHeadsUpActionReceiver.CALL_ACTION_REJECT) - } - - val rejectCallPendingIntent = PendingIntent.getBroadcast( - context, - requestId + 1, - rejectCallActionReceiver, - PendingIntent.FLAG_UPDATE_CURRENT - ) + val rejectCallPendingIntent = buildRejectCallPendingIntent(mxCall.callId) builder.addAction( NotificationCompat.Action( - IconCompat.createWithResource(context, R.drawable.ic_call_end).setTint(ContextCompat.getColor(context, R.color.riotx_notice)), - context.getString(R.string.call_notification_hangup), + IconCompat.createWithResource(context, R.drawable.ic_call_hangup).setTint(ContextCompat.getColor(context, R.color.riotx_notice)), + getActionText(R.string.call_notification_hangup, R.color.riotx_notice), rejectCallPendingIntent) ) builder.setContentIntent(contentPendingIntent) @@ -423,16 +397,12 @@ class NotificationUtils @Inject constructor(private val context: Context, * @return the call notification. */ @SuppressLint("NewApi") - fun buildPendingCallNotification(isVideo: Boolean, - roomName: String, - roomId: String, - matrixId: String, - callId: String, - fromBg: Boolean = false): Notification { - val builder = NotificationCompat.Builder(context, if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID) - .setContentTitle(ensureTitleNotEmpty(roomName)) + fun buildPendingCallNotification(mxCall: MxCall, + title: String): Notification { + val builder = NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID) + .setContentTitle(ensureTitleNotEmpty(title)) .apply { - if (isVideo) { + if (mxCall.isVideoCall) { setContentText(stringProvider.getString(R.string.video_call_in_progress)) } else { setContentText(stringProvider.getString(R.string.call_in_progress)) @@ -441,34 +411,18 @@ class NotificationUtils @Inject constructor(private val context: Context, .setSmallIcon(R.drawable.incoming_call_notification_transparent) .setCategory(NotificationCompat.CATEGORY_CALL) - if (fromBg) { - builder.priority = NotificationCompat.PRIORITY_LOW - builder.setOngoing(true) - } - - val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply { - data = Uri.parse("mxcall://end?$callId") - putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ACTION_KEY, CallHeadsUpActionReceiver.CALL_ACTION_REJECT) - } - - val rejectCallPendingIntent = PendingIntent.getBroadcast( - context, - System.currentTimeMillis().toInt(), - rejectCallActionReceiver, - PendingIntent.FLAG_UPDATE_CURRENT - ) + val rejectCallPendingIntent = buildRejectCallPendingIntent(mxCall.callId) builder.addAction( NotificationCompat.Action( - IconCompat.createWithResource(context, R.drawable.ic_call_end).setTint(ContextCompat.getColor(context, R.color.riotx_notice)), - context.getString(R.string.call_notification_hangup), + IconCompat.createWithResource(context, R.drawable.ic_call_hangup).setTint(ContextCompat.getColor(context, R.color.riotx_notice)), + getActionText(R.string.call_notification_hangup, R.color.riotx_notice), rejectCallPendingIntent) ) val contentPendingIntent = TaskStackBuilder.create(context) .addNextIntentWithParentStack(HomeActivity.newIntent(context)) - // TODO other userId - .addNextIntent(VectorCallActivity.newIntent(context, callId, roomId, "otherUserId", true, isVideo, null)) + .addNextIntent(VectorCallActivity.newIntent(context, mxCall, null)) .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) builder.setContentIntent(contentPendingIntent) @@ -476,12 +430,26 @@ class NotificationUtils @Inject constructor(private val context: Context, return builder.build() } + private fun buildRejectCallPendingIntent(callId: String): PendingIntent { + val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply { + putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ID, callId) + putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ACTION_KEY, CallHeadsUpActionReceiver.CALL_ACTION_REJECT) + } + return PendingIntent.getBroadcast( + context, + System.currentTimeMillis().toInt(), + rejectCallActionReceiver, + PendingIntent.FLAG_UPDATE_CURRENT + ) + } + /** * Build a temporary (because service will be stopped just after) notification for the CallService, when a call is ended */ fun buildCallEndedNotification(): Notification { - return NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID) + return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID) .setContentTitle(stringProvider.getString(R.string.call_ended)) + .setTimeoutAfter(2000) .setSmallIcon(R.drawable.ic_material_call_end_grey) .setCategory(NotificationCompat.CATEGORY_CALL) .build() @@ -901,6 +869,13 @@ class NotificationUtils @Inject constructor(private val context: Context, || setting == NotificationManager.INTERRUPTION_FILTER_ALARMS } + private fun getActionText(@StringRes stringRes: Int, @ColorRes colorRes: Int): Spannable { + return SpannableString(context.getText(stringRes)).apply { + val foregroundColorSpan = ForegroundColorSpan(ContextCompat.getColor(context, colorRes)) + setSpan(foregroundColorSpan, 0, length, 0) + } + } + private fun ensureTitleNotEmpty(title: String?): CharSequence { if (title.isNullOrBlank()) { return stringProvider.getString(R.string.app_name) diff --git a/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt b/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt new file mode 100644 index 0000000000..f80d6d1bbd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2020 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.popup + +import android.app.Activity +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import im.vector.app.R +import im.vector.app.core.extensions.setLeftDrawable +import im.vector.app.core.glide.GlideApp +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.util.MatrixItem + +class IncomingCallAlert(uid: String, + override val shouldBeDisplayedIn: ((Activity) -> Boolean) = { true } +) : DefaultVectorAlert(uid, "", "", 0, shouldBeDisplayedIn) { + + override val priority = PopupAlertManager.INCOMING_CALL_PRIORITY + override val layoutRes = R.layout.alerter_incoming_call_layout + override var colorAttribute: Int? = R.attr.riotx_alerter_background + override val dismissOnClick: Boolean = false + override val isLight: Boolean = true + + class ViewBinder(private val matrixItem: MatrixItem?, + private val avatarRenderer: AvatarRenderer, + private val isVideoCall: Boolean, + private val onAccept: () -> Unit, + private val onReject: () -> Unit) + : VectorAlert.ViewBinder { + + override fun bind(view: View) { + val (callKindText, callKindIcon) = if (isVideoCall) { + Pair(R.string.action_video_call, R.drawable.ic_call_video_small) + } else { + Pair(R.string.action_voice_call, R.drawable.ic_call_audio_small) + } + view.findViewById<TextView>(R.id.incomingCallKindView).apply { + setText(callKindText) + setLeftDrawable(callKindIcon) + } + view.findViewById<TextView>(R.id.incomingCallNameView).text = matrixItem?.getBestName() + view.findViewById<ImageView>(R.id.incomingCallAvatar)?.let { imageView -> + matrixItem?.let { avatarRenderer.render(it, imageView, GlideApp.with(view.context.applicationContext)) } + } + view.findViewById<ImageView>(R.id.incomingCallAcceptView).apply { + setOnClickListener { + onAccept() + } + setImageResource(callKindIcon) + } + view.findViewById<ImageView>(R.id.incomingCallRejectView).setOnClickListener { + onReject() + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index 1a746bba44..6952d5f653 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -21,14 +21,10 @@ import android.os.Handler import android.os.Looper import android.view.View import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS -import android.widget.ImageView import com.tapadoo.alerter.Alerter -import com.tapadoo.alerter.OnHideAlertListener -import dagger.Lazy import im.vector.app.R import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.isAnimationDisabled -import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.pin.PinActivity import im.vector.app.features.themes.ThemeUtils import timber.log.Timber @@ -38,19 +34,25 @@ import javax.inject.Singleton /** * Responsible of displaying important popup alerts on top of the screen. - * Alerts are stacked and will be displayed sequentially + * Alerts are stacked and will be displayed sequentially but sorted by priority. + * So if a new alert is posted with a higher priority than the current one it will show it instead and the current one + * will be back in the queue in first position. */ @Singleton -class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<AvatarRenderer>) { +class PopupAlertManager @Inject constructor() { + + companion object { + const val INCOMING_CALL_PRIORITY = Int.MAX_VALUE + } private var weakCurrentActivity: WeakReference<Activity>? = null private var currentAlerter: VectorAlert? = null - private val alertFiFo = mutableListOf<VectorAlert>() + private val alertQueue = mutableListOf<VectorAlert>() fun postVectorAlert(alert: VectorAlert) { - synchronized(alertFiFo) { - alertFiFo.add(alert) + synchronized(alertQueue) { + alertQueue.add(alert) } weakCurrentActivity?.get()?.runOnUiThread { displayNextIfPossible() @@ -58,8 +60,8 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava } fun cancelAlert(uid: String) { - synchronized(alertFiFo) { - alertFiFo.listIterator().apply { + synchronized(alertQueue) { + alertQueue.listIterator().apply { while (this.hasNext()) { val next = this.next() if (next.uid == uid) { @@ -82,8 +84,8 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava * Cancel all alerts, after a sign out for instance */ fun cancelAll() { - synchronized(alertFiFo) { - alertFiFo.clear() + synchronized(alertQueue) { + alertQueue.clear() } // Cancel any displayed alert @@ -98,7 +100,9 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava if (currentAlerter != null) { weakCurrentActivity?.get()?.let { Alerter.clearCurrent(it) - setLightStatusBar() + if (currentAlerter?.isLight == false) { + setLightStatusBar() + } } } weakCurrentActivity = WeakReference(activity) @@ -135,9 +139,19 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava return } val next: VectorAlert? - synchronized(alertFiFo) { - next = alertFiFo.firstOrNull() - if (next != null) alertFiFo.remove(next) + synchronized(alertQueue) { + next = alertQueue.maxByOrNull { it.priority } + // If next alert with highest priority is higher than the current one, we should display it + // and add the current one to queue again. + if (next != null && next.priority > currentAlerter?.priority ?: Int.MIN_VALUE) { + alertQueue.remove(next) + currentAlerter?.also { + alertQueue.add(0, it) + } + } else { + // otherwise, we don't do anything + return + } } currentAlerter = next next?.let { @@ -192,22 +206,19 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava } private fun showAlert(alert: VectorAlert, activity: Activity, animate: Boolean = true) { - clearLightStatusBar() - + if (!alert.isLight) { + clearLightStatusBar() + } val noAnimation = !animate || isAnimationDisabled(activity) alert.weakCurrentActivity = WeakReference(activity) - val alerter = if (alert is VerificationVectorAlert) Alerter.create(activity, R.layout.alerter_verification_layout) - else Alerter.create(activity) + val alerter = Alerter.create(activity, alert.layoutRes) alerter.setTitle(alert.title) .setText(alert.description) .also { al -> - if (alert is VerificationVectorAlert) { - val tvCustomView = al.getLayoutContainer() - tvCustomView?.findViewById<ImageView>(R.id.ivUserAvatar)?.let { imageView -> - alert.matrixItem?.let { avatarRenderer.get().render(it, imageView) } - } + al.getLayoutContainer()?.also { + alert.viewBinder?.bind(it) } } .apply { @@ -219,7 +230,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava setIcon(it) } alert.actions.forEach { action -> - addButton(action.title, R.style.AlerterButton, View.OnClickListener { + addButton(action.title, R.style.AlerterButton) { if (action.autoClose) { currentIsDismissed() Alerter.hide() @@ -229,21 +240,23 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava } catch (e: java.lang.Exception) { Timber.e("## failed to perform action") } - }) + } } - setOnClickListener(View.OnClickListener { _ -> + setOnClickListener { _ -> alert.contentAction?.let { - currentIsDismissed() - Alerter.hide() + if (alert.dismissOnClick) { + currentIsDismissed() + Alerter.hide() + } try { it.run() } catch (e: java.lang.Exception) { Timber.e("## failed to perform action") } } - }) + } } - .setOnHideListener(OnHideAlertListener { + .setOnHideListener { // called when dismissed on swipe try { alert.dismissedAction?.run() @@ -251,12 +264,14 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava Timber.e("## failed to perform action") } currentIsDismissed() - }) + } .enableSwipeToDismiss() .enableInfiniteDuration(true) .apply { if (alert.colorInt != null) { setBackgroundColorInt(alert.colorInt!!) + } else if (alert.colorAttribute != null) { + setBackgroundColorInt(ThemeUtils.getColor(activity, alert.colorAttribute!!)) } else { setBackgroundColorRes(alert.colorRes ?: R.color.notification_accent_color) } @@ -267,8 +282,9 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava private fun currentIsDismissed() { // current alert has been hidden - setLightStatusBar() - + if (currentAlerter?.isLight == false) { + setLightStatusBar() + } currentAlerter = null Handler(Looper.getMainLooper()).postDelayed({ displayNextIfPossible() diff --git a/vector/src/main/java/im/vector/app/features/popup/VectorAlert.kt b/vector/src/main/java/im/vector/app/features/popup/VectorAlert.kt index 00f80240c5..8b855fa542 100644 --- a/vector/src/main/java/im/vector/app/features/popup/VectorAlert.kt +++ b/vector/src/main/java/im/vector/app/features/popup/VectorAlert.kt @@ -17,10 +17,13 @@ package im.vector.app.features.popup import android.app.Activity +import android.view.View +import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.annotation.DrawableRes -import org.matrix.android.sdk.api.util.MatrixItem +import androidx.annotation.LayoutRes +import im.vector.app.R import java.lang.ref.WeakReference interface VectorAlert { @@ -28,6 +31,9 @@ interface VectorAlert { val title: String val description: String val iconId: Int? + val priority: Int + val dismissOnClick: Boolean + val isLight: Boolean val shouldBeDisplayedIn: ((Activity) -> Boolean) data class Button(val title: String, val action: Runnable, val autoClose: Boolean) @@ -47,22 +53,33 @@ interface VectorAlert { actions.add(Button(title, action, autoClose)) } + var viewBinder: ViewBinder? + + val layoutRes: Int + var colorRes: Int? var colorInt: Int? + + var colorAttribute: Int? + + interface ViewBinder { + fun bind(view: View) + } } /** * Dataclass to describe an important alert with actions. */ -open class DefaultVectorAlert(override val uid: String, - override val title: String, - override val description: String, - @DrawableRes override val iconId: Int?, - /** - * Alert are displayed by default, but let this lambda return false to prevent displaying - */ - override val shouldBeDisplayedIn: ((Activity) -> Boolean) = { true } +open class DefaultVectorAlert( + override val uid: String, + override val title: String, + override val description: String, + @DrawableRes override val iconId: Int?, + /** + * Alert are displayed by default, but let this lambda return false to prevent displaying + */ + override val shouldBeDisplayedIn: ((Activity) -> Boolean) = { true } ) : VectorAlert { // will be set by manager, and accessible by actions at runtime @@ -76,26 +93,23 @@ open class DefaultVectorAlert(override val uid: String, /** If this timestamp is after current time, this alert will be skipped */ override var expirationTimestamp: Long? = null - override fun addButton(title: String, action: Runnable, autoClose: Boolean) { - actions.add(VectorAlert.Button(title, action, autoClose)) - } + @LayoutRes + override val layoutRes = R.layout.alerter_alert_default_layout + + override val dismissOnClick: Boolean = true + + override val priority: Int = 0 + + override val isLight: Boolean = false @ColorRes override var colorRes: Int? = null @ColorInt override var colorInt: Int? = null -} -class VerificationVectorAlert(uid: String, - title: String, - override val description: String, - @DrawableRes override val iconId: Int?, - /** - * Alert are displayed by default, but let this lambda return false to prevent displaying - */ - override val shouldBeDisplayedIn: ((Activity) -> Boolean) = { true }, - val matrixItem: MatrixItem? -) : DefaultVectorAlert( - uid, title, description, iconId, shouldBeDisplayedIn -) + @AttrRes + override var colorAttribute: Int? = null + + override var viewBinder: VectorAlert.ViewBinder? = null +} diff --git a/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt b/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt new file mode 100644 index 0000000000..ee6728f969 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 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.popup + +import android.app.Activity +import android.view.View +import android.widget.ImageView +import androidx.annotation.DrawableRes +import im.vector.app.R +import im.vector.app.core.glide.GlideApp +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.util.MatrixItem + +class VerificationVectorAlert(uid: String, + title: String, + override val description: String, + @DrawableRes override val iconId: Int?, + /** + * Alert are displayed by default, but let this lambda return false to prevent displaying + */ + override val shouldBeDisplayedIn: ((Activity) -> Boolean) = { true } +) : DefaultVectorAlert( + uid, title, description, iconId, shouldBeDisplayedIn +) { + override val layoutRes = R.layout.alerter_verification_layout + + class ViewBinder(private val matrixItem: MatrixItem?, + private val avatarRenderer: AvatarRenderer) + : VectorAlert.ViewBinder { + + override fun bind(view: View) { + view.findViewById<ImageView>(R.id.ivUserAvatar)?.let { imageView -> + matrixItem?.let { avatarRenderer.render(it, imageView, GlideApp.with(view.context.applicationContext)) } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt index 5460411907..d85b7937a2 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt @@ -16,8 +16,10 @@ package im.vector.app.features.roomdirectory.picker +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext @@ -26,9 +28,8 @@ import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initialState: RoomDirectoryPickerViewState, private val session: Session) @@ -53,19 +54,21 @@ class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initial } private fun load() { - session.getThirdPartyProtocol(object : MatrixCallback<Map<String, ThirdPartyProtocol>> { - override fun onSuccess(data: Map<String, ThirdPartyProtocol>) { - setState { - copy(asyncThirdPartyRequest = Success(data)) - } + viewModelScope.launch { + setState { + copy(asyncThirdPartyRequest = Loading()) } - - override fun onFailure(failure: Throwable) { + try { + val thirdPartyProtocols = session.thirdPartyService().getThirdPartyProtocols() + setState { + copy(asyncThirdPartyRequest = Success(thirdPartyProtocols)) + } + } catch (failure: Throwable) { setState { copy(asyncThirdPartyRequest = Fail(failure)) } } - }) + } } override fun handle(action: RoomDirectoryPickerAction) { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt index 6b988df94d..5ab86f7138 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt @@ -54,6 +54,8 @@ class RoomProfileController @Inject constructor( fun createShortcut() fun onSettingsClicked() fun onLeaveRoomClicked() + fun onRoomAliasesClicked() + fun onRoomPermissionsClicked() fun onRoomIdClicked() fun onUrlInTopicLongClicked(url: String) } @@ -174,8 +176,29 @@ class RoomProfileController @Inject constructor( ) // Advanced + buildProfileSection(stringProvider.getString(R.string.room_settings_category_advanced_title)) + + buildProfileAction( + id = "alias", + title = stringProvider.getString(R.string.room_settings_alias_title), + subtitle = stringProvider.getString(R.string.room_settings_alias_subtitle), + dividerColor = dividerColor, + divider = true, + editable = true, + action = { callback?.onRoomAliasesClicked() } + ) + + buildProfileAction( + id = "permissions", + title = stringProvider.getString(R.string.room_settings_permissions_title), + subtitle = stringProvider.getString(R.string.room_settings_permissions_subtitle), + dividerColor = dividerColor, + divider = true, + editable = true, + action = { callback?.onRoomPermissionsClicked() } + ) + if (vectorPreferences.developerMode()) { - buildProfileSection(stringProvider.getString(R.string.room_settings_category_advanced_title)) buildProfileAction( id = "roomId", title = stringProvider.getString(R.string.room_settings_room_internal_id), diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index 0cb57fda4f..70eb4dd40e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -52,7 +52,6 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState -import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem import timber.log.Timber import javax.inject.Inject @@ -116,6 +115,7 @@ class RoomProfileFragment @Inject constructor( .observe() .subscribe { handleQuickActions(it) } .disposeOnDestroyView() + setupClicks() setupLongClicks() } @@ -124,6 +124,29 @@ class RoomProfileFragment @Inject constructor( views.waitingView.waitingStatusText.isVisible = true } + private fun setupClicks() { + // Shortcut to room settings + setOf( + headerViews.roomProfileNameView, + views.matrixProfileToolbarTitleView + ).forEach { + it.setOnClickListener { + roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomSettings) + } + } + // Shortcut to room alias + headerViews.roomProfileAliasView.setOnClickListener { + roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomAliasesSettings) + } + // Open Avatar + setOf( + headerViews.roomProfileAvatarView, + views.matrixProfileToolbarAvatarImageView + ).forEach { view -> + view.setOnClickListener { onAvatarClicked(view) } + } + } + private fun setupLongClicks() { headerViews.roomProfileNameView.copyOnLongClick() headerViews.roomProfileAliasView.copyOnLongClick() @@ -170,7 +193,7 @@ class RoomProfileFragment @Inject constructor( override fun invalidate() = withState(roomProfileViewModel) { state -> views.waitingView.root.isVisible = state.isLoading - state.roomSummary()?.also { + state.roomSummary()?.let { if (it.membership.isLeft()) { Timber.w("The room has been left") activity?.finish() @@ -184,13 +207,6 @@ class RoomProfileFragment @Inject constructor( headerViews.roomProfileDecorationImageView.isVisible = it.roomEncryptionTrustLevel != null headerViews.roomProfileDecorationImageView.setImageResource(it.roomEncryptionTrustLevel.toImageRes()) views.matrixProfileDecorationToolbarAvatarImageView.setImageResource(it.roomEncryptionTrustLevel.toImageRes()) - - headerViews.roomProfileAvatarView.setOnClickListener { view -> - onAvatarClicked(view, matrixItem) - } - views.matrixProfileToolbarAvatarImageView.setOnClickListener { view -> - onAvatarClicked(view, matrixItem) - } } } roomProfileController.setData(state) @@ -269,6 +285,14 @@ class RoomProfileFragment @Inject constructor( } } + override fun onRoomAliasesClicked() { + roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomAliasesSettings) + } + + override fun onRoomPermissionsClicked() { + roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomPermissionsSettings) + } + override fun onRoomIdClicked() { copyToClipboard(requireContext(), roomProfileArgs.roomId) } @@ -286,7 +310,9 @@ class RoomProfileFragment @Inject constructor( ) } - private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) { - navigator.openBigImageViewer(requireActivity(), view, matrixItem) + private fun onAvatarClicked(view: View) = withState(roomProfileViewModel) { state -> + state.roomSummary()?.toMatrixItem()?.let { matrixItem -> + navigator.openBigImageViewer(requireActivity(), view, matrixItem) + } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt index 1984be078d..6e77ceaa36 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt @@ -45,8 +45,6 @@ class RoomSettingsController @Inject constructor( fun onNameChanged(name: String) fun onTopicChanged(topic: String) fun onHistoryVisibilityClicked() - fun onRoomAliasesClicked() - fun onRoomPermissionsClicked() fun onJoinRuleClicked() } @@ -106,26 +104,6 @@ class RoomSettingsController @Inject constructor( } } - buildProfileAction( - id = "alias", - title = stringProvider.getString(R.string.room_settings_alias_title), - subtitle = stringProvider.getString(R.string.room_settings_alias_subtitle), - dividerColor = dividerColor, - divider = true, - editable = true, - action = { callback?.onRoomAliasesClicked() } - ) - - buildProfileAction( - id = "permissions", - title = stringProvider.getString(R.string.room_settings_permissions_title), - subtitle = stringProvider.getString(R.string.room_settings_permissions_subtitle), - dividerColor = dividerColor, - divider = true, - editable = true, - action = { callback?.onRoomPermissionsClicked() } - ) - buildProfileAction( id = "historyReadability", title = stringProvider.getString(R.string.room_settings_room_read_history_rules_pref_title), diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt index 1ca539ea7e..30ba0529ce 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt @@ -41,7 +41,6 @@ import im.vector.app.core.utils.toast import im.vector.app.databinding.FragmentRoomSettingGenericBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.roomprofile.RoomProfileArgs -import im.vector.app.features.roomprofile.RoomProfileSharedAction import im.vector.app.features.roomprofile.RoomProfileSharedActionViewModel import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilitySharedActionViewModel import im.vector.app.features.roomprofile.settings.historyvisibility.RoomHistoryVisibilityBottomSheet @@ -174,14 +173,6 @@ class RoomSettingsFragment @Inject constructor( .show(childFragmentManager, "RoomHistoryVisibilityBottomSheet") } - override fun onRoomAliasesClicked() { - roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomAliasesSettings) - } - - override fun onRoomPermissionsClicked() { - roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomPermissionsSettings) - } - override fun onJoinRuleClicked() = withState(viewModel) { state -> val currentJoinRule = state.newRoomJoinRules.newJoinRules ?: state.currentRoomJoinRules val currentGuestAccess = state.newRoomJoinRules.newGuestAccess ?: state.currentGuestAccess diff --git a/vector/src/main/java/im/vector/app/features/themes/ThemeProvider.kt b/vector/src/main/java/im/vector/app/features/themes/ThemeProvider.kt new file mode 100644 index 0000000000..77245dc4a0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/themes/ThemeProvider.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021 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.themes + +import android.content.Context +import javax.inject.Inject + +/** + * Injectable class to encapsulate ThemeUtils call... + */ +class ThemeProvider @Inject constructor( + private val context: Context +) { + fun isLightTheme() = ThemeUtils.isLightTheme(context) +} diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt index 4caa75b39c..671d018b63 100644 --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt @@ -27,8 +27,7 @@ import dagger.assisted.AssistedFactory import im.vector.app.R import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider -import im.vector.app.features.raw.wellknown.getElementWellknown -import im.vector.app.features.raw.wellknown.isE2EByDefault +import im.vector.app.features.createdirect.DirectRoomHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull @@ -36,7 +35,6 @@ import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkParser -import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.internal.util.awaitCallback @@ -45,6 +43,7 @@ class UserCodeSharedViewModel @AssistedInject constructor( @Assisted val initialState: UserCodeState, private val session: Session, private val stringProvider: StringProvider, + private val directRoomHelper: DirectRoomHelper, private val rawService: RawService) : VectorViewModel<UserCodeState, UserCodeActions, UserCodeShareViewEvents>(initialState) { companion object : MvRxViewModelFactory<UserCodeSharedViewModel, UserCodeState> { @@ -96,39 +95,20 @@ class UserCodeSharedViewModel @AssistedInject constructor( private fun handleStartChatting(withUser: UserCodeActions.StartChattingWithUser) { val mxId = withUser.matrixItem.id - val existing = session.getExistingDirectRoomWithUser(mxId) setState { copy(mode = UserCodeState.Mode.SHOW) } - if (existing != null) { - // navigate to this room - _viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(existing)) - } else { - // we should create the room then navigate - _viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen) - viewModelScope.launch(Dispatchers.IO) { - val adminE2EByDefault = rawService.getElementWellknown(session.myUserId) - ?.isE2EByDefault() - ?: true - - val roomParams = CreateRoomParams() - .apply { - invitedUserIds.add(mxId) - setDirectMessage() - enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault - } - - val roomId = - try { - awaitCallback<String> { session.createRoom(roomParams, it) } - } catch (failure: Throwable) { - _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.invite_users_to_room_failure))) - return@launch - } finally { - _viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen) - } - _viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(roomId)) + _viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen) + viewModelScope.launch(Dispatchers.IO) { + val roomId = try { + directRoomHelper.ensureDMExists(mxId) + } catch (failure: Throwable) { + _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.invite_users_to_room_failure))) + return@launch + } finally { + _viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen) } + _viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(roomId)) } } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/PendingInvitee.kt b/vector/src/main/java/im/vector/app/features/userdirectory/PendingSelection.kt similarity index 73% rename from vector/src/main/java/im/vector/app/features/userdirectory/PendingInvitee.kt rename to vector/src/main/java/im/vector/app/features/userdirectory/PendingSelection.kt index f7213497fa..57f950b2c8 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/PendingInvitee.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/PendingSelection.kt @@ -19,14 +19,14 @@ package im.vector.app.features.userdirectory import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.user.model.User -sealed class PendingInvitee { - data class UserPendingInvitee(val user: User) : PendingInvitee() - data class ThreePidPendingInvitee(val threePid: ThreePid) : PendingInvitee() +sealed class PendingSelection { + data class UserPendingSelection(val user: User) : PendingSelection() + data class ThreePidPendingSelection(val threePid: ThreePid) : PendingSelection() fun getBestName(): String { return when (this) { - is UserPendingInvitee -> user.getBestName() - is ThreePidPendingInvitee -> threePid.value + is UserPendingSelection -> user.getBestName() + is ThreePidPendingSelection -> threePid.value } } } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt index 0c2c4b1f4b..7835232b09 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListAction.kt @@ -21,7 +21,7 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class UserListAction : VectorViewModelAction { data class SearchUsers(val value: String) : UserListAction() object ClearSearchUsers : UserListAction() - data class SelectPendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction() - data class RemovePendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction() + data class AddPendingSelection(val pendingSelection: PendingSelection) : UserListAction() + data class RemovePendingSelection(val pendingSelection: PendingSelection) : UserListAction() object ComputeMatrixToLinkForSharing : UserListAction() } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt index 3e1523d0cc..a7ec9cd8c3 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt @@ -54,10 +54,7 @@ class UserListController @Inject constructor(private val session: Session, // Build generic items if (currentState.searchTerm.isBlank()) { - // For now we remove this option if in invite to existing room flow (and not create DM) - if (currentState.pendingInvitees.isEmpty() - // For now we remove this option if in invite to existing room flow (and not create DM) - && currentState.existingRoomId == null) { + if (currentState.showInviteActions()) { actionItem { id(R.drawable.ic_share) title(stringProvider.getString(R.string.invite_friends)) @@ -67,17 +64,17 @@ class UserListController @Inject constructor(private val session: Session, }) } } - actionItem { - id(R.drawable.ic_baseline_perm_contact_calendar_24) - title(stringProvider.getString(R.string.contacts_book_title)) - actionIconRes(R.drawable.ic_baseline_perm_contact_calendar_24) - clickAction(View.OnClickListener { - callback?.onContactBookClick() - }) + if (currentState.showContactBookAction) { + actionItem { + id(R.drawable.ic_baseline_perm_contact_calendar_24) + title(stringProvider.getString(R.string.contacts_book_title)) + actionIconRes(R.drawable.ic_baseline_perm_contact_calendar_24) + clickAction(View.OnClickListener { + callback?.onContactBookClick() + }) + } } - if (currentState.pendingInvitees.isEmpty() - // For now we remove this option if in invite to existing room flow (and not create DM) - && currentState.existingRoomId == null) { + if (currentState.showInviteActions()) { actionItem { id(R.drawable.ic_qr_code_add) title(stringProvider.getString(R.string.qr_code)) diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt index d06030c301..96f459cbbf 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragment.kt @@ -67,18 +67,22 @@ class UserListFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java) - views.userListTitle.text = args.title - vectorBaseActivity.setSupportActionBar(views.userListToolbar) - + if (args.showToolbar) { + views.userListTitle.text = args.title + vectorBaseActivity.setSupportActionBar(views.userListToolbar) + setupCloseView() + views.userListToolbar.isVisible = true + } else { + views.userListToolbar.isVisible = false + } setupRecyclerView() setupSearchView() - setupCloseView() homeServerCapabilitiesViewModel.subscribe { views.userListE2EbyDefaultDisabled.isVisible = !it.isE2EByDefault } - viewModel.selectSubscribe(this, UserListViewState::pendingInvitees) { + viewModel.selectSubscribe(this, UserListViewState::pendingSelections) { renderSelectedUsers(it) } @@ -105,7 +109,7 @@ class UserListFragment @Inject constructor( override fun onPrepareOptionsMenu(menu: Menu) { withState(viewModel) { - val showMenuItem = it.pendingInvitees.isNotEmpty() + val showMenuItem = it.pendingSelections.isNotEmpty() menu.forEach { menuItem -> menuItem.isVisible = showMenuItem } @@ -114,7 +118,7 @@ class UserListFragment @Inject constructor( } override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) { - sharedActionViewModel.post(UserListSharedAction.OnMenuItemSelected(item.itemId, it.pendingInvitees)) + sharedActionViewModel.post(UserListSharedAction.OnMenuItemSelected(item.itemId, it.pendingSelections)) return@withState true } @@ -156,14 +160,14 @@ class UserListFragment @Inject constructor( userListController.setData(it) } - private fun renderSelectedUsers(invitees: Set<PendingInvitee>) { + private fun renderSelectedUsers(selections: Set<PendingSelection>) { invalidateOptionsMenu() val currentNumberOfChips = views.chipGroup.childCount - val newNumberOfChips = invitees.size + val newNumberOfChips = selections.size views.chipGroup.removeAllViews() - invitees.forEach { addChipToGroup(it) } + selections.forEach { addChipToGroup(it) } // Scroll to the bottom when adding chips. When removing chips, do not scroll if (newNumberOfChips >= currentNumberOfChips) { @@ -173,20 +177,22 @@ class UserListFragment @Inject constructor( } } - private fun addChipToGroup(pendingInvitee: PendingInvitee) { + private fun addChipToGroup(pendingSelection: PendingSelection) { val chip = Chip(requireContext()) chip.setChipBackgroundColorResource(android.R.color.transparent) chip.chipStrokeWidth = dimensionConverter.dpToPx(1).toFloat() - chip.text = pendingInvitee.getBestName() + chip.text = pendingSelection.getBestName() chip.isClickable = true chip.isCheckable = false chip.isCloseIconVisible = true views.chipGroup.addView(chip) chip.setOnCloseIconClickListener { - viewModel.handle(UserListAction.RemovePendingInvitee(pendingInvitee)) + viewModel.handle(UserListAction.RemovePendingSelection(pendingSelection)) } } + fun getCurrentState() = withState(viewModel) { it } + override fun onInviteFriendClick() { viewModel.handle(UserListAction.ComputeMatrixToLinkForSharing) } @@ -197,17 +203,17 @@ class UserListFragment @Inject constructor( override fun onItemClick(user: User) { view?.hideKeyboard() - viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user))) + viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.UserPendingSelection(user))) } override fun onMatrixIdClick(matrixId: String) { view?.hideKeyboard() - viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId)))) + viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.UserPendingSelection(User(matrixId)))) } override fun onThreePidClick(threePid: ThreePid) { view?.hideKeyboard() - viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid))) + viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.ThreePidPendingSelection(threePid))) } override fun onUseQRCode() { diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt index 02fd13b39b..795d45272c 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt @@ -24,5 +24,8 @@ data class UserListFragmentArgs( val title: String, val menuResId: Int, val excludedUserIds: Set<String>? = null, - val existingRoomId: String? = null + val singleSelection: Boolean = false, + val showInviteActions: Boolean = true, + val showContactBookAction: Boolean = true, + val showToolbar: Boolean = true ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt index b2cdee3e63..fca771793b 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListSharedAction.kt @@ -21,7 +21,7 @@ import im.vector.app.core.platform.VectorSharedAction sealed class UserListSharedAction : VectorSharedAction { object Close : UserListSharedAction() object GoBack : UserListSharedAction() - data class OnMenuItemSelected(val itemId: Int, val invitees: Set<PendingInvitee>) : UserListSharedAction() + data class OnMenuItemSelected(val itemId: Int, val selections: Set<PendingSelection>) : UserListSharedAction() object OpenPhoneBook : UserListSharedAction() object AddByQrCode : UserListSharedAction() } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt index 9766d640c7..0e042c8bc1 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewModel.kt @@ -69,21 +69,15 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User } init { - setState { - copy( - myUserId = session.myUserId, - existingRoomId = initialState.existingRoomId - ) - } observeUsers() } override fun handle(action: UserListAction) { when (action) { - is UserListAction.SearchUsers -> handleSearchUsers(action.value) - is UserListAction.ClearSearchUsers -> handleClearSearchUsers() - is UserListAction.SelectPendingInvitee -> handleSelectUser(action) - is UserListAction.RemovePendingInvitee -> handleRemoveSelectedUser(action) + is UserListAction.SearchUsers -> handleSearchUsers(action.value) + is UserListAction.ClearSearchUsers -> handleClearSearchUsers() + is UserListAction.AddPendingSelection -> handleSelectUser(action) + is UserListAction.RemovePendingSelection -> handleRemoveSelectedUser(action) UserListAction.ComputeMatrixToLinkForSharing -> handleShareMyMatrixToLink() }.exhaustive } @@ -169,13 +163,13 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User .disposeOnClear() } - private fun handleSelectUser(action: UserListAction.SelectPendingInvitee) = withState { state -> - val selectedUsers = state.pendingInvitees.toggle(action.pendingInvitee) - setState { copy(pendingInvitees = selectedUsers) } + private fun handleSelectUser(action: UserListAction.AddPendingSelection) = withState { state -> + val selections = state.pendingSelections.toggle(action.pendingSelection, singleElement = state.singleSelection) + setState { copy(pendingSelections = selections) } } - private fun handleRemoveSelectedUser(action: UserListAction.RemovePendingInvitee) = withState { state -> - val selectedUsers = state.pendingInvitees.minus(action.pendingInvitee) - setState { copy(pendingInvitees = selectedUsers) } + private fun handleRemoveSelectedUser(action: UserListAction.RemovePendingSelection) = withState { state -> + val selections = state.pendingSelections.minus(action.pendingSelection) + setState { copy(pendingSelections = selections) } } } diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt index 69135f912d..f1cbbd3b9d 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt @@ -28,24 +28,29 @@ data class UserListViewState( val knownUsers: Async<PagedList<User>> = Uninitialized, val directoryUsers: Async<List<User>> = Uninitialized, val filteredMappedContacts: List<MappedContact> = emptyList(), - val pendingInvitees: Set<PendingInvitee> = emptySet(), - val createAndInviteState: Async<String> = Uninitialized, + val pendingSelections: Set<PendingSelection> = emptySet(), val searchTerm: String = "", - val myUserId: String = "", - val existingRoomId: String? = null + val singleSelection: Boolean, + private val showInviteActions: Boolean, + val showContactBookAction: Boolean ) : MvRxState { constructor(args: UserListFragmentArgs) : this( - existingRoomId = args.existingRoomId + excludedUserIds = args.excludedUserIds, + singleSelection = args.singleSelection, + showInviteActions = args.showInviteActions, + showContactBookAction = args.showContactBookAction ) fun getSelectedMatrixId(): List<String> { - return pendingInvitees + return pendingSelections .mapNotNull { when (it) { - is PendingInvitee.UserPendingInvitee -> it.user.userId - is PendingInvitee.ThreePidPendingInvitee -> null + is PendingSelection.UserPendingSelection -> it.user.userId + is PendingSelection.ThreePidPendingSelection -> null } } } + + fun showInviteActions() = showInviteActions && pendingSelections.isEmpty() } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt index 7c7424df8c..38914478e3 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt @@ -16,15 +16,14 @@ package im.vector.app.features.widgets -import android.content.Context import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.features.themes.ThemeUtils +import im.vector.app.features.themes.ThemeProvider import org.matrix.android.sdk.api.session.widgets.model.Widget import javax.inject.Inject class WidgetArgsBuilder @Inject constructor( private val sessionHolder: ActiveSessionHolder, - private val context: Context + private val themeProvider: ThemeProvider ) { @Suppress("UNCHECKED_CAST") @@ -52,7 +51,8 @@ class WidgetArgsBuilder @Inject constructor( @Suppress("UNCHECKED_CAST") fun buildStickerPickerArgs(roomId: String, widget: Widget): WidgetArgs { val widgetId = widget.widgetId - val baseUrl = widget.computedUrl ?: throw IllegalStateException() + val baseUrl = sessionHolder.getActiveSession().widgetService() + .getWidgetComputedUrl(widget, themeProvider.isLightTheme()) ?: throw IllegalStateException() return WidgetArgs( baseUrl = baseUrl, kind = WidgetKind.STICKER_PICKER, @@ -68,15 +68,13 @@ class WidgetArgsBuilder @Inject constructor( fun buildRoomWidgetArgs(roomId: String, widget: Widget): WidgetArgs { val widgetId = widget.widgetId - val baseUrl = widget.computedUrl ?: throw IllegalStateException() + val baseUrl = sessionHolder.getActiveSession().widgetService() + .getWidgetComputedUrl(widget, themeProvider.isLightTheme()) ?: throw IllegalStateException() return WidgetArgs( baseUrl = baseUrl, kind = WidgetKind.ROOM, roomId = roomId, - widgetId = widgetId, - urlParams = mapOf( - "theme" to getTheme() - ).filterNotNull() + widgetId = widgetId ) } @@ -86,7 +84,7 @@ class WidgetArgsBuilder @Inject constructor( } private fun getTheme(): String { - return if (ThemeUtils.isLightTheme(context)) { + return if (themeProvider.isLightTheme()) { "light" } else { "dark" diff --git a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt index 3accc56680..844a6619b4 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionViewModel.kt @@ -27,6 +27,7 @@ import im.vector.app.R import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.widgets.model.WidgetType @@ -52,11 +53,7 @@ class RoomWidgetPermissionViewModel @AssistedInject constructor(@Assisted val in .filter { it.isNotEmpty() } .map { val widget = it.first() - val domain = try { - URL(widget.computedUrl).host - } catch (e: Throwable) { - null - } + val domain = tryOrNull { URL(widget.widgetContent.url) }?.host // TODO check from widget urls the perms that should be shown? // For now put all if (widget.type == WidgetType.Jitsi) { diff --git a/vector/src/main/res/drawable/bg_rounded_button.xml b/vector/src/main/res/drawable/bg_rounded_button.xml new file mode 100644 index 0000000000..4b5503a92f --- /dev/null +++ b/vector/src/main/res/drawable/bg_rounded_button.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?android:attr/colorControlHighlight"> + <item android:id="@android:id/mask"> + <shape android:shape="oval"> + <solid android:color="#000000" /> + </shape> + </item> + <item> + <shape android:shape="oval"/> + </item> +</ripple> \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_call_answer.xml b/vector/src/main/res/drawable/ic_call_answer.xml new file mode 100644 index 0000000000..dc6e5f3a4e --- /dev/null +++ b/vector/src/main/res/drawable/ic_call_answer.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M8.027,15.9613C9.168,17.1932 11.9148,19.3263 12.6635,19.7641C12.7078,19.79 12.7585,19.8201 12.8152,19.8538C13.9576,20.5329 17.5373,22.6609 20.1454,20.6694C22.1661,19.1266 21.5091,17.3909 20.8289,16.875C20.3633,16.5128 18.9914,15.5145 17.7006,14.6152C16.4331,13.7322 15.7268,14.4397 15.2492,14.918C15.2404,14.9268 15.2317,14.9355 15.2231,14.9442L14.2621,15.9051C14.0174,16.1498 13.6451,16.0605 13.2886,15.7804C12.0092,14.8061 11.0681,13.8659 10.5972,13.395L10.5933,13.391C10.1225,12.9202 9.1939,11.9908 8.2196,10.7114C7.9395,10.3548 7.8502,9.9826 8.0949,9.7379L9.0559,8.7769C9.0645,8.7683 9.0732,8.7596 9.082,8.7508C9.5603,8.2732 10.2678,7.5668 9.3848,6.2994C8.4855,5.0086 7.4872,3.6367 7.125,3.1711C6.6091,2.4909 4.8734,1.8339 3.3306,3.8546C1.3391,6.4627 3.4671,10.0424 4.1462,11.1848C4.1799,11.2415 4.2101,11.2922 4.2359,11.3365C4.6737,12.0851 6.7951,14.8203 8.027,15.9613Z" + android:fillColor="#ffffff"/> +</vector> diff --git a/vector/src/main/res/drawable/ic_call_audio_small.xml b/vector/src/main/res/drawable/ic_call_audio_small.xml new file mode 100644 index 0000000000..9a407cdf7e --- /dev/null +++ b/vector/src/main/res/drawable/ic_call_audio_small.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="14dp" + android:height="14dp" + android:viewportWidth="14" + android:viewportHeight="14"> + <path + android:pathData="M4.3514,9.6408C5.1121,10.4621 6.9433,11.8842 7.4424,12.176C7.4719,12.1933 7.5057,12.2134 7.5435,12.2358C8.3051,12.6886 10.6916,14.1072 12.4304,12.7796C13.7775,11.751 13.3395,10.5939 12.886,10.25C12.5756,10.0085 11.661,9.3429 10.8005,8.7434C9.9555,8.1548 9.4846,8.6264 9.1662,8.9453C9.1603,8.9512 9.1545,8.957 9.1488,8.9627L8.5082,9.6034C8.345,9.7665 8.0968,9.707 7.8591,9.5203C7.0062,8.8707 6.3788,8.2439 6.0649,7.93L6.0623,7.9273C5.7484,7.6135 5.1293,6.9938 4.4798,6.1409C4.2931,5.9032 4.2335,5.655 4.3967,5.4919L5.0373,4.8512C5.0431,4.8455 5.0489,4.8397 5.0547,4.8338C5.3736,4.5154 5.8453,4.0445 5.2566,3.1995C4.6571,2.339 3.9915,1.4244 3.7501,1.114C3.4061,0.6606 2.249,0.2226 1.2205,1.5697C-0.1072,3.3084 1.3115,5.6949 1.7642,6.4565C1.7867,6.4943 1.8068,6.5281 1.824,6.5576C2.1159,7.0567 3.5301,8.8801 4.3514,9.6408Z" + android:fillColor="#737D8C"/> +</vector> diff --git a/vector/src/main/res/drawable/ic_call_conference_small.xml b/vector/src/main/res/drawable/ic_call_conference_small.xml new file mode 100644 index 0000000000..1ba596d4a9 --- /dev/null +++ b/vector/src/main/res/drawable/ic_call_conference_small.xml @@ -0,0 +1,14 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="16dp" + android:height="16dp" + android:viewportWidth="16" + android:viewportHeight="16"> + <path + android:pathData="M8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16ZM5.3333,8.3333C6.4379,8.3333 7.3333,7.3633 7.3333,6.1667C7.3333,4.97 6.4379,4 5.3333,4C4.2288,4 3.3333,4.97 3.3333,6.1667C3.3333,7.3633 4.2288,8.3333 5.3333,8.3333ZM11.5043,9.1296C12.472,9.1296 13.2564,8.2798 13.2564,7.2315C13.2564,6.1832 12.472,5.3333 11.5043,5.3333C10.5366,5.3333 9.7522,6.1832 9.7522,7.2315C9.7522,8.2798 10.5366,9.1296 11.5043,9.1296ZM6.1045,9.4089C7.5698,9.7298 8.6666,11.0353 8.6666,12.5969L8.6666,14.7587H4.6666L1.7144,11.6667C2.3548,10.2875 3.7345,9.3333 5.3333,9.3333C5.5971,9.3333 5.855,9.3593 6.1045,9.4089ZM9.5501,10.611C9.8385,11.2121 10,11.8856 10,12.5969L10,14.7587H11.5043L14.4675,11.6667C13.8465,10.6685 12.7515,10.0057 11.5043,10.0057C10.7807,10.0057 10.1084,10.2288 9.5501,10.611Z" + android:fillColor="#737D8C" + android:fillType="evenOdd"/> + <path + android:pathData="M8,14.6667C11.6819,14.6667 14.6667,11.6819 14.6667,8C14.6667,4.3181 11.6819,1.3333 8,1.3333C4.3181,1.3333 1.3333,4.3181 1.3333,8C1.3333,11.6819 4.3181,14.6667 8,14.6667ZM8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16Z" + android:fillColor="#737D8C" + android:fillType="evenOdd"/> +</vector> diff --git a/vector/src/main/res/drawable/ic_call_dial_pad.xml b/vector/src/main/res/drawable/ic_call_dial_pad.xml new file mode 100644 index 0000000000..a917d592bc --- /dev/null +++ b/vector/src/main/res/drawable/ic_call_dial_pad.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="16dp" + android:height="22dp" + android:viewportWidth="16" + android:viewportHeight="22"> + <path + android:pathData="M8,18C6.9,18 6,18.9 6,20C6,21.1 6.9,22 8,22C9.1,22 10,21.1 10,20C10,18.9 9.1,18 8,18ZM2,0C0.9,0 0,0.9 0,2C0,3.1 0.9,4 2,4C3.1,4 4,3.1 4,2C4,0.9 3.1,0 2,0ZM2,6C0.9,6 0,6.9 0,8C0,9.1 0.9,10 2,10C3.1,10 4,9.1 4,8C4,6.9 3.1,6 2,6ZM2,12C0.9,12 0,12.9 0,14C0,15.1 0.9,16 2,16C3.1,16 4,15.1 4,14C4,12.9 3.1,12 2,12ZM14,4C15.1,4 16,3.1 16,2C16,0.9 15.1,0 14,0C12.9,0 12,0.9 12,2C12,3.1 12.9,4 14,4ZM8,12C6.9,12 6,12.9 6,14C6,15.1 6.9,16 8,16C9.1,16 10,15.1 10,14C10,12.9 9.1,12 8,12ZM14,12C12.9,12 12,12.9 12,14C12,15.1 12.9,16 14,16C15.1,16 16,15.1 16,14C16,12.9 15.1,12 14,12ZM14,6C12.9,6 12,6.9 12,8C12,9.1 12.9,10 14,10C15.1,10 16,9.1 16,8C16,6.9 15.1,6 14,6ZM8,6C6.9,6 6,6.9 6,8C6,9.1 6.9,10 8,10C9.1,10 10,9.1 10,8C10,6.9 9.1,6 8,6ZM8,0C6.9,0 6,0.9 6,2C6,3.1 6.9,4 8,4C9.1,4 10,3.1 10,2C10,0.9 9.1,0 8,0Z" + android:fillColor="#C1C6CD"/> +</vector> diff --git a/vector/src/main/res/drawable/ic_call_hangup.xml b/vector/src/main/res/drawable/ic_call_hangup.xml new file mode 100644 index 0000000000..7a068e5bec --- /dev/null +++ b/vector/src/main/res/drawable/ic_call_hangup.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12.0084,7.7565C10.3211,7.6916 6.8514,8.1295 6.0078,8.3513C5.9579,8.3645 5.9004,8.3791 5.8362,8.3955C4.541,8.7261 0.4827,9.7618 0.0442,13.0436C-0.2955,15.5862 1.4058,16.3558 2.2562,16.2386C2.8448,16.1648 4.5301,15.8983 6.0872,15.6189C7.6163,15.3446 7.6155,14.3359 7.615,13.6538C7.615,13.6413 7.615,13.6288 7.615,13.6165L7.615,12.2453C7.615,11.8961 7.9432,11.6942 8.3958,11.6396C9.9982,11.422 11.3359,11.4213 12.0055,11.4213L12.0112,11.4213C12.6807,11.4213 14.0018,11.422 15.6042,11.6396C16.0569,11.6942 16.385,11.8961 16.385,12.2453L16.385,13.6165C16.385,13.6289 16.385,13.6413 16.385,13.6538C16.3845,14.3359 16.3837,15.3446 17.9128,15.619C19.4699,15.8983 21.1552,16.1648 21.7438,16.2386C22.5942,16.3558 24.2955,15.5862 23.9558,13.0436C23.5173,9.7618 19.459,8.7261 18.1638,8.3955C18.0996,8.3791 18.0421,8.3645 17.9922,8.3513C17.1487,8.1295 13.6956,7.6916 12.0084,7.7565Z" + android:fillColor="#ffffff"/> +</vector> diff --git a/vector/src/main/res/drawable/ic_call_hold_action.xml b/vector/src/main/res/drawable/ic_call_hold_action.xml new file mode 100644 index 0000000000..4a09de3920 --- /dev/null +++ b/vector/src/main/res/drawable/ic_call_hold_action.xml @@ -0,0 +1,7 @@ +<vector android:height="24dp" + android:viewportHeight="20" + android:viewportWidth="20" + android:width="24dp" + xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#C1C6CD" android:pathData="M10,0C4.48,0 0,4.48 0,10C0,15.52 4.48,20 10,20C15.52,20 20,15.52 20,10C20,4.48 15.52,0 10,0ZM8,14C7.45,14 7,13.55 7,13V7C7,6.45 7.45,6 8,6C8.55,6 9,6.45 9,7V13C9,13.55 8.55,14 8,14ZM12,14C11.45,14 11,13.55 11,13V7C11,6.45 11.45,6 12,6C12.55,6 13,6.45 13,7V13C13,13.55 12.55,14 12,14Z"/> +</vector> diff --git a/vector/src/main/res/drawable/ic_call_pip.xml b/vector/src/main/res/drawable/ic_call_pip.xml new file mode 100644 index 0000000000..aaad2d09de --- /dev/null +++ b/vector/src/main/res/drawable/ic_call_pip.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M11,11H19V17H11V11ZM1,19V4.98C1,3.88 1.9,3 3,3H21C22.1,3 23,3.88 23,4.98V19C23,20.1 22.1,21 21,21H3C1.9,21 1,20.1 1,19ZM3,19.02H21V4.97H3V19.02Z" + android:fillColor="#ffffff"/> +</vector> diff --git a/vector/src/main/res/drawable/ic_call_resume_action.xml b/vector/src/main/res/drawable/ic_call_resume_action.xml new file mode 100644 index 0000000000..a73cc87078 --- /dev/null +++ b/vector/src/main/res/drawable/ic_call_resume_action.xml @@ -0,0 +1,8 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +android:width="24dp" +android:height="24dp" +android:viewportWidth="24" +android:viewportHeight="24"> + <path android:fillColor="#C1C6CD" android:pathData="M10,0C4.48,0 0,4.48 0,10C0,15.52 4.48,20 10,20C15.52,20 20,15.52 20,10C20,4.48 15.52,0 10,0ZM8,13.5V6.5C8,6.09 8.47,5.85 8.8,6.1L13.47,9.6C13.74,9.8 13.74,10.2 13.47,10.4L8.8,13.9C8.47,14.15 8,13.91 8,13.5Z"/> +</vector> + diff --git a/vector/src/main/res/drawable/ic_call_small_pause.xml b/vector/src/main/res/drawable/ic_call_small_pause.xml new file mode 100644 index 0000000000..4559ca4238 --- /dev/null +++ b/vector/src/main/res/drawable/ic_call_small_pause.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + <path + android:pathData="M10,0C4.48,0 0,4.48 0,10C0,15.52 4.48,20 10,20C15.52,20 20,15.52 20,10C20,4.48 15.52,0 10,0ZM9,14H7V6H9V14ZM13,14H11V6H13V14Z" + android:fillColor="#ffffff"/> +</vector> diff --git a/vector/src/main/res/drawable/ic_call_transfer.xml b/vector/src/main/res/drawable/ic_call_transfer.xml new file mode 100644 index 0000000000..b2c28d6ba0 --- /dev/null +++ b/vector/src/main/res/drawable/ic_call_transfer.xml @@ -0,0 +1,17 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <group> + <clip-path + android:pathData="M0,0h24v24h-24z"/> + <path + android:pathData="M8.027,15.9613C9.168,17.1932 11.9148,19.3263 12.6635,19.7641C12.7078,19.79 12.7585,19.8201 12.8152,19.8538C13.9576,20.5329 17.5373,22.6609 20.1454,20.6694C22.1661,19.1266 21.5091,17.3909 20.8289,16.875C20.3633,16.5128 18.9914,15.5145 17.7006,14.6152C16.4331,13.7322 15.7268,14.4397 15.2492,14.918C15.2404,14.9268 15.2317,14.9355 15.2231,14.9442L14.2621,15.9051C14.0174,16.1498 13.6451,16.0605 13.2886,15.7804C12.0092,14.8061 11.0681,13.8659 10.5972,13.395L10.5933,13.391C10.1225,12.9202 9.1939,11.9908 8.2196,10.7114C7.9395,10.3548 7.8502,9.9826 8.0949,9.7379L9.0559,8.7769C9.0645,8.7683 9.0732,8.7596 9.082,8.7508C9.5603,8.2732 10.2678,7.5668 9.3848,6.2994C8.4855,5.0086 7.4872,3.6367 7.125,3.1711C6.6091,2.4909 4.8734,1.8339 3.3306,3.8546C1.3391,6.4627 3.4671,10.0424 4.1462,11.1848C4.1799,11.2415 4.2101,11.2922 4.2359,11.3365C4.6737,12.0851 6.7951,14.8203 8.027,15.9613Z" + android:fillColor="#C1C6CD"/> + <path + android:pathData="M13.3084,9.7333C12.9179,10.1238 12.9179,10.757 13.3084,11.1475C13.6989,11.538 14.3321,11.538 14.7226,11.1475L20.4401,5.43L20.4401,9.9101C20.4401,10.4624 20.8878,10.9101 21.4401,10.9101C21.9924,10.9101 22.4401,10.4624 22.4401,9.9101L22.4401,3.0158C22.4401,2.4635 21.9924,2.0158 21.4401,2.0158H14.5458C13.9935,2.0158 13.5458,2.4635 13.5458,3.0158C13.5458,3.5681 13.9935,4.0158 14.5458,4.0158L19.0259,4.0158L13.3084,9.7333Z" + android:fillColor="#C1C6CD" + android:fillType="evenOdd"/> + </group> +</vector> diff --git a/vector/src/main/res/drawable/ic_call_video_small.xml b/vector/src/main/res/drawable/ic_call_video_small.xml new file mode 100644 index 0000000000..abb2d85719 --- /dev/null +++ b/vector/src/main/res/drawable/ic_call_video_small.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="16dp" + android:height="12dp" + android:viewportWidth="16" + android:viewportHeight="12"> + <path + android:pathData="M0,3.6666C0,2.0098 1.3432,0.6666 3,0.6666H8.3333C9.9902,0.6666 11.3333,2.0098 11.3333,3.6666V8.3333C11.3333,9.9901 9.9902,11.3333 8.3333,11.3333H3C1.3431,11.3333 0,9.9902 0,8.3333V3.6666Z" + android:fillColor="#737D8C"/> + <path + android:pathData="M12.6666,3.9999L14.3753,2.633C15.03,2.1092 16,2.5754 16,3.4139V8.586C16,9.4245 15.03,9.8906 14.3753,9.3668L12.6666,7.9999V3.9999Z" + android:fillColor="#737D8C"/> +</vector> diff --git a/vector/src/main/res/drawable/oval_destructive.xml b/vector/src/main/res/drawable/oval_destructive.xml deleted file mode 100644 index 045a50456d..0000000000 --- a/vector/src/main/res/drawable/oval_destructive.xml +++ /dev/null @@ -1,11 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="oval"> - - <size - android:width="40dp" - android:height="40dp" /> - - <solid android:color="@color/riotx_destructive_accent" /> - -</shape> \ No newline at end of file diff --git a/vector/src/main/res/drawable/oval_positive.xml b/vector/src/main/res/drawable/oval_positive.xml deleted file mode 100644 index d2e17d746b..0000000000 --- a/vector/src/main/res/drawable/oval_positive.xml +++ /dev/null @@ -1,11 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="oval"> - - <size - android:width="40dp" - android:height="40dp" /> - - <solid android:color="@color/riotx_positive_accent" /> - -</shape> \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_call.xml b/vector/src/main/res/layout/activity_call.xml index d2d39de67f..ff33c5f17c 100644 --- a/vector/src/main/res/layout/activity_call.xml +++ b/vector/src/main/res/layout/activity_call.xml @@ -1,14 +1,24 @@ -<?xml version="1.0" encoding="utf-8"?><!-- tools:ignore is needed because lint thinks this can be replaced with a merge. Replacing this +<?xml version="1.0" encoding="utf-8"?> + +<!-- tools:ignore is needed because lint thinks this can be replaced with a merge. Replacing this with a merge causes the fullscreen SurfaceView not to be centered. --> + <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/constraintLayout" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?riotx_background" + android:background="@color/bg_call_screen" tools:ignore="MergeRootFrame"> + <ImageView + android:id="@+id/bgCallView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scaleType="centerCrop" + tools:src="@tools:sample/avatars" /> + <org.webrtc.SurfaceViewRenderer android:id="@+id/fullscreenRenderer" android:layout_width="match_parent" @@ -26,49 +36,74 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> - <TextView - android:id="@+id/participantNameText" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/layout_horizontal_margin" - android:layout_marginEnd="@dimen/layout_horizontal_margin" - android:gravity="center" - android:textColor="?riotx_text_primary" - android:textSize="28sp" - android:textStyle="bold" - app:layout_constraintBottom_toTopOf="@id/callTypeText" + <FrameLayout + android:id="@+id/otherKnownCallLayout" + android:layout_width="80dp" + android:layout_height="144dp" + android:layout_marginTop="32dp" + android:layout_marginEnd="16dp" + android:background="@color/riotx_background_light" + android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - tools:text="@sample/matrix.json/data/displayName" /> + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible"> - <TextView - android:id="@+id/callTypeText" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/layout_horizontal_margin" - android:layout_marginEnd="@dimen/layout_horizontal_margin" - android:layout_marginBottom="8dp" - android:gravity="center" - android:textColor="?riotx_text_secondary" - android:textSize="22sp" - app:layout_constraintBottom_toTopOf="@id/otherMemberAvatar" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - tools:text="@string/action_video_call" /> + <ImageView + android:id="@+id/otherKnownCallAvatarView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:foreground="?attr/selectableItemBackground" + android:scaleType="centerCrop" + tools:src="@tools:sample/avatars" /> + + <ImageView + android:id="@+id/otherSmallIsHeldIcon" + android:layout_width="20dp" + android:layout_height="20dp" + android:layout_gravity="center" + android:src="@drawable/ic_call_small_pause" /> + + </FrameLayout> <ImageView android:id="@+id/otherMemberAvatar" - android:layout_width="160dp" - android:layout_height="160dp" + android:layout_width="80dp" + android:layout_height="80dp" android:contentDescription="@string/avatar" android:scaleType="centerCrop" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.3" tools:src="@tools:sample/avatars" /> + <ImageView + android:id="@+id/smallIsHeldIcon" + android:layout_width="20dp" + android:layout_height="20dp" + android:src="@drawable/ic_call_small_pause" + app:layout_constraintBottom_toBottomOf="@id/otherMemberAvatar" + app:layout_constraintEnd_toEndOf="@id/otherMemberAvatar" + app:layout_constraintStart_toStartOf="@id/otherMemberAvatar" + app:layout_constraintTop_toTopOf="@id/otherMemberAvatar" /> + + + <TextView + android:id="@+id/participantNameText" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/layout_horizontal_margin" + android:layout_marginTop="16dp" + android:layout_marginEnd="@dimen/layout_horizontal_margin" + android:gravity="center" + android:textColor="@color/white" + android:textSize="20sp" + android:textStyle="bold" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/otherMemberAvatar" + tools:text="@sample/matrix.json/data/displayName" /> + <TextView android:id="@+id/callStatusText" android:layout_width="0dp" @@ -78,18 +113,32 @@ android:layout_marginEnd="@dimen/layout_horizontal_margin" android:layout_marginBottom="8dp" android:gravity="center" - android:textColor="?riotx_text_secondary" - android:textSize="22sp" + android:textColor="@color/white" + android:textSize="14sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/otherMemberAvatar" + app:layout_constraintTop_toBottomOf="@id/participantNameText" tools:text="@string/call_connecting" /> + <Button + android:id="@+id/callActionText" + style="@style/VectorButtonStyleText" + android:layout_width="wrap_content" + android:layout_height="48dp" + android:layout_margin="8dp" + android:gravity="center" + android:textColor="?attr/colorAccent" + android:textSize="14sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/callStatusText" + tools:text="@string/call_resume_action" /> + <ProgressBar android:id="@+id/callConnectingProgress" style="?android:attr/progressBarStyle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_width="24dp" + android:layout_height="24dp" android:layout_margin="8dp" android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" @@ -102,7 +151,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="visible" - app:constraint_referenced_ids="participantNameText,callTypeText,otherMemberAvatar,callStatusText" /> + app:constraint_referenced_ids="participantNameText, otherMemberAvatar,callStatusText" /> <androidx.constraintlayout.widget.Group android:id="@+id/callVideoGroup" diff --git a/vector/src/main/res/layout/activity_call_transfer.xml b/vector/src/main/res/layout/activity_call_transfer.xml new file mode 100644 index 0000000000..64ddd29319 --- /dev/null +++ b/vector/src/main/res/layout/activity_call_transfer.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/vector_coordinator_layout" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/callTransferToolbar" + style="@style/VectorToolbarStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:elevation="4dp" + app:layout_constraintTop_toTopOf="parent" /> + + <com.google.android.material.tabs.TabLayout + android:id="@+id/callTransferTabLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintTop_toBottomOf="@+id/callTransferToolbar" + app:tabGravity="fill" + app:tabMaxWidth="0dp" + app:tabMode="fixed" /> + + <androidx.viewpager2.widget.ViewPager2 + android:id="@+id/callTransferViewPager" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@+id/callTransferActionsLayout" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/callTransferTabLayout" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + <RelativeLayout + android:background="?riotx_header_panel_background" + android:id="@+id/callTransferActionsLayout" + android:layout_width="match_parent" + android:paddingVertical="8dp" + android:paddingHorizontal="16dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintStart_toEndOf="parent" + android:layout_height="wrap_content"> + + <CheckBox + android:id="@+id/callTransferConsultCheckBox" + android:layout_width="wrap_content" + android:layout_centerVertical="true" + android:layout_alignParentStart="true" + android:enabled="false" + android:layout_height="wrap_content"/> + + <TextView + android:id="@+id/callTransferConsultTitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_toEndOf="@id/callTransferConsultCheckBox" + android:layout_toStartOf="@+id/callTransferConnectAction" + android:layout_marginEnd="8dp" + android:ellipsize="end" + android:text="@string/call_transfer_consult_first" /> + + <Button + android:id="@+id/callTransferConnectAction" + style="@style/VectorButtonStyleText" + android:layout_width="wrap_content" + android:layout_centerVertical="true" + android:layout_alignParentEnd="true" + android:text="@string/call_transfer_connect_action" + android:layout_height="wrap_content"/> + + </RelativeLayout> + + <include + android:id="@+id/waiting_view" + layout="@layout/merge_overlay_waiting_view" /> + + </androidx.constraintlayout.widget.ConstraintLayout> +</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file diff --git a/vector/src/main/res/layout/alerter_incoming_call_layout.xml b/vector/src/main/res/layout/alerter_incoming_call_layout.xml new file mode 100644 index 0000000000..05296dd65a --- /dev/null +++ b/vector/src/main/res/layout/alerter_incoming_call_layout.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingTop="4dp" + android:paddingBottom="4dp" + tools:style="@style/AlertStyle"> + + <ImageView + android:id="@+id/incomingCallAvatar" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_margin="12dp" + android:contentDescription="@string/call_notification_answer" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@tools:sample/avatars" /> + + <TextView + android:id="@+id/incomingCallNameView" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="12dp" + android:layout_marginEnd="12dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?riotx_text_primary" + android:textSize="15sp" + android:textStyle="bold" + app:layout_constraintEnd_toStartOf="@+id/incomingCallRejectView" + app:layout_constraintStart_toEndOf="@id/incomingCallAvatar" + app:layout_constraintTop_toTopOf="@id/incomingCallAvatar" + tools:text="@sample/matrix.json/data/displayName" /> + + <TextView + android:id="@+id/incomingCallKindView" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="3dp" + android:layout_marginEnd="8dp" + android:drawablePadding="4dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?riotx_text_secondary" + android:textSize="15sp" + app:drawableTint="?riotx_text_secondary" + app:layout_constraintEnd_toStartOf="@+id/incomingCallRejectView" + app:layout_constraintStart_toStartOf="@id/incomingCallNameView" + app:layout_constraintTop_toBottomOf="@id/incomingCallNameView" + tools:text="@string/action_voice_call" /> + + <ImageView + android:id="@+id/incomingCallAcceptView" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_marginEnd="12dp" + android:background="@drawable/bg_rounded_button" + android:backgroundTint="@color/riotx_accent" + android:clickable="true" + android:contentDescription="@string/call_notification_answer" + android:focusable="true" + android:padding="8dp" + android:src="@drawable/ic_call_answer" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:tint="@android:color/white" /> + + <ImageView + android:id="@+id/incomingCallRejectView" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_marginEnd="12dp" + android:background="@drawable/bg_rounded_button" + android:backgroundTint="@color/riotx_destructive_accent" + android:clickable="true" + android:contentDescription="@string/call_notification_reject" + android:focusable="true" + android:padding="8dp" + android:src="@drawable/ic_call_hangup" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/incomingCallAcceptView" + app:layout_constraintTop_toTopOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/vector/src/main/res/layout/bottom_sheet_call_controls.xml b/vector/src/main/res/layout/bottom_sheet_call_controls.xml index 85dee93957..f30d44b731 100644 --- a/vector/src/main/res/layout/bottom_sheet_call_controls.xml +++ b/vector/src/main/res/layout/bottom_sheet_call_controls.xml @@ -15,6 +15,7 @@ app:actionTitle="@string/call_select_sound_device" app:leftIcon="@drawable/ic_call_speaker_default" app:tint="?attr/riotx_text_primary" + app:titleTextColor="?attr/riotx_text_primary" tools:actionDescription="Speaker" /> <im.vector.app.core.ui.views.BottomSheetActionButton @@ -24,15 +25,47 @@ app:actionTitle="@string/call_switch_camera" app:leftIcon="@drawable/ic_video_flip" app:tint="?attr/riotx_text_primary" + app:titleTextColor="?attr/riotx_text_primary" tools:actionDescription="Front" /> + <im.vector.app.core.ui.views.BottomSheetActionButton + android:id="@+id/callControlsOpenDialPad" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:actionTitle="@string/call_dial_pad_title" + app:leftIcon="@drawable/ic_call_dial_pad" + app:tint="?attr/riotx_text_primary" + app:titleTextColor="?attr/riotx_text_primary" + tools:actionDescription="" /> + <im.vector.app.core.ui.views.BottomSheetActionButton android:id="@+id/callControlsToggleSDHD" android:layout_width="match_parent" android:layout_height="wrap_content" - app:actionTitle="@string/call_switch_camera" + app:actionTitle="@string/call_format_turn_hd_on" app:leftIcon="@drawable/ic_hd" app:tint="?attr/riotx_text_primary" + app:titleTextColor="?attr/riotx_text_primary" tools:actionDescription="Front" /> + <im.vector.app.core.ui.views.BottomSheetActionButton + android:id="@+id/callControlsToggleHoldResume" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:actionTitle="Hold/resume" + app:leftIcon="@drawable/ic_call_hold_action" + app:tint="?attr/riotx_text_primary" + app:titleTextColor="?attr/riotx_text_primary" + tools:actionDescription="" /> + + <im.vector.app.core.ui.views.BottomSheetActionButton + android:id="@+id/callControlsTransfer" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:actionTitle="@string/call_transfer_title" + app:leftIcon="@drawable/ic_call_transfer" + app:tint="?attr/riotx_text_primary" + app:titleTextColor="?attr/riotx_text_primary" + tools:actionDescription="" /> + </LinearLayout> diff --git a/vector/src/main/res/layout/bottom_sheet_call_dial_pad.xml b/vector/src/main/res/layout/bottom_sheet_call_dial_pad.xml new file mode 100644 index 0000000000..af82f3946b --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_call_dial_pad.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/callDialPad" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?riotx_bottom_sheet_background" + android:orientation="vertical"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="8dp"> + + <TextView + android:id="@+id/callDialPadTitle" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:gravity="center_vertical" + android:text="@string/call_dial_pad_title" + android:textColor="?riotx_text_primary" + android:textSize="20sp" /> + + <ImageView + android:id="@+id/callDialPadClose" + android:layout_width="@dimen/layout_touch_size" + android:layout_height="@dimen/layout_touch_size" + android:scaleType="center" + app:tint="?riotx_text_primary" + android:foreground="?selectableItemBackground" + android:src="@drawable/ic_cross" /> + + </LinearLayout> + + <androidx.fragment.app.FragmentContainerView + android:id="@+id/callDialPadFragmentContainer" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + +</LinearLayout> diff --git a/vector/src/main/res/layout/bottom_sheet_call_dialer_choice.xml b/vector/src/main/res/layout/bottom_sheet_call_dialer_choice.xml new file mode 100644 index 0000000000..7d228d4a2f --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_call_dialer_choice.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/callControlsWrapper" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?riotx_bottom_sheet_background" + android:orientation="vertical"> + + <im.vector.app.core.ui.views.BottomSheetActionButton + android:id="@+id/dialerChoiceDialPad" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:actionTitle="@string/call_dial_pad_title" + app:leftIcon="@drawable/ic_call_dial_pad" + app:tint="?attr/riotx_text_primary" + app:titleTextColor="?attr/riotx_text_primary" /> + + <im.vector.app.core.ui.views.BottomSheetActionButton + android:id="@+id/dialerChoiceVoiceCall" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:actionTitle="@string/action_voice_call" + app:leftIcon="@drawable/ic_call_answer" + app:tint="?attr/riotx_text_primary" + app:titleTextColor="?attr/riotx_text_primary" /> + + +</LinearLayout> diff --git a/vector/src/main/res/layout/fragment_home_detail.xml b/vector/src/main/res/layout/fragment_home_detail.xml index d9a2470343..d25375f3b9 100644 --- a/vector/src/main/res/layout/fragment_home_detail.xml +++ b/vector/src/main/res/layout/fragment_home_detail.xml @@ -65,7 +65,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/syncStateView" /> - <im.vector.app.core.ui.views.ActiveCallView + <im.vector.app.core.ui.views.CurrentCallsView android:id="@+id/activeCallView" android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 705e4cd882..75b12a0621 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -96,7 +96,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/roomToolbar" /> - <im.vector.app.core.ui.views.ActiveCallView + <im.vector.app.core.ui.views.CurrentCallsView android:id="@+id/activeCallView" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -179,7 +179,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/roomToolbar" - tools:visibility="visible" /> + tools:visibility="gone" /> <androidx.constraintlayout.widget.Barrier android:id="@+id/badgeBarrier" diff --git a/vector/src/main/res/layout/item_timeline_event_base_state.xml b/vector/src/main/res/layout/item_timeline_event_base_state.xml index 3f44b11aa7..e68f962a44 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_state.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_state.xml @@ -40,17 +40,24 @@ android:background="@drawable/rounded_rect_shape_8" android:padding="8dp"> + <ViewStub + android:id="@+id/messageCallStub" + style="@style/TimelineContentStubBaseParams" + android:layout="@layout/item_timeline_event_call_tile_stub" + tools:visibility="visible" /> + <ViewStub android:id="@+id/messageVerificationRequestStub" style="@style/TimelineContentStubBaseParams" android:layout="@layout/item_timeline_event_verification_stub" + tools:layout_marginTop= "250dp" tools:visibility="visible" /> <ViewStub android:id="@+id/messageVerificationDoneStub" style="@style/TimelineContentStubBaseParams" + tools:layout_marginTop= "450dp" android:layout="@layout/item_timeline_event_status_tile_stub" - tools:layout_marginTop="180dp" tools:visibility="visible" /> <ViewStub diff --git a/vector/src/main/res/layout/item_timeline_event_call_tile_stub.xml b/vector/src/main/res/layout/item_timeline_event_call_tile_stub.xml new file mode 100644 index 0000000000..9eac9cd6c3 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_call_tile_stub.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:orientation="vertical"> + + <ImageView + android:id="@+id/itemCallCreatorAvatar" + android:layout_width="40dp" + android:layout_height="40dp" + tools:src="@tools:sample/avatars" + android:layout_gravity="center_horizontal" /> + + <TextView + android:id="@+id/itemCallCreatorNameTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:drawablePadding="6dp" + android:layout_marginTop="4dp" + android:gravity="center" + android:textColor="?riotx_text_primary" + android:textSize="15sp" + android:textStyle="bold" + tools:text="@sample/matrix.json/data/displayName" /> + + + <TextView + android:id="@+id/itemCallKindTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:layout_marginTop="4dp" + android:drawablePadding="4dp" + android:layout_marginBottom="12dp" + android:gravity="center" + android:textColor="?riotx_text_primary" + android:textSize="12sp" + tools:text="@string/action_video_call" /> + + <TextView + android:id="@+id/itemCallStatusTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginStart="8dp" + android:layout_marginTop="12dp" + android:layout_marginEnd="8dp" + android:layout_marginBottom="12dp" + android:textColor="?attr/vctr_notice_secondary" + android:textSize="13sp" + tools:text="@string/video_call_in_progress" /> + + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/itemCallAcceptRejectViewGroup" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <Button + android:id="@+id/itemCallAcceptView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="4dp" + android:minWidth="120dp" + style="@style/VectorButtonStylePositive" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toEndOf="@+id/itemCallRejectView" + app:layout_constraintTop_toTopOf="@id/itemCallRejectView" /> + + <Button + android:id="@+id/itemCallRejectView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:layout_marginEnd="4dp" + android:minWidth="120dp" + style="@style/VectorButtonStyleDestructive" + app:layout_constraintEnd_toStartOf="@+id/itemCallAcceptView" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + + + </androidx.constraintlayout.widget.ConstraintLayout> + +</LinearLayout> \ No newline at end of file diff --git a/vector/src/main/res/layout/view_active_conference_view.xml b/vector/src/main/res/layout/view_active_conference_view.xml index a618f36661..063b22481c 100644 --- a/vector/src/main/res/layout/view_active_conference_view.xml +++ b/vector/src/main/res/layout/view_active_conference_view.xml @@ -22,7 +22,7 @@ android:textColorLink="@color/white" app:drawableTint="@color/white" tools:text="@string/ongoing_conference_call" - app:drawableStartCompat="@drawable/ic_call" /> + app:drawableStartCompat="@drawable/ic_call_answer" /> <com.google.android.material.button.MaterialButton android:id="@+id/deleteWidgetButton" diff --git a/vector/src/main/res/layout/view_bottom_sheet_action_button.xml b/vector/src/main/res/layout/view_bottom_sheet_action_button.xml index c0f55df9e6..ec2e7d2bfe 100644 --- a/vector/src/main/res/layout/view_bottom_sheet_action_button.xml +++ b/vector/src/main/res/layout/view_bottom_sheet_action_button.xml @@ -7,7 +7,7 @@ tools:parentTag="android.widget.FrameLayout"> <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/itemVerificationClickableZone" + android:id="@+id/bottomSheetActionClickableZone" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?riotx_bottom_sheet_background" @@ -21,7 +21,7 @@ android:paddingBottom="8dp"> <ImageView - android:id="@+id/itemVerificationLeftIcon" + android:id="@+id/bottomSheetActionLeftIcon" android:layout_width="48dp" android:layout_height="48dp" android:scaleType="center" @@ -36,23 +36,23 @@ <TextView - android:id="@+id/itemVerificationActionTitle" + android:id="@+id/bottomSheetActionTitle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:textColor="@color/riotx_accent" android:textSize="16sp" app:layout_constrainedWidth="true" - app:layout_constraintBottom_toTopOf="@+id/itemVerificationActionSubTitle" - app:layout_constraintEnd_toStartOf="@+id/itemVerificationActionIcon" - app:layout_constraintStart_toEndOf="@+id/itemVerificationLeftIcon" + app:layout_constraintBottom_toTopOf="@+id/bottomSheetActionSubTitle" + app:layout_constraintEnd_toStartOf="@+id/bottomSheetActionIcon" + app:layout_constraintStart_toEndOf="@+id/bottomSheetActionLeftIcon" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" app:layout_goneMarginStart="0dp" tools:text="@string/start_verification" /> <TextView - android:id="@+id/itemVerificationActionSubTitle" + android:id="@+id/bottomSheetActionSubTitle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="4dp" @@ -61,14 +61,14 @@ android:visibility="gone" app:layout_constrainedWidth="true" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/itemVerificationActionIcon" - app:layout_constraintStart_toStartOf="@+id/itemVerificationActionTitle" - app:layout_constraintTop_toBottomOf="@+id/itemVerificationActionTitle" + app:layout_constraintEnd_toStartOf="@+id/bottomSheetActionIcon" + app:layout_constraintStart_toStartOf="@+id/bottomSheetActionTitle" + app:layout_constraintTop_toBottomOf="@+id/bottomSheetActionTitle" tools:text="For maximum security, do this in person" tools:visibility="visible" /> <ImageView - android:id="@+id/itemVerificationActionIcon" + android:id="@+id/bottomSheetActionIcon" android:layout_width="48dp" android:layout_height="48dp" android:scaleType="center" diff --git a/vector/src/main/res/layout/view_call_controls.xml b/vector/src/main/res/layout/view_call_controls.xml index 2487f131e3..81684cafea 100644 --- a/vector/src/main/res/layout/view_call_controls.xml +++ b/vector/src/main/res/layout/view_call_controls.xml @@ -16,33 +16,37 @@ <ImageView android:id="@+id/ringingControlAccept" - android:layout_width="64dp" - android:layout_height="64dp" - android:background="@drawable/oval_positive" + android:layout_width="56dp" + android:layout_height="56dp" + android:background="@drawable/bg_rounded_button" + android:backgroundTint="@color/riotx_accent" android:clickable="true" android:contentDescription="@string/call_notification_answer" android:focusable="true" - android:padding="16dp" - android:src="@drawable/ic_call" + android:padding="12dp" + android:src="@drawable/ic_call_answer" app:tint="@color/white" tools:ignore="MissingConstraints,MissingPrefix" /> <ImageView android:id="@+id/ringingControlDecline" - android:layout_width="64dp" - android:layout_height="64dp" - android:background="@drawable/oval_destructive" + android:layout_width="56dp" + android:layout_height="56dp" + android:background="@drawable/bg_rounded_button" + android:backgroundTint="@color/riotx_destructive_accent" android:clickable="true" android:contentDescription="@string/call_notification_reject" android:focusable="true" - android:padding="16dp" - android:src="@drawable/ic_call_end" + android:padding="12dp" + android:src="@drawable/ic_call_hangup" app:tint="@color/white" tools:ignore="MissingConstraints,MissingPrefix" /> <androidx.constraintlayout.helper.widget.Flow android:layout_width="match_parent" android:layout_height="wrap_content" + app:flow_horizontalStyle="packed" + app:flow_horizontalGap="48dp" app:constraint_referenced_ids="ringingControlDecline, ringingControlAccept" tools:ignore="MissingConstraints" /> @@ -54,30 +58,31 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" + android:paddingStart="32dp" + android:paddingEnd="32dp" android:visibility="gone" tools:background="@color/password_strength_bar_low" tools:layout_marginTop="120dp" tools:visibility="visible"> <ImageView - android:id="@+id/iv_leftMiniControl" - android:layout_width="44dp" - android:layout_height="44dp" - android:background="@drawable/oval_positive" + android:id="@+id/openChatIcon" + android:layout_width="@dimen/layout_touch_size" + android:layout_height="@dimen/layout_touch_size" + android:scaleType="center" android:clickable="true" + android:background="@drawable/bg_rounded_button" android:contentDescription="@string/a11y_open_chat" android:focusable="true" - android:padding="10dp" - android:src="@drawable/ic_home_bottom_chat" - app:backgroundTint="?attr/riotx_background" - app:tint="?attr/riotx_text_primary" + android:src="@drawable/ic_call_pip" + app:tint="@android:color/white" tools:ignore="MissingConstraints,MissingPrefix" /> <ImageView android:id="@+id/muteIcon" - android:layout_width="64dp" - android:layout_height="64dp" - android:background="@drawable/oval_positive" + android:layout_width="56dp" + android:layout_height="56dp" + android:background="@drawable/bg_rounded_button" android:clickable="true" android:focusable="true" android:padding="16dp" @@ -85,27 +90,27 @@ app:backgroundTint="?attr/riotx_background" app:tint="?attr/riotx_text_primary" tools:contentDescription="@string/a11y_mute_microphone" - tools:ignore="MissingConstraints,MissingPrefix" - tools:src="@drawable/ic_microphone_on" /> + tools:ignore="MissingConstraints,MissingPrefix" /> <ImageView - android:id="@+id/iv_end_call" - android:layout_width="64dp" - android:layout_height="64dp" - android:background="@drawable/oval_destructive" + android:id="@+id/endCallIcon" + android:layout_width="56dp" + android:layout_height="56dp" + android:background="@drawable/bg_rounded_button" + android:backgroundTint="@color/riotx_destructive_accent" android:clickable="true" android:contentDescription="@string/call_notification_hangup" android:focusable="true" - android:padding="16dp" - android:src="@drawable/ic_call_end" + android:padding="12dp" + android:src="@drawable/ic_call_hangup" app:tint="@color/white" tools:ignore="MissingConstraints,MissingPrefix" /> <ImageView android:id="@+id/videoToggleIcon" - android:layout_width="64dp" - android:layout_height="64dp" - android:background="@drawable/oval_positive" + android:layout_width="56dp" + android:layout_height="56dp" + android:background="@drawable/bg_rounded_button" android:clickable="true" android:focusable="true" android:padding="16dp" @@ -116,91 +121,26 @@ tools:ignore="MissingConstraints,MissingPrefix" /> <ImageView - android:id="@+id/iv_more" - android:layout_width="44dp" - android:layout_height="44dp" - android:background="@drawable/oval_positive" + android:id="@+id/moreIcon" + android:layout_width="@dimen/layout_touch_size" + android:layout_height="@dimen/layout_touch_size" + android:scaleType="center" android:clickable="true" + android:background="@drawable/bg_rounded_button" android:contentDescription="@string/settings" android:focusable="true" - android:padding="8dp" - android:src="@drawable/ic_more_vertical" - app:backgroundTint="?attr/riotx_background" - app:tint="?attr/riotx_text_primary" + android:src="@drawable/ic_more_horizontal" + app:tint="@android:color/white" tools:ignore="MissingConstraints,MissingPrefix" /> <androidx.constraintlayout.helper.widget.Flow android:layout_width="match_parent" android:layout_height="wrap_content" - app:constraint_referenced_ids="iv_leftMiniControl, muteIcon, iv_end_call,videoToggleIcon,iv_more" + app:flow_horizontalGap="16dp" + app:flow_horizontalStyle="packed" + app:constraint_referenced_ids="openChatIcon, muteIcon, endCallIcon,videoToggleIcon,moreIcon" tools:ignore="MissingConstraints" /> </androidx.constraintlayout.widget.ConstraintLayout> - <!-- <ImageView--> - <!-- android:id="@+id/iv_call_speaker"--> - <!-- android:layout_width="32dp"--> - <!-- android:layout_height="32dp"--> - <!-- android:layout_marginStart="32dp"--> - <!-- android:clickable="true"--> - <!-- android:focusable="true"--> - <!-- android:src="@drawable/ic_call_speaker_default"--> - <!-- android:tint="?colorPrimary"--> - <!-- app:layout_constraintBottom_toBottomOf="parent"--> - <!-- app:layout_constraintStart_toStartOf="parent"--> - <!-- app:layout_constraintTop_toTopOf="parent" />--> - - <!-- <ImageView--> - <!-- android:id="@+id/iv_call_flip_camera"--> - <!-- android:layout_width="32dp"--> - <!-- android:layout_height="32dp"--> - <!-- android:layout_marginStart="32dp"--> - <!-- android:clickable="true"--> - <!-- android:focusable="true"--> - <!-- android:src="@drawable/ic_call_flip_camera_default"--> - <!-- app:layout_constraintBottom_toBottomOf="parent"--> - <!-- app:layout_constraintStart_toStartOf="parent"--> - <!-- app:layout_constraintTop_toTopOf="parent" />--> - - <!-- <ImageView--> - <!-- android:id="@+id/iv_end_call"--> - <!-- android:layout_width="64dp"--> - <!-- android:layout_height="64dp"--> - <!-- android:layout_marginBottom="32dp"--> - <!-- android:background="@drawable/oval_destructive"--> - <!-- android:clickable="true"--> - <!-- android:focusable="true"--> - <!-- android:padding="8dp"--> - <!-- android:src="@drawable/ic_call_end"--> - <!-- android:tint="@color/white"--> - <!-- app:layout_constraintBottom_toBottomOf="parent"--> - <!-- app:layout_constraintBottom_toTopOf="@+id/layout_call_actions"--> - <!-- app:layout_constraintEnd_toEndOf="parent"--> - <!-- app:layout_constraintStart_toStartOf="parent" />--> - - - <!-- <ImageView--> - <!-- android:id="@+id/iv_call_videocam_off"--> - <!-- android:layout_width="32dp"--> - <!-- android:layout_height="32dp"--> - <!-- android:clickable="true"--> - <!-- android:focusable="true"--> - <!-- android:src="@drawable/ic_call_videocam_off_default"--> - <!-- app:layout_constraintBottom_toBottomOf="parent"--> - <!-- app:layout_constraintEnd_toEndOf="parent"--> - <!-- app:layout_constraintStart_toStartOf="parent"--> - <!-- app:layout_constraintTop_toTopOf="parent" />--> - - <!-- <ImageView--> - <!-- android:id="@+id/iv_call_mute"--> - <!-- android:layout_width="32dp"--> - <!-- android:layout_height="32dp"--> - <!-- android:layout_marginEnd="32dp"--> - <!-- android:clickable="true"--> - <!-- android:focusable="true"--> - <!-- android:src="@drawable/ic_call_mute_default"--> - <!-- app:layout_constraintBottom_toBottomOf="parent"--> - <!-- app:layout_constraintEnd_toEndOf="parent"--> - <!-- app:layout_constraintTop_toTopOf="parent" />--> - </merge> \ No newline at end of file diff --git a/vector/src/main/res/layout/view_active_call_view.xml b/vector/src/main/res/layout/view_current_calls.xml similarity index 79% rename from vector/src/main/res/layout/view_active_call_view.xml rename to vector/src/main/res/layout/view_current_calls.xml index e8b21de7e8..4b151a2fba 100644 --- a/vector/src/main/res/layout/view_active_call_view.xml +++ b/vector/src/main/res/layout/view_current_calls.xml @@ -9,38 +9,38 @@ tools:parentTag="android.widget.RelativeLayout"> <TextView - android:id="@+id/activeCallInfo" + android:id="@+id/currentCallsInfo" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_toStartOf="@id/returnToCallButton" - android:background="?attr/selectableItemBackground" android:drawablePadding="10dp" android:gravity="center_vertical" android:paddingStart="16dp" android:paddingTop="12dp" android:paddingEnd="16dp" android:paddingBottom="12dp" - android:text="@string/active_call" + android:textSize="14sp" + android:text="@string/call_only_active" android:textColor="@color/white" app:drawableTint="@color/white" - app:drawableStartCompat="@drawable/ic_call" /> + app:drawableStartCompat="@drawable/ic_call_answer" /> <com.google.android.material.button.MaterialButton android:id="@+id/returnToCallButton" style="@style/Widget.MaterialComponents.Button.TextButton" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignTop="@+id/activeCallInfo" - android:layout_alignBottom="@+id/activeCallInfo" + android:layout_alignTop="@+id/currentCallsInfo" + android:layout_alignBottom="@+id/currentCallsInfo" android:layout_alignParentEnd="true" android:clickable="false" android:focusable="false" android:gravity="center" android:paddingStart="8dp" android:paddingEnd="16dp" - android:text="@string/return_to_call" + android:text="@string/action_return" android:textColor="@color/white" - android:textSize="15sp" + android:textSize="14sp" android:textStyle="bold" /> </merge> diff --git a/vector/src/main/res/values-bg/strings.xml b/vector/src/main/res/values-bg/strings.xml index bd2d0abf88..c2235f3cd0 100644 --- a/vector/src/main/res/values-bg/strings.xml +++ b/vector/src/main/res/values-bg/strings.xml @@ -1103,7 +1103,7 @@ <string name="notification_new_invitation">Нова покана</string> <string name="notification_sender_me">Аз</string> <string name="notification_inline_reply_failed">** Неуспешно изпращане - моля, отворете стаята</string> - <string name="error_jitsi_not_supported_on_old_device">Извиняваме се, но конферентни разговори с Jitsi не се поддържат на стари устройства (устройства с Android OS под 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Извиняваме се, но конферентни разговори с Jitsi не се поддържат на стари устройства (устройства с Android OS под 6.0)</string> <string name="title_activity_verify_device">Удостоверяване на устройство</string> <string name="auth_login_sso">Влез с единично вписване</string> <string name="encryption_information_unknown_ip">неизвестен IP адрес</string> @@ -1640,7 +1640,7 @@ <string name="room_settings_enable_encryption_dialog_content">Веднъж включено, шифроването за дадена стая не може да бъде изключено. Съобщенията изпратени в шифрована стая не могат да бъдат прочетени от сървъра, а само от участниците в стаята. Включването на шифроване може да попречи на работата на немалък брой ботове и мостове към други мрежи.</string> <string name="room_settings_enable_encryption_dialog_title">Включване на шифроване\?</string> <string name="room_settings_enable_encryption_warning">Веднъж включено, шифроването не може да бъде изключено.</string> - <string name="room_settings_enable_encryption">Включи шифроване от-край-до-край</string> + <string name="room_settings_enable_encryption">Включи шифроване от-край-до-край…</string> <string name="settings_category_composer">Редактор на съобщения</string> <string name="settings_category_timeline">Списък със съобщения</string> <string name="command_description_rainbow_emote">Изпраща емоцията оцветена като дъга</string> diff --git a/vector/src/main/res/values-bn-rIN/strings.xml b/vector/src/main/res/values-bn-rIN/strings.xml index a32b20bada..afb13b26f0 100644 --- a/vector/src/main/res/values-bn-rIN/strings.xml +++ b/vector/src/main/res/values-bn-rIN/strings.xml @@ -1026,7 +1026,7 @@ <item quantity="other">%d টা সক্রিয় উইজেট</item> </plurals> - <string name="error_jitsi_not_supported_on_old_device">দুঃখিত, জিটসির সাথে কনফারেন্স কলগুলি পুরোনো ডিভাইসগুলিতে সমর্থিত নয় (৫.০ এর নীচে এন্ড্রোইড অপারেটিং সিস্টেম সহ ডিভাইসগুলি)</string> + <string name="error_jitsi_not_supported_on_old_device">দুঃখিত, জিটসির সাথে কনফারেন্স কলগুলি পুরোনো ডিভাইসগুলিতে সমর্থিত নয় (৬.০ এর নীচে এন্ড্রোইড অপারেটিং সিস্টেম সহ ডিভাইসগুলি)</string> <string name="widget_integration_unable_to_create">উইজেট তৈরি করতে অক্ষম।</string> <string name="widget_integration_failed_to_send_request">অনুরোধ পাঠাতে ব্যর্থ।</string> diff --git a/vector/src/main/res/values-ca/strings.xml b/vector/src/main/res/values-ca/strings.xml index c7600d0cb1..e78fa6cd81 100644 --- a/vector/src/main/res/values-ca/strings.xml +++ b/vector/src/main/res/values-ca/strings.xml @@ -179,7 +179,7 @@ <string name="auth_reset_password_error_unauthorized">No s\'ha pogut verificar l\'adreça del correu electrònic: assegureu-vos que heu fet clic a l\'enllaç del correu electrònic</string> <string name="auth_reset_password_success_message">La contrasenya s\'ha reiniciat. \n -\nS\'ha tancat la sessió a tots els dispositius i no rebràs més notificacions. Per tal de reactivar les notificacions, torna a iniciar sessió a cada dispositiu.</string> +\nSe t\'ha desconnectat de totes les teves sessions i no rebràs més notificacions. Per reactivar les notificacions, torna a iniciar sessió a cada dispositiu.</string> <!-- Login Screen --> <string name="login_error_must_start_http">La URL ha de començar per http[s]://</string> <string name="login_error_network_error">No s\'ha pogut iniciar la sessió: error de xarxa</string> @@ -197,7 +197,7 @@ <string name="login_error_login_email_not_yet">L\'enllaç del correu electrònic que encara no heu fet clic</string> <!-- crypto warnings --> <!-- read receipts list Screen --> - <string name="read_receipts_list">Llegit per</string> + <string name="read_receipts_list">Llista de confirmacions de lectura</string> <!-- accounts list Screen --> <!-- image size selection --> <string name="compression_options">Envia com</string> @@ -279,8 +279,8 @@ <string name="room_creation_add_member">Afegeix un participant</string> <string name="room_title_one_member">1 participant</string> <!-- Chat participants --> - <string name="room_participants_leave_prompt_title">Abandona la sala</string> - <string name="room_participants_leave_prompt_msg">Esteu segurs que voleu sortir de la sala\?</string> + <string name="room_participants_leave_prompt_title">Marxa de la sala</string> + <string name="room_participants_leave_prompt_msg">Estàs segur que vols marxar de la sala\?</string> <string name="room_participants_remove_prompt_msg">Estàs segur que vols eliminar %s d\'aquest xat\?</string> <string name="room_participants_create">Crea</string> <string name="room_participants_online">En línia</string> @@ -289,9 +289,9 @@ <string name="room_participants_header_admin_tools">EINES D\'ADMINISTRACIÓ</string> <string name="room_participants_header_call">TRUCADA</string> <string name="room_participants_header_direct_chats">Xats personals</string> - <string name="room_participants_header_devices">DISPOSITIUS</string> + <string name="room_participants_header_devices">SESSIONS</string> <string name="room_participants_action_invite">Convida</string> - <string name="room_participants_action_leave">Deixa aquesta sala</string> + <string name="room_participants_action_leave">Marxa d\'aquesta sala</string> <string name="room_participants_action_remove">Elimina d\'aquesta sala</string> <string name="room_participants_action_ban">Veta</string> <string name="room_participants_action_unban">Treu el vet</string> @@ -302,7 +302,7 @@ <string name="room_participants_action_unignore">Deixa d\'ignorar</string> <string name="room_participants_invite_search_another_user">ID d\'usuari, nom o correu electrònic</string> <string name="room_participants_action_mention">Menciona</string> - <string name="room_participants_action_devices_list">Mostra la llista de dispositius</string> + <string name="room_participants_action_devices_list">Mostra la llista de sessions</string> <string name="room_participants_power_level_prompt">No podràs desfer aquest canvi ja que estàs donant a l\'usuari el mateix nivell d\'autoritat que el teu. \nN\'estàs segur\?</string> <string name="room_participants_invite_prompt_msg">Estàs segur que vols convidar a %s a aquest xat\?</string> @@ -385,7 +385,7 @@ <string name="room_settings_favourite">Preferit</string> <string name="room_settings_de_prioritize">Treu prioritat</string> <string name="room_settings_direct_chat">Xat directe</string> - <string name="room_settings_leave_conversation">Deixa el xat</string> + <string name="room_settings_leave_conversation">Marxa del xat</string> <string name="room_settings_forget">Oblida</string> <string name="room_settings_add_homescreen_shortcut">Afegeix a la pantalla d\'inici</string> <!-- home sliding menu --> @@ -407,7 +407,7 @@ <string name="settings_app_info_link_title">Informació de l\'aplicació</string> <string name="settings_notification_ringtone">So de les notificacions</string> <string name="settings_enable_all_notif">Habilita les notificacions d\'aquest compte</string> - <string name="settings_enable_this_device">Habilita les notificacions d\'aquest dispositiu</string> + <string name="settings_enable_this_device">Activa les notificacions per aquesta sessió</string> <string name="settings_turn_screen_on">Encén la pantalla durant 3 segons</string> <string name="settings_containing_my_display_name">Missatges que contenen el meu àlies</string> <string name="settings_containing_my_user_name">Missatges que contenen el meu nom d\'usuari</string> @@ -443,7 +443,7 @@ <string name="settings_home_display">Pantalla d\'inici</string> <string name="settings_pin_missed_notifications">Fixa les sales amb notificacions perdudes</string> <string name="settings_pin_unread_messages">Fixa les sales amb missatges sense llegir</string> - <string name="settings_devices_list">Dispositius</string> + <string name="settings_devices_list">Sessions</string> <string name="settings_inline_url_preview">Previsualitzacions dels URL en línia</string> <string name="settings_always_show_timestamps">Mostra sempre l\'hora a tots els missatges</string> <string name="settings_12_24_timestamps">Mostra l\'hora en el format de 12 hores</string> @@ -451,10 +451,10 @@ <!-- analytics --> <string name="settings_analytics">Analítiques</string> <string name="settings_data_save_mode">Mode d\'estalvi de dades</string> - <string name="devices_details_dialog_title">Detalls del dispositiu</string> + <string name="devices_details_dialog_title">Detalls de la sessió</string> <string name="devices_details_id_title">ID</string> <string name="devices_details_name_title">Nom</string> - <string name="devices_details_device_name">Nom del dispositiu</string> + <string name="devices_details_device_name">Actualitza el nom públic</string> <string name="devices_details_last_seen_title">Últim cop vist</string> <string name="devices_details_last_seen_format">%1$s @ %2$s</string> <string name="devices_delete_dialog_text">Aquesta operació necessita que us autentiqueu.\nPer tal de continuar, introduïu la vostra contrasenya.</string> @@ -574,11 +574,11 @@ <string name="encryption_information_algorithm">Algoritme</string> <string name="encryption_information_session_id">ID de la sessió</string> <string name="encryption_information_decryption_error">Error al desxifrar</string> - <string name="encryption_information_sender_device_information">Informació del dispositiu que envia</string> - <string name="encryption_information_device_name">Nom del dispositiu</string> + <string name="encryption_information_sender_device_information">Informació de la sessió del remitent</string> + <string name="encryption_information_device_name">Nom públic</string> <string name="encryption_information_name">Nom</string> <string name="encryption_information_device_id">ID de sessió</string> - <string name="encryption_information_device_key">Clau del dispositiu</string> + <string name="encryption_information_device_key">Clau de sessió</string> <string name="encryption_information_verification">Verificació</string> <string name="encryption_information_ed25519_fingerprint">Empremta digital Ed25519</string> <string name="encryption_export_e2e_room_keys">Exporta les claus de la sala E2E</string> @@ -594,7 +594,7 @@ <string name="encryption_import_room_keys">Importa les claus de la sala</string> <string name="encryption_import_room_keys_summary">Importa les claus de la sala des d\'un fitxer local</string> <string name="encryption_import_import">Importa</string> - <string name="encryption_never_send_to_unverified_devices_title">Encripta només per a dispositius verificats</string> + <string name="encryption_never_send_to_unverified_devices_title">Xifra només en sessions verificades</string> <string name="encryption_never_send_to_unverified_devices_summary">No enviïs mai missatges xifrats a sessions no verificades des d\'aquesta sessió.</string> <string name="encryption_information_not_verified">NO verificat</string> <string name="encryption_information_verified">Verificat</string> @@ -605,9 +605,9 @@ <string name="encryption_information_unverify">No verifiquis</string> <string name="encryption_information_block">Bloqueja</string> <string name="encryption_information_unblock">Deixa de bloquejar</string> - <string name="encryption_information_verify_device">Verifica el dispositiu</string> + <string name="encryption_information_verify_device">Verifica la sessió</string> <string name="encryption_information_verify_device_warning">Verifica comparant el següent amb la configuració d\'usuari de la teva altra sessió:</string> - <string name="encryption_information_verify_device_warning2">Si coincideix, premeu el botó per verificar. Si no coincideix, algú està interceptant aquest dispositiu i probablement voldreu prémer el botó per bloquejar-lo. En un futur aquest procés de verificació serà més sofisticat.</string> + <string name="encryption_information_verify_device_warning2">Si no coincideixen pot ser que la seguretat de la comunicació estigui compromesa.</string> <string name="encryption_information_verify_key_match">Verifica que les claus coincideixen</string> <!-- unknown devices management --> <string name="unknown_devices_alert_title">La sala conté sessions desconegudes</string> @@ -651,8 +651,8 @@ <string name="room_add_matrix_apps">Afegeix aplicacions de Matrix</string> <string name="settings_labs_native_camera">Utilitza la càmera nativa</string> <!-- share keys --> - <string name="you_added_a_new_device">El nou dispositiu \'%s\' que heu afegit, sol·licita les claus d\'encriptació.</string> - <string name="your_unverified_device_requesting">El vostre dispositiu \'%s\' sense verificar, sol·licita les claus d\'encriptació.</string> + <string name="you_added_a_new_device">Has afegit una nova sessió \'%s\' que està sol·licitant les claus de xifrat.</string> + <string name="your_unverified_device_requesting">La teva sessió no verificada \'%s\' està sol·licitant les claus de xifrat.</string> <string name="start_verification">Inicia la verificació</string> <string name="share_without_verifying">Comparteix sense verificar</string> <string name="ignore_request">Ignora la sol·licitut</string> @@ -754,10 +754,10 @@ <string name="option_send_voice">Envia veu</string> <string name="go_on_with">seguir amb…</string> <string name="error_no_external_application_found">Ho sento, no s\'ha trobat cap aplicació externa per completar l\'acció.</string> - <string name="e2e_re_request_encryption_key"><u>Tornar a demanar les claus d\'encriptació</u> als teus altres dispositius.</string> + <string name="e2e_re_request_encryption_key">Tornar a demanar les claus de xifrat de les teves altres sessions.</string> <string name="e2e_re_request_encryption_key_sent">Petició de clau enviada.</string> <string name="e2e_re_request_encryption_key_dialog_title">Sol·licitud enviada</string> - <string name="e2e_re_request_encryption_key_dialog_content">Si us plau, obre Element a un altre dispositiu que pugui desencriptar el missatge de manera que pugui enviar les claus a aquesta sessió.</string> + <string name="e2e_re_request_encryption_key_dialog_content">Si us plau, obre Element a un altre dispositiu que pugui desxifrar el missatge de manera que pugui enviar les claus a aquesta sessió.</string> <string name="settings_notification_privacy_normal">Normal</string> <string name="status_theme">Tema Status.im</string> <string name="missing_permissions_error">No es pot dur a terme aquesta acció per falta de permisos.</string> @@ -848,7 +848,7 @@ <string name="dialog_user_consent_content">Per poder continuar utilitzant el servidor local %1$s has de revisar i acceptar els termes i condicions.</string> <string name="dialog_user_consent_submit">Revisa ara</string> <string name="deactivate_account_title">Desactiva el compte</string> - <string name="deactivate_account_content">Això farà que no puguis utilitzar més el teu compte. No podràs iniciar sessió i ningú podrà tornar-se a registrar amb el mateix ID d\'usuari. Això farà que el teu compte surti de totes les sales a les que estigui participant i eliminarà les dades del compte del teu servidor d\'identitat. <b>Aquesta acció és irreversible</b>. + <string name="deactivate_account_content">Això farà que no puguis utilitzar més el teu compte. No podràs iniciar sessió i ningú podrà tornar-se a registrar amb el mateix ID d\'usuari. Això farà que el teu compte marxi de totes les sales a les que estigui participant i eliminarà les dades del compte del teu servidor d\'identitat. <b>Aquesta acció és irreversible</b>. \n \nDesactivar el compte <b>no implica que s\'oblidin els missatges que has enviat</b>. Si vols que ens n\'oblidem, marca la casella a continuació. \n @@ -908,7 +908,7 @@ \nComprova la configuració del compte.</string> <string name="settings_troubleshoot_test_account_settings_quickfix">Habilita</string> <string name="settings_troubleshoot_test_device_settings_title">Configuració de la sessió.</string> - <string name="settings_troubleshoot_test_device_settings_success">Les notificacions d\'aquest dispositiu estan activades.</string> + <string name="settings_troubleshoot_test_device_settings_success">Les notificacions per aquesta sessió estan activades.</string> <string name="settings_troubleshoot_test_device_settings_quickfix">Habilita</string> <string name="settings_troubleshoot_test_play_services_quickfix">Repara els serveis de Google Play</string> <string name="settings_troubleshoot_test_foreground_service_started_title">Servei de notificacions</string> @@ -920,7 +920,7 @@ <string name="settings_troubleshoot_test_bg_restricted_quickfix">Inhabilita les restriccions</string> <string name="settings_troubleshoot_test_battery_title">Optimització de bateria</string> <string name="settings_troubleshoot_test_battery_success">El Element no està afectat per l\'optimització de bateria.</string> - <string name="settings_show_join_leave_messages">Mostra els esdeveniments d\'entrada i sortida</string> + <string name="settings_show_join_leave_messages">Mostra els esdeveniments d\'unió i sortida</string> <string name="settings_show_avatar_display_name_changes_messages_summary">Inclou els canvis d\'icona i d\'àlies.</string> <string name="startup_notification_fdroid_battery_optim_title">Connexió al rerefons</string> <string name="startup_notification_fdroid_battery_optim_message">Per poder obtenir notificacions fiables, Element necessita mantenir una connexió en segon pla de baixa incidència. @@ -975,8 +975,8 @@ <string name="settings_troubleshoot_test_service_restart_title">Reinici automàtic del servei de notificacions</string> <string name="settings_troubleshoot_test_service_restart_success">El servei s\'ha parat i tornat a iniciar automàticament.</string> <string name="settings_troubleshoot_test_service_restart_failed">No s\'ha pogut iniciar el servei</string> - <string name="settings_troubleshoot_test_service_boot_success">El servei s\'iniciarà quan s\'iniciï el dispositiu.</string> - <string name="settings_troubleshoot_test_service_boot_failed">El servei no s\'iniciarà quan el dispositiu s\'iniciï, per la qual cosa no rebreu notificacions fins que el Element s\'haja obert una vegada.</string> + <string name="settings_troubleshoot_test_service_boot_success">El servei s\'iniciarà quan es reiniciï el dispositiu.</string> + <string name="settings_troubleshoot_test_service_boot_failed">El servei no s\'iniciarà quan el dispositiu es reiniciï, per tant, no rebràs notificacions fins que Element no s\'hagi obert per primera vegada.</string> <string name="settings_troubleshoot_test_service_boot_quickfix">Habilita l\'inici durant l\'arrencada</string> <string name="settings_troubleshoot_test_bg_restricted_title">Comprova les restriccions del rerefons</string> <string name="settings_troubleshoot_test_battery_quickfix">Ignora l\'optimització</string> @@ -986,7 +986,7 @@ <string name="settings_system_preferences_summary">Seleccioneu el color de LED, la vibració, so…</string> <string name="settings_cryptography_manage_keys">Gestió de claus criptogràfiques</string> <string name="settings_show_read_receipts">Mostra les confirmacions de lectura</string> - <string name="settings_show_read_receipts_summary">Feu clic en les confirmacions de lectura per obtenir una llista detallada.</string> + <string name="settings_show_read_receipts_summary">Fes clic a les confirmacions de lectura per obtenir-ne una llista detallada.</string> <string name="startup_notification_fdroid_battery_optim_button_grant">Concedeix el permís</string> <string name="account_email_error">S\'ha produït un error en verificar la vostra adreça de correu electrònic.</string> <string name="account_phone_number_error">S\'ha produït un error en verificar el vostre número de telèfon.</string> @@ -1000,7 +1000,7 @@ <string name="keys_backup_passphrase_not_empty_error_message">Suprimiu la frase si vols que Element generi una clau de recuperació.</string> <string name="keys_backup_no_session_error">No hi ha cap sessió de Matrix disponible</string> <string name="keys_backup_setup_step1_title">No perdeu mai els missatges xifrats</string> - <string name="keys_backup_setup_step1_description">Els missatges en sales encriptades estan protegits amb encriptació d\'extrem a extrem. Només tu i el/s destinatari/s teniu les claus per poder llegir aquests missatges. + <string name="keys_backup_setup_step1_description">Els missatges en sales xifrades estan protegits amb xifrat d\'extrem a extrem. Només tu i el/s destinatari/s teniu les claus per poder llegir aquests missatges. \n \nFes una còpia de seguretat de les teves claus per evitar perdre\'ls.</string> <string name="keys_backup_setup_step2_button_title">Estableix la frase de pas</string> @@ -1016,7 +1016,7 @@ <string name="unexpected_error">Error inesperat</string> <string name="keys_backup_setup_backup_started_title">S\'ha iniciat la còpia de seguretat</string> <string name="keys_backup_setup_skip_title">N\'esteu segur?</string> - <string name="keys_backup_setup_skip_msg">És possible que perdeu l\'accés als vostres missatges si sortiu de la sessió o perdeu el dispositiu.</string> + <string name="keys_backup_setup_skip_msg">És possible que perdis l\'accés als teus missatges si tanques la sessió o perds el dispositiu.</string> <string name="keys_backup_restore_is_getting_backup_version">S\'està recuperant la versió de la còpia de seguretat…</string> <string name="keys_backup_restore_with_passphrase">Utilitza la teva frase de recuperació per desbloquejar el teu històric de missatges xifrats</string> <string name="keys_backup_restore_use_recovery_key">utilitza la clau de recuperació</string> @@ -1049,25 +1049,25 @@ <item quantity="other">S\'ha restaurat una còpia amb %d claus.</item> </plurals> <plurals name="keys_backup_restore_success_description_part2"> - <item quantity="one">S\'ha afegit %d clau nova a aquest dispositiu.</item> - <item quantity="other">S\'han afegit %d claus noves a aquest dispositiu.</item> + <item quantity="one">S\'ha afegit %d clau nova a aquesta sessió.</item> + <item quantity="other">S\'han afegit %d claus noves a aquesta sessió.</item> </plurals> <string name="keys_backup_get_version_error">No s\'ha pogut obtenir la versió de les claus de recuperació més recents (%s).</string> <string name="keys_backup_no_keysbackup_sdk_error">La criptografia de la sessió no és activa</string> - <string name="keys_backup_settings_restore_backup_button">Restaura des de la còpia de seguretat</string> + <string name="keys_backup_settings_restore_backup_button">Restaura des de còpia de seguretat</string> <string name="keys_backup_settings_delete_backup_button">Suprimeix la còpia de seguretat</string> - <string name="keys_backup_settings_status_ok">S\'ha configurat la còpia de seguretat de la clau correctament per a aquest dispositiu.</string> - <string name="keys_backup_settings_status_ko">La còpia de seguretat de la clau no és activa en aquest dispositiu.</string> - <string name="keys_backup_settings_status_not_setup">No s\'està fent còpia de seguretat de les vostres claus en aquest dispositiu.</string> + <string name="keys_backup_settings_status_ok">S\'ha configurat la còpia de seguretat de les claus correctament en aquesta sessió.</string> + <string name="keys_backup_settings_status_ko">La còpia de seguretat de les claus no està activada en aquesta sessió.</string> + <string name="keys_backup_settings_status_not_setup">No s\'està fent còpia de seguretat de les teves claus en aquesta sessió.</string> <string name="keys_backup_settings_deleting_backup">S\'està suprimint la còpia de seguretat…</string> <string name="keys_backup_settings_delete_backup_error">No s\'ha pogut suprimir la còpia de seguretat (%s)</string> <string name="keys_backup_settings_delete_confirm_title">Suprimeix la còpia de seguretat</string> - <string name="keys_backup_settings_signature_from_unknown_device">La còpia de seguretat té una signatura d\'un dispositiu desconegut amb ID %s.</string> - <string name="keys_backup_settings_valid_signature_from_this_device">La còpia de seguretat té una signatura vàlida d\'aquest dispositiu.</string> - <string name="keys_backup_settings_valid_signature_from_verified_device">La còpia de seguretat té una signatura vàlida del dispositiu verificat %s.</string> - <string name="keys_backup_settings_valid_signature_from_unverified_device">La còpia de seguretat té una signatura vàlida del dispositiu no verificat %s</string> - <string name="keys_backup_settings_invalid_signature_from_verified_device">La còpia de seguretat té una signatura no vàlida del dispositiu verificat %s</string> - <string name="keys_backup_settings_invalid_signature_from_unverified_device">La còpia de seguretat té una signatura no vàlida del dispositiu no verificat %s</string> + <string name="keys_backup_settings_signature_from_unknown_device">La còpia de seguretat té una signatura d\'una sessió desconeguda amb ID %s.</string> + <string name="keys_backup_settings_valid_signature_from_this_device">La còpia de seguretat té una signatura vàlida d\'aquesta sessió.</string> + <string name="keys_backup_settings_valid_signature_from_verified_device">La còpia de seguretat té una signatura vàlida de la sessió verificada %s.</string> + <string name="keys_backup_settings_valid_signature_from_unverified_device">La còpia de seguretat té una signatura vàlida de la sessió no verificada %s</string> + <string name="keys_backup_settings_invalid_signature_from_verified_device">La còpia de seguretat té una signatura no vàlida de la sessió verificada %s</string> + <string name="keys_backup_settings_invalid_signature_from_unverified_device">La còpia de seguretat té una signatura no vàlida de la sessió no verificada %s</string> <string name="keys_backup_get_trust_error">No s\'ha pogut obtenir la informació de confiança per a la còpia de seguretat (%s).</string> <string name="keys_backup_settings_delete_confirm_message">Vols eliminar les teves claus de xifrat del servidor\? Ja no podràs utilitzar la clau de recuperació per llegir el teu històric de missatges xifrats.</string> <string name="keys_backup_is_not_finished_please_wait">La còpia de seguretat de les claus no ha finalitzat, espera…</string> @@ -1111,7 +1111,7 @@ <string name="keys_backup_info_title_signature">Signatura</string> <string name="new_recovery_method_popup_was_me">He sigut jo</string> <string name="new_recovery_method_popup_title">Nova còpia de seguretat de clau</string> - <string name="sign_out_bottom_sheet_warning_backup_not_active">Per evitar la pèrdua d\'accés als teus missatges xifrats, hauries d\'activar la còpia de seguretat segura a totes les teves sessions.</string> + <string name="sign_out_bottom_sheet_warning_backup_not_active">Per evitar la pèrdua d\'accés als teus missatges xifrats, hauries d\'activar la còpia de seguretat de les claus a totes les teves sessions.</string> <string name="sign_out_bottom_sheet_will_lose_secure_messages">Perdràs l\'accés als teus missatges xifrats si no fas una còpia de seguretat de les teves claus abans de tancar la sessió.</string> <string name="keys_backup_setup_step2_text_description">Es desarà una còpia xifrada de les teves claus al teu servidor local. Protegeix la còpia de seguretat amb una contrasenya per tal de mantenir-la segura. \n @@ -1131,7 +1131,7 @@ <string name="ignore">Ignora</string> <string name="auth_login_sso">Inicia sessió amb la inscripció única (SSO)</string> <string name="login_error_unknown_host">Aquesta URL no està disponible , si us plau verifiqueu-la</string> - <string name="login_error_ssl_handshake">El vostre dispositiu està usant una versió obsoleta del protocol de seguretat TLS, vulnerable a atacs. Per a la vostra seguretat no us podreu connectar</string> + <string name="login_error_ssl_handshake">El teu dispositiu està utilitzant una versió obsoleta del protocol de seguretat TLS, vulnerable a atacs. Per a la teva pròpia seguretat, no et pots connectar</string> <string name="settings_send_message_with_enter">Envia missatges amb retorn</string> <string name="settings_send_message_with_enter_summary">La tecla retorn (enter) del teclat virtual enviarà el missatge en comptes d\'afegir un salt de línia</string> <string name="settings_change_password_submit">Actualitzar la contrasenya</string> @@ -1164,7 +1164,7 @@ <string name="notification_new_invitation">Nova invitació</string> <string name="notification_sender_me">Jo</string> <string name="notification_inline_reply_failed">** No s\'ha pogut enviar - si us plau, obre la sala</string> - <string name="error_jitsi_not_supported_on_old_device">Ho sentim, les videoconferències amb Jitsi no són compatibles amb dispositius antics (dispositius amb Android inferior a 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Ho sentim, les videoconferències amb Jitsi no són compatibles amb dispositius antics (dispositius amb Android inferior a 6.0)</string> <string name="integration_manager_not_configured">No has configurat cap gestor d\'integracions.</string> <string name="you_added_a_new_device_with_info">Una nova sessió està sol·licitant claus de xifrat. \nNom de la sessió: %1$s @@ -1178,7 +1178,7 @@ <string name="share_without_verifying_short_label">Compartir</string> <string name="key_share_request">Sol·licituds d\'intercanvi de claus</string> <string name="ignore_request_short_label">Ignorar</string> - <string name="keys_backup_setup_override_backup_prompt_tile">Ja existeix una còpia de seguretat al vostre HomeServer</string> + <string name="keys_backup_setup_override_backup_prompt_tile">Ja existeix una còpia de seguretat al teu servidor local</string> <string name="keys_backup_setup_override_backup_prompt_description">Sembla que ja has configurat una còpia de seguretat de claus des d\'una altra sessió. Vols reemplaçar-la amb la que estàs creant\?</string> <string name="keys_backup_setup_override_replace">Reemplaçar</string> <string name="keys_backup_setup_override_stop">Aturar</string> @@ -1186,20 +1186,20 @@ <string name="autodiscover_well_known_autofill_dialog_title">Opcions de compleció automàtica del servidor</string> <string name="autodiscover_well_known_autofill_dialog_message">Element ha detectat una configuració de servidor personalitzada pel teu domini d\'ID d\'usuari \"%1$s\": \n%2$s</string> - <string name="invalid_or_expired_credentials">Us heu desconnectat a causa de credencials incorrectes o caducades.</string> + <string name="invalid_or_expired_credentials">Se t\'ha desconnectat a causa de credencials incorrectes o caducades.</string> <string name="sas_verify_title">Verificar comparant una cadena de text curta.</string> - <string name="sas_security_advise">Per la màxima seguretat us recomanem fer això en persona o usar un altre medi de comunicació confiable.</string> + <string name="sas_security_advise">Per a la màxima seguretat et recomanem fer-ho en persona o utilitzar un altre mètode de comunicació segur.</string> <string name="sas_verify_start_button_title">Començar la verificació</string> <string name="sas_incoming_request_title">Sol·licitud de verificació entrant</string> <string name="sas_incoming_request_description">Verifica aquesta sessió per fer-la de confiança. Les sessions de confiança amb els contactes et donen un alleujament addicional quan utilitzes missatges xifrats d\'extrem a extrem.</string> - <string name="sas_incoming_request_description_2">Verificant aquest dispositiu el marcareu com a confiable, i també marcareu el vostre dispositiu com a confiable pel vostre company.</string> - <string name="sas_emoji_description">Verificar aquest dispositiu confirmant els següents emojis que apareguin a la pantalla del vostre company</string> - <string name="sas_decimal_description">Verificar aquest dispositiu confirmant els següents números que sortiran a la pantalla del vostre company</string> + <string name="sas_incoming_request_description_2">Si verifiques aquesta sessió, es farà de confiança. L\'altre usuari també la veurà com a sessió de confiança.</string> + <string name="sas_emoji_description">Verifica aquesta sessió confirmant que les següents emoticones apareixen a la pantalla de l\'altre usuari</string> + <string name="sas_decimal_description">Verifica aquesta sessió confirmant que els següents números apareixen a la pantalla de l\'altre usuari</string> <string name="sas_incoming_verification_request_dialog">Heu rebut una sol·licitud de verificació entrant.</string> <string name="sas_view_request_action">Veure sol·licitud</string> <string name="sas_waiting_for_partner">Esperant que el vostre company confirmi…</string> <string name="sas_verified">Verificat!</string> - <string name="sas_verified_successful">Heu verificat aquest dispositiu amb èxit.</string> + <string name="sas_verified_successful">Has verificat aquesta sessió correctament.</string> <string name="sas_verified_successful_description">Els missatges segurs amb aquest usuari estan encriptats end-to-end i no serà possible llegir-los per tercers.</string> <string name="sas_got_it">Entesos</string> <string name="sas_verifying_keys">No surt res\? Encara no tots els clients suporten la verificació interactiva. Useu el mètode de verificació antic.</string> @@ -1210,16 +1210,16 @@ \n%s</string> <string name="sas_cancelled_by_me">S\'ha cancel·lat la verificació. \nMotiu: %s</string> - <string name="sas_verification_request_notification_channel">Verificació de dispositiu interactiva</string> + <string name="sas_verification_request_notification_channel">Verificació de sessió interactiva</string> <string name="sas_incoming_request_notif_title">Sol·licitud de verificació</string> - <string name="sas_incoming_request_notif_content">%s vol verificar el vostre dispositiu</string> + <string name="sas_incoming_request_notif_content">%s vol verificar la teva sessió</string> <string name="sas_error_m_user">L\'usuari ha cancel·lat la verificació</string> <string name="sas_error_m_timeout">El marge de temps pel procés de verificació ha expirat</string> - <string name="sas_error_m_unknown_transaction">El dispositiu no coneix la transacció</string> + <string name="sas_error_m_unknown_transaction">La sessió no reconeix aquesta transacció</string> <string name="sas_error_m_unknown_method">La sessió no pot acceptar un acord de claus, hash, MAC o amb mètode SAS</string> <string name="sas_error_m_mismatched_commitment">El compromís del hash no ha coincidit</string> <string name="sas_error_m_mismatched_sas">El SAS no ha coincidit</string> - <string name="sas_error_m_unexpected_message">El dispositiu ha rebut un missatge inesperat</string> + <string name="sas_error_m_unexpected_message">La sessió ha rebut un missatge inesperat</string> <string name="sas_error_m_invalid_message">S\'ha rebut un missatge invàlid</string> <string name="sas_error_m_key_mismatch">La clau no coincideix</string> <string name="sas_error_m_user_error">L\'usuari no coincideix</string> @@ -1244,7 +1244,7 @@ <string name="message_add_reaction">Afegir reacció</string> <string name="message_view_reaction">Veure reaccions</string> <string name="reactions">Reaccions</string> - <string name="event_redacted_by_user_reason">Esdeveniment eliminat per l\'usuari</string> + <string name="event_redacted_by_user_reason">Esdeveniment eliminat per usuari</string> <string name="event_redacted_by_admin_reason">Esdeveniment moderat per l\'administrador de la sala</string> <string name="last_edited_info_message">Última edició per %1$s el %2$s</string> <string name="malformed_message">Esdeveniment mal format, no es pot mostrar</string> @@ -1627,7 +1627,7 @@ \n \nPer evitar que torni a unir-s\'hi, l\'hauries de vetar.</string> <string name="room_participants_action_cancel_invite_prompt_msg">Segur que vols cancel·lar la invitació d\'aquest usuari\?</string> - <string name="room_participants_leave_private_warning">Aquesta sala no és pública. No podràs tornar a unir-t\'hi sense una invitació.</string> + <string name="room_participants_leave_private_warning">Aquesta sala no és pública. No podràs tornar-t\'hi a unir sense una invitació.</string> <string name="call_failed_no_connection_description">No s\'ha pogut establir una connexió en temps real. \nDemana a l\'administrador del servidor local que configuri un servidor TURN perquè les trucades funcionin correctament.</string> <string name="call_failed_no_ice_description">Demana a l\'administrador del servidor local (%1$s) que configuri un servidor TURN perquè les trucades funcionin correctament. @@ -1635,7 +1635,7 @@ \nAlternativament, pots provar d\'utilitzar el servidor públic %2$s, però no serà tant fiable i es compartirà la teva adreça IP amb el servidor. També pots gestionar-ho a Configuració.</string> <string name="encryption_enabled">Xifrat activat</string> <string name="alert_push_are_disabled_title">Les notificacions estan desactivades</string> - <string name="login_reset_password_success_notice_2">S\'ha tancat la sessió a tots els dispositius i no rebràs més notificacions. Per tal de reactivar les notificacions, torna a iniciar sessió a cada dispositiu.</string> + <string name="login_reset_password_success_notice_2">Se t\'ha desconnectat de totes les teves sessions i no rebràs més notificacions. Per reactivar les notificacions, torna a iniciar sessió a cada dispositiu.</string> <string name="push_gateway_item_format">Format:</string> <string name="push_gateway_item_url">Url:</string> <string name="push_gateway_item_device_name">session_name:</string> @@ -1722,7 +1722,7 @@ <string name="settings_show_redacted_summary">Indica els missatges eliminats</string> <string name="keys_backup_recovery_key_error_decrypt">La còpia de seguretat no s\'ha pogut desxifrar amb aquesta clau de recuperació: comprova que has introduït la clau de recuperació correcta.</string> <string name="bootstrap_migration_backup_recovery_key">Clau de recuperació de la còpia de seguretat de claus</string> - <string name="bootstrap_migration_use_recovery_key">utilitza la clau de recuperació de la còpia de seguretat de claus</string> + <string name="bootstrap_migration_use_recovery_key">utilitzar la clau de recuperació de còpia de seguretat de claus</string> <string name="your_recovery_key">Clau de recuperació</string> <string name="use_recovery_key">Utilitza clau de recuperació</string> <string name="save_recovery_key_chooser_hint">Desa la clau de recuperació a</string> @@ -1958,7 +1958,7 @@ <string name="recovery_passphrase">Frase de recuperació</string> <string name="verification_cancelled">Verificació cancel·lada</string> <string name="verify_cancelled_notice">Verifica els teus dispositius des de la configuració.</string> - <string name="verify_not_me_self_verification">Alguna de les següents coses pot haver estat compromesa: + <string name="verify_not_me_self_verification">Alguna de les següents pot haver estat compromesa: \n \n- La teva contrasenya \n- El servidor local @@ -1992,7 +1992,7 @@ <string name="new_signin">Nou inici de sessió</string> <string name="verification_use_passphrase">Si no pots accedir a una sessió existent</string> <string name="verification_cannot_access_other_session">Utilitza una frase de recuperació o una clau</string> - <string name="command_description_poll">Crea una enquesta simple</string> + <string name="command_description_poll">Crea una votació simple</string> <string name="poll_item_selected_aria">Opció seleccionada</string> <plurals name="poll_info_final"> <item quantity="one">%d vot - Resultat final</item> @@ -2151,4 +2151,240 @@ <string name="soft_logout_clear_data_notice">Atenció: les teves dades personals (incloses les claus de xifrat) encara estan desades en aquest dispositiu. \n \nEsborra-les si ja no vols utilitzar aquest dispositiu o vols iniciar sessió amb un altre compte.</string> + <string name="error_unauthorized">No estàs autoritzat, no s\'han trobat credencials d\'autenticació vàlides</string> + <string name="new_session_review_with_info">%1$s (%2$s)</string> + <string name="bootstrap_progress_generating_ssss_recovery">Generant clau SSSS a partir de clau de recuperació</string> + <string name="bootstrap_progress_generating_ssss_with_info">Generant clau SSSS a partir de frase (%s)</string> + <string name="bootstrap_progress_generating_ssss">Generant clau SSSS a partir de frase</string> + <string name="bootstrap_progress_checking_backup_with_info">Comprovant clau de còpia de seguretat (%s)</string> + <string name="bootstrap_progress_checking_backup">Comprovant clau de còpia de seguretat</string> + <string name="enter_backup_passphrase">Introdueix %s</string> + <string name="use_file">Utilitza un fitxer</string> + <string name="bootstrap_enter_recovery">Introdueix la %s per a continuar</string> + <string name="upgrade_security">Actualització de xifrat disponible</string> + <string name="room_message_placeholder">Missatge…</string> + <string name="settings_troubleshoot_title">Resolució de problemes</string> + <string name="settings_when_rooms_are_upgraded">Quan les sales s\'actualitzin</string> + <string name="settings_messages_in_e2e_one_to_one">Missatges xifrats en xats individuals</string> + <string name="settings_messages_in_e2e_group_chat">Missatges xifrats en xats de grup</string> + <string name="settings_messages_at_room">Missatges que contenen @room</string> + <string name="settings_notification_configuration">Configuració de notificacions</string> + <string name="error_failed_to_import_keys">No s\'han pogut importar les claus</string> + <string name="qr_code_scanned_verif_waiting">Esperant %s…</string> + <string name="qr_code_scanned_verif_waiting_notice">Ja gairebé has acabat! Esperant la confirmació…</string> + <string name="qr_code_scanned_self_verif_notice">Ja gairebé has acabat! Veus el mateix escut a l\'altre dispositiu\?</string> + <string name="direct_room_created_summary_item_by_you">T\'hi has unit.</string> + <string name="direct_room_created_summary_item">%s s\'ha unit.</string> + <string name="room_created_summary_item_by_you">Has creat i configurat la sala.</string> + <string name="room_created_summary_item">%s ha creat i configurat la sala.</string> + <string name="encryption_not_enabled">El xifrat no està activat</string> + <string name="bootstrap_finish_title">Ja has acabat!</string> + <string name="bootstrap_loading_text">Això pot tardar uns segons, un moment, si us plau.</string> + <string name="bootstrap_info_confirm_text">Torna a introduir la %s per a confirmar-la.</string> + <string name="verification_profile_device_new_signing">%1$s (%2$s) ha entrat amb una nova sessió:</string> + <string name="not_trusted">No és de confiança</string> + <string name="verification_request_alert_description">Per a més seguretat, verifica %s comprovant un codi d\'un sol ús als vostres dos dispositius. +\n +\nPer a la màxima seguretat, fes-ho en persona.</string> + <string name="authentication_error">Ha fallat l\'autenticació</string> + <string name="wrong_pin_message_last_remaining_attempt">Atenció! Últim intent disponible abans de tancar la sessió!</string> + <plurals name="wrong_pin_message_remaining_attempts"> + <item quantity="one">Codi incorrecte, %d intent restant</item> + <item quantity="other">Codi incorrecte, %d intents restants</item> + </plurals> + <string name="phone_book_perform_lookup">Busca contactes a Matrix</string> + <string name="search_in_my_contacts">Busca als meus contactes</string> + <string name="element_login_splash_brand">element</string> + <string name="element_disclaimer_positive_button">MÉS INFORMACIÓ</string> + <string name="element_disclaimer_title">Riot ara és Element!</string> + <string name="crypto_error_withheld_generic">No pots accedir al missatge perquè el remitent, intencionadament, no ha enviat les claus</string> + <string name="crypto_error_withheld_unverified">No pots accedir al missatge perquè la teva sessió no és de confiança pel remitent</string> + <string name="crypto_error_withheld_blacklisted">No pots accedir al missatge perquè el remitent t\'ha bloquejat</string> + <string name="crypto_utd">No s\'ha pogut desxifrar</string> + <string name="notice_crypto_unable_to_decrypt_friendly">Esperant el missatge, pot tardar una estona</string> + <string name="notice_crypto_unable_to_decrypt_final">No tens accés a aquest missatge</string> + <string name="room_settings_save_success">Has canviat la configuració de la sala correctament</string> + <string name="room_settings_name_hint">Nom de la sala</string> + <string name="initialize_cross_signing">Inicia la signatura creuada</string> + <string name="failed_to_initialize_cross_signing">No s\'ha pogut configurar la signatura creuada</string> + <string name="setup_cross_signing">Activa la signatura creuada</string> + <string name="reset_cross_signing">Restableix les claus</string> + <string name="error_empty_field_choose_user_name">Introdueix un nom d\'usuari.</string> + <string name="error_empty_field_choose_password">Introdueix una contrasenya.</string> + <string name="save_your_security_key_notice">Desa la clau de seguretat en un lloc segur, com ara un gestor de contrasenyes o una caixa forta.</string> + <string name="bottom_sheet_save_your_recovery_key_content">Desa la clau de seguretat en un lloc segur, com ara un gestor de contrasenyes o una caixa forta.</string> + <string name="bottom_sheet_setup_secure_backup_security_key_subtitle">Genera una clau de seguretat per desar-la en un lloc molt segur, com ara un gestor de contrasenyes o una caixa forta.</string> + <string name="bottom_sheet_setup_secure_backup_security_phrase_title">Utilitza una frase de seguretat</string> + <string name="bottom_sheet_setup_secure_backup_security_key_title">Utilitza una clau de seguretat</string> + <string name="bottom_sheet_setup_secure_backup_title">Còpia de seguretat segura</string> + <string name="identity_server_set_alternative_submit">Envia</string> + <string name="identity_server_set_alternative_notice_no_default">Introdueix l\'URL del servidor d\'identitat</string> + <string name="identity_server_set_alternative_notice">També pots introduir qualsevol altre enllaç d\'un servidor d\'identitat</string> + <string name="identity_server_set_default_submit">Utilitza %1$s</string> + <string name="identity_server_set_default_notice">El servidor local (%1$s) proposa utilitzar %2$s com a servidor d\'identitat</string> + <string name="trusted">De confiança</string> + <string name="room_member_profile_sessions_section_title">Sessions</string> + <string name="room_member_profile_failed_to_get_devices">No s\'han pogut obtenir les sessions</string> + <string name="verification_profile_warning">Atenció</string> + <string name="verification_profile_verified">Verificat</string> + <string name="verification_profile_verify">Verifica</string> + <string name="complete_security">Completa la seguretat</string> + <string name="crosssigning_other_user_not_trust">Pot ser que altres usuaris no hi confiïn</string> + <string name="unignore">Deixa d\'ignorar</string> + <string name="rendering_event_error_exception">Element ha trobat un problema al renderitzar el contingut de l\'esdeveniment amb ID \'%1$s\'</string> + <string name="rendering_event_error_type_of_message_not_handled">Element no gestiona els missatges de tipus \'%1$s\'</string> + <string name="rendering_event_error_type_of_event_not_handled">Element no gestiona els esdeveniments de tipus \'%1$s\'</string> + <string name="room_profile_leaving_room">Marxant de la sala…</string> + <string name="direct_room_profile_section_more_leave">Marxa</string> + <string name="room_profile_section_more_leave">Marxa de la sala</string> + <string name="room_profile_section_more_uploads">Pujades</string> + <plurals name="room_profile_section_more_member_list"> + <item quantity="one">Una persona</item> + <item quantity="other">%1$d persones</item> + </plurals> + <string name="direct_room_profile_section_more_settings">Configuració</string> + <string name="room_profile_section_more_settings">Configuració de sala</string> + <string name="room_profile_section_admin">Accions d\'administrador</string> + <string name="room_profile_section_more">Més</string> + <string name="room_profile_section_security_learn_more">Més informació</string> + <string name="room_profile_section_security">Seguretat</string> + <string name="verification_request_waiting_for">Esperant %s…</string> + <string name="verification_verified_user">%s verificat</string> + <string name="verification_verify_user">Verifica %s</string> + <string name="verification_scan_their_code">Escaneja el seu codi</string> + <string name="verification_scan_notice">Escaneja el codi amb el dispositiu de l\'altre usuari per a verificar-vos</string> + <string name="you">Tu</string> + <string name="verification_verify_device_manually">Verificació manual</string> + <string name="verification_request">Verificació sol·licitada</string> + <string name="verification_sent">Verificació enviada</string> + <string name="verification_request_you_accepted">Has acceptat</string> + <string name="verification_request_other_accepted">%s ha acceptat</string> + <string name="verification_request_you_cancelled">Has cancel·lat</string> + <string name="verification_request_other_cancelled">%s ha cancel·lat</string> + <string name="verification_request_waiting">Esperant…</string> + <string name="sent_verification_conclusion">Conclusió de la verificació</string> + <string name="sent_a_reaction">Ha reaccionat amb: %s</string> + <string name="sent_a_bot_buttons">Botons de bot</string> + <string name="sent_a_poll">Votació</string> + <string name="send_a_sticker">Adhesiu</string> + <string name="sent_an_image">Imatge.</string> + <string name="sent_a_video">Vídeo.</string> + <string name="cross_signing_verify_by_emoji">Verifica interactivament mitjançant emoticones</string> + <string name="verification_emoji_notice">Compara les emoticones, assegura\'t que apareixen en el mateix ordre.</string> + <string name="verify_by_emoji_description">Si no pots escanejar el codi de dalt, verifica comparant una petita selecció d\'emoticones única.</string> + <string name="verify_by_emoji_title">Verifica mitjançant emoticones</string> + <string name="verification_no_scan_emoji_title">Verificació comparant emoticones</string> + <string name="verification_scan_emoji_subtitle">Si no us trobeu físicament junts, compara les emoticones</string> + <string name="verification_scan_emoji_title">No s\'ha pogut escanejar</string> + <string name="verify_user_sas_emoji_help_text">Verifica aquest usuari confirmant que les següents emoticones apareixen a la pantalla de l\'altre usuari en el mateix ordre.</string> + <string name="confirm_your_identity">Confirma la teva identitat verificant aquest inici de sessió des d\'una altra de les teves sessions i, així, poder-li donar accés als missatges xifrats.</string> + <string name="verify_this_session">Verifica el nou inici de sessió que està accedint al teu compte: %1$s</string> + <string name="verification_open_other_to_verify">Utilitza aquesta sessió per a verificar-ne una de nova i poder-li donar accés als missatges xifrats.</string> + <string name="verification_profile_device_untrust_info">Fins que aquest usuari no confiï en aquesta sessió, els missatges enviats i rebuts es marcaran amb una alerta. Com a alternativa, pots verificar-lo manualment.</string> + <string name="crosssigning_verify_this_session">Verifica aquest inici de sessió</string> + <string name="settings_active_sessions_unverified_device_desc">Verifica aquesta sessió per fer-la de confiança i permetre que accedeixi als missatges xifrats. Si no has estat tu el que ha iniciat sessió aquí, pot ser que el teu compte estigui compromès:</string> + <string name="verification_verify_device">Verifica aquesta sessió</string> + <string name="verification_conclusion_compromised">Alguna de les següents pot haver estat compromesa: +\n +\n- El teu servidor local +\n- El servidor on està connectat l\'usuari que estàs intentant verificar +\n- La teva connexió a internet a la de l\'altre usuari +\n- El teu dispositiu o el de l\'altre usuari</string> + <string name="verification_conclusion_not_secure">No segur</string> + <string name="verification_green_shield">Busca l\'escut verd per assegurar-te que l\'usuari és de confiança. Per assegurar-te que una sala és segura, tots els usuaris han de ser de confiança.</string> + <string name="verify_user_sas_emoji_security_tip">Per a una seguretat màxima, utilitza un altre mètode segur de comunicació o fes-ho en persona.</string> + <string name="room_member_jump_to_read_receipt">Vés a confirmacions de lectura</string> + <string name="soft_logout_signin_e2e_warning_notice">Inicia sessió per recuperar les claus de xifrat emmagatzemades exclusivament en aquest dispositiu. Les necessites per poder llegir, en qualsevol dispositiu, els teus missatges segurs.</string> + <plurals name="invitations_sent_to_one_and_more_users"> + <item quantity="one">Invitacions enviades a %1$s i un més</item> + <item quantity="other">Invitacions enviades a %1$s i %2$d més</item> + </plurals> + <string name="invitations_sent_to_two_users">Invitacions enviades a %1$s i %2$s</string> + <string name="invitation_sent_to_one_user">Invitació enviada a %1$s</string> + <string name="invite_users_to_room_title">Convida usuaris</string> + <string name="inviting_users_to_room">Convidant usuaris…</string> + <string name="invite_users_to_room_action_invite">CONVIDA</string> + <string name="external_link_confirmation_message">L\'enllaç %1$s t\'està intentant portar a un altre lloc: %2$s. +\n +\nSegur que vols continuar\?</string> + <string name="external_link_confirmation_title">Comprova aquest enllaç</string> + <string name="mark_as_verified">Marca-ho com a segur</string> + <string name="use_other_session_content_description">Utilitza l\'última versió d\'Element als teus altres dispositius, Element Web, Element per escriptori, Element iOS, Element per Android, o algun altre client de Matrix que admeti signatura creuada</string> + <string name="bootstrap_progress_storing_in_sss">Desant secret de clau de còpia de seguretat a SSSS</string> + <string name="security_prompt_text">Verifica\'t a tu i als altres per mantenir els teus xats segurs</string> + <string name="command_description_plain">Envia un missatge com a text pla, sense tenir en compte la formatació markdown</string> + <string name="bootstrap_save_key_description">Utilitza aquesta %1$s com a alternativa en cas de que perdis la teva %2$s.</string> + <string name="finish">Acaba</string> + <string name="keep_it_safe">Mantén-la segura</string> + <string name="bootstrap_loading_title">Configurant recuperació.</string> + <string name="bootstrap_info_text_2">Introdueix una frase secreta que només coneguis tu per protegir els secrets del teu servidor.</string> + <string name="bootstrap_info_text">Protegeix i desbloqueja missatges xifrats amb la %s.</string> + <string name="secure_backup_reset_all">Restableix-ho tot</string> + <string name="failed_to_access_secure_storage">No s\'ha pogut accedir a l\'emmagatzematge segur</string> + <string name="change_password_summary">Configura una nova contrasenya pel compte…</string> + <string name="error_saving_media_file">No s\'ha pogut desar el fitxer multimèdia</string> + <string name="error_adding_media_file_to_gallery">No s\'ha pogut afegir el fitxer multimèdia a la galeria</string> + <string name="media_file_added_to_gallery">S\'ha afegit el fitxer multimèdia a la galeria</string> + <string name="confirm_your_identity_quad_s">Confirma la teva identitat verificant aquest inici de sessió i, així, poder-li donar accés als missatges xifrats.</string> + <string name="crosssigning_verify_session">Verifica l\'inici de sessió</string> + <string name="verify_other_sessions">Verifica totes les teves sessions per assegurar-te que el teu compte i missatges estan segurs</string> + <string name="review_logins">Comprova on has iniciat sessió</string> + <string name="encrypted_unverified">Xifrat amb un dispositiu no verificat</string> + <string name="unencrypted">No xifrat</string> + <plurals name="secure_backup_reset_devices_you_can_verify"> + <item quantity="one">Veure el dispositiu amb el qual et pots verificar</item> + <item quantity="other">Veure els %d dispositius amb el quals et pots verificar</item> + </plurals> + <string name="secure_backup_reset_if_you_reset_all">Si ho restableixes tot</string> + <string name="secure_backup_reset_all_no_other_devices">Fes això només si no tens cap altre dispositiu per poder verificar-ne aquest.</string> + <string name="re_authentication_default_confirm_text">Element necessita que introdueixis les teves credencials per poder realitzar aquesta acció.</string> + <string name="re_authentication_activity_title">Re-autenticació necessària</string> + <string name="contacts_book_title">Llista de contactes</string> + <string name="empty_phone_book">La teva agenda està buida</string> + <string name="phone_book_title">Agenda</string> + <string name="empty_contact_book">La teva llista de contactes està buida</string> + <string name="loading_contact_book">Obtenint contactes…</string> + <string name="element_disclaimer_negative_button">D\'ACORD</string> + <string name="element_disclaimer_content">Estem contents d\'anunciar que hem canviat de nom! L\'aplicació està actualitzada i s\'ha iniciat sessió amb el teu compte.</string> + <string name="save_your_security_key_title">Desa la clau de seguretat</string> + <string name="set_a_security_phrase_again_notice">Torna a introduir la frase de seguretat per a confirmar-la.</string> + <string name="set_a_security_phrase_hint">Frase de seguretat</string> + <string name="set_a_security_phrase_notice">Introdueix una frase secreta que només coneguis tu per protegir els secrets del teu servidor.</string> + <string name="set_a_security_phrase_title">Configura una frase de seguretat</string> + <string name="bottom_sheet_setup_secure_backup_security_phrase_subtitle">Introdueix una frase secreta que només coneguis tu per generar una clau per a la còpia de seguretat.</string> + <string name="bottom_sheet_setup_secure_backup_submit">Configura</string> + <string name="settings_setup_secure_backup">Configura la còpia de seguretat segura</string> + <string name="identity_server_error_no_current_binding_error">Ara mateix no hi ha cap associació amb aquest identificador.</string> + <string name="identity_server_error_binding_error">L\'associació ha fallat.</string> + <string name="identity_server_error_no_identity_server_configured">Primer configura un servidor d\'identitat.</string> + <string name="identity_server_error_outdated_home_server">No es pot realitzar aquesta operació. El servidor local no està actualitzat.</string> + <string name="identity_server_error_outdated_identity_server">Aquest servidor d\'identitat no està actualitzat. Element només és compatible amb l\'API V2.</string> + <string name="disconnect_identity_server_dialog_content">Vols desconnectar-te del servidor d\'identitat %s\?</string> + <string name="choose_locale_loading_locales">Carregant idiomes disponibles…</string> + <string name="choose_locale_other_locales_title">Altres idiomes disponibles</string> + <string name="choose_locale_current_locale_title">Idioma actual</string> + <string name="delete_event_dialog_reason_hint">Motiu de l\'eliminació</string> + <string name="enter_secret_storage_passphrase">Introdueix la frase per l\'emmagatzematge secret</string> + <string name="enter_secret_storage_passphrase_warning_text">Només hauries d\'accedir a l\'emmagatzematge secret amb dispositius de confiança</string> + <string name="verification_profile_device_verified_because">Aquesta sessió és de confiança per a xats segurs ja que %1$s (%2$s) l\'ha verificat:</string> + <string name="bootstrap_progress_compute_curve_key">Obtenint clau de corba</string> + <string name="create_room_dm_failure">No s\'ha pogut crear el xat. Comprova els usuaris que vols convidar i torna-ho a provar.</string> + <string name="cross_signing_verify_by_text">Verifica manualment mitjançant text</string> + <string name="enter_secret_storage_input_key">Selecciona la clau de recuperació o introdueix-la manualment escrivint-la o copiant-la</string> + <string name="command_description_discard_session">Obliga a descartar la sessió de grup sortint actual, en una sala xifrada</string> + <string name="bootstrap_migration_with_passphrase_helper_with_link">Si no coneixes la frase clau de còpia de seguretat, pots %s.</string> + <string name="bootstrap_migration_enter_backup_password">Introdueix la frase clau de còpia de seguretat per continuar.</string> + <string name="bootstrap_cancel_text">Si t\'atures ara, pot ser que perdis dades i missatges xifrats en cas de que perdis l\'accés a les teves sessions. +\n +\nTambé pots configurar una copia de seguretat de seguretat i gestionar les teves claus a la configuració.</string> + <string name="bootstrap_skip_text">Si configures una frase de recuperació, podràs protegir i desbloquejar missatges xifrats. +\n +\nSi no vols configurar una contrasenya de missatges, genera una clau de missatges.</string> + <string name="bootstrap_skip_text_no_gen_key">Si configures una frase de recuperació, podràs protegir i desbloquejar missatges xifrats.</string> + <string name="bootstrap_cross_signing_success">La teva %2$s i %1$s s\'han configurat. +\n +\nGuarda-les en un lloc segur! Les necessitaràs per desbloquejar missatges xifrats i informació protegida en cas de que perdis l\'accés a totes les teves sessions actives.</string> + <string name="settings_export_trail">Exporta informe</string> + <string name="verify_cannot_cross_sign">Aquesta sessió no pot compartir la verificació amb les teves altres sessions. +\nLa verificació es desarà localment i es compartirà més endavant en noves versions de l\'aplicació.</string> </resources> \ No newline at end of file diff --git a/vector/src/main/res/values-cs/strings.xml b/vector/src/main/res/values-cs/strings.xml index 1b467817c8..0edb6a69db 100644 --- a/vector/src/main/res/values-cs/strings.xml +++ b/vector/src/main/res/values-cs/strings.xml @@ -173,7 +173,7 @@ <string name="login_error_unable_register">Nelze se registrovat</string> <string name="login_error_unable_register_mail_ownership">Nelze se registrovat: chyba ověření vlastnictví e-mailu</string> <string name="login_error_invalid_home_server">Prosím, zadejte platné URL</string> - <string name="login_error_forbidden">Neplatný uživatelské jméno/heslo</string> + <string name="login_error_forbidden">Neplatné uživatelské jméno nebo heslo</string> <string name="login_error_bad_json">Poškozený JSON</string> <string name="login_error_limit_exceeded">Bylo odesláno příliš mnoho požadavků</string> <string name="login_error_user_in_use">Toto uživatelské jméno je již použito</string> @@ -525,7 +525,7 @@ <string name="settings_add_3pid_flow_not_supported">Nelze provést z Element mobile</string> <string name="settings_add_3pid_authentication_needed">Ověření je nutné</string> <string name="settings_notification_privacy">Soukromí oznámení</string> - <string name="settings_notification_troubleshoot">Řešit oznámení</string> + <string name="settings_notification_troubleshoot">Odstraňování problémů s oznámeními</string> <string name="settings_troubleshoot_diagnostic">Řešit diagnostiku</string> <string name="settings_troubleshoot_diagnostic_run_button_title">Spustit testy</string> <string name="settings_troubleshoot_diagnostic_running_status">Spouštím… (%1$d z %2$d)</string> @@ -843,7 +843,7 @@ <string name="encryption_export_room_keys">Export klíčů místností</string> <string name="encryption_export_room_keys_summary">Export klíčů do místního souboru</string> <string name="encryption_export_export">Export</string> - <string name="encryption_export_notice">Prosím, založte heslo k zašifrování exportovaných klíčů. Pro import klíčů budete muset zadat stejné heslo.</string> + <string name="encryption_export_notice">Prosím, vytvořte frázi k zašifrování exportovaných klíčů. Pro import klíčů budete muset zadat stejnou přístupovou frázi.</string> <string name="encryption_export_saved_as">E2E klíče místností byly uloženy do \'%s\'. \n \nVarování: může dojít k smazání souboru, pokud bude aplikace odinstalována.</string> @@ -975,7 +975,7 @@ <string name="room_widget_permission_theme">Váš motiv</string> <string name="room_widget_permission_widget_id">ID widgetu</string> <string name="room_widget_permission_room_id">ID místnosti</string> - <string name="error_jitsi_not_supported_on_old_device">Promiňte, konferenční hovory s Jitsi nejsou podporovány na starších zařízeních (zařízení s Androidem nižším než 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Promiňte, konferenční hovory s Jitsi nejsou podporovány na starších zařízeních (zařízení s Androidem nižším než 6.0)</string> <string name="room_widget_resource_permission_title">Widget si žádá použití následujících zdrojů:</string> <string name="room_widget_resource_grant_permission">Povolit</string> <string name="room_widget_resource_decline_permission">Zamítnout vše</string> @@ -1083,7 +1083,7 @@ <string name="deactivate_account_delete_checkbox">Prosím, při deaktivaci mého účtu zapomeňte všechny zprávy, které jsem poslal(a) (Varování: způsobí, že budoucí uživatelé uvidí neúplné konverzace)</string> <string name="deactivate_account_prompt_password">Pokračujte po zadání svého hesla:</string> <string name="deactivate_account_submit">Deaktivovat účet</string> - <string name="error_empty_field_enter_user_name">Prosím, zadejte uživatelské jméno.</string> + <string name="error_empty_field_enter_user_name">Zadejte, prosím, uživatelské jméno.</string> <string name="error_empty_field_your_password">Prosím, zadejte své heslo.</string> <string name="room_tombstone_versioned_description">Tato místnost byla nahrazena a není již aktivní</string> <string name="room_tombstone_continuation_link">Konverzace pokračuje tady</string> @@ -1113,13 +1113,13 @@ <string name="plus_x">+%d</string> <string name="x_plus">%d+</string> <string name="no_valid_google_play_services_apk">Žádný platný APK Google Plaz Services nenalezen. Oznámení možná nebudou pracovat spolehlivě.</string> - <string name="passphrase_create_passphrase">Založit heslo</string> - <string name="passphrase_confirm_passphrase">Potvrdit heslo</string> - <string name="passphrase_enter_passphrase">Zadat heslo</string> - <string name="passphrase_passphrase_does_not_match">Heslo se neshoduje</string> - <string name="passphrase_empty_error_message">Prosím, zadejte heslo</string> - <string name="passphrase_passphrase_too_weak">Heslo je příliš slabé</string> - <string name="keys_backup_passphrase_not_empty_error_message">Prosím, smažte heslo, přejete-lis si, aby Element generoval klíč pro obnovu.</string> + <string name="passphrase_create_passphrase">Založit přístupovou frázi</string> + <string name="passphrase_confirm_passphrase">Potvrdit přístupovou frázi</string> + <string name="passphrase_enter_passphrase">Zadat přístupovou frázi</string> + <string name="passphrase_passphrase_does_not_match">Přistupová fráze se neshoduje</string> + <string name="passphrase_empty_error_message">Prosím, zadejte přístupovou frázi</string> + <string name="passphrase_passphrase_too_weak">Přístupová fráze je příliš slabá</string> + <string name="keys_backup_passphrase_not_empty_error_message">Prosím, smažte přístupovou frázi, přejete-li si, aby Element generoval klíč pro obnovu.</string> <string name="keys_backup_no_session_error">Žádná relace Matrix není dostupná</string> <string name="keys_backup_setup_step1_title">Nikdy neztraťte šifrované zprávy</string> <string name="keys_backup_setup_step1_description">Zprávy v šifrovaných místnostech jsou zabezpečeny pomocí end-to-end šifrováním. Pouze Vy a adresát(i) máte klíče ke čtení těchto zpráv. @@ -1127,16 +1127,16 @@ \nKlíče bezpečně zálohujte, abyste je neztratili.</string> <string name="keys_backup_setup">Začít používat zálohu klíčů</string> <string name="keys_backup_setup_step1_advanced">(Pokročilé)</string> - <string name="keys_backup_setup_step2_text_title">Zabezpečit zálohu heslem.</string> - <string name="keys_backup_setup_step2_text_description">Uložíme zašifrovanou kopii Vašich klíčů na Vašem homeserveru. Chraňte svoji zálohu heslem, abyste ji udrželi v bezpečí. + <string name="keys_backup_setup_step2_text_title">Zabezpečit zálohu přístupovou frází.</string> + <string name="keys_backup_setup_step2_text_description">Uložíme zašifrovanou kopii Vašich klíčů na Vašem homeserveru. Chraňte svoji zálohu přístupovou frází, abyste ji udrželi v bezpečí. \n -\nZ důvodu nejvyšší bezpečnosti by se mělo lišit od hesla účtu.</string> - <string name="keys_backup_setup_step2_button_title">Nastavit heslo</string> +\nZ důvodu nejvyšší bezpečnosti by se měla lišit od hesla účtu.</string> + <string name="keys_backup_setup_step2_button_title">Nastavit přístupovou frází</string> <string name="keys_backup_setup_creating_backup">Zálohuji</string> <string name="keys_backup_setup_step1_recovery_key_alternative">Nebo zabezpečte svoji zálohu pomocí klíče obnovy, uloženého někde v bezpečí.</string> <string name="keys_backup_setup_step2_skip_button_title">(Pokročilé) Nastavit s klíčem obnovy</string> <string name="keys_backup_setup_step3_success_title">Podařilo se!</string> - <string name="keys_backup_setup_step3_text_line2">Váš klíč obnovy je záchranná síť - lze jej použít pro obnovu Vašich šifrovaných zpráv, pokud zapomenete své heslo. + <string name="keys_backup_setup_step3_text_line2">Váš klíč obnovy je záchranná síť - lze jej použít pro obnovu Vašich šifrovaných zpráv, pokud zapomenete svou přístupovou frázi. \nUchovávejte svůj klíč obnovy velmi bezpečně, např. ve správci hesel (nebo trezoru)</string> <string name="keys_backup_setup_step3_text_line2_no_passphrase">Uchovávejte svůj klíč obnovy velmi bezpečně, např. ve správci hesel (nebo trezoru)</string> <string name="keys_backup_setup_step3_button_title">Hotovo</string> @@ -1153,21 +1153,21 @@ <string name="keys_backup_setup_override_stop">Zastavit</string> <string name="keys_backup_setup_step3_please_make_copy">Prosím, udělat kopii</string> <string name="keys_backup_setup_step3_share_intent_chooser_title">Sdílet klíč obnovy s…</string> - <string name="keys_backup_setup_step3_generating_key_status">Generuji klíč obnovy pomocí hesla, tento proces může trvat několi vteřin.</string> + <string name="keys_backup_setup_step3_generating_key_status">Generuji klíč obnovy pomocí přístupové fráze, tento proces může trvat několi vteřin.</string> <string name="recovery_key">Klíč obnovy</string> <string name="unexpected_error">Neočekávaná chyba</string> <string name="keys_backup_setup_backup_started_title">Zálohování se spustilo</string> <string name="keys_backup_setup_skip_title">Jste si jisti\?</string> <string name="keys_backup_setup_skip_msg">Můžete ztratit přístup ke svým zprávám, pokud se odhlásíte nebo ztratíte toto zařízení.</string> <string name="keys_backup_restore_is_getting_backup_version">Načítám verzi zálohy…</string> - <string name="keys_backup_restore_with_passphrase">Použijte heslo obnovy k odemknutí historie svých šifrovaných zpráv</string> + <string name="keys_backup_restore_with_passphrase">Použijte přístupovou frázi k odemknutí historie svých šifrovaných zpráv</string> <string name="keys_backup_restore_use_recovery_key">použít klíč obnovy</string> - <string name="keys_backup_restore_with_passphrase_helper_with_link">Neznám Vaše heslo obnovy, můžete %s.</string> + <string name="keys_backup_restore_with_passphrase_helper_with_link">Neznám Vaší přístupovou frázi pro obnovení, můžete %s.</string> <string name="keys_backup_restore_with_recovery_key">Použijte svůj klíč obnovy k odemknutí historie svých šifrovaných zpráv</string> <string name="keys_backup_restore_key_enter_hint">Zadat klíč obnovy</string> <string name="keys_backup_restore_setup_recovery_key">Obnova zpráv</string> <string name="keys_backup_restore_with_key_helper">Ztratili jste klíč obnovy\? Můžete nastavit nový v nastavení.</string> - <string name="keys_backup_passphrase_error_decrypt">Zálohu nebylo možno s tímto heslem dešifrovat: prosím, ověřit, že jste zadali správné heslo obnovy.</string> + <string name="keys_backup_passphrase_error_decrypt">Zálohu nebylo možno s touto přístupovou frází dešifrovat: prosím, ověřit, že jste zadali správnou přístupovou frázi pro obnovu.</string> <string name="network_error_please_check_and_retry">Síťová chyba: prosím, ověřte své spojení a opakujte.</string> <string name="keys_backup_restoring_waiting_message">Obnovuji zálohu:</string> <string name="keys_backup_restoring_computing_key_waiting_message">Kalkuluji klíč zálohy…</string> @@ -1189,7 +1189,7 @@ <string name="keys_backup_settings_invalid_signature_from_verified_device">Záloha má neplatný podpis z ověřené relace %s</string> <string name="keys_backup_settings_invalid_signature_from_unverified_device">Záloha má neplatný podpis z neověřené relace %s</string> <string name="keys_backup_get_trust_error">Načtení informace o důvěryhodnosti zálohy (%s) se nezdařilo.</string> - <string name="keys_backup_settings_untrusted_backup">Abyste použili zálohu klíčů na tuto relaci, obnovte nyní pomocí svého hesla nebo klíče obnovy.</string> + <string name="keys_backup_settings_untrusted_backup">Abyste použili zálohu klíčů na tuto relaci, obnovte je nyní pomocí své přístupové fráze nebo klíče obnovy.</string> <string name="keys_backup_settings_deleting_backup">Odstraňuji zálohu…</string> <string name="keys_backup_settings_delete_backup_error">Odstranění zálohy (%s) selhalo</string> <string name="keys_backup_settings_checking_backup_state">Kontroluji stav zálohy</string> @@ -1304,7 +1304,7 @@ <string name="quick_reactions">Rychlé reakce</string> <string name="settings_general_title">Obecná</string> <string name="settings_preferences">Možnosti</string> - <string name="settings_security_and_privacy">Bezpečí & Soukromí</string> + <string name="settings_security_and_privacy">Bezpečí a soukromí</string> <string name="settings_expert">Expert</string> <string name="settings_push_rules">Push pravidla</string> <string name="settings_push_rules_no_rules">Žádná push pravidla nejsou definována</string> @@ -1315,8 +1315,8 @@ <string name="push_gateway_item_device_name">jméno_relace:</string> <string name="push_gateway_item_url">Url:</string> <string name="push_gateway_item_format">Formát:</string> - <string name="preference_voice_and_video">Hlas & Video</string> - <string name="preference_root_help_about">Nápověda & O aplikaci</string> + <string name="preference_voice_and_video">Hlas a video</string> + <string name="preference_root_help_about">Nápověda a O aplikaci</string> <string name="settings_troubleshoot_test_token_registration_quick_fix">Registrovat token</string> <string name="send_suggestion">Učinit návrh</string> <string name="send_suggestion_content">Prosím, zapište svůj návrh níže.</string> @@ -1449,15 +1449,15 @@ <string name="login_splash_title">Je to Vaše konverzace. Vlastněte ji.</string> <string name="login_splash_text1">Chatujte s lidmi přímo nebo ve skupinách</string> <string name="login_splash_text2">Udržujte konverzace soukromé pomocí šifrování</string> - <string name="login_splash_text3">Rozšiřte & upravte si svůj zážitek</string> + <string name="login_splash_text3">Rozšiřte a upravte si svůj zážitek</string> <string name="login_splash_submit">Můžeme začít</string> <string name="login_server_title">Vybrat server</string> <string name="login_server_text">Jako email, účty mají jeden domov, ačkoli můžete mluvit s kýmkoli</string> <string name="login_server_matrix_org_text">Přidejte se k miliónům svobodným na největším veřejném serveru</string> <string name="login_server_modular_text">Prémiový hosting pro organizace</string> - <string name="login_server_modular_learn_more">Dozvědět se víc</string> + <string name="login_server_modular_learn_more">Dozvědět se více</string> <string name="login_server_other_title">Další</string> - <string name="login_server_other_text">Vlastní & pokročilá nastavení</string> + <string name="login_server_other_text">Vlastní a pokročilá nastavení</string> <string name="login_continue">Pokračovat</string> <string name="login_connect_to">Připojit k %1$s</string> <string name="login_connect_to_modular">Připojit k Element Matrix Services</string> @@ -1637,7 +1637,7 @@ \n \nVaše zprávy jsou zabezpečeny zámky a pouze Vy a příjemce máte jedinečné klíče k jejich odemknutí.</string> <string name="room_profile_section_security">Zabezpečení</string> - <string name="room_profile_section_security_learn_more">Dozvědět se víc</string> + <string name="room_profile_section_security_learn_more">Dozvědět se více</string> <string name="room_profile_section_more">Více</string> <string name="room_profile_section_more_settings">Nastavení místnosti</string> <string name="room_profile_section_more_notifications">Oznámení</string> @@ -1696,7 +1696,7 @@ <string name="settings_active_sessions_signout_device">Odhlásit se z této relace</string> <string name="settings_failed_to_get_crypto_device_info">Žádná kryptografická informace není k dispozici</string> <string name="settings_active_sessions_verified_device_desc">Tato relace je důvěryhodná pro bezpečnou komunikaci, protože jste ji ověřili:</string> - <string name="settings_active_sessions_unverified_device_desc">Ověřte tuto relaci a tím ji označíte za důvěryhodnou & dovolíte jí přístup k zašifrovaným zprávám. Pokud jste se do této relace nepřihlásili, může být Váš účet ohrožen:</string> + <string name="settings_active_sessions_unverified_device_desc">Ověřte tuto relaci a tím ji označíte za důvěryhodnou a dovolíte jí přístup k zašifrovaným zprávám. Pokud jste se do této relace nepřihlásili, může být Váš účet ohrožen:</string> <plurals name="settings_active_sessions_count"> <item quantity="one">%d aktivní relace</item> <item quantity="few">%d aktivní relace</item> @@ -1713,8 +1713,8 @@ <string name="room_member_profile_sessions_section_title">Relace</string> <string name="trusted">Důvěryhodné</string> <string name="not_trusted">Nedůvěryhodné</string> - <string name="verification_profile_device_verified_because">Tato relace je důvěryhodná pro bezpečnou komunikaci, protože %1$s (%2$s) ji ověřili:</string> - <string name="verification_profile_device_new_signing">%1$s (%2$s) se příhlásili skrze novou relaci:</string> + <string name="verification_profile_device_verified_because">Tato relace je důvěryhodná pro bezpečnou komunikaci, protože %1$s (%2$s) ji ověřil:</string> + <string name="verification_profile_device_new_signing">%1$s (%2$s) se přihlásil skrze novou relaci:</string> <string name="verification_profile_device_untrust_info">Dokud tento uživatel nezačne důvěřovat této relaci, zprávy z ní odeslané a v ní přijaté budou označeny varováním. Volitelně ji můžete manuálně ověřit.</string> <string name="initialize_cross_signing">Spustit křížové podepsání</string> <string name="reset_cross_signing">Resetovat klíče</string> @@ -1741,7 +1741,7 @@ <string name="verification_use_passphrase">Pokud se nemůžete dostat do existující relace</string> <string name="new_signin">Nové přihlášení</string> <string name="enter_secret_storage_invalid">Nemohu najít přihlašovací data v úložišti</string> - <string name="enter_secret_storage_passphrase">Zadejte heslo pro úložište údajů</string> + <string name="enter_secret_storage_passphrase">Zadejte přístupovou frázi pro úložište údajů</string> <string name="enter_secret_storage_passphrase_warning">Varování:</string> <string name="enter_secret_storage_passphrase_warning_text">Měli byste otevřít úložiště údajů z důvěryhodného zařízení</string> <string name="message_action_item_redact">Odstranit…</string> @@ -1763,7 +1763,7 @@ <string name="e2e_use_keybackup">Odemknout zašifrovanou historii zpráv</string> <string name="refresh">Obnovit</string> <string name="new_session">Nové přihlášení. Byli jste to Vy\?</string> - <string name="new_session_review">Klepněte pro přehled & ověření</string> + <string name="new_session_review">Klepněte pro přehled a ověření</string> <string name="verify_new_session_notice">Použijte tuto relaci k ověření relace nové, a tím ji udělíte přístup k zašifrovaným zprávám.</string> <string name="verify_new_session_was_not_me">To jsem nebyl(a) já</string> <string name="verify_new_session_compromized">Váš účet může být ohrožen</string> @@ -1777,17 +1777,17 @@ \n- Toto zařízení nebo to druhé \n- Spojení do internetu obou zařízení \n -\nDoporučujeme, abyste okamžitě změnili heslo & klíč obnovy v nastavení.</string> +\nDoporučujeme, abyste okamžitě změnili heslo a klíč obnovy v nastavení.</string> <string name="verify_cancelled_notice">Ověřit svá zařízení v nastavení.</string> <string name="verification_cancelled">Ověření zrušeno</string> - <string name="recovery_passphrase">Heslo obnovy</string> + <string name="recovery_passphrase">Přístupová fráze pro obnovu</string> <string name="message_key">Klíč zpráv</string> <string name="account_password">Heslo účtu</string> <string name="set_recovery_passphrase">Nastavte %s</string> <string name="generate_message_key">Generovat klíč zpráv</string> <string name="confirm_recovery_passphrase">Potvrďte %s</string> <string name="enter_account_password">Zadejte své %s a pokračujte.</string> - <string name="bootstrap_info_text">Zabezpečit & odemknout zašifrované zprávy a důvěru s %s.</string> + <string name="bootstrap_info_text">Zabezpečit a odemknout zašifrované zprávy a důvěru s %s.</string> <string name="bootstrap_info_confirm_text">Zadejte opět své %s a potvrďte.</string> <string name="bootstrap_dont_reuse_pwd">Nepoužívejte heslo účtu opakovaně.</string> <string name="bootstrap_loading_text">To může několik vteřin trvat, prosím, buďte trpěliví.</string> @@ -1798,7 +1798,7 @@ <string name="finish">Dokončit</string> <string name="bootstrap_save_key_description">Použijte tento %1$s jako záchrannou síť v případě, že zapomenete své %2$s.</string> <string name="bootstrap_crosssigning_progress_initializing">Zvěřejňuji založené klíče identity</string> - <string name="bootstrap_crosssigning_progress_pbkdf2">Generuji zabezpečené klíče z hesla</string> + <string name="bootstrap_crosssigning_progress_pbkdf2">Generuji zabezpečené klíče z přístupové fráze</string> <string name="bootstrap_crosssigning_progress_default_key">Určuji výchozí klíč SSSS</string> <string name="bootstrap_crosssigning_progress_save_msk">Synchronizuji hlavní klíč</string> <string name="bootstrap_crosssigning_progress_save_usk">Synchronizuji uživatelský klíč</string> @@ -1811,10 +1811,10 @@ <string name="bootstrap_crosssigning_save_usb">Uložte je na USB nebo zálohový disk</string> <string name="bootstrap_crosssigning_save_cloud">Nahrajte do svého osobního úložiště v cloudu</string> <string name="auth_flow_not_supported">To nelze provést z mobilního zařízení</string> - <string name="bootstrap_skip_text">Nastavení hesla pro obnovení Vám umožní zabezpečit & odemknout zašifrované zprávy a důvěryhodnost. + <string name="bootstrap_skip_text">Nastavení přístupové fráze pro obnovení Vám umožní zabezpečit a odemknout zašifrované zprávy a důvěryhodnost. \n -\nPokud nechcete nastavit heslo pro zprávy, založte klíč pro zprávy.</string> - <string name="bootstrap_skip_text_no_gen_key">Nastavení hesla pro obnovení Vám umožní zabezpečit & odemknout zašifrované zprávy a důvěryhodnost.</string> +\nPokud nechcete nastavit bezpečnostní frázi pro zprávy, založte klíč pro zprávy.</string> + <string name="bootstrap_skip_text_no_gen_key">Nastavení přístupové fráze pro obnovení Vám umožní zabezpečit a odemknout zašifrované zprávy a důvěryhodnost.</string> <string name="encryption_enabled">Šifrování zapnuto</string> <string name="encryption_enabled_tile_description">Zprávy v této místnosti jsou end-to-end šifrovány. Zjistěte více a ověřte uživatele v jejich profilech.</string> <string name="encryption_not_enabled">Šifrování není zapnuto</string> @@ -1829,30 +1829,30 @@ <string name="settings_messages_in_e2e_one_to_one">Zašifrované zprávy v chatech one-to-one</string> <string name="settings_messages_in_e2e_group_chat">Zašifrované zprávy ve skupinových chatech</string> <string name="settings_when_rooms_are_upgraded">Když dojde k upgradu místností</string> - <string name="settings_troubleshoot_title">Řešit problémy</string> + <string name="settings_troubleshoot_title">Řešení problémů</string> <string name="settings_notification_advanced_summary">Nastavit důležitost oznámení podle události</string> <string name="command_description_plain">Odešle zprávu jako prostý text bez interpretace markdown</string> - <string name="auth_invalid_login_param_space_in_password">Nesprávné uživatelské jméno a/nebo heslo. Zadané heslo začíná nebo končí mezerami, prosím, zkontrolovat.</string> + <string name="auth_invalid_login_param_space_in_password">Nesprávné uživatelské jméno nebo heslo. Zadané heslo začíná nebo končí mezerami, prosím, zkontrolovat.</string> <string name="room_message_placeholder">Zpráva…</string> <string name="upgrade_security">Upgrade šifrování je k dispozici</string> - <string name="security_prompt_text">Ověřit sebe & ostatní za účelem bezpečí chatů</string> + <string name="security_prompt_text">Ověřit sebe a ostatní za účelem bezpečí chatů</string> <string name="bootstrap_enter_recovery">Zadejte svůj %s a pokračujte</string> <string name="use_file">Použít soubor</string> <string name="enter_backup_passphrase">Zadejte %s</string> - <string name="backup_recovery_passphrase">Heslo obnovení</string> + <string name="backup_recovery_passphrase">Přístupové fráze pro obnovení</string> <string name="bootstrap_invalid_recovery_key">To není platný klíč obnovení</string> <string name="recovery_key_empty_error_message">Prosím, zadejte klíč obnovení</string> <string name="bootstrap_progress_checking_backup">Kontroluji klíč zálohy</string> <string name="bootstrap_progress_checking_backup_with_info">Kontroluji klíč zálohy (%s)</string> <string name="bootstrap_progress_compute_curve_key">Generuji klíč curve</string> - <string name="bootstrap_progress_generating_ssss">Generuji klíč SSSS z hesla</string> - <string name="bootstrap_progress_generating_ssss_with_info">Generuji klíč SSSS z hesla (%s)</string> + <string name="bootstrap_progress_generating_ssss">Generuji klíč SSSS z přístupové fráze</string> + <string name="bootstrap_progress_generating_ssss_with_info">Generuji klíč SSSS z přístupové fráze (%s)</string> <string name="bootstrap_progress_generating_ssss_recovery">Generuji klíč SSSS z klíče obnovení</string> <string name="bootstrap_progress_storing_in_sss">Ukládám heslo pro zálohu klíče v SSSS</string> <string name="new_session_review_with_info">%1$s (%2$s)</string> - <string name="bootstrap_migration_enter_backup_password">Zadejte své heslo pro zálohu klíče a pokračujte.</string> + <string name="bootstrap_migration_enter_backup_password">Zadejte svoji přístupovou frázi pro zálohu klíče a pokračujte.</string> <string name="bootstrap_migration_use_recovery_key">použít svůj klíč obnovy zálohy klíčů</string> - <string name="bootstrap_migration_with_passphrase_helper_with_link">Neznáte-li své heslo zálohy klíčů, můžete %s.</string> + <string name="bootstrap_migration_with_passphrase_helper_with_link">Neznáte-li svoji přístupovou frázi pro zálohu klíčů, můžete %s.</string> <string name="bootstrap_migration_backup_recovery_key">Klíč pro obnovu zálohy klíčů</string> <string name="settings_security_prevent_screenshots_title">Zamezit screenshotům aplikace</string> <string name="settings_security_prevent_screenshots_summary">Zapnutí toho nastavení doplní značku FLAG_SECURE ke všem aktivitám. Pro aktivaci změny restartujte aplikaci.</string> @@ -1903,7 +1903,7 @@ <string name="cross_signing_verify_by_emoji">Interaktivně ověřit pomocí Emoji</string> <string name="confirm_your_identity">Potvrďte svou identitu ověřením tohoto přihlášení v některé z Vašich dalších relacích a udělte přístup k zašifrovaným zprávám.</string> <string name="mark_as_verified">Označit za důvěryhodné</string> - <string name="error_empty_field_choose_user_name">Prosím, zvolte uživatelské jméno.</string> + <string name="error_empty_field_choose_user_name">Zvolte si, prosím, uživatelské jméno.</string> <string name="error_empty_field_choose_password">Prosím, zvolte heslo.</string> <string name="external_link_confirmation_title">Překontrolovat tento odkaz</string> <string name="external_link_confirmation_message">Odkaz %1$s Vás převede na jiný site: %2$s. @@ -2009,9 +2009,9 @@ <string name="settings_secure_backup_setup">Založit bezpečnou zálohu</string> <string name="settings_secure_backup_reset">Resetovat bezpečnou zálohu</string> <string name="settings_secure_backup_enter_to_setup">Nastavit na tomto zařízení</string> - <string name="settings_secure_backup_section_info">Ochrana před ztrátou přístupu k šifrovaným zprávám & datům pomocí zálohy šifrovacích klíčů na Vašem serveru.</string> - <string name="reset_secure_backup_title">Generovat nový bezpečnostní klíč nebo nastavit nové bezpečnostní heslo pro existující zálohu.</string> - <string name="reset_secure_backup_warning">To nahradí Váš nynější klíč nebo heslo.</string> + <string name="settings_secure_backup_section_info">Ochrana před ztrátou přístupu k šifrovaným zprávám a datům pomocí zálohy šifrovacích klíčů na Vašem serveru.</string> + <string name="reset_secure_backup_title">Generovat nový bezpečnostní klíč nebo nastavit novou bezpečnostní frázi pro existující zálohu.</string> + <string name="reset_secure_backup_warning">To nahradí Váš nynější klíč nebo frázi.</string> <string name="disabled_integration_dialog_title">Integrace jsou vypnuty</string> <string name="disabled_integration_dialog_content">Zapnout \'Povolit integrace\' v nastavení.</string> <string name="settings_emails_and_phone_numbers_title">Emailové adresy a telefonní čísla</string> @@ -2033,7 +2033,7 @@ <string name="room_no_active_widgets">Žádné aktivní widgety</string> <string name="recovery_key_export_saved">Klíč pro obnovu byl uložen.</string> <string name="secure_backup_banner_setup_line1">Bezpečná záloha</string> - <string name="secure_backup_banner_setup_line2">Ochrana před ztrátou přístupu k šifrovaným zprávám & datům</string> + <string name="secure_backup_banner_setup_line2">Ochrana před ztrátou přístupu k šifrovaným zprávám a datům</string> <string name="secure_backup_setup">Nastavit bezpečnou zálohu</string> <string name="create_room_federation_error">Místnost byla založena, ale některé pozvánky nebyly odeslány z těchto důvodů: \n @@ -2056,8 +2056,8 @@ <string name="send_a_sticker">Nálepka</string> <string name="room_profile_section_admin">Akce správce</string> <string name="room_member_power_level_default_in">Výchozí v %1$s</string> - <string name="settings_hs_admin_e2e_disabled">Správce Vašeho serveru vypnul end-to-end šifrování jako výchozí v soukromých místnostech & přímých zprávách.</string> - <string name="bootstrap_info_text_2">Zadejte bezpečnostní heslo, jež znáte pouze Vy, k zabezpečení tajností na Vašem serveru.</string> + <string name="settings_hs_admin_e2e_disabled">Správce Vašeho serveru vypnul end-to-end šifrování jako výchozí v soukromých místnostech a přímých zprávách.</string> + <string name="bootstrap_info_text_2">Zadejte přístupovou frázi, jež znáte pouze Vy, k zabezpečení klíčů na vašem serveru.</string> <string name="bootstrap_cancel_text">Pokud nyní přerušíte, mohli byste přijít o šifrované zprávy & data, pokud ztratíte přístup k Vašemu účtu. \n \nMůžete nastavit bezpečnou zálohu & správu klíčů též v nastaveních.</string> @@ -2076,18 +2076,18 @@ <string name="a11y_start_camera">Spustit fotoaparát</string> <string name="settings_setup_secure_backup">Nastavit bezpečnou zálohu</string> <string name="bottom_sheet_setup_secure_backup_title">Bezpečná záloha</string> - <string name="bottom_sheet_setup_secure_backup_subtitle">Ochrana před ztrátou přístupu k šifrovaným zprávám & datům pomocí zálohy šifrovacích klíčů na Vašem serveru.</string> + <string name="bottom_sheet_setup_secure_backup_subtitle">Ochrana před ztrátou přístupu k šifrovaným zprávám a datům pomocí zálohy šifrovacích klíčů na Vašem serveru.</string> <string name="bottom_sheet_setup_secure_backup_submit">Nastavit</string> <string name="bottom_sheet_setup_secure_backup_security_key_title">Použít bezpečnostní klíč</string> <string name="bottom_sheet_setup_secure_backup_security_key_subtitle">Generovat bezpečnostní klíč k uložení na bezpečném místě např. správci hesel nebo sejfu.</string> - <string name="bottom_sheet_setup_secure_backup_security_phrase_title">Použít bezpečnostní heslo</string> - <string name="bottom_sheet_setup_secure_backup_security_phrase_subtitle">Zadejte bezpečnostní heslo známé pouze Vám a generujte klíč pro zálohy.</string> + <string name="bottom_sheet_setup_secure_backup_security_phrase_title">Použít bezpečnostní frázi</string> + <string name="bottom_sheet_setup_secure_backup_security_phrase_subtitle">Zadejte bezpečnostní frázi známou pouze Vám a generujte klíč pro zálohy.</string> <string name="bottom_sheet_save_your_recovery_key_title">Uložit bezpečnostní klíč</string> <string name="bottom_sheet_save_your_recovery_key_content">Uložte bezpečnostní klíč na bezpečném místě, např. správci hesel nebo sejfu.</string> - <string name="set_a_security_phrase_title">Nastavit bezpečnostní heslo</string> - <string name="set_a_security_phrase_notice">Zadejte bezpečnostní heslo známé pouze Vám pro zabezpečení tajností na svém serveru.</string> - <string name="set_a_security_phrase_hint">Bezpečnostní heslo</string> - <string name="set_a_security_phrase_again_notice">Zadejte své bezpečnostní heslo znovu pro potvrzení.</string> + <string name="set_a_security_phrase_title">Nastavit bezpečnostní frázi</string> + <string name="set_a_security_phrase_notice">Zadejte bezpečnostní frázi známou pouze Vám pro zabezpečení klíčů na vašem serveru.</string> + <string name="set_a_security_phrase_hint">Bezpečnostní fráze</string> + <string name="set_a_security_phrase_again_notice">Pro potvrzení zadejte znovu svoji bezpečnostní frázi.</string> <string name="save_your_security_key_title">Uložit bezpečnostní klíč</string> <string name="save_your_security_key_notice">Uložte své bezpečnostní heslo na bezpečném místě, např. správci hesel nebo sejfu.</string> <string name="room_settings_name_hint">Název místnosti</string> @@ -2104,7 +2104,7 @@ <string name="element_disclaimer_title">Riot je nyní Element!</string> <string name="element_disclaimer_content">Jsme nadšení, že smíme oznámit změnu jména! Vaše app je aktuální a jste přihlášeni do svého účtu.</string> <string name="element_disclaimer_negative_button">ROZUMÍM</string> - <string name="element_disclaimer_positive_button">DOZVĚDĚT SE VÍC</string> + <string name="element_disclaimer_positive_button">DOZVĚDĚT SE VÍCE</string> <string name="element_login_splash_brand">element</string> <string name="save_recovery_key_chooser_hint">Uložit klíče pro obnovu v</string> <string name="add_from_phone_book">Přidat z mého adresáře</string> @@ -2212,7 +2212,7 @@ <string name="settings_troubleshoot_test_notification_notification_clicked">Na oznámení bylo kliknuto!</string> <string name="settings_troubleshoot_test_notification_notice">Prosím, klikněte na oznámení. Pokud je nevidíte, zkontrolujte v nastavení.</string> <string name="settings_troubleshoot_test_notification_title">Zobrazení oznámení</string> - <string name="settings_troubleshoot_test_push_notification_content">Vidíte oznámení! Kliknout!</string> + <string name="settings_troubleshoot_test_push_notification_content">Vidíte oznámení! Klikněte!</string> <string name="settings_troubleshoot_test_push_loop_failed">Příjem PUSH se nezdařil. Řešením by mohla být reinstalace aplikace.</string> <string name="settings_troubleshoot_test_push_loop_success">Aplikace přijímá PUSH</string> <string name="settings_troubleshoot_test_push_loop_waiting_for_push">Aplikace čeká na PUSH</string> @@ -2260,7 +2260,7 @@ <string name="attachment_viewer_item_x_of_y">%1$d z %2$d</string> <string name="a11y_create_direct_message_by_mxid">Založit novou přímou konverzaci pomocí Matrix ID</string> <string name="a11y_create_direct_message_by_qr_code">Založit novou přímou konverzaci pomocí skenu QR kódu</string> - <string name="identity_server_consent_dialog_content">Abyste nalezli existující kontakty, jež znáte, souhlasíte s odesláním svých kontaktních údajů (telefonní čísla a/nebo emailové adresy) na nastavený server pro identity (%1$s)\? + <string name="identity_server_consent_dialog_content">Abyste nalezli existující kontakty, jež znáte, souhlasíte s odesláním svých kontaktních údajů (telefonní čísla nebo emailové adresy) na nastavený server pro identity (%1$s)\? \n \nZa účelem soukromí budou data před odesláním hašována.</string> <string name="identity_server_consent_dialog_title">Poslat emailové adresy a telefonní čísla</string> @@ -2353,4 +2353,9 @@ <string name="action_unpublish">Vzít zpět</string> <string name="action_add">Přidat</string> <string name="system_theme">Výchozí téma</string> + <string name="authentication_error">Ověření se nezdařilo</string> + <string name="re_authentication_default_confirm_text">Element vyžaduje zadání přihlašovacích údajů k provedení této akce.</string> + <string name="re_authentication_activity_title">Je nutné opětovné ověření</string> + <string name="failed_to_initialize_cross_signing">Nastavení křížového podepisování se nezdařilo</string> + <string name="error_unauthorized">Neautorizováno, chybí platné ověřovací údaje</string> </resources> \ No newline at end of file diff --git a/vector/src/main/res/values-de/strings.xml b/vector/src/main/res/values-de/strings.xml index 662dbab4fd..07ba05fcd2 100644 --- a/vector/src/main/res/values-de/strings.xml +++ b/vector/src/main/res/values-de/strings.xml @@ -1155,7 +1155,7 @@ <string name="notification_new_invitation">Neue Einladung</string> <string name="notification_sender_me">Ich</string> <string name="notification_inline_reply_failed">** Fehler beim Senden - bitte Raum öffnen</string> - <string name="error_jitsi_not_supported_on_old_device">Entschuldigung, Konferenzanrufe mit Jitsi werden auf älteren Geräten (mit älteren Android-Versionen als 5.0) nicht unterstützt</string> + <string name="error_jitsi_not_supported_on_old_device">Entschuldigung, Konferenzanrufe mit Jitsi werden auf älteren Geräten (mit älteren Android-Versionen als 6.0) nicht unterstützt</string> <string name="title_activity_verify_device">Sitzung verifizieren</string> <string name="encryption_information_unknown_ip">Unbekannte IP-Adresse</string> <string name="you_added_a_new_device_with_info">Eine neue Sitzung fordert Verschlüsselungsschlüssel an. @@ -1671,7 +1671,7 @@ <string name="room_profile_section_security">Sicherheit</string> <string name="room_profile_section_security_learn_more">Mehr erfahren</string> <string name="room_profile_section_more">Mehr</string> - <string name="room_profile_section_more_settings">Raum-Einstellungen</string> + <string name="room_profile_section_more_settings">Raumeinstellungen</string> <string name="room_profile_section_more_notifications">Benachrichtigungen</string> <plurals name="room_profile_section_more_member_list"> <item quantity="one">Eine Person</item> @@ -2370,4 +2370,9 @@ <string name="room_settings_permissions_subtitle">Rollen, die zum Ändern verschiedener Teile des Raums erforderlich sind, anzeigen und aktualisieren.</string> <string name="room_permissions_send_m_room_server_acl_events">m.room.server_acl-Ereignisse senden</string> <string name="room_permissions_change_permissions">Berechtigungen ändern</string> + <string name="authentication_error">Authentifizierung fehlgeschlagen</string> + <string name="re_authentication_default_confirm_text">Deine Anmeldeinformationen müssen für Element eingegeben werden, um diese Aktion auszuführen.</string> + <string name="re_authentication_activity_title">Erneute Authentifizierung erforderlich</string> + <string name="failed_to_initialize_cross_signing">Cross Signing konnte nicht eingerichtet werden</string> + <string name="error_unauthorized">Nicht autorisierte, fehlende gültige Authentifizierungsdaten</string> </resources> \ No newline at end of file diff --git a/vector/src/main/res/values-eo/strings.xml b/vector/src/main/res/values-eo/strings.xml index 4037664fc3..776dbdf076 100644 --- a/vector/src/main/res/values-eo/strings.xml +++ b/vector/src/main/res/values-eo/strings.xml @@ -1212,7 +1212,7 @@ <string name="room_widget_resource_decline_permission">Ĉion bloki</string> <string name="room_widget_resource_grant_permission">Permesi</string> <string name="room_widget_resource_permission_title">Ĉi tiu fenestraĵo volas uzi la jenajn rimedojn:</string> - <string name="error_jitsi_not_supported_on_old_device">Pardonu, grupaj vokoj ne estas subtenataj sur malnovaj aparatoj (Android je versio sub 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Pardonu, grupaj vokoj ne estas subtenataj sur malnovaj aparatoj (Android je versio sub 6.0)</string> <string name="room_widget_permission_room_id">Identigilo de ĉambro</string> <string name="room_widget_permission_widget_id">Identigilo de fenestraĵo</string> <string name="room_widget_permission_theme">Via haŭto</string> diff --git a/vector/src/main/res/values-es/strings.xml b/vector/src/main/res/values-es/strings.xml index 2743a71664..1dbba1c25c 100644 --- a/vector/src/main/res/values-es/strings.xml +++ b/vector/src/main/res/values-es/strings.xml @@ -57,7 +57,7 @@ <string name="bottom_action_home">Inicio</string> <string name="bottom_action_favourites">Favoritos</string> <string name="bottom_action_people">Personas</string> - <string name="bottom_action_rooms">Salas</string> + <string name="bottom_action_rooms">Salas y Grupos</string> <!-- Home screen --> <string name="home_filter_placeholder_home">Filtrar salas</string> <string name="home_filter_placeholder_favorites">Filtrar favoritos</string> @@ -244,7 +244,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua <string name="preview">Vista Previa</string> <string name="reject">Rechazar</string> <!-- Room --> - <string name="room_jump_to_first_unread">Ir al primer mensaje no leído.</string> + <string name="room_jump_to_first_unread">Mensajes no leídos.</string> <!-- Room Preview --> <string name="room_preview_invitation_format">Has sido invitado por %s a unirte a esta sala</string> <string name="room_preview_unlinked_email_warning">Esta invitación fue enviada a %s, que no esta asociado a esta cuenta. @@ -1040,7 +1040,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua <string name="notification_new_invitation">Nueva invitación</string> <string name="notification_sender_me">Yo</string> <string name="notification_inline_reply_failed">** Error al enviar - por favor abra la sala</string> - <string name="error_jitsi_not_supported_on_old_device">Lo sentimos, las llamadas de grupo con Jitsi no están soportadas en dispositivos antiguos (dispositivos con Android inferior a 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Lo sentimos, las llamadas de grupo con Jitsi no están soportadas en dispositivos antiguos (dispositivos con Android inferior a 6.0)</string> <string name="settings_labs_native_camera_summary">Iniciar la cámara del sistema en lugar de la pantalla de cámara personalizada.</string> <string name="settings_labs_enable_send_voice_summary">Esta opción requiere una aplicación de terceros para grabar los mensajes.</string> <string name="command_problem_with_parameters">El comando \"%s\" necesita mas parámetros o algunos parámetros son incorrectos.</string> @@ -1181,7 +1181,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua <string name="sas_security_advise">Para más seguridad, te recomendamos que hagas esto en persona o por otros medios confiables.</string> <string name="sas_verify_start_button_title">Empezar verificación</string> <string name="sas_incoming_request_title">Solicitud de verificación</string> - <string name="sas_incoming_request_description">Verifica esta sesión para marcarla de confianza. Marcar sesiones de otros como de confianza te da aún más tranquilidad cuando usas encriptacion de Extremo-a-Extremo.</string> + <string name="sas_incoming_request_description">Verifica esta sesión para marcarla de confianza. Marcar sesiones de otros como de confianza te da aún más tranquilidad cuando usas cifrado de Extremo-a-Extremo.</string> <string name="sas_incoming_request_description_2">Verificar esta sesión la marcará como confiable, y también marcará como confiable tu sesión para la contraparte.</string> <string name="sas_emoji_description">Verifica esta sesión confirmando los emojis que aparecen en la pantalla de la contraparte</string> <string name="sas_decimal_description">Verifica esta sesión confirmando que los siguietes números aparecen en la pantalla de la contraparte</string> @@ -1219,7 +1219,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua <string name="room_list_catchup_empty_body">No tienes más mensajes sin leer</string> <string name="room_list_catchup_welcome_title">¡Bienvenido!</string> <string name="room_list_people_empty_title">Conversaciones</string> - <string name="room_list_people_empty_body">Tus conversaciones directas (1 a 1) se mostrarán aquí</string> + <string name="room_list_people_empty_body">Tus conversaciones se mostrarán aquí. Toque + en la derecha para comenzar.</string> <string name="room_list_rooms_empty_title">Salas</string> <string name="room_list_rooms_empty_body">Tus salas se mostrarán aquí</string> <string name="title_activity_emoji_reaction_picker">Reacciones</string> @@ -1241,7 +1241,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua <string name="room_preview_no_preview">Esta sala no se puede previsualizar</string> <string name="room_preview_world_readable_room_not_supported_yet">La previsualización de salas públicas no es posible todavía con Element</string> <string name="fab_menu_create_room">Salas</string> - <string name="fab_menu_create_chat">Mensajes directos</string> + <string name="fab_menu_create_chat">Chats</string> <string name="create_room_title">Nueva sala</string> <string name="create_room_action_create">CREAR</string> <string name="create_room_name_hint">Nombre</string> @@ -1577,7 +1577,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua <string name="failed_to_add_widget">Fallo al añadir Widget</string> <string name="failed_to_remove_widget">Fallo al eliminar Widget</string> <string name="settings_call_show_confirmation_dialog_title">Confirmar llamada</string> - <string name="settings_call_show_confirmation_dialog_summary">Pedir confirmacion antes de iniciar una llamda</string> + <string name="settings_call_show_confirmation_dialog_summary">Pedir confirmacion antes de iniciar una llamada</string> <string name="room_participants_kick_reason">Rason de expulsion</string> <string name="room_participants_ban_title">Banear usuario</string> <string name="room_participants_ban_reason">Rason de baneo</string> @@ -1601,7 +1601,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua <string name="settings_developer_mode_fail_fast_title">Fallar rápido (Test)</string> <string name="settings_developer_mode_fail_fast_summary">Element puede fallar con más frecuencia cuando ocurre un error inesperado</string> <string name="command_description_shrug">Antepone ¯\\_(ツ)_/¯ a un mensaje de texto sin formato</string> - <string name="create_room_encryption_title">Habilitar encriptacion</string> + <string name="create_room_encryption_title">Habilitar crifrado</string> <string name="create_room_encryption_description">Una vez habilitada, la encriptación no se puede deshabilitar.</string> <string name="login_error_threepid_denied">Su dominio de correo electrónico no está autorizado para registrarse en este servidor</string> <string name="verification_conclusion_warning">Inicio de sesión no confiable</string> @@ -1648,7 +1648,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua <item quantity="one">Una persona</item> <item quantity="other">%1$d personas</item> </plurals> - <string name="room_profile_section_more_uploads">Archivos, Medias y Documentos</string> + <string name="room_profile_section_more_uploads">Archivos subidos</string> <string name="room_profile_section_more_leave">Abandonar Sala</string> <string name="room_profile_leaving_room">Saliendo de la sala…</string> <string name="room_member_power_level_admins">Administradores</string> @@ -1670,7 +1670,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua <string name="command_description_rainbow">Envía el mensaje dado en colores</string> <string name="settings_category_timeline">Línea de tiempo</string> <string name="settings_category_composer">Editor de mensage</string> - <string name="room_settings_enable_encryption">Encriptar (end-to-end)</string> + <string name="room_settings_enable_encryption">Encriptacion habilitada (end-to-end) …</string> <string name="room_settings_enable_encryption_warning">Una vez habilitada, la encriptación no se puede deshabilitar.</string> <string name="room_settings_enable_encryption_dialog_title">Encriptar \?</string> <string name="room_settings_enable_encryption_dialog_submit">Habilitar la encriptación</string> @@ -1702,7 +1702,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua <string name="poll_item_selected_aria">Seleccionar Opcion</string> <string name="new_signin">Nuevo inicio de sesión</string> <string name="enter_secret_storage_passphrase_warning">Advertencia:</string> - <string name="message_action_item_redact">Remover…</string> + <string name="message_action_item_redact">Eliminar…</string> <string name="delete_event_dialog_reason_checkbox">Razón</string> <string name="delete_event_dialog_reason_hint">Razón para redactar</string> <string name="login_default_session_public_name">Element Android</string> @@ -2174,7 +2174,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua <string name="direct_room_profile_not_encrypted_subtitle">Los mensajes aquí no están encriptados de Extremo-a-Extremo.</string> <string name="sent_a_bot_buttons">Botones de Bot</string> <string name="sent_a_poll">Encuesta</string> - <string name="room_list_quick_actions_low_priority_remove">Remover de Baja prioridad</string> + <string name="room_list_quick_actions_low_priority_remove">Eliminar de baja prioridad</string> <string name="room_list_quick_actions_low_priority_add">Añadir a Baja prioridad</string> <string name="rotate_and_crop_screen_title">Rotar y recortar</string> <plurals name="seconds"> @@ -2248,4 +2248,58 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua <string name="settings_show_room_member_state_events">Mostrar eventos de los miembros de la sala</string> <string name="settings_troubleshoot_test_notification_notification_clicked">¡La notificación ha sido cliqueada!</string> <string name="system_theme">Tema del sistema</string> + <string name="permissions_denied_add_contact">Permitir acceder a sus contactos.</string> + <string name="error_unauthorized">Desautorizado, credenciales de autenticación no autorizadas</string> + <string name="login_clear_homeserver_history">Limpiar Historial</string> + <string name="re_authentication_default_confirm_text">Element requiere que ingrese sus credenciales para realizar esta acción.</string> + <string name="re_authentication_activity_title">Se necesita una nueva autenticación</string> + <string name="matrix_to_card_title">Enlace Matrix</string> + <string name="qr_code_not_scanned">¡Código QR no escaneado!</string> + <string name="invalid_qr_code_uri">Código QR no válido (URL no válida)!</string> + <string name="cannot_dm_self">No puede DM usted mismo!</string> + <string name="share_by_text">Compartir por texto</string> + <string name="settings_security_pin_code_change_pin_title">Cambiar PIN</string> + <string name="settings_security_pin_code_change_pin_summary">Cambie su PIN actual</string> + <string name="authentication_error">Fallo al autenticar</string> + <string name="this_is_the_beginning_of_room_no_name">Estes es el inicio de la conversacion.</string> + <string name="this_is_the_beginning_of_room">Este es el comienzo de %s.</string> + <string name="topic_prefix">"Tema: "</string> + <string name="add_a_topic_link_text">Agregar un tema</string> + <string name="room_created_summary_no_topic_creation_text">%s para dar a saber de qué se trata la sala.</string> + <string name="this_is_the_beginning_of_dm">Este es el inicio de mensajes con %s.</string> + <string name="start_chatting">Iniciar Chat</string> + <string name="action_unpublish">Despublicar</string> + <string name="action_add">Adicionar</string> + <string name="room_settings_enable_encryption_no_permission">No tiene permisos para encryptar esta sala.</string> + <string name="not_a_valid_qr_code">No es un ID matrix valido</string> + <string name="user_code_my_code">Mi codigo</string> + <string name="user_code_share">Compartir mi codigo</string> + <string name="user_code_scan">Escanear QR</string> + <string name="invite_friends_rich_title">🔐️ Unirme a Element</string> + <string name="invite_friends_text">Hey, contactame por Element:%s</string> + <string name="add_people">Adicionar persona</string> + <string name="user_code_info_text">Compartir ese codigo para que puedan contactarloa usted.</string> + <string name="a11y_create_direct_message_by_mxid">Crear una converzacion por ID Matrix</string> + <string name="a11y_create_direct_message_by_qr_code">Crear una converzacion escanenado QR</string> + <string name="settings_show_emoji_keyboard_summary">Adicionar botón en el redactor de mensajes para abrir el teclado emoji</string> + <string name="settings_show_emoji_keyboard">Mostrar teclado emoji</string> + <string name="direct_room_user_list_recent_title">Reciente</string> + <string name="add_by_qr_code">Adicionar por codigo QR</string> + <string name="user_directory_search_hint">Buscar por nick o ID</string> + <string name="qr_code">Codigo QR</string> + <string name="direct_room_user_list_suggestions_title">Sugerencias</string> + <string name="direct_room_user_list_contacts_title">Contactos</string> + <string name="direct_room_user_list_known_title">Usuarios conocidos</string> + <string name="settings_chat_effects_title">Mostrar efectos de chat</string> + <string name="permissions_denied_qr_code">Para escanear el codigo QR , necesita darle permisos a su camara.</string> + <string name="invite_friends">Invitar contactos</string> + <string name="room_settings_permissions_subtitle">Modifique los roles y privilegios de la sala.</string> + <string name="room_participants_leave_private_warning">Sala no pública. No puede acceder sin una invitacion.</string> + <string name="room_permissions_upgrade_the_room">Actualizar sala</string> + <string name="room_permissions_change_topic">Cambiar tema</string> + <string name="room_permissions_send_messages">Enviar mensajes</string> + <string name="room_permissions_change_history_visibility">Editar historial de visibilidad</string> + <string name="room_permissions_change_permissions">Editar permisos</string> + <string name="room_permissions_change_room_name">Cambiar el nombre de la sala</string> + <string name="room_settings_permissions_title">Administracion de sala</string> </resources> \ No newline at end of file diff --git a/vector/src/main/res/values-et/strings.xml b/vector/src/main/res/values-et/strings.xml index b350f22f23..cd643eccd6 100644 --- a/vector/src/main/res/values-et/strings.xml +++ b/vector/src/main/res/values-et/strings.xml @@ -1020,7 +1020,7 @@ <string name="notification_ticker_text_dm">%1$s: %2$s</string> <string name="notification_ticker_text_group">%1$s: %2$s %3$s</string> <string name="historical_placeholder">Otsi ajaloost</string> - <string name="error_jitsi_not_supported_on_old_device">Vabandust, aga rühmakõned Jitsi vahendusel ei ole vanades seadmetes toetatud (Androidi versioon alla 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Vabandust, aga rühmakõned Jitsi vahendusel ei ole vanades seadmetes toetatud (Androidi versioon alla 6.0)</string> <string name="room_widget_resource_permission_title">See vidin soovib kasutada järgmisi andmeid:</string> <string name="room_widget_resource_grant_permission">Luba</string> <string name="room_widget_resource_decline_permission">Ära luba mitte midagi</string> @@ -2315,4 +2315,9 @@ <string name="room_settings_permissions_title">Õigused jututoas</string> <string name="room_participants_leave_private_warning">See ei ole avalik jututuba. Ilma kutseta sa ei saa uuesti liituda.</string> <string name="system_theme">Süsteemi vaikeseadistused</string> + <string name="authentication_error">Autentimine ei õnnestunud</string> + <string name="re_authentication_default_confirm_text">Selle tegevuse jaoks palub Element sul sisestada oma kasutajanime ja salasõna.</string> + <string name="re_authentication_activity_title">Palun korda autentimist</string> + <string name="failed_to_initialize_cross_signing">Risttunnustamise alustamine ei õnnestunud</string> + <string name="error_unauthorized">Volitused puuduvad, kasutajakonto ja/või salasõna on valed</string> </resources> \ No newline at end of file diff --git a/vector/src/main/res/values-eu/strings.xml b/vector/src/main/res/values-eu/strings.xml index f77ea9dc37..cf14fee03f 100644 --- a/vector/src/main/res/values-eu/strings.xml +++ b/vector/src/main/res/values-eu/strings.xml @@ -1338,7 +1338,7 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada.</string> <string name="notification_sender_me">Ni</string> <string name="notification_inline_reply_failed">** Bidalketak huts egin du, ireki gela</string> - <string name="error_jitsi_not_supported_on_old_device">Sentitzen dugu, gailu zaharretan ezin dira Jitsi bidezko konferentzia deiak egin (Android OS 5.0 baino zaharragoak)</string> + <string name="error_jitsi_not_supported_on_old_device">Sentitzen dugu, gailu zaharretan ezin dira Jitsi bidezko konferentzia deiak egin (Android OS 6.0 baino zaharragoak)</string> <string name="title_activity_verify_device">Egiaztatu saioa</string> diff --git a/vector/src/main/res/values-fi/strings.xml b/vector/src/main/res/values-fi/strings.xml index 97b66322c9..3b945069f7 100644 --- a/vector/src/main/res/values-fi/strings.xml +++ b/vector/src/main/res/values-fi/strings.xml @@ -1171,7 +1171,7 @@ <string name="notification_new_invitation">Uusi kutsu</string> <string name="notification_sender_me">Minä</string> <string name="notification_inline_reply_failed">** Lähetys epäonnistui — avaathan huoneen</string> - <string name="error_jitsi_not_supported_on_old_device">Valitettavasti konferenssipuhelut Jitsin kanssa eivät toimi vanhoissa laitteissa (laitteet, joissa on Android 5.0 tai vanhempi)</string> + <string name="error_jitsi_not_supported_on_old_device">Valitettavasti konferenssipuhelut Jitsin kanssa eivät toimi vanhoissa laitteissa (laitteet, joissa on Android 6.0 tai vanhempi)</string> <string name="encryption_information_unknown_ip">tuntematon IP-osoite</string> <string name="you_added_a_new_device_with_info">Uusi istunto pyytää salausavaimia. \nIstunnon nimi: %1$s diff --git a/vector/src/main/res/values-fr/strings.xml b/vector/src/main/res/values-fr/strings.xml index 32507dc979..2d11932681 100644 --- a/vector/src/main/res/values-fr/strings.xml +++ b/vector/src/main/res/values-fr/strings.xml @@ -1103,7 +1103,7 @@ <string name="notification_new_invitation">Nouvelle invitation</string> <string name="notification_sender_me">Moi</string> <string name="notification_inline_reply_failed">** Échec de l’envoi − veuillez ouvrir le salon</string> - <string name="error_jitsi_not_supported_on_old_device">Désolé, les appels en visioconférence avec Jitsi ne sont pas pris en charge sur les vieux appareils (avec une version d\'Android antérieure à 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Désolé, les appels en visioconférence avec Jitsi ne sont pas pris en charge sur les vieux appareils (avec une version d\'Android antérieure à 6.0)</string> <string name="title_activity_verify_device">Vérifier la session</string> <string name="encryption_information_unknown_ip">adresse IP inconnue</string> <string name="you_added_a_new_device_with_info">Une nouvelle session demande les clés de chiffrement. diff --git a/vector/src/main/res/values-gl/strings.xml b/vector/src/main/res/values-gl/strings.xml index 09520465e0..2fec2ecf61 100644 --- a/vector/src/main/res/values-gl/strings.xml +++ b/vector/src/main/res/values-gl/strings.xml @@ -563,4 +563,10 @@ <string name="dialog_user_consent_content">Para continuar usando o servidor %1$s ten que revisar primeiro os seus termos e condicións.</string> <string name="deactivate_account_delete_checkbox">Esquezan todas as mensaxes que eu enviara no momento en que elimine a miña conta. (Aviso: iso suporá que os seguintes participantes só verán unha versión incompleta das conversas.)</string> <string name="error_empty_field_your_password">Introduza o seu contrasinal.</string> + <string name="title_activity_verify_device">Verificar sesión</string> + <string name="title_activity_keys_backup_restore">Usar copia da Chave</string> + <string name="title_activity_keys_backup_setup">Copia de apoio da chave</string> + <string name="notification_sync_init">Iniciando o servizo</string> + <string name="status_theme">Decorado Status.im</string> + <string name="system_theme">Por defecto no sistema</string> </resources> \ No newline at end of file diff --git a/vector/src/main/res/values-hr/strings.xml b/vector/src/main/res/values-hr/strings.xml index f27774a02c..9013b8fd65 100644 --- a/vector/src/main/res/values-hr/strings.xml +++ b/vector/src/main/res/values-hr/strings.xml @@ -1386,7 +1386,7 @@ <string name="room_widget_permission_room_id">Identitet sobe</string> - <string name="error_jitsi_not_supported_on_old_device">Nažalost konferencijski pozivi kroz Jitsi nisu podržani na starim uređajima (uređaji s Androidom inačice manje od 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Nažalost konferencijski pozivi kroz Jitsi nisu podržani na starim uređajima (uređaji s Androidom inačice manje od 6.0)</string> <string name="command_description_clear_scalar_token">Za popravak upravljanja aplikacijama unutar Matrixa</string> <string name="group_details_home">Početna</string> diff --git a/vector/src/main/res/values-hu/strings.xml b/vector/src/main/res/values-hu/strings.xml index 075a470b09..e25626c2e6 100644 --- a/vector/src/main/res/values-hu/strings.xml +++ b/vector/src/main/res/values-hu/strings.xml @@ -1101,7 +1101,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró <string name="notification_new_invitation">Új meghívók</string> <string name="notification_sender_me">Én</string> <string name="notification_inline_reply_failed">** A küldés nem sikerült - kérlek nyisd meg a szobát</string> - <string name="error_jitsi_not_supported_on_old_device">Elnézést, Jitsi konferencia hívások a régi eszközökön (Android OS 5.0-nál régebbi) nem támogatottak</string> + <string name="error_jitsi_not_supported_on_old_device">Elnézést, Jitsi konferencia hívások a régi eszközökön (Android OS 6.0-nál régebbi) nem támogatottak</string> <string name="title_activity_verify_device">Munkamenet ellenőrzése</string> <string name="encryption_information_unknown_ip">ismeretlen ip</string> <string name="you_added_a_new_device_with_info">Új munkamenet kér titkosítási kulcsokat. diff --git a/vector/src/main/res/values-it/strings.xml b/vector/src/main/res/values-it/strings.xml index a6891788ac..c5db349d35 100644 --- a/vector/src/main/res/values-it/strings.xml +++ b/vector/src/main/res/values-it/strings.xml @@ -1161,7 +1161,7 @@ <string name="notification_new_invitation">Nuovo invito</string> <string name="notification_sender_me">Io</string> <string name="notification_inline_reply_failed">** Invio fallito - per favore apri la stanza</string> - <string name="error_jitsi_not_supported_on_old_device">Purtroppo i vecchi dispositivi (quelli con Android precedenti al 5.0) non supportano le conferenze con Jitsi</string> + <string name="error_jitsi_not_supported_on_old_device">Purtroppo i vecchi dispositivi (quelli con Android precedenti al 6.0) non supportano le conferenze con Jitsi</string> <string name="title_activity_verify_device">Verifica sessione</string> <string name="encryption_information_unknown_ip">IP sconosciuto</string> <string name="you_added_a_new_device_with_info">Una nuova sessione sta chiedendo le chiavi crittografiche. diff --git a/vector/src/main/res/values-kab/strings.xml b/vector/src/main/res/values-kab/strings.xml index e7a2fd4bfc..74075318d4 100644 --- a/vector/src/main/res/values-kab/strings.xml +++ b/vector/src/main/res/values-kab/strings.xml @@ -1360,7 +1360,7 @@ <item quantity="one">1 uwiǧit i yettwaremden</item> <item quantity="other">%d n yiwiǧiten i yettwaremden</item> </plurals> - <string name="error_jitsi_not_supported_on_old_device">Nesḥassef, asarag s usiwel s Jitsi ur yettusefrak ara ɣef yibenkan iqburen (ibenkan s Android OS ddaw 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Nesḥassef, asarag s usiwel s Jitsi ur yettusefrak ara ɣef yibenkan iqburen (ibenkan s Android OS ddaw 6.0)</string> <string name="room_widget_resource_permission_title">Iwiǧit-a yebɣa ad isseqdec tiɣbula-a:</string> <string name="room_widget_webview_access_camera">Seqdec takamiṛat</string> <string name="room_widget_webview_access_microphone">Seqdec asawaḍ</string> diff --git a/vector/src/main/res/values-ko/strings.xml b/vector/src/main/res/values-ko/strings.xml index da06ae6ca6..c14f0466fa 100644 --- a/vector/src/main/res/values-ko/strings.xml +++ b/vector/src/main/res/values-ko/strings.xml @@ -993,7 +993,7 @@ <item quantity="other">%d개의 활성 위젯</item> </plurals> - <string name="error_jitsi_not_supported_on_old_device">죄송합니다, Jitsi로 회의 전화는 오래된 기기에서 지원하지 않습니다 (안드로이드 OS가 5.0 이하인 기기)</string> + <string name="error_jitsi_not_supported_on_old_device">죄송합니다, Jitsi로 회의 전화는 오래된 기기에서 지원하지 않습니다 (안드로이드 OS가 6.0 이하인 기기)</string> <string name="widget_integration_unable_to_create">위젯을 만들 수 없습니다.</string> <string name="widget_integration_failed_to_send_request">요청을 보낼 수 없습니다.</string> diff --git a/vector/src/main/res/values-lv/strings.xml b/vector/src/main/res/values-lv/strings.xml index 1bb4e7f49f..88975c01f4 100644 --- a/vector/src/main/res/values-lv/strings.xml +++ b/vector/src/main/res/values-lv/strings.xml @@ -1,15 +1,14 @@ -<?xml version='1.0' encoding='UTF-8'?> +<?xml version="1.0" encoding="utf-8"?> <resources> <string name="title_activity_home">Ziņas</string> <string name="title_activity_room">Istaba</string> <string name="title_activity_settings">Iestatījumi</string> <string name="title_activity_member_details">Dalībnieka informācija</string> <string name="title_activity_historical">Bijušie</string> - <string name="ok">Lai notiek</string> <string name="cancel">Tomēr nē</string> <string name="save">Saglabāt</string> - <string name="leave">Atstāt</string> + <string name="leave">Pamest</string> <string name="send">Nosūtīt</string> <string name="resend">Sūtīt atkārtoti</string> <string name="redact">Rediģēt</string> @@ -18,17 +17,18 @@ <string name="later">Vēlāk</string> <string name="forward">Pārsūtīt</string> <string name="permalink">Iekšējā saite</string> - <string name="view_source">Skatīt avotu</string> - <string name="view_decrypted_source">Skatīt atšifrētu avotu</string> + <string name="view_source">Skatīt pirmkodu</string> + <string name="view_decrypted_source">Skatīt atšifrētu pirmkodu</string> <string name="delete">Dzēst</string> <string name="rename">Pārdēvēt</string> <string name="report_content">Ziņot par saturu</string> <string name="active_call">Aktīvs zvans</string> - <string name="ongoing_conference_call">"Ienākošs konferences zvans.\nPievienojies ar %1$s vai %2$s iespēju."</string> - <string name="ongoing_conference_call_voice">balss</string> - <string name="ongoing_conference_call_video">video</string> + <string name="ongoing_conference_call">Turpinās konferences zvans. +\nPievienojies ar %1$s vai %2$s iespēju</string> + <string name="ongoing_conference_call_voice">Balss</string> + <string name="ongoing_conference_call_video">Video</string> <string name="cannot_start_call">Nevarēja sākt zvanu. Lūdzu, pamēģini vēlāk</string> - <string name="missing_permissions_warning">Dēļ nepietiekamām atļaujām, dažas iespējas var nebūt pieejamas..</string> + <string name="missing_permissions_warning">Dēļ nepietiekamām atļaujām, dažas iespējas var nebūt pieejamas…</string> <string name="missing_permissions_to_start_conf_call">Tev ir nepieciešams iegūt atļauju, lai varētu sākt konferenci šajā istabā</string> <string name="missing_permissions_title_to_start_conf_call">Nav iespējams sākt zvanu</string> <string name="device_information">Ierīces informācija</string> @@ -36,7 +36,6 @@ <string name="send_anyway">Nosūtīt jebkurā gadījumā</string> <string name="or">vai</string> <string name="invite">Uzaicināt</string> - <string name="action_sign_out">Izrakstīties</string> <string name="action_voice_call">Balss zvans</string> <string name="action_video_call">Video zvans</string> @@ -48,30 +47,24 @@ <string name="action_close">Aizvērt</string> <string name="copied_to_clipboard">Nokopēts starpliktuvē</string> <string name="disable">Atslēgt</string> - <string name="dialog_title_confirmation">Apstiprinājums</string> <string name="dialog_title_warning">Brīdinājums</string> - <string name="bottom_action_home">Uz sākumu</string> - <string name="bottom_action_favourites">Favorīti</string> + <string name="bottom_action_favourites">Izlase</string> <string name="bottom_action_people">Cilvēki</string> <string name="bottom_action_rooms">Istabas</string> - <string name="home_filter_placeholder_home">Meklēt istabas</string> <string name="home_filter_placeholder_favorites">Meklēt favorītus</string> <string name="home_filter_placeholder_people">Meklēt cilvēkus</string> <string name="home_filter_placeholder_rooms">Meklēt istabas</string> - <string name="invitations_header">Uzaicinājumi</string> <string name="low_priority_header">Zema prioritāte</string> - <string name="direct_chats_header">Sarunas</string> <string name="local_address_book_header">Vietējā adrešu grāmata</string> <string name="matrix_only_filter">Vienīgi Matrix kontakti</string> <string name="no_conversation_placeholder">Nav sarunu</string> <string name="no_contact_access_placeholder">Tu neesi atļāvis/usi Element piekļūt taviem vietējiem kontaktiem</string> <string name="no_result_placeholder">Nav rezultātu</string> - <string name="rooms_header">Istabas</string> <string name="rooms_directory_header">Istabu katalogs</string> <string name="no_room_placeholder">Nav istabu</string> @@ -81,24 +74,20 @@ <item quantity="one">%d lietotāji</item> <item quantity="other">%d lietotāji</item> </plurals> - <string name="send_bug_report_include_logs">Nosūtīt logfailus</string> <string name="send_bug_report_include_crash_logs">Nosūtīt sistēmas avārijas logfailus</string> <string name="send_bug_report_include_screenshot">Nosūtīt ekrānattēlu</string> - <string name="send_bug_report">Kļūdu paziņojumi</string> + <string name="send_bug_report">Ziņot par kļūdu</string> <string name="send_bug_report_description">Lūdzu apraksti kļūdu. Kāda darbība tika veikta? Kāds bija gaidāmais rezultāts? Kas tieši notika?</string> - <string name="send_bug_report_placeholder">Šeit apraksti problēmu</string> + <string name="send_bug_report_placeholder">Aprakstiet savu problēmu šeit</string> <string name="send_bug_report_logs_description">Lai diagnosticētu problēmu, logfaili no šīs lietotnes tiks nosūtīti kopā ar šo kļūdas paziņojumu. Ja vēlies nosūtīt vienīgi augstākminēto tekstu, lūdzu noņem:</string> <string name="send_bug_report_alert_message">Jūs, šķiet, satricinājāt tālruni. Vai vēlaties iesniegt kļūdu ziņojumu?</string> <string name="send_bug_report_app_crashed">Šī programma iepriekš \"salūza\". Vai vēlies iesniegt paziņojumu par kļūdu?</string> - <string name="send_bug_report_sent">Paziņojums par kļūdu tika veiksmīgi nosūtīts</string> <string name="send_bug_report_failed">Paziņojumu par kļūdu neizdevās nosūtīt (%s)</string> <string name="send_bug_report_progress">Progress (%s%%)</string> - <string name="send_files_in">Nosūtīt iekš</string> <string name="read_receipt">Lasīt</string> - <string name="join_room">Pievienoties istabai</string> <string name="username">Lietotājvārds</string> <string name="create_account">Reģistrēties</string> @@ -107,17 +96,13 @@ <string name="hs_url">Bāzes servera URL adrese</string> <string name="identity_url">Identitifikācijas servera URL adrese</string> <string name="search">Meklēt</string> - <string name="start_new_chat">Sākt jaunu čatu</string> <string name="offline">Nav tiešsaistē</string> - <string name="user_directory_header">Lietotāju katalogs</string> <string name="start_voice_call">Sākt audio zvanu</string> <string name="start_video_call">Sākt video zvanu</string> - <string name="option_send_files">Sūtīt failus</string> <string name="option_take_photo_video">Uzņemt foto vai video</string> - <string name="auth_login">Pierakstīties</string> <string name="auth_register">Reģistrēties</string> <string name="auth_submit">Iesniegt</string> @@ -128,40 +113,31 @@ <string name="auth_password_placeholder">Parole</string> <string name="auth_new_password_placeholder">Jauna parole</string> <string name="auth_user_name_placeholder">Lietotājvārds</string> - <string name="light_theme">Gaiša ādiņa</string> - <string name="dark_theme">Tumša ādiņa</string> - <string name="black_them">Melna ādiņa</string> - - <string name="notification_sync_in_progress">Sinhronizācija</string> + <string name="light_theme">Gaiša tēma</string> + <string name="dark_theme">Tumša tēma</string> + <string name="black_them">Melna tēma</string> + <string name="notification_sync_in_progress">Sinhronizācija…</string> <string name="notification_listening_for_events">Notikumu monitorings</string> <string name="notification_noisy_notifications">Skaņi paziņojumi</string> <string name="notification_silent_notifications">Klusi paziņojumi</string> - <string name="title_activity_bug_report">Kļūdas atskaite</string> <string name="title_activity_group_details">Kopienas informācija</string> - <string name="loading">Ielādējas…</string> - <string name="action_exit">Izslēgt</string> <string name="bottom_action_groups">Kopienas</string> - <string name="home_filter_placeholder_groups">Meklēt kopienas</string> - <string name="groups_invite_header">Uzaicināt</string> <string name="groups_header">Kopienas</string> - <string name="no_group_placeholder">nav kopienu</string> - + <string name="no_group_placeholder">Nav grupu</string> <string name="start_new_chat_prompt_msg">Vai tiešām vēlies sākt jaunu čatu ar %s?</string> - <string name="start_voice_call_prompt_msg">Tiešām vēlies veikt AUDIO zvanu?</string> - <string name="start_video_call_prompt_msg">Tiešām vēlies veikt VIDEO zvanu?</string> - + <string name="start_voice_call_prompt_msg">Tiešām vēlies uzsākt balss zvanu\?</string> + <string name="start_video_call_prompt_msg">Tiešām vēlies uzsākt video zvanu\?</string> <string name="option_take_photo">Uzņemt foto</string> <string name="option_take_video">Uzņemt video</string> - <string name="auth_email_placeholder">Epasta adrese</string> - <string name="auth_opt_email_placeholder">Epasta adrese (papildus)</string> - <string name="auth_phone_number_placeholder">Tālruņa #</string> - <string name="auth_opt_phone_number_placeholder">Tālruņa # (papildus)</string> + <string name="auth_opt_email_placeholder">Epasta adrese (izvēles)</string> + <string name="auth_phone_number_placeholder">Tālruņa numurs</string> + <string name="auth_opt_phone_number_placeholder">Tālruņa numurs (izvēles)</string> <string name="auth_repeat_password_placeholder">Parole (atkārtoti)</string> <string name="auth_repeat_new_password_placeholder">Apstiprini savu jauno paroli</string> <string name="auth_invalid_login_param">Nepareizs lietotājvārds un/vai parole</string> @@ -179,9 +155,9 @@ <string name="auth_forgot_password">Aizmirsi paroli?</string> <string name="auth_use_server_options">Izmantot servera īpašus parametrus</string> <string name="auth_email_validation_message">Pārbaudi epastu, lai turpinātu reģistrāciju</string> - <string name="auth_threepid_warning_message">Reģistrēšanās ar epastu un tālruņa # vienlaicīgi pagaidām netiek atbalstīta. Ar kontu būs saistīts vienīgi tālruņa #. - -Savu epastu vari pievienot profila iestatījumos.</string> + <string name="auth_threepid_warning_message">Reģistrēšanās ar epastu un tālruņa numuru vienlaicīgi pagaidām netiek atbalstīta. Ar kontu būs saistīts vienīgi tālruņa numuru. +\n +\nSavu epastu varat pievienot profilam iestatījumos.</string> <string name="auth_recaptcha_message">Bāzes serveris vēlas pārbaudīt, vai neesi robots</string> <string name="auth_username_in_use">Šāds lietotājvārds jau ir aizņemts</string> <string name="auth_home_server">Bāzes serveris:</string> @@ -192,10 +168,9 @@ Savu epastu vari pievienot profila iestatījumos.</string> <string name="auth_reset_password_missing_password">Jāievada jauna parole.</string> <string name="auth_reset_password_email_validation_message">Epasts ir nosūtīts uz %s. Pēc tam, kad nospiedīsi uz tajā ietverto tīmekļa saiti, noklikšķini zemāk.</string> <string name="auth_reset_password_error_unauthorized">Neizdevās verificēt epasta adresi: pārbaudi vai esi noklikšķinājis(usi) uz saiti atsūtītajā epastā</string> - <string name="auth_reset_password_success_message">Tava parole ir atstatīta. - -Tu esi izrakstīts no visām ierīcēm un vairāk nesaņemsi \"push\" paziņojumus. Lai atjaunotu \"push\" paziņojumu saņemšanu, pieraksties katrā no savām ierīcēm.</string> - + <string name="auth_reset_password_success_message">Jūsu parole ir atstatīta. +\n +\nJūs esat izrakstīts no visām sesijām un nesaņemsit push paziņojumus. Lai atkārtoti iespējotu paziņojumus, pierakstieties katrā savā ierīcē par jaunu.</string> <string name="login_error_must_start_http">URL adresei jāsākas ar http[s]://</string> <string name="login_error_network_error">Pierakstīšanās neizdevās: tīkla kļūda</string> <string name="login_error_unable_login">Pierakstīšanās neizdevās</string> @@ -203,7 +178,6 @@ Tu esi izrakstīts no visām ierīcēm un vairāk nesaņemsi \"push\" paziņojum <string name="login_error_unable_register">Reģistrācija neizdevās</string> <string name="login_error_unable_register_mail_ownership">Registrācija neizdevās: epasta verifikācijas kļūda</string> <string name="login_error_invalid_home_server">Ievadi korektu URL adresi</string> - <string name="login_error_forbidden">Nepareizs lietotājvārds / parole</string> <string name="login_error_unknown_token">Attiecīgais pieejas tokens netika atpazīts</string> <string name="login_error_bad_json">Bojāts JSON</string> @@ -211,28 +185,21 @@ Tu esi izrakstīts no visām ierīcēm un vairāk nesaņemsi \"push\" paziņojum <string name="login_error_limit_exceeded">Nosūtīti par daudz pieprasījumi</string> <string name="login_error_user_in_use">Šis lietotājvārds ir aizņemts</string> <string name="login_error_login_email_not_yet">Nav noklikšķināts uz tīmekļa saites saņemtajā epastā</string> - <string name="read_receipts_list">Lasīt jaunbiedru sarakstu</string> - <string name="groups_list">Grupu saraksts</string> - - <string name="compression_options">"Nosūtīt kā "</string> - <string name="compression_opt_list_original">oriģinālu</string> - <string name="compression_opt_list_large">lielu</string> - <string name="compression_opt_list_medium">vidēju</string> - <string name="compression_opt_list_small">mazu</string> - + <string name="compression_options">Nosūtīt kā</string> + <string name="compression_opt_list_original">Oriģinālu</string> + <string name="compression_opt_list_large">Lielu</string> + <string name="compression_opt_list_medium">Vidēju</string> + <string name="compression_opt_list_small">Mazu</string> <string name="attachment_cancel_download">Vai atcelt lejupielādi?</string> <string name="attachment_cancel_upload">Vai atcelt augšupielādi (nosūtīšanu)?</string> <string name="attachment_remaining_time_seconds">%d s</string> <string name="attachment_remaining_time_minutes">%1$dm %2$ds</string> - - <string name="yesterday">vakar</string> - <string name="today">šodien</string> - + <string name="yesterday">Vakar</string> + <string name="today">Šodien</string> <string name="room_info_room_name">Istabas nosaukums</string> <string name="room_info_room_topic">Istabas temats</string> - <string name="call">Zvans</string> <string name="call_connected">Zvans savienots</string> <string name="call_connecting">Tiek veidots savienojums…</string> @@ -241,78 +208,65 @@ Tu esi izrakstīts no visām ierīcēm un vairāk nesaņemsi \"push\" paziņojum <string name="incoming_call">Ienākošs zvans</string> <string name="incoming_video_call">Ienākošs VIDEO zvans</string> <string name="incoming_voice_call">Ienākošs AUDIO zvans</string> - <string name="call_in_progress">Notiek zvanīšana (savienojuma izveide)</string> - + <string name="call_in_progress">Notiek zvans…</string> <string name="call_error_user_not_responding">Adresāts neatbildēja uz zvanu.</string> <string name="call_error_ice_failed">Mēdiju savienojums neizdevās</string> <string name="call_error_camera_init_failed">Nav iespējams inicializēt kameru</string> <string name="call_error_answered_elsewhere">zvans atbildēts uz citas ierīces</string> - <string name="media_picker_both_capture_title">Uzņemt foto vai video</string> <string name="media_picker_cannot_record_video">Neizdodas ierakstīt video</string> - <string name="permissions_rationale_popup_title">Element informācija</string> - <string name="permissions_rationale_msg_storage">Element-am nepieciešama atļauja piekļūt foto un video bibliotēkai, lai nosūtītu un saglabātu pielikumus. - -Lūdzu dod piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespēja nosūtīt failus no Tava tālruņa.</string> + <string name="permissions_rationale_msg_storage">Element nepieciešama atļauja piekļūt jūsu fotoattēlu un video bibliotēkai, lai nosūtītu un saglabātu pielikumus. +\n +\nLūdzu, atļaujiet piekļuvi nākamajā uznirstošajā logā, lai varētu nosūtīt failus no sava tālruņa.</string> <string name="permissions_rationale_msg_camera">Element-am nepieciešama atļauja piekļūt kamerai, lai uzņemtu foto un nodrošinātu video zvanus.</string> - <string name="permissions_rationale_msg_camera_explanation"> - -Lūdzu dot piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespēja veikt zvanus.</string> + <string name="permissions_rationale_msg_camera_explanation">" +\n +\nLūdzu dot piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespēja veikt zvanus."</string> <string name="permissions_rationale_msg_record_audio">Element-am nepieciešama atļauja piekļūt mikrofonam, lai nodrošinātu audio zvanus.</string> - <string name="permissions_rationale_msg_record_audio_explanation"> - -Lūdzu dod piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespēja veikt zvanus.</string> - <string name="permissions_rationale_msg_camera_and_audio">Element-am nepieciešama atļauja piekļūt kamerai un mikrofonam, lai veiktu videozvanus. - -Lūdzu dod piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespēja veikt zvanus.</string> + <string name="permissions_rationale_msg_record_audio_explanation">" +\n +\nLūdzu dod piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespēja veikt zvanus."</string> + <string name="permissions_rationale_msg_camera_and_audio">Element nepieciešama atļauja piekļūt kamerai un mikrofonam, lai veiktu videozvanus. +\n +\nLūdzu, dodiet piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespēja veikt zvanus.</string> <string name="permissions_rationale_msg_contacts">Element-am nepieciešama atļauja piekļūt kontaktiem, lai varētu atrast citus lietotājus tīklā pēc to epasta adreses vai tālruņa #. Lūdzu dod piekļuves atļauju nākamajā uznirstošajā logā, lai būtu iespēja atrast Tavus kontaktus, kuri ir sasniedzami Elementā.</string> - <string name="permissions_msg_contacts_warning_other_androids">Element-am nepieciešama atļauja piekļūt kontaktiem, lai varētu atrast citus lietotājus tīklā pēc to epasta adreses vai tālruņa #. - -Vai dot Elementam piekļuvi kontaktiem?</string> - + <string name="permissions_msg_contacts_warning_other_androids">Element var pārbaudīt jūsu adrešu grāmatu, lai atrastu citus Matrix lietotājus pāc viņu epasta adresēm un tālruņa numuriem. +\n +\nVai piekrītat koplietot savu adrešu grāmatu šim nolūkam\?</string> <string name="permissions_action_not_performed_missing_permissions">Atvaino… Darbība nav veikta dēļ nepietiekamām piekļuves atļaujām</string> - <string name="media_slider_saved">Saglabāts</string> - <string name="media_slider_saved_message">Saglabāt lejupielādēs</string> + <string name="media_slider_saved_message">Saglabāt lejupielādēs\?</string> <string name="yes">JĀ</string> <string name="no">NĒ</string> <string name="_continue">Turpināt</string> - <string name="remove">Dzēst</string> <string name="join">Pievienoties</string> - <string name="preview">Priekšskats</string> + <string name="preview">Piekšskatījums</string> <string name="reject">Noraidīt</string> - <string name="room_jump_to_first_unread">Pāriet uz pirmo neizlasīto ziņu.</string> - - <string name="room_preview_invitation_format">%s Tevi uzaicināja pievienoties šai istabai</string> - <string name="room_preview_unlinked_email_warning">Uzaicinājums tika nosūtīts uz %s, kura nav piesaistīta šim kontam. -Tu vari pierakstīties ar citu kontu vai arī pievienot šo epastu šim kontam.</string> - <string name="room_preview_try_join_an_unknown_room">Tu centies piekļūt %s. Vai Tu vēlies pievienoties, lai piedalītos diskusijā?</string> + <string name="room_preview_invitation_format">%s jūs uzaicināja pievienoties šai istabai</string> + <string name="room_preview_unlinked_email_warning">Uzaicinājums tika nosūtīts uz %s, kura nav piesaistīta šim kontam. +\nJūs varat pierakstīties ar citu kontu vai arī pievienot šo epastu šim kontam.</string> + <string name="room_preview_try_join_an_unknown_room">Jūs cenšaties piekļūt %s. Vai vēlāties pievienoties, lai piedalītos diskusijā\?</string> <string name="room_preview_try_join_an_unknown_room_default">istaba</string> - <string name="room_preview_room_interactions_disabled">Šis ir istabas priekšskats. Tu esi \"lasīšanas\" režīmā, bez iespējām mijiedarboties.</string> - + <string name="room_preview_room_interactions_disabled">Šis ir istabas priekšskatījums. Iespējām mijiedarboties istabā ir atspējotas.</string> <string name="room_creation_title">Jauns čats</string> - <string name="room_creation_add_member">Pievienots dalībnieku</string> - <string name="room_title_one_member">1 dalībnieks</string> - - <string name="room_participants_leave_prompt_title">Atstāt istabu</string> - <string name="room_participants_leave_prompt_msg">Vai tiešām vēlies atstāt istabu?</string> + <string name="room_creation_add_member">Pievienot dalībnieku</string> + <string name="room_title_one_member">1 biedrs</string> + <string name="room_participants_leave_prompt_title">Pamest istabu</string> + <string name="room_participants_leave_prompt_msg">Vai tiešām vēlies pamest istabu\?</string> <string name="room_participants_remove_prompt_msg">Vai tiešām vēlies dzēst (izmest) %s no šī čata?</string> <string name="room_participants_create">Izveidot</string> - <string name="room_participants_online">Tiešsaistē</string> - <string name="room_participants_offline">Nesaistē</string> + <string name="room_participants_offline">Bezsaistē</string> <string name="room_participants_idle">Dīkstāvē</string> - - <string name="room_participants_header_admin_tools">ADMIN rīki</string> + <string name="room_participants_header_admin_tools">Administratora rīki</string> <string name="room_participants_header_call">ZVANS</string> <string name="room_participants_header_direct_chats">TIEŠIE ČATI</string> - <string name="room_participants_header_devices">IERĪCES</string> - + <string name="room_participants_header_devices">SESIJAS</string> <string name="room_participants_action_invite">Uzaicināt</string> <string name="room_participants_action_leave">Atstāt šo istabu</string> <string name="room_participants_action_remove">Izmest no šīs istabas</string> @@ -321,26 +275,22 @@ Tu vari pierakstīties ar citu kontu vai arī pievienot šo epastu šim kontam.< <string name="room_participants_action_set_default_power_level">Padarīt par ierindas dalībnieku</string> <string name="room_participants_action_set_moderator">Padarīt par moderatoru</string> <string name="room_participants_action_set_admin">Padarīt par administratoru</string> - <string name="room_participants_action_ignore">Nerādīt nevienu šī dalībnieka ziņu</string> - <string name="room_participants_action_unignore">Rīdīt visas ziņas no šī dalībnieka</string> + <string name="room_participants_action_ignore">Ignorēt</string> + <string name="room_participants_action_unignore">Atcelt ignorēšanu</string> <string name="room_participants_invite_search_another_user">Dalībnieka ID, vārds vai epasts</string> <string name="room_participants_action_mention">Pieminēt</string> <string name="room_participants_action_devices_list">Parādīt ierīču sarakstu</string> - <string name="room_participants_power_level_prompt">Tu nevarēsi atcelt šo darbību, jo lietotājs iegūs tādu pašu statusu kāds ir Tev. -Vai vēlies turpināt?</string> - + <string name="room_participants_power_level_prompt">Jūs nevarēsiet atsaukt šīs izmaiņas, jo lietotājam būtu tāds pats piekļuves līmenis kā jums pašam. +\nVai esat pārliecināts\?</string> <string name="room_participants_invite_prompt_msg">Vai tiešām vēlies uzaicināt %s uz šo čatu?</string> - <string name="room_participants_ban_prompt_msg">Vai tiešām vēlies \"nobanot\" šo lietotāju šajā čatā?</string> - + <string name="room_participants_ban_prompt_msg">Liedzot pieeju lietotājam, šī darbība izmetīs viņu no šīs istabas un neļaus pievienoties atkārtoti.</string> <string name="people_search_invite_by_id"><u>Uzaicināt pēc ID</u></string> <string name="people_search_local_contacts">LOKĀLIE KONTAKTI (%d)</string> <string name="people_search_user_directory">LIETOTĀJU KATALOGS (%s)</string> <string name="people_search_filter_text">TIkai Matrix lietotāji</string> - <string name="people_search_invite_by_id_dialog_title">Uzaicināt dalībnieku pēc ID</string> <string name="people_search_invite_by_id_dialog_description">Lūdzu ievadi vienu vai vairākas epasta adreses vai Matrix ID</string> <string name="people_search_invite_by_id_dialog_hint">Epasts vai Matrix ID</string> - <string name="room_menu_search">Meklēt</string> <string name="room_one_user_is_typing">%s raksta…</string> <string name="room_two_users_are_typing">%1$s & %2$s raksta…</string> @@ -351,12 +301,11 @@ Vai vēlies turpināt?</string> <string name="room_unsent_messages_notification">Ziņas nav nosūtītas. %1$s vai %2$s tagad?</string> <string name="room_unknown_devices_messages_notification">Ziņas nav nosūtītas dēļ nezināmu ierīču klātbūtnes. %1$s vai %2$s tagad?</string> <string name="room_prompt_resend">Nosūtīt visas atkārtoti</string> - <string name="room_prompt_cancel">atcelt visas</string> + <string name="room_prompt_cancel">Atcelt visas</string> <string name="room_resend_unsent_messages">Nosūtīt nenosūtītās ziņas</string> <string name="room_delete_unsent_messages">Dzēst nenosūtītās ziņas</string> <string name="room_message_file_not_found">Fails nav atrasts</string> <string name="room_do_not_have_permission_to_post">Tev nav tiesību rakstīt ziņas šajā istabā</string> - <string name="ssl_trust">Uzticēties</string> <string name="ssl_do_not_trust">Neuzticēties</string> <string name="ssl_logout_account">Izrakstīties</string> @@ -368,7 +317,6 @@ Vai vēlies turpināt?</string> <string name="ssl_unexpected_existing_expl">Servera sertifikāts ir izmanījies un Tava ierīce tam tagad neuzticas. Tas ir ĻOTI NEPARASTI. Iesakām NEUZTICĒTIES šim jaunajam sertifikātam.</string> <string name="ssl_expected_existing_expl">Sertifikāts izmainījās no iepriekš uzticama uz neuzticamu. Iespējams, serveris ir aktualizējis savu sertifikātu. Sazinies ar servera administratoru, lai saņemtu sagaidāmo nospiedumu (fingerprint).</string> <string name="ssl_only_accept">Akceptē sertifikātu TIKAI tad, kad servera administrators publicējis sertifikāta nospiedumu, kurš atbilst augstāk redzamajam.</string> - <string name="room_details_title">Istabas informācija</string> <string name="room_details_people">Cilvēki</string> <string name="room_details_files">Faili</string> @@ -376,20 +324,19 @@ Vai vēlies turpināt?</string> <string name="malformed_id">Nekorekts ID. Izmantojiet epastu vai Matrix ID formā: \'@localpart:domain\'</string> <string name="room_details_people_invited_group_name">UZAICINĀTI</string> <string name="room_details_people_present_group_name">PIEVIENOJUŠIES</string> - <string name="room_event_action_report_prompt_reason">Iemesls ziņojumam par šo saturu</string> - <string name="room_event_action_report_prompt_ignore_user">Vai vēlies neredzēt nevienu ziņu no šī dalībnieka?</string> - <string name="room_event_action_cancel_upload">Atcelt augšuplādi (nosūtīšanu)</string> - <string name="room_event_action_cancel_download">Atcelt lejuplādi (saņemšanu)</string> - + <string name="room_event_action_report_prompt_ignore_user">Vai vēlaties paslēpt visas ziņas no šī lietotāja\? +\n +\nŅemiet vērā, ka ar šo darbību tiks restartēta lietotne, un tas var aizņemt kādu laiku.</string> + <string name="room_event_action_cancel_upload">Atcelt augšupielādi</string> + <string name="room_event_action_cancel_download">Atcelt lejupielādi</string> <string name="search_hint">Meklēšana</string> <string name="search_members_hint">Istabas dalībnieku filtrs</string> <string name="search_no_results">Rezultātu nav</string> <string name="tab_title_search_rooms">ISTABAS</string> <string name="tab_title_search_messages">ZIŅAS</string> - <string name="tab_title_search_people">ĻAUDIS</string> + <string name="tab_title_search_people">CILVĒKI</string> <string name="tab_title_search_files">FAILI</string> - <string name="room_recents_join">PIEVIENOTIES</string> <string name="room_recents_directory">KATALOGS</string> <string name="room_recents_favourites">IZLASE</string> @@ -401,10 +348,8 @@ Vai vēlies turpināt?</string> <string name="room_recents_join_room">Ieiet istabā</string> <string name="room_recents_join_room_title">Ieiet istabā</string> <string name="room_recents_join_room_prompt">Ievadiet istabas ID vai aliasi</string> - <string name="directory_search_results_title">Skatīt katalogu</string> <string name="directory_searching_title">Meklēju katalogā…</string> - <string name="room_settings_all_messages_noisy">Visas ziņas (ar skaņu)</string> <string name="room_settings_all_messages">Visas ziņas</string> <string name="room_settings_mention_only">Tikai pieminējumi</string> @@ -414,30 +359,26 @@ Vai vēlies turpināt?</string> <string name="room_settings_direct_chat">Tiešais čats</string> <string name="room_settings_leave_conversation">Atstāt sarunu</string> <string name="room_settings_forget">Aizmirst</string> - <string name="room_settings_add_homescreen_shortcut">Pievienot galvenā ekrāna īstaustiņu (homescreen shortcut)</string> - + <string name="room_settings_add_homescreen_shortcut">Pievienot uz galvenā ekrāna</string> <string name="room_sliding_menu_messages">Ziņas</string> <string name="room_sliding_menu_settings">Iestatījumi</string> - <string name="room_sliding_menu_version">"Versija "</string> + <string name="room_sliding_menu_version">Versija</string> <string name="room_sliding_menu_term_and_conditions">Lietošanas noteikumi</string> <string name="room_sliding_menu_third_party_notices">Trešo pušu paziņojumi</string> <string name="room_sliding_menu_copyright">Autortiesības</string> <string name="room_sliding_menu_privacy_policy">Privātuma politika</string> - - <string name="settings_profile_picture">Avatars (profila bilde)</string> + <string name="settings_profile_picture">Profila attēls</string> <string name="settings_display_name">Attēlojamais vārds</string> <string name="settings_email_address">Epasts</string> <string name="settings_add_email_address">Pievienot epasta adresi</string> <string name="settings_phone_number">Tālrunis</string> - <string name="settings_add_phone_number">Pievienot tālruņa #</string> - <string name="settings_app_info_link_summary">Doties uz Android Lietotņu informācijas sadaļu</string> + <string name="settings_add_phone_number">Pievienot tālruņa numuru</string> + <string name="settings_app_info_link_summary">Rādīt lietotnes informāciju sistēmas iestatījumos.</string> <string name="settings_app_info_link_title">Lietotnes informācija</string> - <string name="settings_notification_ringtone">Paziņojumu skaņa</string> <string name="settings_enable_all_notif">Iespējot šim kontam paziņojumus</string> <string name="settings_enable_this_device">Iespējot šai ierīcei paziņojumus</string> <string name="settings_turn_screen_on">Ieslēgt ekrānu uz 3 sekundēm</string> - <string name="settings_containing_my_display_name">Ziņas, kuras satur manu rādāmo vārdu</string> <string name="settings_containing_my_user_name">Ziņas, kuras satur manu lietotājvārdu</string> <string name="settings_messages_in_one_to_one">Ziņas tiešajos (privātajos) čatos</string> @@ -445,15 +386,13 @@ Vai vēlies turpināt?</string> <string name="settings_invited_to_room">Uzaicinājumi uz istabām</string> <string name="settings_call_invitations">Uzaicinājumi ar zvanu</string> <string name="settings_messages_sent_by_bot">Ziņas no bota</string> - <string name="settings_start_on_boot">Startēt pie ierīces ielādes</string> <string name="settings_background_sync">Sinhronizācija</string> <string name="settings_enable_background_sync">Iespējot sinhronizāciju fonā</string> <string name="settings_set_sync_timeout">Sinhronizācijas pieprasījuma noildze</string> - <string name="settings_set_sync_delay">Intervāls starp 2 sinhronizācijas pieprasījumiem</string> - + <string name="settings_set_sync_delay">Intervāls starp sinhronizācijām</string> <string name="settings_version">Versija</string> - <string name="settings_olm_version">Olm versija</string> + <string name="settings_olm_version">olm versija</string> <string name="settings_app_term_conditions">Lietošanas noteikumi</string> <string name="settings_third_party_notices">Trešo pušu paziņojumi</string> <string name="settings_copyright">Autortiesības</string> @@ -461,7 +400,6 @@ Vai vēlies turpināt?</string> <string name="settings_clear_cache">Iztīrīt ķešu</string> <string name="settings_clear_media_cache">Iztīrīt mēdija ķešu</string> <string name="settings_keep_media">Turēt mēdiju (?)</string> - <string name="settings_user_settings">Lietotāja iestatījumi</string> <string name="settings_notifications">Paziņojumi</string> <string name="settings_ignored_users">Ignorētie dalībnieki</string> @@ -480,52 +418,43 @@ Vai vēlies turpināt?</string> <string name="settings_always_show_timestamps">Vienmēr rādīt ziņu laiku</string> <string name="settings_12_24_timestamps">Rādīt ziņu laiku 12 stundu formātā (piem. 12:12pm)</string> <string name="settings_vibrate_on_mention">Vibrācija, kad pieminējums</string> - <string name="settings_analytics">Analītika</string> - <string name="settings_data_save_mode">Trafika ekonomijas režīms</string> - <string name="devices_details_dialog_title">Ierīces informācija</string> <string name="devices_details_id_title">ID</string> <string name="devices_details_name_title">Vārds</string> <string name="devices_details_device_name">Ierīces nosaukums</string> <string name="devices_details_last_seen_title">Pēdējo reizi manīts</string> <string name="devices_details_last_seen_format">%1$s @ %2$s</string> - <string name="devices_delete_dialog_text">Šīs operācijas veikšanai nepieciešama papildus autentifikācija. -Lai turpinātu, ievadi savu paroli.</string> + <string name="devices_delete_dialog_text">Lai veiktu šo darbību, nepieciešama papildu autentifikācija. +\nLai turpinātu, lūdzu, ievadiet savu paroli.</string> <string name="devices_delete_dialog_title">Autentifikācija</string> <string name="devices_delete_pswd">Parole:</string> <string name="devices_delete_submit_button_label">Iesniegt</string> - <string name="settings_logged_in">Pierakstījies kā</string> <string name="settings_home_server">Bāzes serveris</string> <string name="settings_identity_server">Identitāšu serveris</string> - <string name="settings_user_interface">Lietotāja saskarne</string> <string name="settings_interface_language">Saskarnes valoda</string> <string name="settings_select_language">Izvēlies valodu</string> - <string name="account_email_validation_title">Tiek verificēts</string> <string name="account_email_validation_message">Pārbaudi savu epastu un noklikšķini uz epasata sūtījumā esošās tīmekļa saites. Kad tas paveikts, klikšķini uz \"turpināt\".</string> - <string name="account_email_validation_error">Neizdevās verificēt epasta adresi. Lūdzu pārbaudi savu epastu un noklikšķini uz tajā esošo tīmekļa saiti. Kad tas paveikts, noklikšķini uz \"Turpināt\"</string> - <string name="account_email_already_used_error">Šī epasta adrese sistēmā tiek jau lietota</string> - <string name="account_email_not_found_error">Neizdevās nosūtīt epastu: šī epasta adrese netika atrasta</string> - <string name="account_phone_number_already_used_error">Šis tālruņa # sistēmā tiek jau lietots</string> - + <string name="account_email_validation_error">Neizdevās verificēt epasta adresi. Lūdzu pārbaudiet savu epastu un atveriet tajā esošo tīmekļa saiti. Kad tas paveikts, noklikšķini uz \"Turpināt”.</string> + <string name="account_email_already_used_error">Šī epasta adrese sistēmā tiek jau lietota.</string> + <string name="account_email_not_found_error">Šī epasta adrese netika atrasta.</string> + <string name="account_phone_number_already_used_error">Šis tālruņa numurs sistēmā tiek jau lietots.</string> <string name="settings_change_password">Nomainīt paroli</string> - <string name="settings_old_password">vecā parole</string> - <string name="settings_new_password">jaunā parole</string> - <string name="settings_confirm_password">apstiprini paroli</string> + <string name="settings_old_password">Pašreizējā parole</string> + <string name="settings_new_password">Jaunā parole</string> + <string name="settings_confirm_password">Apstipriniet jauno paroli</string> <string name="settings_fail_to_update_password">Paroles nomaiņas kļūda</string> <string name="settings_password_updated">Parole nomainīta</string> - <string name="settings_unignore_user">Vai parādīt visas ziņas no %s?</string> - + <string name="settings_unignore_user">Rādīt visas ziņas no %s\? +\n +\nŅemiet vērā, ka ar šo darbību tiks restartēta lietotne, un tas var aizņemt kādu laiku.</string> <string name="settings_delete_notification_targets_confirmation">Vai tiešām vēlies turpmāk paziņojumus uz šo ierīci nesūtīt?</string> - <string name="settings_delete_threepid_confirmation">Vai tiešām vēlies dzēst %1$s %2$s?</string> - <string name="settings_select_country">Izvēlies valsti</string> - <string name="settings_phone_number_country_label">Valsts</string> <string name="settings_phone_number_country_error">Lūdzu izvēlies valsti</string> <string name="settings_phone_number_label">Tālruņa #</string> @@ -535,25 +464,19 @@ Lai turpinātu, ievadi savu paroli.</string> <string name="settings_phone_number_verification_error_empty_code">Ievadi aktivācijas kodu</string> <string name="settings_phone_number_verification_error">Tālruņa # validācija nesekmīga</string> <string name="settings_phone_number_code">Kods</string> - - <string name="settings_flair">Gaidas</string> - <string name="media_saving_period_3_days">3 dienas</string> <string name="media_saving_period_1_week">1 nedēļa</string> <string name="media_saving_period_1_month">1 mēnesis</string> <string name="media_saving_period_forever">Pastāvīgi</string> - <string name="room_settings_room_photo">Istabas avatars</string> <string name="room_settings_room_name">Istabas nosaukums</string> <string name="room_settings_topic">Temats</string> <string name="room_settings_room_tag">Istabas birkas</string> <string name="room_settings_tag_pref_dialog_title">Iebirkota kā:</string> - <string name="room_settings_tag_pref_entry_favourite">Izlase</string> <string name="room_settings_tag_pref_entry_low_priority">Zema prioritāte</string> <string name="room_settings_tag_pref_entry_none">Nav</string> - <string name="room_settings_category_access_visibility_title">Pieejamība un redzamība</string> <string name="room_settings_directory_visibility">Iekļaut šo istabu katalogā</string> <string name="room_settings_room_notifications_title">Paziņojumi</string> @@ -561,64 +484,55 @@ Lai turpinātu, ievadi savu paroli.</string> <string name="room_settings_room_read_history_rules_pref_title">Piekļuve Istabas vēsturei</string> <string name="room_settings_room_read_history_rules_pref_dialog_title">Kas var lasīt vēsturi?</string> <string name="room_settings_room_access_rules_pref_dialog_title">Kas var piekļūt šai istabai?</string> - <string name="room_settings_read_history_entry_anyone">Jebkurš</string> <string name="room_settings_read_history_entry_members_only_option_time_shared">Tikai dalībnieki (no šī parametra iespējošanas brīža)</string> <string name="room_settings_read_history_entry_members_only_invited">Tikai dalībnieki (kopš tie tika uzaicināti)</string> <string name="room_settings_read_history_entry_members_only_joined">Tikai dalībnieki (kopš tie pievienojušies)</string> - <string name="room_settings_room_access_warning">Lai ģenerētu saiti uz istabu, ir jābūt dotai adresei.</string> <string name="room_settings_room_access_entry_only_invited">Tikai uzaicinātie</string> <string name="room_settings_room_access_entry_anyone_with_link_apart_guest">Visi, kuri zin saiti uz istabu, izņemot viesus</string> <string name="room_settings_room_access_entry_anyone_with_link_including_guest">Visi, kuri zin saiti uz istabu, ieskatot viesus</string> - - <string name="room_settings_banned_users_title">Banotie lietotāji</string> - + <string name="room_settings_banned_users_title">Lietotāji, kuriem liegta pieeja</string> <string name="room_settings_category_advanced_title">Papildus</string> - <string name="room_settings_room_internal_id">Šīs istabas iekšējais Id</string> + <string name="room_settings_room_internal_id">Šīs istabas iekšējais ID</string> <string name="room_settings_addresses_pref_title">Adreses</string> <string name="room_settings_labs_pref_title">Izmēģinājumu lauciņš</string> <string name="room_settings_labs_warning_message">Šīs ir eksperimentālas funkcijas, kuras var radīt pārsteidzošus rezultātus! Lietot ar piesardzību.</string> - <string name="room_settings_labs_end_to_end">\"End-to-end\" šifrēšana</string> - <string name="room_settings_labs_end_to_end_is_active">\"End-to-End\" šifrēšana ir aktivizēta</string> + <string name="room_settings_labs_end_to_end">Pilnīga šifrēšana</string> + <string name="room_settings_labs_end_to_end_is_active">Pilnīga šifrēšana ir aktivizēta</string> <string name="room_settings_labs_end_to_end_warnings">Tev nepieciešams izrakstīties, lai iespējotu šifrēšanu.</string> <string name="room_settings_never_send_to_unverified_devices_title">Šifrēt ziņas tikai verificētām ierīcēm</string> <string name="room_settings_never_send_to_unverified_devices_summary">Nekad nesūtīt šifrētas ziņas neverificētām ierīcēm šajā istabā no šīs ierīces.</string> - <string name="room_settings_addresses_no_local_addresses">Šai istabai nav lokālās adreses</string> <string name="room_settings_addresses_add_new_address">Jaunā adrese (e.g #foo:matrix.org)</string> - - <string name="room_settings_no_flair">" Šī istaba nerāda gaidas nevienā kopienā"</string> + <string name="room_settings_no_flair">Šajā istabā neparādās noskaņas, kas uzstādītas kopienās</string> <string name="room_settings_add_new_group">Kopienas jaunais Id (piem. +foo:matrix.org)</string> - <string name="room_settings_invalid_group_format_dialog_title">Nederīgs kopienas Id</string> + <string name="room_settings_invalid_group_format_dialog_title">Nederīgs kopienas ID</string> <string name="actions">Darbības</string> <string name="send_bug_report_rage_shake">Ņipri papurināt ierīci, lai paziņotu par kļūdu</string> - <plurals name="membership_changes"> - <item quantity="zero">%d biedru izmaiņa</item> - <item quantity="one">%d biedru izmaiņas</item> + <item quantity="zero">%d biedru izmaiņu</item> + <item quantity="one">%d biedru izmaiņa</item> <item quantity="other">%d biedru izmaiņas</item> </plurals> - <string name="list_members">Biedru katalogs</string> <string name="open_chat_header">Atvērt galveni</string> <string name="room_sync_in_progress">Notiek sinhronizācija…</string> <plurals name="room_header_active_members_count"> - <item quantity="zero">%d aktīvs biedrs</item> - <item quantity="one">%d aktīvi biedri</item> - <item quantity="other">%d aktīvu biedru</item> + <item quantity="zero">%d aktīvi biedri</item> + <item quantity="one">%d aktīvs biedrs</item> + <item quantity="other">%d aktīvi biedri</item> </plurals> <plurals name="room_title_members"> - <item quantity="zero">%d biedrs</item> - <item quantity="one">%d biedri</item> - <item quantity="other">%d biedru</item> + <item quantity="zero">%d biedri</item> + <item quantity="one">%d biedrs</item> + <item quantity="other">%d biedri</item> </plurals> <plurals name="room_new_messages_notification"> - <item quantity="zero">%d jauna ziņa</item> - <item quantity="one">%d jaunas ziņas</item> - <item quantity="other">%d jaunu ziņu</item> + <item quantity="zero">%d jaunu ziņu</item> + <item quantity="one">%d jauna ziņa</item> + <item quantity="other">%d jaunas ziņas</item> </plurals> - <plurals name="directory_search_rooms"> <item quantity="zero">%d istaba</item> <item quantity="one">%d istabas</item> @@ -629,94 +543,78 @@ Lai turpinātu, ievadi savu paroli.</string> <item quantity="one">%1$s istabas atrastas priekš %2$s</item> <item quantity="other">%1$s istabas atrastas priekš %2$s</item> </plurals> - <string name="room_settings_invalid_group_format_dialog_body">\'%s\' ir nederīgs kopienas Id</string> - - + <string name="room_settings_invalid_group_format_dialog_body">\'%s\' ir nederīgs kopienas ID</string> <string name="room_settings_addresses_invalid_format_dialog_title">Nepareizs alias formāts</string> <string name="room_settings_addresses_invalid_format_dialog_body">\'%s\' nav pareizs formāts priekš aliases (pseidonīma)</string> - <string name="room_settings_addresses_disable_main_address_prompt_msg">Tev nebūs norādīta galvenā adrese. Pēc noklusējuma šīs istabas galvenā adrese tiks izvēlēta uz labu laimi</string> + <string name="room_settings_addresses_disable_main_address_prompt_msg">Šai istabai nebūs norādīta galvenā adrese.</string> <string name="room_settings_addresses_disable_main_address_prompt_title">Galvenās adreses brīdinājums</string> - - <string name="room_settings_set_main_address">Iestatīt kā Galveno adresi</string> - <string name="room_settings_unset_main_address">Atstatīt kā Galveno adresi</string> - <string name="room_settings_copy_room_id">Kopēt istabas Id</string> + <string name="room_settings_set_main_address">Iestatīt kā galveno adresi</string> + <string name="room_settings_unset_main_address">Atiestatīt kā galveno adresi</string> + <string name="room_settings_copy_room_id">Kopēt istabas ID</string> <string name="room_settings_copy_room_address">Kopēt istabas adresi</string> - <string name="room_settings_addresses_e2e_enabled">Šajā istabā darbojas šifrēšana.</string> <string name="room_settings_addresses_e2e_disabled">Šajā istabā nav ieslēgta šifrēšana.</string> - <string name="room_settings_addresses_e2e_encryption_warning">Ieslēgt šifrēšanu -(brīdinājums: vēlāk to nav iespējams izslēgt!)</string> - + <string name="room_settings_addresses_e2e_encryption_warning">Iespējot šifrēšanu +\n(brīdinājums: vēlāk to nav iespējams izslēgt!)</string> <string name="directory_title">Katalogs</string> - <string name="settings_theme">Ādiņa</string> - + <string name="settings_theme">Tēma</string> <string name="failed_to_load_timeline_position">%s centās ielādēt istabas vēsturi, bet nevarēja to atrast.</string> - - <string name="encryption_information_title">End-to-end šifrēšanas informācija</string> - + <string name="encryption_information_title">Pilnīgas šifrēšanas informācija</string> <string name="encryption_information_device_info">Notikuma informācija</string> <string name="encryption_information_user_id">Lietotāta Id</string> <string name="encryption_information_curve25519_identity_key">Curve25519 identitātes atslēga</string> <string name="encryption_information_claimed_ed25519_fingerprint_key">Nepieciešams Ed25519 pirkstnospieduma atslēga</string> <string name="encryption_information_algorithm">Algoritms</string> - <string name="encryption_information_session_id">Sesijas Id</string> + <string name="encryption_information_session_id">Sesijas ID</string> <string name="encryption_information_decryption_error">Atšifrēšanas kļūda</string> - <string name="encryption_information_sender_device_information">Nosūtītāja ierīces informācija</string> <string name="encryption_information_device_name">Ierīces nosaukums</string> - <string name="encryption_information_name">Vārds</string> - <string name="encryption_information_device_id">Ierīces Id</string> - <string name="encryption_information_device_key">Ierīces atslēga</string> - <string name="encryption_information_verification">Pārbaude (verifikācija)</string> + <string name="encryption_information_name">Publiskais nosaukums</string> + <string name="encryption_information_device_id">Sesijas ID</string> + <string name="encryption_information_device_key">Sesijas atslēga</string> + <string name="encryption_information_verification">Verifikācija</string> <string name="encryption_information_ed25519_fingerprint">Ed25519 pirkstnospiedums</string> - - <string name="encryption_export_e2e_room_keys">Eksportēt E2E istabas atslēgas</string> + <string name="encryption_export_e2e_room_keys">Eksportēt istabas šifrēšanas atslēgas</string> <string name="encryption_export_room_keys">Eksportēt istabas atslēgas</string> <string name="encryption_export_room_keys_summary">Eksportēt atslēgas vietējā failā</string> - <string name="encryption_export_export">Eksports</string> + <string name="encryption_export_export">Eksportēt</string> <string name="passphrase_enter_passphrase">Ievadi paroli (paroles frāzi)</string> <string name="passphrase_confirm_passphrase">Apstiprināt paroli</string> - <string name="encryption_export_saved_as">E2E istabas atslēgas ir saglabātas failā \'%s\'</string> - + <string name="encryption_export_saved_as">Istabas šifrēšanas atslēgas tika salabātas \'%s\'. +\n +\nBrīdinājums: fails var tikt izdzēsts, ja lietotne tiek atinstalēta.</string> <string name="encryption_import_e2e_room_keys">Importēt E2E istabas atslēgas</string> <string name="encryption_import_room_keys">Importēt istabas atslēgas</string> <string name="encryption_import_room_keys_summary">Importēt atslēgas no vietējā faila</string> <string name="encryption_import_import">Imports</string> <string name="encryption_never_send_to_unverified_devices_title">Šifrēt vienīgi uz pārbaudītām ierīcēm</string> - <string name="encryption_never_send_to_unverified_devices_summary">Nekad nesūtīt šifrētas ziņas uz nepārbaudītām ierīcēm no šīs ierīces</string> - - <string name="encryption_information_not_verified">NEpārbaudīta</string> - <string name="encryption_information_verified">Pārbaudīta</string> + <string name="encryption_never_send_to_unverified_devices_summary">Nekad nesūtīt šifrētas ziņas uz nepārbaudītām ierīcēm no šīs ierīces.</string> + <string name="encryption_information_not_verified">Neverificēta</string> + <string name="encryption_information_verified">Verificēta</string> <string name="encryption_information_blocked">Melnajā sarakstā</string> - <string name="encryption_information_unknown_device">nepazīstama ierīce</string> <string name="encryption_information_none">nekā</string> - <string name="encryption_information_verify">Apstiprināt</string> <string name="encryption_information_unverify">Apstiprinājumu atcelt</string> <string name="encryption_information_block">Ietvert melnajā sarakstā</string> <string name="encryption_information_unblock">Izņemt no melnā saraksta</string> - <string name="encryption_information_verify_device">Verificēt ierīci</string> - <string name="encryption_information_verify_device_warning">" Lai pārbaudītu vai šī ierīce ir uzticama, lūdzu sazinies ar tās īpašnieku izmantojot kādus citus saziņas līdzekļus (klātienē vai ar telefona zvana starpniecību) un pavaicā viņam vai viņa ierīces atslēga Lietotāja iestatījumos sakrīt ar to, kura redzama zemāk:"</string> + <string name="encryption_information_verify_device_warning">Apstipriniet, salīdzinot sekojošo ar lietotāja iestatījumiem citā savā sesijā:</string> <string name="encryption_information_verify_device_warning2">Ja tā sakrīt, nospied zemāk esošo verifikācijas pogu. Ja tā nesakrīt, tad kāds ir pārtvēris šo ierīci un Tu droši vien vēlies šo ierīci pievienot melnajam sarakstam. Nākotnē šī pārbaudes procedūra plānota sarežģītāka.</string> <string name="encryption_information_verify_key_match">Apstiprinu, ka atslēgas sakrīt</string> - <string name="unknown_devices_alert_title">Istabā atrodas nepazīstamas ierīces</string> - <string name="unknown_devices_alert_message">Šajā istabā atrodas nepazīstamas ierīces, kuras nav verificētas. -Tas nozīmē, ka nav garantijas, ka ierīces pieder tiem lietotājiem kuri par tādiem uzdodas. -Tiek rekomendēts veikt verificēšanas procedūru katrai ierīcei pirms turpināt, bet, ja vēlies, vari arī veikt atkārtotu nosūtīšanu bez verificēšanas. - -Nepazīstamas ierīces:</string> - + <string name="unknown_devices_alert_message">Šajā istabā ir nezināmas sesijas, kas nav verificētas. +\nTas nozīmē, ka nav garantijas, ka sesijas pieder lietotājiem, par kuriem tiek uzdotas. +\nPirms turpināšanas iesakām iziet katras sesijas verificēšanas procesu, taču varat atkārtoti nosūtīt ziņu bez verificēšanas, ja tā vēlaties. +\n +\nNezināmas sesijas:</string> <string name="select_room_directory">Izvēlies istabu katalogu</string> <string name="directory_server_fail_to_retrieve_server">Serveris, iespējams, ir nepieejams vai pārslogots</string> <string name="directory_server_type_homeserver">Ievadi pamatserveri no kura pieprasīt tā publisko istabu sarakstu</string> <string name="directory_server_placeholder">Pamatservera URL</string> <string name="directory_server_all_rooms_on_server">Visas istabas %s serverī</string> <string name="directory_server_native_rooms">Visas vietējās %s istabas</string> - <plurals name="notification_unread_notified_messages"> <item quantity="zero">%d nelasīta paziņota ziņa</item> <item quantity="one">%d nelasītas paziņotas ziņas</item> @@ -733,9 +631,7 @@ Nepazīstamas ierīces:</string> <item quantity="other">%d istabu</item> </plurals> <string name="notification_unread_notified_messages_in_room">%1$s iekš %2$s</string> - <string name="historical_placeholder">Meklēt vēsturē</string> - <string name="font_size">Šrifta izmērs</string> <string name="tiny">Sīks</string> <string name="small">Mazs</string> @@ -744,7 +640,6 @@ Nepazīstamas ierīces:</string> <string name="larger">Lielāks</string> <string name="largest">Lielākais</string> <string name="huge">Milzīgs</string> - <string name="widget_no_power_to_manage">Tev nepietiek atļauju, lai rīkotos ar vidžetiem šajā istabā</string> <string name="widget_creation_failure">Vidžeta izveide neizdevās</string> <string name="settings_labs_create_conference_with_jitsi">Konferences zvaniem izmantot Jitsi</string> @@ -754,7 +649,6 @@ Nepazīstamas ierīces:</string> <item quantity="one">%d aktīvi vidžeti</item> <item quantity="other">%d aktīvu vidžetu</item> </plurals> - <!-- Widget Integration Manager --> <string name="widget_integration_unable_to_create">Neizdevās uzstādīt vidžetu.</string> <string name="widget_integration_failed_to_send_request">Neizdevās nosūtīt pieprasījumu.</string> @@ -764,30 +658,24 @@ Nepazīstamas ierīces:</string> <string name="widget_integration_missing_room_id">Pieprasījumā iztrūkst room_id.</string> <string name="widget_integration_missing_user_id">Pieprasījumā iztrūkst user_id.</string> <string name="widget_integration_room_not_visible">Istaba %s ir neredzama.</string> - <string name="room_add_matrix_apps">Pievienot Matrix programmiņu</string> + <string name="room_add_matrix_apps">Pievienot Matrix lietotnes</string> <string name="settings_labs_native_camera">Lietot iebūvēto kameru</string> - <!-- share keys --> <string name="you_added_a_new_device">Tu pievienoji jaunu ierīci \'%s\', kura pieprasa šifrēšanas atslēgas.</string> <string name="your_unverified_device_requesting">Tava neverificētā ierīce \'%s\' pieprasa šifrēšanas atslēgas.</string> <string name="start_verification">Sākt tās verifikāciju</string> <string name="share_without_verifying">Dalīties bez verificēšanas</string> <string name="ignore_request">Ignorēt pieprasījumu</string> - <!-- conference call --> <string name="conference_call_warning_title">Uzmanību!</string> <string name="conference_call_warning_message">Konferences zvans ir izstrādes stadijā un var nebūt uzticams.</string> - <!-- slash commands --> <string name="command_error">Komandas kļūda</string> <string name="unrecognized_command">Nepazīstama komanda: %s</string> - <!-- notification statuses --> <string name="notification_off">Izslēgts</string> <string name="notification_noisy">Ar skaņu</string> - <string name="encrypted_message">Šifrēta ziņa</string> - <!-- groups creation --> <string name="create">Izveidot</string> <string name="create_community">Izveidot kopienu</string> @@ -795,29 +683,343 @@ Nepazīstamas ierīces:</string> <string name="community_name_hint">Piemērs</string> <string name="community_id">Kopienas Id</string> <string name="community_id_hint">piemērs</string> - <!-- group details --> <string name="group_details_home">Sākums</string> <string name="group_details_people">Cilvēki</string> <string name="group_details_rooms">Istabas</string> <string name="no_users_placeholder">Nav lietotāju</string> - <string name="rooms">Istabas</string> <string name="joined">Pievienojās</string> <string name="invited">Uzaicināts</string> <string name="filter_group_members">Grupas biedru filtrs</string> <string name="filter_group_rooms">Grupas istabu filtrs</string> - - <string name="group_no_long_description">Kopienas admins nav izveidojis šīs kopienas garo aprakstu.</string> - - <string name="has_been_kicked">Tevi no %1$s padzina %2$s</string> - <string name="has_been_banned">Tev pieeju %1$s liedza (nobanoja) %2$s</string> + <string name="group_no_long_description">Kopienas administrators nav izveidojis šīs kopienas garo aprakstu.</string> + <string name="has_been_kicked">%2$s padzina jūs no %1$s</string> + <string name="has_been_banned">%2$s liedza jums pieeju %1$s</string> <string name="reason_colon">Iemesls: %1$s</string> <string name="rejoin">Pievienoties par jaunu</string> <string name="forget_room">Aizmirst istabu</string> - <string name="receipt_avatar">Saņemt avataru</string> <string name="notice_avatar">Ievērot avataru</string> <string name="avatar">Avatars</string> - -</resources> + <string name="contacts_book_title">Kontaktu grāmata</string> + <string name="room_settings_topic_hint">Temats</string> + <string name="room_settings_name_hint">Istabas nosaukums</string> + <string name="save_your_security_key_title">Saglabājiet savu drošības atslēgu</string> + <string name="set_a_security_phrase_hint">Slepenā frāze</string> + <string name="set_a_security_phrase_title">Iestatiet slepeno frāzi</string> + <string name="bottom_sheet_save_your_recovery_key_title">Saglabājiet savu drošības atslēgu</string> + <string name="bottom_sheet_setup_secure_backup_subtitle">Nodrošinieties pret piekļuves zaudēšanu šifrētām ziņām un datiem, dublējot šifrēšanas atslēgas savā serverī.</string> + <string name="settings_setup_secure_backup">Iestatīt drošu rezerves dublēšanu</string> + <string name="identity_server_set_alternative_submit">Iesniegt</string> + <string name="invite_friends">Uzaicināt draugus</string> + <string name="invite_users_to_room_title">Uzaicināt lietotājus</string> + <string name="confirm_your_identity">Apstipriniet savu identitāti, verificējot šo pierakstīšanos no kādas citas savas sesijas, tādējādi ļaujot piekļūt šifrētajām ziņām.</string> + <string name="cross_signing_verify_by_text">Manuāli verificēt ar tekstu</string> + <string name="verify_other_sessions">Verificējiet visas savas sesijas, lai nodrošinātos, ka jūsu konts un ziņas ir drošībā</string> + <string name="review_logins">Pārskatiet savas pierakstīšanās</string> + <string name="unencrypted">Nešifrēts</string> + <string name="or_other_mx_capabale_client">vai kāda cita Matrix lietotne ar cross-signing atbalstu</string> + <string name="auth_invalid_login_deactivated_account">Šis konts ir deaktivizēts.</string> + <string name="settings_messages_in_e2e_group_chat">Šifrētas ziņas grupas čatos</string> + <string name="settings_messages_in_e2e_one_to_one">Šifrētas ziņas viens-pret-vienu čatos</string> + <string name="settings_messages_at_room">Ziņas, kuras satur @room</string> + <string name="encryption_not_enabled">Šifrēšana nav iespējota</string> + <string name="direct_room_encryption_enabled_tile_description">Ziņas šajā istabā ir nodrošinātas ar pilnīgu šifrēšanu.</string> + <string name="encryption_enabled">Šifrēšana iespējota</string> + <string name="new_session">Jauna pierakstīšanās. Vai tas bijāt jūs\?</string> + <string name="delete_event_dialog_content">Vai tiešām vēlies dzēst šo notikumu\? Ņem vērā, ka istabas nosaukuma vai tēmas nosaukuma maiņa var ietekmēt (atsaukt) izmaiņas.</string> + <string name="delete_event_dialog_title">Apstipriniet dzēšanu</string> + <string name="qr_code_scanned_by_other_no">Nē</string> + <string name="qr_code_scanned_by_other_yes">Jā</string> + <string name="a11y_qr_code_for_verification">QA kods</string> + <string name="not_trusted">Neuzticama</string> + <string name="trusted">Uzticama</string> + <string name="room_member_profile_sessions_section_title">Sesijas</string> + <string name="room_member_profile_failed_to_get_devices">Neizdevās iegūt sesijas</string> + <string name="verification_profile_warning">Brīdinājums</string> + <string name="verification_profile_verified">Pārbaudīta</string> + <string name="verification_profile_verify">Apstiprināt</string> + <string name="crosssigning_verify_this_session">Verificējiet šo sesiju</string> + <string name="settings_hs_admin_e2e_disabled">Jūsu servera administrators privātajās telpās un tiešajās ziņās pēc noklusējuma ir atspējojis pilnīgu šifrēšanu.</string> + <string name="verification_conclusion_ok_self_notice">Jaunā sesija ir verificēta un ir dota piekļuve jūsu šifrētajām ziņām, kā arī citi lietotāji redzēs, ka šī sesija ir uzticama.</string> + <string name="verification_conclusion_ok_notice">Saziņa ar šo lietotāju ir pilnībā šifrēta un trešās puses to nevar nolasīt.</string> + <string name="room_settings_enable_encryption_dialog_title">Iespējot šifrēšanu\?</string> + <string name="room_settings_enable_encryption_warning">Šifrēšana nevar tikt atspējota, ja reiz tikusi iespējota.</string> + <string name="room_settings_enable_encryption">Iespējot pilnīgu šifrēšanu…</string> + <string name="unignore">Pārstāt ignorēt</string> + <string name="room_member_jump_to_read_receipt">Pāriet uz pēdējo skatīto ziņu</string> + <string name="room_member_open_or_create_dm">Uzsākt dialogu</string> + <string name="room_member_power_level_users">Lietotāji</string> + <string name="room_member_power_level_invites">Uzaicinājumi</string> + <string name="room_profile_leaving_room">Pamet istabu…</string> + <string name="direct_room_profile_section_more_leave">Pamest</string> + <string name="room_profile_section_more_leave">Pamest istabu</string> + <string name="room_profile_section_more_uploads">Augšupielādes</string> + <string name="room_profile_section_more_notifications">Paziņojumi</string> + <string name="direct_room_profile_section_more_settings">Iestatījumi</string> + <string name="room_profile_section_more_settings">Istabas iestatījumi</string> + <string name="room_profile_section_admin">Administratora darības</string> + <string name="room_profile_section_more">Vairāk</string> + <string name="room_profile_section_security_learn_more">Uzzināt vairāk</string> + <string name="room_profile_section_security">Drošība</string> + <string name="direct_room_profile_encrypted_subtitle">Ziņas šeit ir nodrošinātas ar pilnīgu šifrēšanu. +\n +\nJūsu ziņas tiek nodrošināti ar slēdzenēm, un tikai jums un saņēmējam ir unikālas atslēgas, lai tās atbloķētu.</string> + <string name="room_profile_encrypted_subtitle">Ziņas šeit ir nodrošinātas ar pilnīgu šifrēšanu. +\n +\nJūsu ziņas tiek nodrošināti ar slēdzenēm, un tikai jums un saņēmējam ir unikālas atslēgas, lai tās atbloķētu.</string> + <string name="direct_room_profile_not_encrypted_subtitle">Ziņas šeit nav nodrošinātas ar pilnīgu šifrēšanu.</string> + <string name="room_profile_not_encrypted_subtitle">Ziņām šajā istabā netiek piemērota pilnīga šifrēšana.</string> + <string name="verification_request_you_accepted">Jūs akceptējāt</string> + <string name="sent_a_file">Fails</string> + <string name="sent_an_audio_file">Audio</string> + <string name="sent_an_image">Attēls.</string> + <string name="sent_a_video">Video.</string> + <string name="create_room_alias_empty">Lūdzu, ievadiet istabas adresi</string> + <string name="create_room_alias_already_in_use">Šī adrese jau tiek izmantota</string> + <string name="create_room_alias_hint">Istabas adrese</string> + <string name="create_room_disable_federation_description">Jūs varat iespējot šo situācijā, kad istaba paredzēta izmantošanai tikai saziņai starp jūsu bāzes serverī esošajām komandām. Tas nav maināms vēlāk.</string> + <string name="hide_advanced">Slēpt papildu iestatījumus</string> + <string name="show_advanced">Rādīt papildu iestatījumus</string> + <string name="create_room_encryption_description">Šifrēšana nevar tikt atspējota, ja reiz tikusi iespējota.</string> + <string name="settings">Iestatījumi</string> + <string name="soft_logout_clear_data_title">Dzēst personas datus</string> + <string name="soft_logout_signin_password_hint">Parole</string> + <string name="soft_logout_signin_submit">Pierakstīties</string> + <string name="soft_logout_signin_title">Pierakstīties</string> + <string name="login_signin_matrix_id_hint">Matrix Id</string> + <string name="login_signup_cancel_confirmation_title">Brīdinājums</string> + <string name="login_signup_error_user_in_use">Šāds lietotājvārds jau ir aizņemts</string> + <string name="login_signup_submit">Tālāk</string> + <string name="login_signup_password_hint">Parole</string> + <string name="login_signup_username_hint">Lietotājvārds</string> + <string name="login_signin_username_hint">Lietotājvārds vai epasts</string> + <string name="login_signup_to">Reģistrēties uz %1$s</string> + <string name="login_msisdn_error_other">Tālruņa numurs šķietami ir nepareizs. Lūdzu pārbaudiet to</string> + <string name="login_msisdn_error_not_international">Starptautiskajiem tālruņu numuriem jāsākas ar “+”</string> + <string name="login_msisdn_notice">Lūdzu, izmantojiet starptautisko formātu (tālruņa numuram jāsākas ar “+”)</string> + <string name="login_msisdn_confirm_submit">Tālāk</string> + <string name="login_msisdn_confirm_send_again">Nosūtīt atkārtoti</string> + <string name="login_msisdn_confirm_hint">Ievadīt kodu</string> + <string name="login_msisdn_confirm_notice">Mēs tikko nosūtījām kodu uz %1$s. Ievadiet to zemāk, lai apstiprinātu, ka tas esat jūs.</string> + <string name="login_msisdn_confirm_title">Apstipriniet tālruņa numuru</string> + <string name="login_set_msisdn_submit">Tālāk</string> + <string name="login_set_msisdn_optional_hint">Tālruņa numurs (izvēles)</string> + <string name="login_set_msisdn_mandatory_hint">Tālruņa numurs</string> + <string name="login_set_email_mandatory_hint">Epasts</string> + <string name="login_reset_password_cancel_confirmation_title">Brīdinājums</string> + <string name="login_reset_password_success_submit">Atgriezties uz pierakstīšanos</string> + <string name="login_reset_password_success_notice_2">Jūs esat izrakstījies no visām sesijām un vairs nesaņemsit push paziņojumus. Lai atkārtoti iespējotu paziņojumus, vēlreiz pierakstieties katrā ierīcē.</string> + <string name="login_reset_password_success_notice">Jūsu parole ir atiestatīta.</string> + <string name="login_reset_password_mail_confirmation_submit">Esmu verificējis(usi) savu epasta adresi</string> + <string name="login_reset_password_warning_submit">Turpināt</string> + <string name="login_reset_password_warning_content">Mainot paroli, tiks atiestatītas visas pilnīgas šifrēšanas atslēgas visās jūsu sesijās, padarot šifrēto tērzēšanas vēsturi neizlasāmu. Pirms paroles atiestatīšanas iestatiet atslēgu dublēšanu vai eksportējiet istabas atslēgas no citas sesijas.</string> + <string name="login_reset_password_warning_title">Uzmanību!</string> + <string name="login_reset_password_password_hint">Jauna parole</string> + <string name="login_reset_password_email_hint">Epasts</string> + <string name="login_reset_password_submit">Tālāk</string> + <string name="login_reset_password_notice">Apstiprinājuma vēstule tiks nosūtīta uz tavu epasta adresi, lai apstiprinātu paroles nomaiņu.</string> + <string name="login_signin">Pierakstīties</string> + <string name="login_signup">Reģistrēties</string> + <string name="login_continue">Turpināt</string> + <string name="login_server_other_title">Citi</string> + <string name="room_list_quick_actions_settings">Iestatījumi</string> + <string name="room_list_quick_actions_notifications_mute">Izslēgt skaņu</string> + <string name="room_list_quick_actions_notifications_mentions">Tikai pieminējumi</string> + <string name="room_list_quick_actions_notifications_all">Visas ziņas</string> + <string name="room_list_quick_actions_notifications_all_noisy">Visas ziņas (ar skaņu)</string> + <string name="report_content_custom_hint">Iemesls ziņojumam par šo saturu</string> + <string name="uploads_files_title">FAILI</string> + <string name="attachment_type_camera">Kamera</string> + <string name="settings_text_message_sent_hint">Kods</string> + <string name="direct_room_user_list_known_title">Zināmie lietotāji</string> + <string name="user_directory_search_hint">Meklēt pēc nosaukuma vai ID</string> + <string name="send_file_step_idle">Gaida…</string> + <string name="bottom_action_people_x">Tiešās ziņas</string> + <string name="send_suggestion_failed">Ieteikumu neizdevās nosūtīt (%s)</string> + <string name="send_suggestion_sent">Paldies, ieteikums ir veiksmīgi nosūtīts</string> + <string name="send_suggestion_report_placeholder">Aprakstiet savu ieteikumu šeit</string> + <string name="send_suggestion_content">Lūdzu, rakstiet savu ieteikumu zemāk.</string> + <string name="send_suggestion">Ieteikumi</string> + <string name="preference_root_help_about">Palīdzība un par lietotni</string> + <string name="settings_security_and_privacy">Drošība un konfidencialitāte</string> + <string name="settings_general_title">Vispārīgi</string> + <string name="quick_reactions">Ātra reaģēšana</string> + <string name="create_room_directory_title">Istabu katalogs</string> + <string name="create_room_public_title">Publiska</string> + <string name="create_room_settings_section">Istabas iestatījumi</string> + <string name="create_room_topic_hint">Temats</string> + <string name="create_room_topic_section">Istabas temats (izvēles)</string> + <string name="create_room_name_hint">Nosaukums</string> + <string name="create_room_name_section">Istabas nosaukums</string> + <string name="create_room_action_create">IZVEIDOT</string> + <string name="create_room_title">Jauna istaba</string> + <string name="fab_menu_create_chat">Tiešās ziņas</string> + <string name="fab_menu_create_room">Istabas</string> + <string name="room_preview_no_preview">Šo istabu nevar priekšskatīt</string> + <string name="group_all_communities">Visas kopienas</string> + <string name="event_redacted">Ziņa ir dzēsta</string> + <string name="reactions">Reaģēšana</string> + <string name="title_activity_emoji_reaction_picker">Reaģēšana</string> + <string name="room_list_rooms_empty_title">Istabas</string> + <string name="room_list_people_empty_title">Sarunas</string> + <string name="reply">Atbildēt</string> + <string name="edit">Rediģēt</string> + <string name="sas_got_it">Sapratu</string> + <string name="sas_verified_successful_description">Saziņa ar šo lietotāju ir nodrošināta ar pilnīgu šifrēšanu un nav nolasāma trešajām pusēm.</string> + <string name="sas_verified">Verificēts!</string> + <string name="keys_backup_info_title_algorithm">Algoritms</string> + <string name="keys_backup_info_title_version">Versija</string> + <string name="secure_backup_banner_setup_line1">Droša reze</string> + <string name="keys_backup_setup_skip_title">Vai tiešām to vēlaties\?</string> + <string name="keys_backup_setup_step3_share_recovery_file">Dalīties</string> + <string name="keys_backup_setup_step3_button_title">Gatavs</string> + <string name="merged_events_collapse">sakļaut</string> + <string name="merged_events_expand">izvērst</string> + <string name="deactivate_account_submit">Deaktivizēt kontu</string> + <string name="deactivate_account_prompt_password">Lai turpinātu, lūdzu, ievadiet savu paroli:</string> + <string name="deactivate_account_title">Deaktivizēt kontu</string> + <string name="command_description_nick">Nomaina jūsu parādāmo vārdu</string> + <string name="command_description_kick_user">Padzen lietotāju ar norādīto id</string> + <string name="command_description_part_room">Atstāt istabu</string> + <string name="command_description_invite_user">Uzaicina lietotāju ar norādīto id uz pašreizējo istabu</string> + <string name="command_description_deop_user">Atceļ operatora statusu lietotājam ar norādīto Id</string> + <string name="command_description_op_user">Definē lietotāja statusu</string> + <string name="command_description_ban_user">Liedz pieeju lietotājam ar norādīto id</string> + <string name="command_description_emote">Parāda darbību</string> + <string name="ignore_request_short_label">Nerādīt nevienu šī dalībnieka ziņu</string> + <string name="share_without_verifying_short_label">Dalīties</string> + <string name="start_verification_short_label">Apstiprināt</string> + <string name="room_widget_permission_display_name">Jūsu parādāmais vārds</string> + <string name="notification_unknown_room_name">Istaba</string> + <string name="device_name_warning">Sesijas publiskais nosaukums ir redzams cilvēkiem, ar kuriem jūs komunicējat</string> + <string name="encryption_information_device_name_with_warning">Publiskais nosaukums (redzams cilvēkiem, ar kuriem jūs komunicējat)</string> + <string name="room_alias_local_address_empty">Šai istabai nav lokālās adreses</string> + <string name="room_alias_local_address_title">Lokālās adreses</string> + <string name="room_alias_address_hint">Jauna publiska adrese (piemēram, #alias:server)</string> + <string name="room_alias_published_other">Citas publiskotās adreses:</string> + <string name="room_alias_published_alias_subtitle">Publiskotas adreses ikviens var izmantot jebkurā serverī, lai pievienotos jūsu istabai. Lai varētu publiskot adresi, tai vispirms jābūt iestatītai kā lokālajai adresei.</string> + <string name="room_alias_published_alias_title">Publiskotās adreses</string> + <string name="room_alias_title">Istabas adreses</string> + <string name="room_settings_room_read_history_dialog_subtitle">Izmaiņas attiecībā uz to, kas var lasīt vēsturi, attieksies tikai uz nākamajiem ziņojumiem šajā telpā. Esošās vēstures redzamība nemainīsies.</string> + <string name="settings_password">Parole</string> + <string name="disabled_integration_dialog_title">Integrācijas ir atspējotas</string> + <string name="settings_opt_in_of_analytics">Sūtīt analītikas datus</string> + <string name="settings_deactivate_account_section">Deaktivizēt kontu</string> + <string name="settings_secure_backup_section_info">Nodrošinieties pret piekļuves zaudēšanu šifrētām ziņām un datiem, dublējot šifrēšanas atslēgas savā serverī.</string> + <string name="settings_secure_backup_setup">Iestatīt drošu rezerves dublēšanu</string> + <string name="settings_secure_backup_section_title">Droša reze</string> + <string name="settings_send_typing_notifs">Sūtīt paziņojumus par rakstīšanu</string> + <string name="settings_notification_privacy_normal">Normāls</string> + <string name="settings_troubleshoot_test_service_boot_title">Startēt pie ierīces ielādes</string> + <string name="settings_troubleshoot_test_device_settings_quickfix">Iespējot</string> + <string name="settings_troubleshoot_test_account_settings_quickfix">Iespējot</string> + <string name="settings_remove_three_pid_confirmation_content">Dzēst %s\?</string> + <string name="settings_phone_numbers">Tālruņa numuri</string> + <string name="settings_emails_empty">Jūsu kontam nav pievienota neviena epasta adrese</string> + <string name="settings_emails">Epasta adreses</string> + <string name="settings_add_3pid_authentication_needed">Nepieciešama autentifikācija</string> + <string name="settings_add_3pid_flow_not_supported">Jūs nevarat to izdarīt ar Element mobile</string> + <string name="settings_add_3pid_confirm_password_title">Apstipriniet paroli</string> + <string name="settings_phone_number_empty">Jūsu kontam nav pievienots neviens tālruņa numurs</string> + <string name="room_sliding_menu_version_x">Versija %s</string> + <string name="search_is_not_supported_in_e2e_room">Meklēšana šifrētās istabās pagaidām netiek atbalstīta.</string> + <string name="room_permissions_change_topic">Mainīt tematu</string> + <string name="room_permissions_change_permissions">Mainīt atļaujas</string> + <string name="room_permissions_change_room_name">Nomainīt istabas nosaukumu</string> + <string name="room_permissions_change_history_visibility">Mainīt vēstures redzamību</string> + <string name="room_permissions_enable_room_encryption">Iespējot istabas šifrēšanu</string> + <string name="room_permissions_change_main_address_for_the_room">Mainīt istabas galveno adresi</string> + <string name="room_permissions_change_room_avatar">Mainīt istabas avataru</string> + <string name="room_permissions_notify_everyone">Apziņot visus</string> + <string name="room_permissions_remove_messages_sent_by_others">Dzēst citu sūtītas ziņas</string> + <string name="room_permissions_ban_users">Pieejas liegumi lietotājiem</string> + <string name="room_permissions_kick_users">Padzīt lietotājus</string> + <string name="room_permissions_change_settings">Mainīt iestatījumus</string> + <string name="room_permissions_invite_users">Uzaicināt lietotājus</string> + <string name="room_permissions_send_messages">Sūtīt ziņas</string> + <string name="room_permissions_default_role">Noklusējuma loma</string> + <string name="room_permissions_notice">Izvēlēties lomas, kuras nepieciešamas, lai mainītu atsevišķas istabas daļas</string> + <string name="room_permissions_title">Atļaujas</string> + <string name="room_settings_permissions_title">Istabas atļaujas</string> + <string name="room_message_placeholder_reply_to_not_encrypted">Sūtīt atbildi (nešifrētā veidā)…</string> + <string name="room_message_placeholder_reply_to_encrypted">Sūtīt šifrētu atbildi…</string> + <string name="room_participants_invite_join_names_combined">%1$s %2$s</string> + <string name="room_participants_invite_join_names_and">%1$s un %2$s</string> + <string name="room_participants_invite_join_names">"%1$s, "</string> + <string name="reason_hint">Iemesls</string> + <string name="room_participants_action_cancel_invite_title">Atcelt uzaicinājumu</string> + <string name="room_participants_action_unignore_title">Atcelt lietotāja ignorēšanu</string> + <string name="room_participants_action_ignore_title">Ignorēt lietotāju</string> + <string name="room_participants_power_level_demote_warning_prompt">Jūs nevarēsiet atcelt šīs izmaiņas pēc sava statusa pazemināšanas. Gadījumā, ja esat pēdējais priviliģētais lietotājs istabā, būs neiespējami atgūt šīs privilēģijas.</string> + <string name="room_participants_action_kick">Padzīt</string> + <string name="room_participants_action_cancel_invite">Atcelt uzaicinājumu</string> + <string name="room_participants_ago">%1$s %2$s iepriekš</string> + <string name="room_participants_now">%1$s pašlaik</string> + <string name="room_participants_leave_private_warning">Šī istaba nav publiska un jūs nevarēsiet atkārtoti pievienoties bez uzaicinājuma.</string> + <string name="invite_no_identity_server_error">Lai veiktu šo darbību, iestatījumos pievienojiet identitātes serveri.</string> + <string name="permissions_denied_add_contact">Atļaujiet piekļuvi savām kontaktpersonām.</string> + <string name="permissions_denied_qr_code">Lai skenētu QR kodu, jums jāatļauj piekļuve kamerai.</string> + <string name="return_to_call">Atgriezties pie zvana</string> + <string name="active_call_with_duration">Aktīvs zvans (%s)</string> + <string name="video_call_in_progress">Notiek video zvans…</string> + <string name="e2e_re_request_encryption_key_sent">Atslēgas pieprasījums nosūtīts.</string> + <string name="auth_login_sso">Pierakstieties, izmantojot vienoto pierakstīšanos</string> + <string name="call_camera_back">Atpakaļ</string> + <string name="sound_device_phone">Tālrunis</string> + <string name="call_failed_no_ice_title">Zvans neizdevās nekorekti nokonfigurēta servera dēļ</string> + <string name="option_send_voice">Sūtīt balsi</string> + <string name="send_bug_report_description_in_english">Ja iespējams, lūdzu, rakstiet aprakstu angļu valodā.</string> + <string name="no_more_results">Vairāk nekādu rezultātu nav</string> + <string name="bottom_action_notification">Paziņojumi</string> + <string name="dialog_title_success">Izdevās</string> + <string name="dialog_title_error">Kļūda</string> + <string name="action_add">Pievienot</string> + <string name="action_copy">Kopēt</string> + <string name="action_mark_room_read">Atzīmēt kā izlasītu</string> + <string name="action_sign_out_confirmation_simple">Vai tiešām vēlies izrakstīties\?</string> + <string name="call_notification_reject">Noraidīt</string> + <string name="call_notification_answer">Pieņemt</string> + <string name="decline">Noraidīt</string> + <string name="review">Pārlūkot</string> + <string name="ignore">Nerādīt nevienu šī dalībnieka ziņu</string> + <string name="abort">Pārtraukt</string> + <string name="done">Gatavs</string> + <string name="skip">Izlaist</string> + <string name="accept">Pieņemt</string> + <string name="no_permissions_to_start_conf_call">Šajā istabā nav atļaujas sākt konferences zvanu</string> + <string name="reset">Atiestatīt</string> + <string name="dismiss">Aizvērt</string> + <string name="pause_video">Pauzēt</string> + <string name="play_video">Atskaņot</string> + <string name="disconnect">Atvienoties</string> + <string name="revoke">Atsaukt</string> + <string name="none">Nav</string> + <string name="clear">Notīrīt</string> + <string name="speak">Runāt</string> + <string name="download">Lejupielādēt</string> + <string name="stay">Palikt</string> + <string name="are_you_sure">Vai tiešām to vēlaties\?</string> + <string name="title_activity_verify_device">Verificēt ierīci</string> + <string name="status_theme">Status.im tēma</string> + <string name="system_theme">Sistēmas noklusējuma</string> + <string name="room_member_power_level_custom_in">Pielāgots (%1$d) iekš %2$s</string> + <string name="room_member_power_level_default_in">Noklusējums iekš %1$s</string> + <string name="room_member_power_level_moderator_in">Moderators iekš %1$s</string> + <string name="room_member_power_level_admin_in">Administrators iekš %1$s</string> + <string name="room_member_power_level_custom">Pielāgots</string> + <string name="room_member_power_level_moderators">Moderators</string> + <string name="room_member_power_level_admins">Administrators</string> + <string name="encryption_information_unknown_ip">nepazīstama ip</string> + <plurals name="room_settings_banned_users_count"> + <item quantity="zero">%d lietotājs, kuram liegta pieeja</item> + <item quantity="one">%d lietotāji, kuriem liegta pieeja</item> + <item quantity="other">%d lietotāji, kuriem liegta pieeja</item> + </plurals> + <plurals name="seconds"> + <item quantity="zero">%d sekunde</item> + <item quantity="one">%d sekundes</item> + <item quantity="other">%d sekundes</item> + </plurals> +</resources> \ No newline at end of file diff --git a/vector/src/main/res/values-nl/strings.xml b/vector/src/main/res/values-nl/strings.xml index 7e946511f4..d15f7870a4 100644 --- a/vector/src/main/res/values-nl/strings.xml +++ b/vector/src/main/res/values-nl/strings.xml @@ -1159,7 +1159,7 @@ <string name="autodiscover_well_known_autofill_dialog_message">Element heeft een aangepaste serverconfiguratie gedetecteerd voor uw gebruikers-ID-domein ‘%1$s’: \n%2$s</string> <string name="autodiscover_well_known_autofill_confirm">Configuratie gebruiken</string> - <string name="error_jitsi_not_supported_on_old_device">Sorry, vergadergesprekken met Jitsi worden nog niet ondersteund op oudere apparaten (met een Android-versie lager dan 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Sorry, vergadergesprekken met Jitsi worden nog niet ondersteund op oudere apparaten (met een Android-versie lager dan 6.0)</string> <string name="title_activity_verify_device">Sessie verifiëren</string> <string name="encryption_information_unknown_ip">onbekend IP-adres</string> <string name="you_added_a_new_device_with_info">Een nieuwe sessie vraagt versleutelingssleutels aan. diff --git a/vector/src/main/res/values-pl/strings.xml b/vector/src/main/res/values-pl/strings.xml index 820add2ce2..c336c9882c 100644 --- a/vector/src/main/res/values-pl/strings.xml +++ b/vector/src/main/res/values-pl/strings.xml @@ -1241,7 +1241,7 @@ Spróbuj uruchomić ponownie aplikację.</string> <string name="room_widget_permission_avatar_url">Adres URL awatara</string> <string name="room_widget_permission_user_id">Twój ID użytkownika</string> <string name="room_widget_permission_widget_id">ID Widżetu</string> - <string name="error_jitsi_not_supported_on_old_device">Przepraszamy, połączenia konferencyjne za pomocą Jitsi nie są wspierane na starszych urządzeniach (urządzenia z systemem Android poniżej wersji 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Przepraszamy, połączenia konferencyjne za pomocą Jitsi nie są wspierane na starszych urządzeniach (urządzenia z systemem Android poniżej wersji 6.0)</string> <string name="room_widget_resource_permission_title">Widżet chce użyć następujących zasobów:</string> <string name="room_widget_resource_decline_permission">Zablokuj wszystko</string> <string name="room_widget_webview_access_camera">Użyj aparatu</string> diff --git a/vector/src/main/res/values-pt-rBR/strings.xml b/vector/src/main/res/values-pt-rBR/strings.xml index cdb52f26de..cb34acd429 100644 --- a/vector/src/main/res/values-pt-rBR/strings.xml +++ b/vector/src/main/res/values-pt-rBR/strings.xml @@ -1176,7 +1176,7 @@ <string name="room_widget_permission_theme">Seu tema</string> <string name="room_widget_permission_widget_id">ID do widget</string> <string name="room_widget_permission_room_id">ID da sala</string> - <string name="error_jitsi_not_supported_on_old_device">Desculpe, as chamadas em grupo com o Jitsi não são suportadas em aparelhos antigos (com versões do Android anteriores a 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Desculpe, as chamadas em grupo com o Jitsi não são suportadas em aparelhos antigos (com versões do Android anteriores a 6.0)</string> <string name="room_widget_resource_permission_title">Este wigdet deseja utilizar os seguintes recursos:</string> <string name="room_widget_resource_grant_permission">Permitir</string> <string name="room_widget_resource_decline_permission">Bloquear Tudo</string> @@ -2376,4 +2376,8 @@ <string name="room_settings_permissions_title">Permissões da sala</string> <string name="room_participants_leave_private_warning">Esta sala não é pública. Você não poderá entrar novamente sem um convite.</string> <string name="system_theme">Padrão do sistema</string> + <string name="authentication_error">Falha ao se autenticar</string> + <string name="re_authentication_default_confirm_text">O Element precisa que você insira suas credenciais para executar esta ação.</string> + <string name="re_authentication_activity_title">Necessário autenticar-se novamente</string> + <string name="error_unauthorized">Não autorizado, sem credenciais de autenticação válidas</string> </resources> \ No newline at end of file diff --git a/vector/src/main/res/values-ru/strings.xml b/vector/src/main/res/values-ru/strings.xml index 18b20d0e91..32039144d6 100644 --- a/vector/src/main/res/values-ru/strings.xml +++ b/vector/src/main/res/values-ru/strings.xml @@ -1193,7 +1193,7 @@ <string name="settings_play_shutter_sound">Воспроизвести звук затвора</string> <string name="encryption_information_unknown_ip">неизвестный IP</string> <string name="notification_inline_reply_failed">** Отправить не удалось - пожалуйста откройте комнату</string> - <string name="error_jitsi_not_supported_on_old_device">К сожалению, конференц-звонки с Jitsi не поддерживаются на старых устройствах (ниже Android OS - 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">К сожалению, конференц-звонки с Jitsi не поддерживаются на старых устройствах (ниже Android OS - 6.0)</string> <string name="you_added_a_new_device_with_info">Новая сессия запрашивает ключи шифрования. \nИмя сессии: %1$s \nПоследний раз в сети: %2$s @@ -2427,4 +2427,8 @@ <string name="room_settings_permissions_title">Права доступа комнаты</string> <string name="room_participants_leave_private_warning">Эта комната не публичная. Вы не сможете повторно присоединиться без приглашения.</string> <string name="system_theme">Системная тема</string> + <string name="authentication_error">Не удалось пройти аутентификацию</string> + <string name="re_authentication_default_confirm_text">Element требует от вас ввести свои учетные данные для выполнения этого действия.</string> + <string name="re_authentication_activity_title">Требуется повторная аутентификация</string> + <string name="failed_to_initialize_cross_signing">Не удалось настроить перекрестную подпись</string> </resources> \ No newline at end of file diff --git a/vector/src/main/res/values-sk/strings.xml b/vector/src/main/res/values-sk/strings.xml index 401500270d..1a4418e3a3 100644 --- a/vector/src/main/res/values-sk/strings.xml +++ b/vector/src/main/res/values-sk/strings.xml @@ -1396,7 +1396,7 @@ Na ďalšej obrazovke vás systém požiada o povolenie vždy bežať na pozadí <string name="room_widget_permission_room_id">ID miestnosti</string> - <string name="error_jitsi_not_supported_on_old_device">Prepáčte, konferenčné hovory Jitsi nie sú podporované na starších zariadeniach (starší systém než Android 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Prepáčte, konferenčné hovory Jitsi nie sú podporované na starších zariadeniach (starší systém než Android 6.0)</string> <string name="room_widget_resource_permission_title">Tento widget žiada o nasledujúce zdroje:</string> <string name="room_widget_resource_grant_permission">Povoliť</string> <string name="room_widget_resource_decline_permission">Zablokovať všetko</string> diff --git a/vector/src/main/res/values-sq/strings.xml b/vector/src/main/res/values-sq/strings.xml index 6df9939dde..9cc4d6ad7c 100644 --- a/vector/src/main/res/values-sq/strings.xml +++ b/vector/src/main/res/values-sq/strings.xml @@ -1094,7 +1094,7 @@ <string name="notification_new_invitation">Ftesë e Re</string> <string name="notification_sender_me">Unë</string> <string name="notification_inline_reply_failed">** S’u arrit të dërgohej - ju lutemi, hapni dhomë</string> - <string name="error_jitsi_not_supported_on_old_device">Na ndjeni, thirrjet konferencë me Jitsi-n nuk mbulohen në pajisje të vjetra (pajisje me Android OS nën 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Na ndjeni, thirrjet konferencë me Jitsi-n nuk mbulohen në pajisje të vjetra (pajisje me Android OS nën 6.0)</string> <string name="title_activity_verify_device">Verifiko sesion</string> <string name="encryption_information_unknown_ip">ip e panjohur</string> <string name="you_added_a_new_device_with_info">Një sesion i ri po kërkon emër kyçe fshehtëzimi. diff --git a/vector/src/main/res/values-sr/strings.xml b/vector/src/main/res/values-sr/strings.xml index 5fbeda97de..37a3213ce3 100644 --- a/vector/src/main/res/values-sr/strings.xml +++ b/vector/src/main/res/values-sr/strings.xml @@ -74,7 +74,7 @@ <string name="rooms_header">Собе</string> <string name="rooms_directory_header">Листа соба</string> <string name="no_room_placeholder">Нема соба</string> - <string name="groups_invite_header">Пошаљи позивницу</string> + <string name="groups_invite_header">Позивница</string> <string name="send_bug_report_placeholder">Опишите ваш проблем овде</string> <string name="read_receipt">Прочитај</string> <string name="join_room">Придружи се соби</string> @@ -119,4 +119,64 @@ <string name="revoke">Повуци</string> <string name="none">Ништа</string> <string name="backup">Резервна копија</string> + <string name="enter_secret_storage_passphrase_or_key">Употребите %1$s или %2$s да наставите.</string> + <string name="enter_secret_storage_passphrase_warning">Упозорење:</string> + <string name="rageshake_detected">Дрмусање детектовано!</string> + <string name="settings_rageshake_detection_threshold_summary">Протресите телефон да испробамо праг осетљивости</string> + <string name="settings_rageshake_detection_threshold">Праг осетљивости</string> + <string name="settings_rageshake">Бесно дрмусање</string> + <string name="start_video_call">Почни видео позив</string> + <string name="start_voice_call">Почни гласовни позив</string> + <string name="start_new_chat">Почни ново ћаскање</string> + <string name="search">Тражи</string> + <string name="identity_url">УРЛ сервера идентитета</string> + <string name="hs_url">УРЛ домаћег сервера</string> + <string name="send_files_in">Шаљи на</string> + <string name="send_bug_report_progress">Напредак (%s%%)</string> + <string name="send_bug_report_rage_shake">Дрмусање за пријаву грешке</string> + <string name="send_bug_report">Пријави грешку</string> + <string name="send_bug_report_include_screenshot">Шаљи снимак екрана</string> + <string name="send_bug_report_include_crash_logs">Слање записника грешке</string> + <string name="send_bug_report_include_logs">Слање записника</string> + <string name="no_group_placeholder">Нема група</string> + <string name="groups_header">Заједнице</string> + <plurals name="public_room_nb_users"> + <item quantity="one">%d корисник</item> + <item quantity="few">%d корисника</item> + <item quantity="other">%d корисника</item> + </plurals> + <string name="no_public_room_placeholder">Нема доступних јавних соба</string> + <string name="no_more_results">Нема више резултата</string> + <string name="no_contact_access_placeholder">Нисте доволили да Елемент приступи вашим контактима</string> + <string name="no_conversation_placeholder">Нема разговора</string> + <string name="system_alerts_header">Системска упозорења</string> + <string name="bottom_action_groups">Заједнице</string> + <string name="bottom_action_notification">Обавештења</string> + <string name="bottom_action_home">Почетна</string> + <string name="dialog_title_success">Успешно</string> + <string name="copied_to_clipboard">Копирано у клипборд</string> + <string name="action_unpublish">Укини објаву</string> + <string name="action_add">Додај</string> + <string name="action_copy">Копирај</string> + <string name="action_historical">Историја</string> + <string name="call_notification_hangup">Прекини</string> + <string name="call_notification_reject">Одбаци</string> + <string name="call_notification_answer">Прихвати</string> + <string name="offline">Ван везе</string> + <string name="invite">Позивница</string> + <string name="or">или</string> + <string name="send_anyway">Свеједно пошаљи</string> + <string name="call_anyway">Свеједно позови</string> + <string name="room_no_conference_call_in_encrypted_rooms">Групни позиви нису подржани у шифрованим собама</string> + <string name="failed_to_remove_widget">Не могу да уклоним виџет</string> + <string name="failed_to_add_widget">Не могу да додам виџет</string> + <string name="device_information">Подаци о сесији</string> + <string name="audio_meeting">Почни аудио састанак</string> + <string name="video_meeting">Почни видео састанак</string> + <string name="ongoing_conference_call_video">Видео</string> + <string name="ongoing_conference_call_voice">Глас</string> + <string name="start_chatting">Почни ћаскање</string> + <string name="reset">Ресет</string> + <string name="dismiss">Одбаци</string> + <string name="pause_video">Паузирај</string> </resources> \ No newline at end of file diff --git a/vector/src/main/res/values-sv/strings.xml b/vector/src/main/res/values-sv/strings.xml index e90e930de6..36d4ea69ad 100644 --- a/vector/src/main/res/values-sv/strings.xml +++ b/vector/src/main/res/values-sv/strings.xml @@ -1487,7 +1487,7 @@ <string name="room_widget_permission_user_id">Ditt användar-ID</string> <string name="room_widget_permission_widget_id">Widget-ID</string> <string name="room_widget_permission_room_id">Rums-ID</string> - <string name="error_jitsi_not_supported_on_old_device">Tyvärr, gruppsamtal med Jitsi stöds inte på gamla enheter (enheter med Android äldre än 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Tyvärr, gruppsamtal med Jitsi stöds inte på gamla enheter (enheter med Android äldre än 6.0)</string> <string name="room_widget_resource_permission_title">Den här widgeten vill använda följande resurser:</string> <string name="room_widget_resource_grant_permission">Tillåt</string> <string name="room_widget_resource_decline_permission">Blockera alla</string> @@ -2315,4 +2315,9 @@ <string name="room_settings_permissions_title">Rumsbehörigheter</string> <string name="room_participants_leave_private_warning">Det är rummet är inte offentligt. Du kommer inte kunna gå med igen utan en inbjudan.</string> <string name="system_theme">Systemets förval</string> + <string name="authentication_error">Misslyckades att autentisera</string> + <string name="re_authentication_default_confirm_text">Element kräver att du anger dina autentiseringsuppgifter innan du gör det här.</string> + <string name="re_authentication_activity_title">Omautentisering krävs</string> + <string name="failed_to_initialize_cross_signing">Mislyckades att ställa in korssignering</string> + <string name="error_unauthorized">Obehörig, saknar giltiga autentiseringsuppgifter</string> </resources> \ No newline at end of file diff --git a/vector/src/main/res/values-tr/strings.xml b/vector/src/main/res/values-tr/strings.xml index 4c4154d8f0..2f4c163d6d 100644 --- a/vector/src/main/res/values-tr/strings.xml +++ b/vector/src/main/res/values-tr/strings.xml @@ -1148,7 +1148,7 @@ <string name="room_widget_permission_theme">Teman</string> <string name="room_widget_permission_widget_id">Widget kimliği</string> <string name="room_widget_permission_room_id">Oda kimliği</string> - <string name="error_jitsi_not_supported_on_old_device">Üzgünüz, jitsi konferans aramaları eski cihazlarda desteklenmiyor (Android 5.0 altı)</string> + <string name="error_jitsi_not_supported_on_old_device">Üzgünüz, jitsi konferans aramaları eski cihazlarda desteklenmiyor (Android 6.0 altı)</string> <string name="room_widget_resource_permission_title">Bu widget şu kaynakları kullanmak istiyor:</string> <string name="room_widget_resource_grant_permission">İzin ver</string> <string name="room_widget_resource_decline_permission">Hepsini reddet</string> diff --git a/vector/src/main/res/values-uk/strings.xml b/vector/src/main/res/values-uk/strings.xml index 2bf3f8dddd..ac06e1ed0a 100644 --- a/vector/src/main/res/values-uk/strings.xml +++ b/vector/src/main/res/values-uk/strings.xml @@ -1218,7 +1218,7 @@ <string name="room_widget_resource_decline_permission">Заблокувати все</string> <string name="room_widget_resource_grant_permission">Дозволити</string> <string name="room_widget_resource_permission_title">Цей віджет хоче використовувати такі ресурси:</string> - <string name="error_jitsi_not_supported_on_old_device">На жаль, конференц-дзвінки з Jitsi не підтримуються на старих пристроях (пристрої з ОС Android нижче 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">На жаль, конференц-дзвінки з Jitsi не підтримуються на старих пристроях (пристрої з ОС Android нижче 6.0)</string> <string name="room_widget_permission_room_id">Ідентифікатор кімнати</string> <string name="room_widget_permission_widget_id">Ідентифікатор віджета</string> <string name="room_widget_permission_theme">Ваша тема</string> diff --git a/vector/src/main/res/values-zh-rCN/strings.xml b/vector/src/main/res/values-zh-rCN/strings.xml index 43eb44ad64..8bffa6e437 100644 --- a/vector/src/main/res/values-zh-rCN/strings.xml +++ b/vector/src/main/res/values-zh-rCN/strings.xml @@ -1085,7 +1085,7 @@ <string name="notification_new_invitation">新邀请</string> <string name="notification_sender_me">我</string> <string name="notification_inline_reply_failed">** 发送失败 - 请打开聊天室</string> - <string name="error_jitsi_not_supported_on_old_device">抱歉,旧设备(Android 系统版本低于 5.0)不支持使用 Jitsi 创建电话会议</string> + <string name="error_jitsi_not_supported_on_old_device">抱歉,旧设备(Android 系统版本低于 6.0)不支持使用 Jitsi 创建电话会议</string> <string name="title_activity_verify_device">验证会话</string> <string name="encryption_information_unknown_ip">未知 IP</string> <string name="you_added_a_new_device_with_info">一个新设备正在请求加密密钥。 diff --git a/vector/src/main/res/values-zh-rTW/strings.xml b/vector/src/main/res/values-zh-rTW/strings.xml index 88bc9da6d6..e4368652bf 100644 --- a/vector/src/main/res/values-zh-rTW/strings.xml +++ b/vector/src/main/res/values-zh-rTW/strings.xml @@ -1077,7 +1077,7 @@ <string name="notification_new_invitation">新邀請</string> <string name="notification_sender_me">我</string> <string name="notification_inline_reply_failed">** 傳送失敗 - 請開啟聊天室</string> - <string name="error_jitsi_not_supported_on_old_device">抱歉,使用 Jitsi 建立會議通話在舊裝置上並不支援(Android 系統版本小於 5.0 的裝置)</string> + <string name="error_jitsi_not_supported_on_old_device">抱歉,使用 Jitsi 建立會議通話在舊裝置上並不支援(Android 系統版本小於 6.0 的裝置)</string> <string name="title_activity_verify_device">驗證工作階段</string> <string name="encryption_information_unknown_ip">未知的 IP</string> <string name="you_added_a_new_device_with_info">有新工作階段正在要求加密金鑰。 @@ -2277,4 +2277,9 @@ <string name="system_theme">系統預設值</string> <string name="settings_show_emoji_keyboard_summary">在訊息編輯器上新增按鈕以開啟表情符號鍵盤</string> <string name="settings_show_emoji_keyboard">顯示表情符號鍵盤</string> + <string name="authentication_error">驗證失敗</string> + <string name="re_authentication_default_confirm_text">Element 需要您輸入您的憑證來執行此動作。</string> + <string name="re_authentication_activity_title">需要重新驗證</string> + <string name="failed_to_initialize_cross_signing">未能設定交叉簽章</string> + <string name="error_unauthorized">未授權,缺少有效的身份驗證憑證</string> </resources> \ No newline at end of file diff --git a/vector/src/main/res/values/colors_riotx.xml b/vector/src/main/res/values/colors_riotx.xml index 957d30398b..35d0c797b5 100644 --- a/vector/src/main/res/values/colors_riotx.xml +++ b/vector/src/main/res/values/colors_riotx.xml @@ -18,6 +18,7 @@ <color name="riotx_positive_accent_alpha12">#1E0DBD8B</color> <color name="riotx_button_disabled_alpha12">#1E61708B</color> + <color name="bg_call_screen">#99000000</color> <color name="riotx_notice">#FFFF4B55</color> <color name="riotx_notice_secondary">#FF61708B</color> @@ -47,6 +48,7 @@ 'riotx_<name in the palette snake case>_<theme>' --> + <attr name="riotx_background" format="color" /> <color name="riotx_background_light">#FFFFFFFF</color> <color name="riotx_background_dark">#FF15191E</color> @@ -244,9 +246,16 @@ <color name="riotx_reaction_background_on_dark">#4011BC8A</color> <color name="riotx_reaction_background_on_black">#4011BC8A</color> + <attr name="riotx_alerter_background" format="color" /> + <color name="riotx_alerter_background_light">#FFF3F8FD</color> + <color name="riotx_alerter_background_dark">#FF282C35</color> + <color name="riotx_alerter_background_black">#FF282C35</color> + <!-- (color from RiotWeb) --> <attr name="riotx_keys_backup_banner_accent_color" format="color" /> <color name="riotx_keys_backup_banner_accent_color_light">#FFF8E3</color> <color name="riotx_keys_backup_banner_accent_color_dark">#22262E</color> + + </resources> \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 2d54948116..cd4ac16ea4 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -136,6 +136,7 @@ <string name="action_unpublish">Unpublish</string> <string name="copied_to_clipboard">Copied to clipboard</string> <string name="disable">Disable</string> + <string name="action_return">Return</string> <!-- dialog titles --> <string name="dialog_title_confirmation">Confirmation</string> @@ -401,6 +402,10 @@ <string name="video_call_in_progress">Video Call In Progress…</string> <string name="active_call_with_duration">Active Call (%s)</string> <string name="return_to_call">Return to call</string> + <string name="call_resume_action">Resume</string> + <string name="call_hold_action">Hold</string> + <string name="call_held_by_user">%s held the call</string> + <string name="call_held_by_you">You held the call</string> <string name="call_error_user_not_responding">The remote side failed to pick up.</string> <string name="call_error_ice_failed">Media Connection Failed</string> @@ -1309,7 +1314,7 @@ <string name="room_widget_permission_room_id">Room ID</string> - <string name="error_jitsi_not_supported_on_old_device">Sorry, conference calls with Jitsi are not supported on old devices (devices with Android OS below 5.0)</string> + <string name="error_jitsi_not_supported_on_old_device">Sorry, conference calls with Jitsi are not supported on old devices (devices with Android OS below 6.0)</string> <string name="room_widget_resource_permission_title">This widget wants to use the following resources:</string> <string name="room_widget_resource_grant_permission">Allow</string> <string name="room_widget_resource_decline_permission">Block All</string> @@ -2793,6 +2798,36 @@ <string name="matrix_to_card_title">Matrix Link</string> + <string name="call_tile_you_started_call">You started a call</string> + <string name="call_tile_other_started_call">%1$s started a call</string> + <string name="call_tile_in_call">You\'re currently in this call</string> + <!-- Pattern can be replaced by the value of string call_tile_call_back --> + <string name="call_tile_you_declined">You declined this call %1$s</string> + <string name="call_tile_other_declined">%1$s declined this call</string> + <string name="call_tile_ended">This call has ended</string> + <string name="call_tile_call_back">Call back</string> + + <string name="call_dial_pad_title">Dial pad</string> + <string name="call_dial_pad_lookup_error">"There was an error looking up the phone number"</string> + + + <string name="call_only_active">Active call (%1$s)</string> + <plurals name="call_only_paused"> + <item quantity="one">Paused call</item> + <item quantity="other">%1$d paused calls</item> + </plurals> + <plurals name="call_one_active_and_other_paused"> + <item quantity="one">1 active call (%1$s) · 1 paused call</item> + <item quantity="other">1 active call (%1$s) · %2$d paused calls</item> + </plurals> + + <string name="call_transfer_consult_first">Consult first</string> + <string name="call_transfer_connect_action">Connect</string> + <string name="call_transfer_title">Transfer</string> + <string name="call_transfer_failure">An error occurred while transferring call</string> + <string name="call_transfer_users_tab_title">Users</string> + + <string name="re_authentication_activity_title">Re-Authentication Needed</string> <string name="re_authentication_default_confirm_text">Element requires you to enter your credentials to perform this action.</string> <string name="authentication_error">Failed to authenticate</string> diff --git a/vector/src/main/res/values/styles_dial_pad.xml b/vector/src/main/res/values/styles_dial_pad.xml new file mode 100644 index 0000000000..614923caad --- /dev/null +++ b/vector/src/main/res/values/styles_dial_pad.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style name="DialpadKeyNumberStyle"> + <item name="android:textColor">?attr/riotx_text_primary</item> + <item name="android:textSize">@dimen/dialpad_key_numbers_default_size</item> + <item name="android:fontFamily">sans-serif-light</item> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_marginBottom">@dimen/dialpad_key_number_default_margin_bottom</item> + <item name="android:gravity">center</item> + </style> +</resources> \ No newline at end of file diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index 2dff0ab39c..d81b4cb6e9 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -135,11 +135,13 @@ <item name="android:textSize">14sp</item> <item name="android:textAllCaps">false</item> <item name="android:textColor">@color/button_destructive_text_color_selector</item> + <item name="drawableTint">@color/riotx_notice</item> </style> <style name="VectorButtonStylePositive" parent="VectorButtonStyleDestructive"> <item name="backgroundTint">@color/button_positive_background_selector</item> <item name="android:textColor">@color/button_positive_text_color_selector</item> + <item name="drawableTint">@color/riotx_accent</item> </style> <style name="VectorButtonStyleInlineBot" parent="VectorButtonStyleDestructive"> diff --git a/vector/src/main/res/values/theme_black.xml b/vector/src/main/res/values/theme_black.xml index ab0ecbe4e9..e5c6851f40 100644 --- a/vector/src/main/res/values/theme_black.xml +++ b/vector/src/main/res/values/theme_black.xml @@ -39,6 +39,7 @@ <item name="riotx_room_active_widgets_banner_text">@color/riotx_room_active_widgets_banner_text_black</item> <item name="riotx_reaction_background_off">@color/riotx_reaction_background_off_black</item> <item name="riotx_reaction_background_on">@color/riotx_reaction_background_on_black</item> + <item name="riotx_alerter_background">@color/riotx_alerter_background_black</item> <item name="riotx_bottom_nav_icon_color">@color/riotx_bottom_nav_icon_color_black</item> diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index c31fc8240b..953691a6b7 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -37,7 +37,7 @@ <item name="riotx_room_active_widgets_banner_text">@color/riotx_room_active_widgets_banner_text_dark</item> <item name="riotx_reaction_background_off">@color/riotx_reaction_background_off_dark</item> <item name="riotx_reaction_background_on">@color/riotx_reaction_background_on_dark</item> - + <item name="riotx_alerter_background">@color/riotx_alerter_background_dark</item> <item name="riotx_bottom_nav_icon_color">@color/riotx_bottom_nav_icon_color_dark</item> <item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_dark</item> diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index 56faaeb325..cae2ef7794 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -38,7 +38,7 @@ <item name="riotx_room_active_widgets_banner_text">@color/riotx_room_active_widgets_banner_text_light</item> <item name="riotx_reaction_background_off">@color/riotx_reaction_background_off_light</item> <item name="riotx_reaction_background_on">@color/riotx_reaction_background_on_light</item> - + <item name="riotx_alerter_background">@color/riotx_alerter_background_light</item> <item name="riotx_bottom_nav_icon_color">@color/riotx_bottom_nav_icon_color_light</item> <!-- Material color: Note: this block should be the same in all theme because it references only common colors and ?riotx attributes -->