diff --git a/AUTHORS.md b/AUTHORS.md
index a85beb2d6f..4fb5b8c994 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -1,4 +1,4 @@
-A full developer contributors list can be found [here](https://github.com/vector-im/element-android/graphs/contributors).
+A full developer contributors list can be found [here](https://github.com/vector-im/element-android/graphs/contributors).
# Core team:
@@ -33,3 +33,8 @@ First of all, we thank all contributors who use Element and report problems on t
We do not forget all translators, for their work of translating Element into many languages. They are also the authors of Element.
Feel free to add your name below, when you contribute to the project!
+
+Name | Matrix ID | GitHub
+--------|---------------------|--------------------------------------
+gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower)
+
diff --git a/CHANGES.md b/CHANGES.md
index 99058117b6..8dceffbcf0 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,30 @@
+Changes in Element 1.0.10 (2020-11-04)
+===================================================
+
+Improvements 🙌:
+ - Rework sending Event management (#154)
+ - New room creation screen: set topic and avatar in the room creation form (#2078)
+ - Toggle Low priority tag (#1490)
+ - Add option to send with enter (#1195)
+ - Use Hardware keyboard enter to send message (use shift-enter for new line) (#1881, #1440)
+ - Edit and remove icons are now visible on image attachment preview screen (#2294)
+ - Room profile: BigImageViewerActivity now only display the image. Use the room setting to change or delete the room Avatar
+ - Better visibility of text reactions in dark theme (#1118)
+ - Room member profile: Add action to create (or open) a DM (#2310)
+ - Prepare changelog for F-Droid (#2296)
+ - Add graphic resources for F-Droid (#812, #2220)
+ - Highlight text in the body of the displayed result (#2200)
+ - Considerably faster QR-code bitmap generation (#2331)
+
+Bugfix 🐛:
+ - Fixed ringtone handling (#2100 & #2246)
+ - Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252)
+ - Incoming call continues to ring if call is answered on another device (#1921)
+ - Search Result | scroll jumps after pagination (#2238)
+ - Badly formatted mentions in body (#1506)
+ - KeysBackup: Avoid using `!!` (#2262)
+ - Two elements in the task switcher (#2299)
+
Changes in Element 1.0.9 (2020-10-16)
===================================================
diff --git a/build.gradle b/build.gradle
index 05dcaa43ed..0c4b35b060 100644
--- a/build.gradle
+++ b/build.gradle
@@ -65,9 +65,8 @@ allprojects {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
// Warnings are potential errors, so stop ignoring them
// You can override by passing `-PallWarningsAsErrors=false` in the command line
- kotlinOptions.allWarningsAsErrors = project.properties['allWarningsAsErrors']?.toBoolean() ?: true
+ kotlinOptions.allWarningsAsErrors = project.getProperties().getOrDefault("allWarningsAsErrors", "true").toBoolean()
}
-
}
task clean(type: Delete) {
diff --git a/fastlane/metadata/android/bg/full_description.txt b/fastlane/metadata/android/bg/full_description.txt
new file mode 100644
index 0000000000..032e7111f8
--- /dev/null
+++ b/fastlane/metadata/android/bg/full_description.txt
@@ -0,0 +1,30 @@
+Element е приложение от нов тип за съобщения и сътрудничество:
+
+1. Дава Ви контрол, за да запазите поверителността си
+2. Позволява ви да комуникирате с всеки в мрежата на Matrix и дори извън него, като се интегрира с приложения като Slack
+3. Предпазва ви от реклами, изтичане на данни и търговско следене
+4. Защитава ви чрез шифроване от край до край, с кръстосано подписване, за да проверите другите
+
+Element е напълно различен от другите приложения за съобщения и сътрудничество, понеже е децентрализиран и с отворен код.
+
+Element ви позволява да го хоствате самостоятелно - или да изберете хост - така че да имате поверителност, собственост и контрол върху Вашите данни и разговори. Дава ви достъп до отворена мрежа, така че комуникацията Ви не е ограничена до потребителите на Element. И е много сигурно.
+
+Element е в състояние да направи всичко това, защото работи върху Matrix - стандартът за отворена, децентрализирана комуникация.
+
+Element ви дава контрол, като ви позволява да изберете кой да хоства Вашите разговори. От приложението Element можете да изберете хостване по различни начини:
+
+1. Вземете безплатен профил на публичния сървър на matrix.org, хостван от разработчиците на Matrix, или изберете от хиляди публични сървъри, хоствани от доброволци
+2. Самостоятелно хоствайте профила си, като пуснете сървър на собствен хардуер
+3. Регистрирайте се за профил на персонализиран сървър, като се абонирате за хостинг платформата Element Matrix Services
+
+Защо да изберете Element?
+
+ПРИТЕЖАВАЙТЕ ДАННИТЕ СИ : Вие решавате къде да съхранявате вашите данни и съобщения. Вие ги притежавате и контролирате, а не някаква МЕГАКОРПОРАЦИЯ, която складира вашите данни или дава достъп на трети страни.
+
+ОТВОРЕНИ СЪОБЩЕНИЯ И СЪТРУДНИЧЕСТВО : Можете да разговаряте с всеки друг в мрежата на Matrix, независимо дали използва Element или друго приложение на Matrix и дори ако използва различна система за съобщения като Slack, IRC or XMPP.
+
+СВРЪХ СИГУРНО : Реално шифроване от край до край (само тези в разговора могат да дешифрират съобщения) и кръстосано подписване за проверка на устройствата на участниците в разговора.
+
+ПЪЛНА КОМУНИКАЦИЯ : Съобщения, гласови и видео разговори, споделяне на файлове, споделяне на екран и цял куп интеграции, ботове и джаджи. Изграждайте стаи, общности, поддържайте връзка и направете нещата завършени.
+
+НАВСЯКЪДЕ КЪДЕТО СТЕ : Поддържайте връзка, където и да сте, с напълно синхронизирана история на съобщенията на всичките ви устройства и чрез web на https://app.element.io.
diff --git a/fastlane/metadata/android/bg/short_description.txt b/fastlane/metadata/android/bg/short_description.txt
new file mode 100644
index 0000000000..7726f381fe
--- /dev/null
+++ b/fastlane/metadata/android/bg/short_description.txt
@@ -0,0 +1 @@
+Сигурен децентрализиран чат и VoIP. Пазете данните си от външни лица.
diff --git a/fastlane/metadata/android/bg/title.txt b/fastlane/metadata/android/bg/title.txt
new file mode 100644
index 0000000000..5e0d1280d7
--- /dev/null
+++ b/fastlane/metadata/android/bg/title.txt
@@ -0,0 +1 @@
+Element (предишен Riot.im)
diff --git a/fastlane/metadata/android/en-US/changelogs/40100100.txt b/fastlane/metadata/android/en-US/changelogs/40100100.txt
new file mode 100644
index 0000000000..c1821d2475
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40100100.txt
@@ -0,0 +1,2 @@
+This new version mainly contains bug fixes and improvements. Sending a message is now much faster.
+Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.0.10
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png
new file mode 100644
index 0000000000..97f45aafd3
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png
new file mode 100644
index 0000000000..e449d60ca9
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
new file mode 100644
index 0000000000..f514bbece3
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
new file mode 100644
index 0000000000..59883465a0
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
new file mode 100644
index 0000000000..c103144063
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
new file mode 100644
index 0000000000..9903d47d37
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
new file mode 100644
index 0000000000..f5b842311c
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
new file mode 100644
index 0000000000..c45b1b8cbc
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png
new file mode 100644
index 0000000000..2e6b92089e
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png differ
diff --git a/fastlane/metadata/android/eo/full_description.txt b/fastlane/metadata/android/eo/full_description.txt
new file mode 100644
index 0000000000..f564ab61d8
--- /dev/null
+++ b/fastlane/metadata/android/eo/full_description.txt
@@ -0,0 +1,30 @@
+Element estas nova speco de mesaĝilo kaj kunlaborilo, kiu:
+
+1. Lasas vin regi vian komunikadon por protekti vian privatecon
+2. Lasas vin komuniki kun ĉiu ajn en la reto de Matrix, kaj eĉ pliaj, per interkompreno kun aplikaĵoj kiel ekzemple Slack
+3. Protektas vin de reklamoj, kolektado de datumoj, kaj muritaj ĝardenoj
+4. Sekurigas vian komunikadon per tutvoja ĉifrado, kun la eblo kontroli aliajn per delegaj subskriboj
+
+Element malsamas al aliaj mesaĝiloj kaj kunlaboriloj, ĉar ĝi estas sencentra kaj malfermitkoda.
+
+Element lasas vin gastigi vian propran servilon, aŭ elekti servilon, kiu plaĉas al vi, por ke vi ne perdu privatecon, kaj por ke vi daŭre regu kaj posedu viajn datumojn kaj interparolojn. Ĝi donas al vi aliron al malfermita reto; por ke via interparolado ne estu limigita nur al aliaj uzantoj de Element. Kaj ĝi estas tre sekura.
+
+Element povas fari ĉi ĉion, ĉar ĝi funkcias per Matrix – publika normo por malfermita, sencentra komunikado.
+
+Element lasas vi elekti, kiu gastigos viajn interparolojn. Per la aplikaĵo Element, vi povas elekti diversajn specojn de gastigado:
+
+1. Akiri senpagan konton ĉe la publika servilo matrix.org, gastigata de la programistoj de Matrix, aŭ elekti unu el miloj da publikaj serviloj, gastigataj de volontuloj
+2. Memgastiĝi per via propra servilo ĉe via propra aparato
+3. Registriĝi ĉe propra servilo per simpla pagaliĝo al la gastiga platformo «Element Matrix Services»
+
+Kial Element?
+
+POSEDU VIAJN DATUMOJN : Vi decidu, kie vi tenu viajn datumojn kaj mesaĝojn. Vi posedas kaj regas ilin, ne iu granda komerca firmao, kiu kolektas viajn datumojn aŭ donas aliron al aliuloj.
+
+MALFERMAJ MESAĜADO KAJ KUNLABORADO : Vi povas babili kun ĉiu alia en la reto de Matrix, ĉu ĝi uzas Elementon aŭ alian aplikaĵon de Matrix, kaj eĉ se ĝi uzas tute alian mesaĝilon, kiel ekzemple Slack, IRC, aŭ XMPP.
+
+TRE SEKURA : Vera tutvoja ĉifrado (nur la interparolantoj povas malĉifri siajn mesaĝojn), kaj delegaj subskriboj por kontroli la aparatojn de partoprenantoj.
+
+SENMANKA KOMUNIKADO : Mesaĝoj, voĉvokoj kaj vidvokoj, havigado de dosieroj, ekrano, kaj multaj diversaj kunigoj, robotoj kaj fenestraĵoj. Kreu ĉambrojn, komunumojn, komuniku kaj kunlaboru.
+
+ĈIE KUN VI : Tenu vin ĝisdata per historio de mesaĝoj plene spegulita trans ĉiuj viaj aparatoj, kaj sur la reto per https://app.element.io.
diff --git a/fastlane/metadata/android/eo/short_description.txt b/fastlane/metadata/android/eo/short_description.txt
new file mode 100644
index 0000000000..33013ce78f
--- /dev/null
+++ b/fastlane/metadata/android/eo/short_description.txt
@@ -0,0 +1 @@
+Sekura kaj sencentrigita vokado kaj babilado. Tenu viajn datumojn sekuraj.
diff --git a/fastlane/metadata/android/eo/title.txt b/fastlane/metadata/android/eo/title.txt
new file mode 100644
index 0000000000..f56927e529
--- /dev/null
+++ b/fastlane/metadata/android/eo/title.txt
@@ -0,0 +1 @@
+Element (antaŭe Riot.im)
diff --git a/fastlane/metadata/android/et/full_description.txt b/fastlane/metadata/android/et/full_description.txt
new file mode 100644
index 0000000000..7c7f7195a8
--- /dev/null
+++ b/fastlane/metadata/android/et/full_description.txt
@@ -0,0 +1,30 @@
+Element on uut tüüpi suhtlus- ja koostöörakendus, mis:
+
+1. Võimaldab täielikku kontrolli privaatsuse üle
+2. Võimaldab suhelda kõigiga Matrixi võrgus ja isegi väljaspool seda, olles integreeritud selliste rakendustega nagu Slack
+3. Kaitseb sind reklaamide ja andmekogumise eest
+4. Tagab turvalisuse läbiva krüptimise abil, kasutades risttunnustamist vestlejate tuvastamiseks
+
+Element erineb täielikult teistest sõnumside- ja koostöörakendustest, kuna see on detsentraliseeritud ja avatud lähtekoodiga.
+
+Element võimaldab ise hostida - või valida hosti -, et oleks tagatud privaatsus ja kontroll oma andmete ja vestluste üle. Element annab ka juurdepääsu avatud võrgule, nii et te ei pea vaid Elemendi kasutajatega rääkima. Ning kogu süsteem on väga turvaline.
+
+Element töötab Matrixil - avatud, detsentraliseeritud suhtlus-standardil.
+
+Võimaldades valida, kes vestlusi korraldab, annab Element annab kontrolli sinule. Rakendust Element saad kasutada mitmel viisil.
+
+1. Tasuta konto Matrixi arendajate hostitud avalikus serveris matrix.org või vali tuhandete avalike serverite hulgast, mida haldavad vabatahtlikud
+2. Hosti oma kontot ise, paigaldades serveri oma riistvarale
+3. Registreeruge serveris olevale kontole, tellides Element Matrix Services teenuseplatvormi
+
+ Miks valida element?
+
+ KONTROLL ANDMETE ÜLE : otsustad ise, kus oma andmeid ja sõnumeid hoida. Need kuuluvad sulle ja sinu käes on kontroll, mitte mõne MEGAFIRMA käes, mis andmeid oma kasuks kaevandab või kolmandatele isikutele juurdepääsu annab.
+
+ AVATUD SUHTLUS JA KOOSTÖÖ : saad vestelda kõigi teistega Matrixi võrgus, olenemata sellest, kas nad kasutavad Elementi või mõnda muud Matrixi rakendust, ja isegi kui nad kasutavad teistsugust suhtlussüsteemi nagu Slack, IRC või XMPP.
+
+ ÜLITURVALINE : tõeline läbiv krüptimine (ainult vestluses osalejad saavad sõnumeid lugeda) ja risttunnustamine vestluses osalejate tuvastamiseks.
+
+ KÕIK SUHTLUSVÕIMALUSED : sõnumid, hääl- ja videokõned, failide jagamine, ekraani jagamine ja terve hulk lõiminguid, roboteid ja vidinaid. Loo tubasid, kogukondi, hoia ühendust ja saa asjad aetud.
+
+ KÕIKJAL, KUS VIIBITE : saad suhelda kõigis oma seadmetes ja ka veebis aadressil https://app.element.io ning sealjuures täielikult sünkroonitud sõnumite ajalooga.
diff --git a/fastlane/metadata/android/fa/full_description.txt b/fastlane/metadata/android/fa/full_description.txt
new file mode 100644
index 0000000000..0a93676bbf
--- /dev/null
+++ b/fastlane/metadata/android/fa/full_description.txt
@@ -0,0 +1,30 @@
+المنت گونهای جدید از کارههای پیامرسانی و همکاری است که:
+
+۱. کنترل محرمانگیتان را در دست خودتان میگذارد
+۲. میگذارد با هرکسی در شبکهٔ ماتریکس و حتا فراتر از آن، ارتباط برقرار کنید
+۳. از شما در برابر تبلیغات، دادهکاوری و دیوارهای پرداختی، محافظت میکند
+۴. با رمزنگاری سرتاسری با ورود چندگانه، امنتان میکند
+
+المنت به خاطر نامتمرکز و نرمافزار آزاد بودن، کاملاً با دیگر کارههای پیامرسانی و همکاری، فرق دارد.
+
+المنت میگذارد خودمیزبانی کرده یا میزبانی برگزینید که امنیت، مالکیت و واپایش دادهها و گفتوگوهایتان را در اختیار داشته باشید. این کاره شما را به شبکهای باز و شدیداً امن وصل کرده تا مجبور نباشید فقط با دیگر کاربران المنت صحبت کنید.
+
+المنت میتواند همهٔ این کارها را بکند، چرا که روی ماتریکس، استانداردی برای گفتوگوی باز و نامتمرکز عمل میکند.
+
+المنت با اجازه برای گزینش کسی که گفتوگوهایتان را میزبانی میکند، کنترل را به شما میدهد. با کارهٔ المنت، میتوانید برگزینید که به روشهای مختلفی میزبانی شوید:
+
+۱. گرفتن حسابی رایگان روی کارساز عمومی matrix.org که به دست توسعهدهندگان ماتریکس میزبانی میشود، یا گرینش از میان هزاران کارساز عمومی میزبانیشده به دست داوطلبان
+۲. خودمیزبانی حسابتان با اجرای کراسازی روی سختافزار خودتان
+۳. ثبتنام برای حسابی روی یک کارساز سفارشی با اشتراک در بنیازهٔ میزبانی خدمات ماتریکس المنت
+
+چرا المنت را برگزینیم؟
+
+مالک دادههایتان باشید : خوتان تصمیم میگیرید که دادهها و پیامهایتان را کجا نگه دارید. شما صاحبشان هستید و واپایششان میکنید، نه شرکتهای بزرگی که دادههایتان را کاویده و به شرکتهای دیگر دسترسی میدهند.
+
+پیامرسانی و همکاری باز : میتوانید با هرکسی در شبکهٔ ماتریکس گپ بزنید، چه از المنت استفاده کنند و چه از هر کارهٔ ماتریکس دیگری؛ و حتا اگر از سامانهٔ پیامرسانی متفاوتی مثل اسلک، آیآرسی یا جبر استفاده کنند.
+
+فوق امن : رمزنگاری سرتاسری واقعی (فقط کسانی که در گفتوگو هستند،میتوانند پیامها را رمزگشایی کنند) و ورود چندگانه برای تأیید هویت افزارههای شرکتکنندگان در گفتوگو.
+
+ارتباط کامل : پیامرسانی، تماسهای صوتی و تصویری،همرسانی پرونده، همرسانی صفحه و یه عالمه یکپارچگی، بات و ابزارک. اتاق و اجتماع ساخته، در دسترس بوده و کارها را انجام دهید.
+
+هرجا که هستید : هر کجا که هستید، با همگام سازی کامل تاریخچهٔ پیامها بین همهٔ افزارههایتان و وب روی https://app.element.io در دسترس باشید.
diff --git a/fastlane/metadata/android/fa/short_description.txt b/fastlane/metadata/android/fa/short_description.txt
new file mode 100644
index 0000000000..4cfa767649
--- /dev/null
+++ b/fastlane/metadata/android/fa/short_description.txt
@@ -0,0 +1 @@
+گپ و تماس نامتمرکز امن. دادههایتان را از شرکتها امن نگه دارید.
diff --git a/fastlane/metadata/android/fa/title.txt b/fastlane/metadata/android/fa/title.txt
new file mode 100644
index 0000000000..fb4ea4125e
--- /dev/null
+++ b/fastlane/metadata/android/fa/title.txt
@@ -0,0 +1 @@
+المنت (ریوت سابق)
diff --git a/fastlane/metadata/android/fr/full_description.txt b/fastlane/metadata/android/fr/full_description.txt
new file mode 100644
index 0000000000..2b17d8f846
--- /dev/null
+++ b/fastlane/metadata/android/fr/full_description.txt
@@ -0,0 +1,30 @@
+Element est une nouvelle application de messagerie et de collaboration qui :
+
+1) Vous place aux commandes de votre vie privée
+2) Vous permet de communiquer avec n'importe qui du réseau Matrix, et plus encore par des intégrations d'autres applications comme Slack ou Discord
+3) Vous protège de la publicité et de la collecte de données
+4) Vous sécurise grâce à du chiffrement bout-à-bout, avec de la signature croisée pour authentifier les autres utilisateurs
+
+Element est complètement différent des autres applications de messagerie et de collaboration puisque l'application est décentralisée et open-source.
+
+Element vous permet d'héberger vous-même -ou de choisir un hôte- vous permettant d'assurer votre vie privée, la propriété et le contrôle de vos données et de vos conversations. Cela vous offre l'accès à un réseau ouvert, vous n'êtes donc pas condamné à parler à d'autres utilisateurs d'Element seulement. Et c'est très sécurisé.
+
+Element peut faire tout ça car il est basé sur Matrix, le protocole standard pour la communication ouverte et décentralisée.
+
+Element vous donne le contrôle en vous laissant choisir qui héberge vos conversations. Depuis l'application Element, vous pouvez choisir votre hôte de différentes manières :
+
+1) Créer un compte gratuit sur le serveur public matrix.org hébergé par les développeurs de Matrix, ou choisir parler les milliers de serveurs public hébergés par des bénévoles
+2) Héberger vous-même votre compte en installant un serveur sur votre propre machine
+3) Créer un compte sur un serveur personnalisé en souscrivant sur la plateforme d'hébergement « Element Matrix Services » (EMS)
+
+Pourquoi choisir Element ?
+
+POSSÉDEZ VOS DONNÉES : Vous décidez où conserver vos données et vos messages. Vous les possédez et vous les contrôlez, et non une MEGACORP qui mine vos données ou les donnent à des tiers
+
+UNE MESSAGERIE OUVERTE ET COLLABORATIVE : Vous pouvez discuter avec n'importe qui sur le réseau Matrix, qu'ils utilisent Element ou une autre application basée sur Matrix, et même s'ils utilisent un système de messagerie différent comment Slack, Discord, IRC ou XMPP.
+
+SUPER SÉCURISÉ : Un réel chiffrement bout-à-bout (seulement ceux deux la conversation peuvent déchiffrer les messages), et une signature croisée pour vérifier les appareils des participants de la conversation.
+
+COMMUNICATION COMPLÈTE : Messagerie, appels vocaux et vidéo, transfert de fichiers, partage d'écran et un tas d'intégrations, robots et widgets. Construisez des salons, des communautés, restez en contact et accomplissez de grandes choses.
+
+PARTOUT OÙ VOUS ÊTES : Restez connectés peu import où vous êtes avec la synchronisation complète de l'historique des messages sur tous vos appareils et sur le web sur https://app.element.io.
diff --git a/fastlane/metadata/android/fr/short_description.txt b/fastlane/metadata/android/fr/short_description.txt
new file mode 100644
index 0000000000..2fb9762e97
--- /dev/null
+++ b/fastlane/metadata/android/fr/short_description.txt
@@ -0,0 +1 @@
+Chat & VoIP sûr et décentralisé. Gardez vos données en sécurité.
diff --git a/fastlane/metadata/android/hu/title.txt b/fastlane/metadata/android/hu/title.txt
index cc39ddefdd..8e493d2d08 100644
--- a/fastlane/metadata/android/hu/title.txt
+++ b/fastlane/metadata/android/hu/title.txt
@@ -1 +1 @@
-Element (előzőleg Riot.im)
+Element (régebben Riot.im)
diff --git a/fastlane/metadata/android/sv/full_description.txt b/fastlane/metadata/android/sv/full_description.txt
index afd0975586..d130e9214a 100644
--- a/fastlane/metadata/android/sv/full_description.txt
+++ b/fastlane/metadata/android/sv/full_description.txt
@@ -21,7 +21,7 @@ Element sätter dig i kontroll genom att låta dig välja att vara värd för di
ÄG DIN DATA : Du väljer var du vill ha din data och dina meddelanden. Du äger den och kontrollerar den, inte nåt stort företag som samlar in din data och ger den till tredje parter.
-ÖPPEN KOMMUNIKATION OCH ÖPPET SAMARBETE : Du kan chatta med med vem som helst på Matrix-nätverket, oavsett om de använder Element eller en annan Matrix-app, och till och med om de använder ett annat meddelandesystem som Slack, IRC eller XMPP.
+ÖPPEN KOMMUNIKATION OCH ÖPPET SAMARBETE : Du kan chatta med vem som helst på Matrix-nätverket, oavsett om de använder Element eller en annan Matrix-app, och till och med om de använder ett annat meddelandesystem som Slack, IRC eller XMPP.
SUPERSÄKER : Riktig totalsträckskryptering (bara de in konversationen kan avkryptera meddelandena), och korssingering för att verifiera konversationsmedlemmars enheter.
diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt
index 228e83faff..86f2d26808 100644
--- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt
+++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt
@@ -142,6 +142,10 @@ class RxRoom(private val room: Room) {
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder {
room.updateAvatar(avatarUri, fileName, it)
}
+
+ fun deleteAvatar(): Completable = completableBuilder {
+ room.deleteAvatar(it)
+ }
}
fun Room.rx(): RxRoom {
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
index 1c912b365f..cbe4cca8a3 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
@@ -88,7 +88,10 @@ class CommonTestHelper(context: Context) {
fun syncSession(session: Session) {
val lock = CountDownLatch(1)
- GlobalScope.launch(Dispatchers.Main) { session.open() }
+ val job = GlobalScope.launch(Dispatchers.Main) {
+ session.open()
+ }
+ runBlocking { job.join() }
session.startSync(true)
@@ -341,7 +344,7 @@ class CommonTestHelper(context: Context) {
}
// Transform a method with a MatrixCallback to a synchronous method
- inline fun doSync(block: (MatrixCallback) -> Unit): T {
+ inline fun doSync(timeout: Long? = TestConstants.timeOutMillis, block: (MatrixCallback) -> Unit): T {
val lock = CountDownLatch(1)
var result: T? = null
@@ -354,7 +357,7 @@ class CommonTestHelper(context: Context) {
block.invoke(callback)
- await(lock)
+ await(lock, timeout)
assertNotNull(result)
return result!!
@@ -366,8 +369,9 @@ class CommonTestHelper(context: Context) {
fun Iterable.signOutAndClose() = forEach { signOutAndClose(it) }
fun signOutAndClose(session: Session) {
- doSync { session.signOut(true, it) }
- session.close()
+ doSync(60_000) { session.signOut(true, it) }
+ // no need signout will close
+ // session.close()
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
index 370b416f54..1a9165ade4 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
@@ -32,9 +32,6 @@ import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
-import org.matrix.android.sdk.api.session.room.timeline.Timeline
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
-import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
@@ -197,47 +194,16 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
- val lock = CountDownLatch(1)
-
- val bobEventsListener = object : Timeline.Listener {
- override fun onTimelineFailure(throwable: Throwable) {
- // noop
- }
-
- override fun onNewTimelineEvents(eventIds: List) {
- // noop
- }
-
- override fun onTimelineUpdated(snapshot: List) {
- val messages = snapshot.filter { it.root.getClearType() == EventType.MESSAGE }
- .groupBy { it.root.senderId!! }
-
- // Alice has sent 2 messages and Bob has sent 3 messages
- if (messages[aliceSession.myUserId]?.size == 2 && messages[bobSession.myUserId]?.size == 3) {
- lock.countDown()
- }
- }
- }
-
- val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20))
- bobTimeline.start()
- bobTimeline.addListener(bobEventsListener)
-
// Alice sends a message
- roomFromAlicePOV.sendTextMessage(messagesFromAlice[0])
+ mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[0], 1)
+// roomFromAlicePOV.sendTextMessage(messagesFromAlice[0])
// Bob send 3 messages
- roomFromBobPOV.sendTextMessage(messagesFromBob[0])
- roomFromBobPOV.sendTextMessage(messagesFromBob[1])
- roomFromBobPOV.sendTextMessage(messagesFromBob[2])
-
+ mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1)
+ mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[1], 1)
+ mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[2], 1)
// Alice sends a message
- roomFromAlicePOV.sendTextMessage(messagesFromAlice[1])
-
- mTestHelper.await(lock)
-
- bobTimeline.removeListener(bobEventsListener)
- bobTimeline.dispose()
+ mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[1], 1)
return cryptoTestData
}
@@ -279,20 +245,14 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
fun createFakeMegolmBackupCreationInfo(): MegolmBackupCreationInfo {
return MegolmBackupCreationInfo(
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP,
- authData = createFakeMegolmBackupAuthData()
+ authData = createFakeMegolmBackupAuthData(),
+ recoveryKey = "fake"
)
}
fun createDM(alice: Session, bob: Session): String {
val roomId = mTestHelper.doSync {
- alice.createRoom(
- CreateRoomParams().apply {
- invitedUserIds.add(bob.myUserId)
- setDirectMessage()
- enableEncryptionIfInvitedUsersSupportIt = true
- },
- it
- )
+ alice.createDirectRoom(bob.myUserId, it)
}
mTestHelper.waitWithLatch { latch ->
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
index ca8993fb00..606f57b467 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
@@ -115,9 +115,8 @@ class KeysBackupTest : InstrumentedTest {
}
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, megolmBackupCreationInfo.algorithm)
- assertNotNull(megolmBackupCreationInfo.authData)
- assertNotNull(megolmBackupCreationInfo.authData!!.publicKey)
- assertNotNull(megolmBackupCreationInfo.authData!!.signatures)
+ assertNotNull(megolmBackupCreationInfo.authData.publicKey)
+ assertNotNull(megolmBackupCreationInfo.authData.signatures)
assertNotNull(megolmBackupCreationInfo.recoveryKey)
stateObserver.stopAndCheckStates(null)
@@ -258,14 +257,14 @@ class KeysBackupTest : InstrumentedTest {
// - Check encryptGroupSession() returns stg
val keyBackupData = keysBackup.encryptGroupSession(session)
assertNotNull(keyBackupData)
- assertNotNull(keyBackupData.sessionData)
+ assertNotNull(keyBackupData!!.sessionData)
// - Check pkDecryptionFromRecoveryKey() is able to create a OlmPkDecryption
val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey)
assertNotNull(decryption)
// - Check decryptKeyBackupData() returns stg
val sessionData = keysBackup
- .decryptKeyBackupData(keyBackupData,
+ .decryptKeyBackupData(keyBackupData!!,
session.olmInboundGroupSession!!.sessionIdentifier(),
cryptoTestData.roomId,
decryption!!)
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt
index 1713578932..94303dda08 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt
@@ -25,6 +25,8 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.internal.session.room.send.pills.MentionLinkSpecComparator
+import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils
/**
* It will not be possible to test all combinations. For the moment I add a few tests, then, depending on the problem discovered in the wild,
@@ -45,7 +47,8 @@ class MarkdownParserTest : InstrumentedTest {
*/
private val markdownParser = MarkdownParser(
Parser.builder().build(),
- HtmlRenderer.builder().build()
+ HtmlRenderer.builder().softbreak(" ").build(),
+ TextPillsUtils(MentionLinkSpecComparator())
)
@Test
@@ -144,12 +147,14 @@ class MarkdownParserTest : InstrumentedTest {
)
}
+ // TODO. Improve testTypeNewLines function to cover test
@Test
- fun parseCodeNewLines() {
+ fun parseCodeNewLines_not_passing() {
testTypeNewLines(
name = "code",
- markdownPattern = "`",
- htmlExpectedTag = "code"
+ markdownPattern = "```",
+ htmlExpectedTag = "code",
+ softBreak = "\n"
)
}
@@ -163,7 +168,7 @@ class MarkdownParserTest : InstrumentedTest {
}
@Test
- fun parseCode2NewLines() {
+ fun parseCode2NewLines_not_passing() {
testTypeNewLines(
name = "code",
markdownPattern = "``",
@@ -181,7 +186,7 @@ class MarkdownParserTest : InstrumentedTest {
}
@Test
- fun parseCode3NewLines() {
+ fun parseCode3NewLines_not_passing() {
testTypeNewLines(
name = "code",
markdownPattern = "```",
@@ -243,7 +248,7 @@ class MarkdownParserTest : InstrumentedTest {
}
@Test
- fun parseBoldNewLines_not_passing() {
+ fun parseBoldNewLines2() {
"**bold**\nline2".let { markdownParser.parse(it).expect(it, "bold line2") }
}
@@ -334,13 +339,14 @@ class MarkdownParserTest : InstrumentedTest {
private fun testTypeNewLines(name: String,
markdownPattern: String,
- htmlExpectedTag: String) {
+ htmlExpectedTag: String,
+ softBreak: String = " ") {
// With new line inside the block
"$markdownPattern$name\n$name$markdownPattern"
.let {
markdownParser.parse(it)
.expect(expectedText = it,
- expectedFormattedText = "<$htmlExpectedTag>$name $name$htmlExpectedTag>")
+ expectedFormattedText = "<$htmlExpectedTag>$name$softBreak$name$htmlExpectedTag>")
}
// With new line between two blocks
@@ -348,7 +354,7 @@ class MarkdownParserTest : InstrumentedTest {
.let {
markdownParser.parse(it)
.expect(expectedText = it,
- expectedFormattedText = "<$htmlExpectedTag>$name$htmlExpectedTag><$htmlExpectedTag>$name$htmlExpectedTag>")
+ expectedFormattedText = "<$htmlExpectedTag>$name$htmlExpectedTag> <$htmlExpectedTag>$name$htmlExpectedTag>")
}
}
diff --git a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/database/RealmDebugTools.kt b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/database/RealmDebugTools.kt
deleted file mode 100644
index e5f4af2377..0000000000
--- a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/database/RealmDebugTools.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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.database
-
-import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
-import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntity
-import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntity
-import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
-import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
-import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
-import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity
-import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
-import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
-import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
-import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
-import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
-import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
-import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
-import io.realm.Realm
-import io.realm.RealmConfiguration
-import io.realm.kotlin.where
-import timber.log.Timber
-
-object RealmDebugTools {
- /**
- * Log info about the crypto DB
- */
- fun dumpCryptoDb(realmConfiguration: RealmConfiguration) {
- Realm.getInstance(realmConfiguration).use {
- Timber.d("Realm located at : ${realmConfiguration.realmDirectory}/${realmConfiguration.realmFileName}")
-
- val key = realmConfiguration.encryptionKey.joinToString("") { byte -> "%02x".format(byte) }
- Timber.d("Realm encryption key : $key")
-
- // Check if we have data
- Timber.e("Realm is empty: ${it.isEmpty}")
-
- Timber.d("Realm has CryptoMetadataEntity: ${it.where().count()}")
- Timber.d("Realm has CryptoRoomEntity: ${it.where().count()}")
- Timber.d("Realm has DeviceInfoEntity: ${it.where().count()}")
- Timber.d("Realm has KeysBackupDataEntity: ${it.where().count()}")
- Timber.d("Realm has OlmInboundGroupSessionEntity: ${it.where().count()}")
- Timber.d("Realm has OlmSessionEntity: ${it.where().count()}")
- Timber.d("Realm has UserEntity: ${it.where().count()}")
- Timber.d("Realm has KeyInfoEntity: ${it.where().count()}")
- Timber.d("Realm has CrossSigningInfoEntity: ${it.where().count()}")
- Timber.d("Realm has TrustLevelEntity: ${it.where().count()}")
- Timber.d("Realm has GossipingEventEntity: ${it.where().count()}")
- Timber.d("Realm has IncomingGossipingRequestEntity: ${it.where().count()}")
- Timber.d("Realm has OutgoingGossipingRequestEntity: ${it.where().count()}")
- Timber.d("Realm has MyDeviceLastSeenInfoEntity: ${it.where().count()}")
- }
- }
-}
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 f8dc906502..56609610f1 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
@@ -238,4 +238,9 @@ interface Session :
}
val sharedSecretStorageService: SharedSecretStorageService
+
+ /**
+ * Maintenance API, allows to print outs info on DB size to logcat
+ */
+ fun logDbUsageInfo()
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
index 121d9fb401..0eefca1b4c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.crypto
import android.content.Context
import androidx.lifecycle.LiveData
+import androidx.paging.PagedList
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
@@ -40,6 +41,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
+import kotlin.jvm.Throws
interface CryptoService {
@@ -142,12 +144,17 @@ interface CryptoService {
fun removeSessionListener(listener: NewSessionListener)
fun getOutgoingRoomKeyRequests(): List
+ fun getOutgoingRoomKeyRequestsPaged(): LiveData>
fun getIncomingRoomKeyRequests(): List
+ fun getIncomingRoomKeyRequestsPaged(): LiveData>
- fun getGossipingEventsTrail(): List
+ fun getGossipingEventsTrail(): LiveData>
+ fun getGossipingEvents(): List
// For testing shared session
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap
fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent?
+
+ fun logDbUsageInfo()
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
index 965e7e23bb..b772225f51 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
@@ -35,6 +35,22 @@ interface RoomService {
fun createRoom(createRoomParams: CreateRoomParams,
callback: MatrixCallback): Cancelable
+ /**
+ * Create a direct room asynchronously. This is a facility method to create a direct room with the necessary parameters
+ */
+ fun createDirectRoom(otherUserId: String,
+ callback: MatrixCallback): Cancelable {
+ return createRoom(
+ CreateRoomParams()
+ .apply {
+ invitedUserIds.add(otherUserId)
+ setDirectMessage()
+ enableEncryptionIfInvitedUsersSupportIt = true
+ },
+ callback
+ )
+ }
+
/**
* Join a room by id
* @param roomIdOrAlias the roomId or the room alias of the room to join
@@ -113,5 +129,16 @@ interface RoomService {
*/
fun getChangeMembershipsLive(): LiveData>
- fun getExistingDirectRoomWithUser(otherUserId: String): Room?
+ /**
+ * Return the roomId of an existing DM with the other user, or null if such room does not exist
+ * A room is a DM if:
+ * - it is listed in the `m.direct` account data
+ * - the current user has joined the room
+ * - the other user is invited or has joined the room
+ * - it has exactly 2 members
+ * Note:
+ * - the returning room can be encrypted or not
+ * - the power level of the users are not taken into account. Normally in a DM, the 2 members are admins of the room
+ */
+ fun getExistingDirectRoomWithUser(otherUserId: String): String?
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
index f170c098bc..9455a83aff 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
@@ -63,8 +63,13 @@ data class RoomSummary constructor(
val hasNewMessages: Boolean
get() = notificationCount != 0
+ val isLowPriority: Boolean
+ get() = hasTag(RoomTag.ROOM_TAG_LOW_PRIORITY)
+
val isFavorite: Boolean
- get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE }
+ get() = hasTag(RoomTag.ROOM_TAG_FAVOURITE)
+
+ fun hasTag(tag: String) = tags.any { it.name == tag }
val canStartCall: Boolean
get() = joinedMembersCount == 2
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt
index 0860b25d69..892a865751 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt
@@ -16,6 +16,7 @@
package org.matrix.android.sdk.api.session.room.model.create
+import android.net.Uri
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
@@ -51,6 +52,11 @@ class CreateRoomParams {
*/
var topic: String? = null
+ /**
+ * If this is not null, the image uri will be sent to the media server and will be set as a room avatar.
+ */
+ var avatarUri: Uri? = null
+
/**
* A list of user IDs to invite to the room.
* This will tell the server to invite everyone in the list to the newly created room.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt
index 5b5c9e6886..733d6c37e8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt
@@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass
data class ReactionInfo(
@Json(name = "rel_type") override val type: String?,
@Json(name = "event_id") override val eventId: String,
- val key: String,
+ @Json(name = "key") val key: String,
// always null for reaction
@Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
@Json(name = "option") override val option: Int? = null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
index 36f6e538a9..152a018e78 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
@@ -123,11 +123,6 @@ interface SendService {
*/
fun deleteFailedEcho(localEcho: TimelineEvent)
- /**
- * Delete all the events in one of the sending states
- */
- fun clearSendingQueue()
-
/**
* Cancel sending a specific event. It has to be in one of the sending states
*/
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
index 8c08743972..e4baa58c30 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
@@ -58,6 +58,11 @@ interface StateService {
*/
fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable
+ /**
+ * Delete the avatar of the room
+ */
+ fun deleteAvatar(callback: MatrixCallback): Cancelable
+
fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback): Cancelable
fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
new file mode 100644
index 0000000000..e8a70615e1
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.crypto
+
+import com.zhuinden.monarchy.Monarchy
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.EventEntityFields
+import org.matrix.android.sdk.internal.database.query.whereType
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
+import org.matrix.android.sdk.internal.util.fetchCopied
+import javax.inject.Inject
+
+/**
+ * The crypto module needs some information regarding rooms that are stored
+ * in the session DB, this class encapsulate this functionality
+ */
+internal class CryptoSessionInfoProvider @Inject constructor(
+ @SessionDatabase private val monarchy: Monarchy
+) {
+
+ fun isRoomEncrypted(roomId: String): Boolean {
+ val encryptionEvent = monarchy.fetchCopied { realm ->
+ EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
+ .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
+ .isNotNull(EventEntityFields.STATE_KEY) // should be an empty key
+ .findFirst()
+ }
+ return encryptionEvent != null
+ }
+
+ /**
+ * @param allActive if true return joined as well as invited, if false, only joined
+ */
+ fun getRoomUserIds(roomId: String, allActive: Boolean): List {
+ var userIds: List = emptyList()
+ monarchy.doWithRealm { realm ->
+ userIds = if (allActive) {
+ RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
+ } else {
+ RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds()
+ }
+ }
+ return userIds
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
index b78afe6d41..d3a3fd9fbd 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
@@ -17,12 +17,10 @@
package org.matrix.android.sdk.internal.crypto
import android.content.Context
-import android.os.Handler
-import android.os.Looper
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
+import androidx.paging.PagedList
import com.squareup.moshi.Types
-import com.zhuinden.monarchy.Monarchy
import dagger.Lazy
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
@@ -51,9 +49,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
-import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
-import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
@@ -68,7 +64,6 @@ import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
-import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
@@ -82,21 +77,15 @@ import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask
import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
-import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
-import org.matrix.android.sdk.internal.database.model.EventEntity
-import org.matrix.android.sdk.internal.database.model.EventEntityFields
-import org.matrix.android.sdk.internal.database.query.whereType
import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.MoshiProvider
-import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.extensions.foldToCallback
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
-import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.sync.model.SyncResponse
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.TaskThread
@@ -104,11 +93,11 @@ import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.task.launchToCallback
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
-import org.matrix.android.sdk.internal.util.fetchCopied
import org.matrix.olm.OlmManager
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
+import kotlin.jvm.Throws
import kotlin.math.max
/**
@@ -171,28 +160,16 @@ internal class DefaultCryptoService @Inject constructor(
private val setDeviceNameTask: SetDeviceNameTask,
private val uploadKeysTask: UploadKeysTask,
private val loadRoomMembersTask: LoadRoomMembersTask,
- @SessionDatabase private val monarchy: Monarchy,
+ private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor,
private val cryptoCoroutineScope: CoroutineScope,
- private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
- private val sendToDeviceTask: SendToDeviceTask,
- private val messageEncrypter: MessageEncrypter
+ private val eventDecryptor: EventDecryptor
) : CryptoService {
- init {
- verificationService.cryptoService = this
- }
-
- private val uiHandler = Handler(Looper.getMainLooper())
-
private val isStarting = AtomicBoolean(false)
private val isStarted = AtomicBoolean(false)
- // The date of the last time we forced establishment
- // of a new session for each user:device.
- private val lastNewSessionForcedDates = MXUsersDevicesMap()
-
fun onStateEvent(roomId: String, event: Event) {
when (event.getClearType()) {
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
@@ -209,6 +186,8 @@ internal class DefaultCryptoService @Inject constructor(
}
}
+ val gossipingBuffer = mutableListOf()
+
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) {
setDeviceNameTask
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
@@ -335,6 +314,10 @@ internal class DefaultCryptoService @Inject constructor(
}
// Just update
fetchDevicesList(NoOpMatrixCallback())
+
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+ cryptoStore.tidyUpDataBase()
+ }
}
fun ensureDevice() {
@@ -410,7 +393,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
fun close() = runBlocking(coroutineDispatchers.crypto) {
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
-
+ incomingGossipingRequestManager.close()
olmDevice.release()
cryptoStore.close()
}
@@ -452,6 +435,13 @@ internal class DefaultCryptoService @Inject constructor(
incomingGossipingRequestManager.processReceivedGossipingRequests()
}
}
+
+ tryOrNull {
+ gossipingBuffer.toList().let {
+ cryptoStore.saveGossipingEvents(it)
+ }
+ gossipingBuffer.clear()
+ }
}
}
@@ -612,13 +602,7 @@ internal class DefaultCryptoService @Inject constructor(
* @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM
*/
override fun isRoomEncrypted(roomId: String): Boolean {
- val encryptionEvent = monarchy.fetchCopied { realm ->
- EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
- .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
- .isNotNull(EventEntityFields.STATE_KEY)
- .findFirst()
- }
- return encryptionEvent != null
+ return cryptoSessionInfoProvider.isRoomEncrypted(roomId)
}
/**
@@ -660,11 +644,8 @@ internal class DefaultCryptoService @Inject constructor(
eventType: String,
roomId: String,
callback: MatrixCallback) {
+ // moved to crypto scope to have uptodate values
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-// if (!isStarted()) {
-// Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init")
-// internalStart(false)
-// }
val userIds = getRoomUserIds(roomId)
var alg = roomEncryptorsStore.get(roomId)
if (alg == null) {
@@ -720,14 +701,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param callback the callback to return data or null
*/
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) {
- cryptoCoroutineScope.launch {
- val result = runCatching {
- withContext(coroutineDispatchers.crypto) {
- internalDecryptEvent(event, timeline)
- }
- }
- result.foldToCallback(callback)
- }
+ eventDecryptor.decryptEventAsync(event, timeline, callback)
}
/**
@@ -739,42 +713,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
@Throws(MXCryptoError::class)
private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
- val eventContent = event.content
- if (eventContent == null) {
- Timber.e("## CRYPTO | decryptEvent : empty event content")
- throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
- } else {
- val algorithm = eventContent["algorithm"]?.toString()
- val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
- if (alg == null) {
- val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
- Timber.e("## CRYPTO | decryptEvent() : $reason")
- throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
- } else {
- try {
- return alg.decryptEvent(event, timeline)
- } catch (mxCryptoError: MXCryptoError) {
- Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
- if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
- if (mxCryptoError is MXCryptoError.Base
- && mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
- // need to find sending device
- val olmContent = event.content.toModel()
- cryptoStore.getUserDevices(event.senderId ?: "")
- ?.values
- ?.firstOrNull { it.identityKey() == olmContent?.senderKey }
- ?.let {
- markOlmSessionForUnwedging(event.senderId ?: "", it)
- }
- ?: run {
- Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device")
- }
- }
- }
- throw mxCryptoError
- }
- }
- }
+ return eventDecryptor.decryptEvent(event, timeline)
}
/**
@@ -796,19 +735,19 @@ internal class DefaultCryptoService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) {
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
- cryptoStore.saveGossipingEvent(event)
+ gossipingBuffer.add(event)
// Keys are imported directly, not waiting for end of sync
onRoomKeyEvent(event)
}
EventType.REQUEST_SECRET,
EventType.ROOM_KEY_REQUEST -> {
// save audit trail
- cryptoStore.saveGossipingEvent(event)
+ gossipingBuffer.add(event)
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
incomingGossipingRequestManager.onGossipingRequestEvent(event)
}
EventType.SEND_SECRET -> {
- cryptoStore.saveGossipingEvent(event)
+ gossipingBuffer.add(event)
onSecretSendReceived(event)
}
EventType.ROOM_KEY_WITHHELD -> {
@@ -828,7 +767,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
private fun onRoomKeyEvent(event: Event) {
val roomKeyContent = event.getClearContent().toModel() ?: return
- Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>")
+ Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>")
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : missing fields")
return
@@ -935,19 +874,9 @@ internal class DefaultCryptoService @Inject constructor(
}
private fun getRoomUserIds(roomId: String): List {
- var userIds: List = emptyList()
- monarchy.doWithRealm { realm ->
- // Check whether the event content must be encrypted for the invited members.
- val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
- && shouldEncryptForInvitedMembers(roomId)
-
- userIds = if (encryptForInvitedMembers) {
- RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
- } else {
- RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds()
- }
- }
- return userIds
+ val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
+ && shouldEncryptForInvitedMembers(roomId)
+ return cryptoSessionInfoProvider.getRoomUserIds(roomId, encryptForInvitedMembers)
}
/**
@@ -1257,38 +1186,38 @@ internal class DefaultCryptoService @Inject constructor(
incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
}
- private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
- val deviceKey = deviceInfo.identityKey()
-
- val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
- val now = System.currentTimeMillis()
- if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
- Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
- return
- }
-
- Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
- lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
-
- cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
- ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
-
- // Now send a blank message on that session so the other side knows about it.
- // (The keyshare request is sent in the clear so that won't do)
- // We send this first such that, as long as the toDevice messages arrive in the
- // same order we sent them, the other end will get this first, set up the new session,
- // then get the keyshare request and send the key over this new session (because it
- // is the session it has most recently received a message on).
- val payloadJson = mapOf("type" to EventType.DUMMY)
-
- val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
- val sendToDeviceMap = MXUsersDevicesMap()
- sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
- Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
- val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
- sendToDeviceTask.execute(sendToDeviceParams)
- }
- }
+// private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
+// val deviceKey = deviceInfo.identityKey()
+//
+// val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
+// val now = System.currentTimeMillis()
+// if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
+// Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
+// return
+// }
+//
+// Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
+// lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
+//
+// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+// ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
+//
+// // Now send a blank message on that session so the other side knows about it.
+// // (The keyshare request is sent in the clear so that won't do)
+// // We send this first such that, as long as the toDevice messages arrive in the
+// // same order we sent them, the other end will get this first, set up the new session,
+// // then get the keyshare request and send the key over this new session (because it
+// // is the session it has most recently received a message on).
+// val payloadJson = mapOf("type" to EventType.DUMMY)
+//
+// val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
+// val sendToDeviceMap = MXUsersDevicesMap()
+// sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
+// Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
+// val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
+// sendToDeviceTask.execute(sendToDeviceParams)
+// }
+// }
/**
* Provides the list of unknown devices
@@ -1339,14 +1268,26 @@ internal class DefaultCryptoService @Inject constructor(
return cryptoStore.getOutgoingRoomKeyRequests()
}
+ override fun getOutgoingRoomKeyRequestsPaged(): LiveData> {
+ return cryptoStore.getOutgoingRoomKeyRequestsPaged()
+ }
+
+ override fun getIncomingRoomKeyRequestsPaged(): LiveData> {
+ return cryptoStore.getIncomingRoomKeyRequestsPaged()
+ }
+
override fun getIncomingRoomKeyRequests(): List {
return cryptoStore.getIncomingRoomKeyRequests()
}
- override fun getGossipingEventsTrail(): List {
+ override fun getGossipingEventsTrail(): LiveData> {
return cryptoStore.getGossipingEventsTrail()
}
+ override fun getGossipingEvents(): List {
+ return cryptoStore.getGossipingEvents()
+ }
+
override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap {
return cryptoStore.getSharedWithInfo(roomId, sessionId)
}
@@ -1354,6 +1295,11 @@ internal class DefaultCryptoService @Inject constructor(
override fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? {
return cryptoStore.getWithHeldMegolmSession(roomId, sessionId)
}
+
+ override fun logDbUsageInfo() {
+ cryptoStore.logDbUsageInfo()
+ }
+
/* ==========================================================================================
* For test only
* ========================================================================================== */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
index ab30d3052d..42df6b354b 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
@@ -377,7 +377,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
}
// Update devices trust for these users
- dispatchDeviceChange(downloadUsers)
+ // dispatchDeviceChange(downloadUsers)
return onKeysDownloadSucceed(filteredUsers, response.failures)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
new file mode 100644
index 0000000000..38488f1ca7
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
@@ -0,0 +1,169 @@
+/*
+ * 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.crypto
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.api.MatrixCallback
+import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+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.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
+import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
+import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
+import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
+import org.matrix.android.sdk.internal.extensions.foldToCallback
+import org.matrix.android.sdk.internal.session.SessionScope
+import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.jvm.Throws
+
+@SessionScope
+internal class EventDecryptor @Inject constructor(
+ private val cryptoCoroutineScope: CoroutineScope,
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
+ private val roomDecryptorProvider: RoomDecryptorProvider,
+ private val messageEncrypter: MessageEncrypter,
+ private val sendToDeviceTask: SendToDeviceTask,
+ private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
+ private val cryptoStore: IMXCryptoStore
+) {
+
+ // The date of the last time we forced establishment
+ // of a new session for each user:device.
+ private val lastNewSessionForcedDates = MXUsersDevicesMap()
+
+ /**
+ * Decrypt an event
+ *
+ * @param event the raw event.
+ * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
+ * @return the MXEventDecryptionResult data, or throw in case of error
+ */
+ @Throws(MXCryptoError::class)
+ fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+ return internalDecryptEvent(event, timeline)
+ }
+
+ /**
+ * Decrypt an event asynchronously
+ *
+ * @param event the raw event.
+ * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
+ * @param callback the callback to return data or null
+ */
+ fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) {
+ // is it needed to do that on the crypto scope??
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+ runCatching {
+ internalDecryptEvent(event, timeline)
+ }.foldToCallback(callback)
+ }
+ }
+
+ /**
+ * Decrypt an event
+ *
+ * @param event the raw event.
+ * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
+ * @return the MXEventDecryptionResult data, or null in case of error
+ */
+ @Throws(MXCryptoError::class)
+ private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+ val eventContent = event.content
+ if (eventContent == null) {
+ Timber.e("## CRYPTO | decryptEvent : empty event content")
+ throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
+ } else {
+ val algorithm = eventContent["algorithm"]?.toString()
+ val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
+ if (alg == null) {
+ val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
+ Timber.e("## CRYPTO | decryptEvent() : $reason")
+ throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
+ } else {
+ try {
+ return alg.decryptEvent(event, timeline)
+ } catch (mxCryptoError: MXCryptoError) {
+ Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
+ if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
+ if (mxCryptoError is MXCryptoError.Base
+ && mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
+ // need to find sending device
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+ val olmContent = event.content.toModel()
+ cryptoStore.getUserDevices(event.senderId ?: "")
+ ?.values
+ ?.firstOrNull { it.identityKey() == olmContent?.senderKey }
+ ?.let {
+ markOlmSessionForUnwedging(event.senderId ?: "", it)
+ }
+ ?: run {
+ Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device")
+ }
+ }
+ }
+ }
+ throw mxCryptoError
+ }
+ }
+ }
+ }
+
+ // coroutineDispatchers.crypto scope
+ private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
+ val deviceKey = deviceInfo.identityKey()
+
+ val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
+ val now = System.currentTimeMillis()
+ if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
+ Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
+ return
+ }
+
+ Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
+ lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
+
+ // offload this from crypto thread (?)
+ cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
+ ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
+
+ // Now send a blank message on that session so the other side knows about it.
+ // (The keyshare request is sent in the clear so that won't do)
+ // We send this first such that, as long as the toDevice messages arrive in the
+ // same order we sent them, the other end will get this first, set up the new session,
+ // then get the keyshare request and send the key over this new session (because it
+ // is the session it has most recently received a message on).
+ val payloadJson = mapOf("type" to EventType.DUMMY)
+
+ val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
+ val sendToDeviceMap = MXUsersDevicesMap()
+ sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
+ Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
+ withContext(coroutineDispatchers.io) {
+ val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
+ sendToDeviceTask.execute(sendToDeviceParams)
+ }
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
new file mode 100644
index 0000000000..06c667ee4a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.crypto
+
+import android.util.LruCache
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
+import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
+import timber.log.Timber
+import java.util.Timer
+import java.util.TimerTask
+import javax.inject.Inject
+
+/**
+ * Allows to cache and batch store operations on inbound group session store.
+ * Because it is used in the decrypt flow, that can be called quite rapidly
+ */
+internal class InboundGroupSessionStore @Inject constructor(
+ private val store: IMXCryptoStore,
+ private val cryptoCoroutineScope: CoroutineScope,
+ private val coroutineDispatchers: MatrixCoroutineDispatchers) {
+
+ private data class CacheKey(
+ val sessionId: String,
+ val senderKey: String
+ )
+
+ private val sessionCache = object : LruCache(30) {
+ override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: OlmInboundGroupSessionWrapper2?, newValue: OlmInboundGroupSessionWrapper2?) {
+ if (evicted && oldValue != null) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+ Timber.v("## Inbound: entryRemoved ${oldValue.roomId}-${oldValue.senderKey}")
+ store.storeInboundGroupSessions(listOf(oldValue))
+ }
+ }
+ }
+ }
+
+ private val timer = Timer()
+ private var timerTask: TimerTask? = null
+
+ private val dirtySession = mutableListOf()
+
+ @Synchronized
+ fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
+ synchronized(sessionCache) {
+ val known = sessionCache[CacheKey(sessionId, senderKey)]
+ Timber.v("## Inbound: getInboundGroupSession in cache ${known != null}")
+ return known ?: store.getInboundGroupSession(sessionId, senderKey)?.also {
+ Timber.v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
+ sessionCache.put(CacheKey(sessionId, senderKey), it)
+ }
+ }
+ }
+
+ @Synchronized
+ fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2) {
+ Timber.v("## Inbound: getInboundGroupSession mark as dirty ${wrapper.roomId}-${wrapper.senderKey}")
+ // We want to batch this a bit for performances
+ dirtySession.add(wrapper)
+
+ timerTask?.cancel()
+ timerTask = object : TimerTask() {
+ override fun run() {
+ batchSave()
+ }
+ }
+ timer.schedule(timerTask!!, 2_000)
+ }
+
+ @Synchronized
+ private fun batchSave() {
+ val toSave = mutableListOf().apply { addAll(dirtySession) }
+ dirtySession.clear()
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+ Timber.v("## Inbound: getInboundGroupSession batching save of ${dirtySession.size}")
+ tryOrNull {
+ store.storeInboundGroupSessions(toSave)
+ }
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt
index 8869e73432..97ae0b9d83 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt
@@ -38,6 +38,7 @@ import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import timber.log.Timber
+import java.util.concurrent.Executors
import javax.inject.Inject
@SessionScope
@@ -52,6 +53,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope) {
+ private val executor = Executors.newSingleThreadExecutor()
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
// we received in the current sync.
private val receivedGossipingRequests = ArrayList()
@@ -64,6 +66,10 @@ internal class IncomingGossipingRequestManager @Inject constructor(
receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
}
+ fun close() {
+ executor.shutdownNow()
+ }
+
// Recently verified devices (map of deviceId and timestamp)
private val recentlyVerifiedDevices = HashMap()
@@ -99,7 +105,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
fun onGossipingRequestEvent(event: Event) {
Timber.v("## CRYPTO | GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
val roomKeyShare = event.getClearContent().toModel()
- val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
+ // val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
when (roomKeyShare?.action) {
GossipingToDeviceObject.ACTION_SHARE_REQUEST -> {
if (event.getClearType() == EventType.REQUEST_SECRET) {
@@ -108,8 +114,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
// ignore, it was sent by me as *
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
} else {
- // save in DB
- cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
+// // save in DB
+// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
receivedGossipingRequests.add(it)
}
}
@@ -119,7 +125,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
// ignore, it was sent by me as *
Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
} else {
- cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
+// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
receivedGossipingRequests.add(it)
}
}
@@ -144,13 +150,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
fun processReceivedGossipingRequests() {
val roomKeyRequestsToProcess = receivedGossipingRequests.toList()
receivedGossipingRequests.clear()
- for (request in roomKeyRequestsToProcess) {
- if (request is IncomingRoomKeyRequest) {
- processIncomingRoomKeyRequest(request)
- } else if (request is IncomingSecretShareRequest) {
- processIncomingSecretShareRequest(request)
- }
- }
+
+ Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : ${roomKeyRequestsToProcess.size} request to process")
var receivedRequestCancellations: List? = null
@@ -161,24 +162,35 @@ internal class IncomingGossipingRequestManager @Inject constructor(
}
}
- receivedRequestCancellations?.forEach { request ->
- Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
- // we should probably only notify the app of cancellations we told it
- // about, but we don't currently have a record of that, so we just pass
- // everything through.
- if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) {
- // ignore remote echo
- return@forEach
+ executor.execute {
+ cryptoStore.storeIncomingGossipingRequests(roomKeyRequestsToProcess)
+ for (request in roomKeyRequestsToProcess) {
+ if (request is IncomingRoomKeyRequest) {
+ processIncomingRoomKeyRequest(request)
+ } else if (request is IncomingSecretShareRequest) {
+ processIncomingSecretShareRequest(request)
+ }
}
- val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "")
- if (matchingIncoming == null) {
- // ignore that?
- return@forEach
- } else {
- // If it was accepted from this device, keep the information, do not mark as cancelled
- if (matchingIncoming.state != GossipingRequestState.ACCEPTED) {
- onRoomKeyRequestCancellation(request)
- cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER)
+
+ receivedRequestCancellations?.forEach { request ->
+ Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
+ // we should probably only notify the app of cancellations we told it
+ // about, but we don't currently have a record of that, so we just pass
+ // everything through.
+ if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) {
+ // ignore remote echo
+ return@forEach
+ }
+ val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "")
+ if (matchingIncoming == null) {
+ // ignore that?
+ return@forEach
+ } else {
+ // If it was accepted from this device, keep the information, do not mark as cancelled
+ if (matchingIncoming.state != GossipingRequestState.ACCEPTED) {
+ onRoomKeyRequestCancellation(request)
+ cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER)
+ }
}
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
index 7a546993b8..1a4d1136c8 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
@@ -44,7 +44,9 @@ internal class MXOlmDevice @Inject constructor(
/**
* The store where crypto data is saved.
*/
- private val store: IMXCryptoStore) {
+ private val store: IMXCryptoStore,
+ private val inboundGroupSessionStore: InboundGroupSessionStore
+ ) {
/**
* @return the Curve25519 key for the account.
@@ -657,7 +659,7 @@ internal class MXOlmDevice @Inject constructor(
timelineSet.add(messageIndexKey)
}
- store.storeInboundGroupSessions(listOf(session))
+ inboundGroupSessionStore.storeInBoundGroupSession(session)
val payload = try {
val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE)
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
@@ -745,7 +747,7 @@ internal class MXOlmDevice @Inject constructor(
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
}
- val session = store.getInboundGroupSession(sessionId, senderKey)
+ val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
if (session != null) {
// Check that the room id matches the original one for the session. This stops
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt
index efda663230..c86f2be0a3 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt
@@ -88,7 +88,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
* @param requestBody requestBody
*/
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
- cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
cancelRoomKeyRequest(requestBody, false)
}
}
@@ -99,7 +99,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
* @param requestBody requestBody
*/
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
- cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
cancelRoomKeyRequest(requestBody, true)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
index 1185ea7962..e55cf37118 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
@@ -16,10 +16,13 @@
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
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.events.model.EventType
import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
@@ -39,6 +42,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
+import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.convertToUTF8
import timber.log.Timber
@@ -54,7 +58,9 @@ internal class MXMegolmEncryption(
private val sendToDeviceTask: SendToDeviceTask,
private val messageEncrypter: MessageEncrypter,
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
- private val taskExecutor: TaskExecutor
+ private val taskExecutor: TaskExecutor,
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
+ private val cryptoCoroutineScope: CoroutineScope
) : IMXEncrypting {
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
@@ -84,15 +90,18 @@ internal class MXMegolmEncryption(
}
private fun notifyWithheldForSession(devices: MXUsersDevicesMap, outboundSession: MXOutboundSessionInfo) {
- mutableListOf>().apply {
- devices.forEach { userId, deviceId, withheldCode ->
- this.add(UserDevice(userId, deviceId) to withheldCode)
+ // offload to computation thread
+ cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
+ mutableListOf>().apply {
+ devices.forEach { userId, deviceId, withheldCode ->
+ this.add(UserDevice(userId, deviceId) to withheldCode)
+ }
+ }.groupBy(
+ { it.second },
+ { it.first }
+ ).forEach { (code, targets) ->
+ notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code)
}
- }.groupBy(
- { it.second },
- { it.first }
- ).forEach { (code, targets) ->
- notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code)
}
}
@@ -247,6 +256,15 @@ internal class MXMegolmEncryption(
for ((userId, devicesToShareWith) in devicesByUser) {
for ((deviceId) in devicesToShareWith) {
session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex)
+ cryptoStore.saveGossipingEvent(Event(
+ type = EventType.ROOM_KEY,
+ senderId = credentials.userId,
+ content = submap.apply {
+ this["session_key"] = ""
+ // we add a fake key for trail
+ this["_dest"] = "$userId|$deviceId"
+ }
+ ))
}
}
@@ -420,7 +438,7 @@ internal class MXMegolmEncryption(
sendToDeviceTask.execute(sendToDeviceParams)
true
} catch (failure: Throwable) {
- Timber.v("## CRYPTO | CRYPTO | reshareKey() : fail to send <$sessionId> to $userId:$deviceId")
+ Timber.e(failure, "## CRYPTO | CRYPTO | reshareKey() : fail to send <$sessionId> to $userId:$deviceId")
false
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt
index 8f651692fc..f0cc15fb63 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt
@@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.crypto.algorithms.megolm
+import kotlinx.coroutines.CoroutineScope
import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
@@ -26,6 +27,7 @@ import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepo
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.task.TaskExecutor
+import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import javax.inject.Inject
internal class MXMegolmEncryptionFactory @Inject constructor(
@@ -38,7 +40,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
private val sendToDeviceTask: SendToDeviceTask,
private val messageEncrypter: MessageEncrypter,
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
- private val taskExecutor: TaskExecutor) {
+ private val taskExecutor: TaskExecutor,
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
+ private val cryptoCoroutineScope: CoroutineScope) {
fun create(roomId: String): MXMegolmEncryption {
return MXMegolmEncryption(
@@ -52,7 +56,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
sendToDeviceTask,
messageEncrypter,
warnOnUnknownDevicesRepository,
- taskExecutor
+ taskExecutor,
+ coroutineDispatchers,
+ cryptoCoroutineScope
)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt
index b5056a0efd..1871dba0e2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt
@@ -17,6 +17,8 @@
package org.matrix.android.sdk.internal.crypto.crosssigning
import androidx.lifecycle.LiveData
+import androidx.work.BackoffPolicy
+import androidx.work.ExistingWorkPolicy
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
@@ -39,15 +41,20 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.withoutPrefix
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-import org.greenrobot.eventbus.EventBus
+import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.internal.di.SessionId
+import org.matrix.android.sdk.internal.di.WorkManagerProvider
+import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility
import timber.log.Timber
+import java.util.concurrent.TimeUnit
import javax.inject.Inject
@SessionScope
internal class DefaultCrossSigningService @Inject constructor(
@UserId private val userId: String,
+ @SessionId private val sessionId: String,
private val cryptoStore: IMXCryptoStore,
private val deviceListManager: DeviceListManager,
private val initializeCrossSigningTask: InitializeCrossSigningTask,
@@ -55,7 +62,7 @@ internal class DefaultCrossSigningService @Inject constructor(
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope,
- private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
+ private val workManagerProvider: WorkManagerProvider) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
private var olmUtility: OlmUtility? = null
@@ -360,6 +367,12 @@ internal class DefaultCrossSigningService @Inject constructor(
// First let's get my user key
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
+ checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId))
+
+ return UserTrustResult.Success
+ }
+
+ fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult {
val myUserKey = myCrossSigningInfo?.userKey()
?: return UserTrustResult.CrossSigningNotConfigured(userId)
@@ -368,15 +381,15 @@ internal class DefaultCrossSigningService @Inject constructor(
}
// Let's get the other user master key
- val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey()
- ?: return UserTrustResult.UnknownCrossSignatureInfo(otherUserId)
+ val otherMasterKey = otherInfo?.masterKey()
+ ?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "")
val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
?.get(userId) // Signatures made by me
?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
- Timber.d("## CrossSigning checkUserTrust false for $otherUserId, not signed by my UserSigningKey")
+ Timber.d("## CrossSigning checkUserTrust false for ${otherInfo.userId}, not signed by my UserSigningKey")
return UserTrustResult.KeyNotSigned(otherMasterKey)
}
@@ -396,6 +409,15 @@ internal class DefaultCrossSigningService @Inject constructor(
// and that MSK is trusted (i know the private key, or is signed by a trusted device)
val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
+ return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(userId))
+ }
+
+ fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List?): UserTrustResult {
+ // Special case when it's me,
+ // I have to check that MSK -> USK -> SSK
+ // and that MSK is trusted (i know the private key, or is signed by a trusted device)
+// val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
+
val myMasterKey = myCrossSigningInfo?.masterKey()
?: return UserTrustResult.CrossSigningNotConfigured(userId)
@@ -423,7 +445,7 @@ internal class DefaultCrossSigningService @Inject constructor(
// Maybe it's signed by a locally trusted device?
myMasterKey.signatures?.get(userId)?.forEach { (key, value) ->
val potentialDeviceId = key.withoutPrefix("ed25519:")
- val potentialDevice = cryptoStore.getUserDevice(userId, potentialDeviceId)
+ val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId)
if (potentialDevice != null && potentialDevice.isVerified) {
// Check signature validity?
try {
@@ -561,6 +583,8 @@ internal class DefaultCrossSigningService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
checkSelfTrust()
+ // re-verify all trusts
+ onUsersDeviceUpdate(listOf(userId))
}
}
@@ -666,6 +690,55 @@ internal class DefaultCrossSigningService @Inject constructor(
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
}
+ fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo) : DeviceTrustResult {
+ val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified()
+ myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
+
+ if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
+
+ otherKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherDevice.userId))
+
+ // TODO should we force verification ?
+ if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys))
+
+ // Check if the trust chain is valid
+ /*
+ * ┏━━━━━━━━┓ ┏━━━━━━━━┓
+ * ┃ ALICE ┃ ┃ BOB ┃
+ * ┗━━━━━━━━┛ ┗━━━━━━━━┛
+ * MSK ┌────────────▶MSK
+ * │
+ * │ │ │
+ * │ SSK │ └──▶ SSK ──────────────────┐
+ * │ │ │
+ * │ │ USK │
+ * └──▶ USK ────────────┘ (not visible by │
+ * Alice) │
+ * ▼
+ * ┌──────────────┐
+ * │ BOB's Device │
+ * └──────────────┘
+ */
+
+ val otherSSKSignature = otherDevice.signatures?.get(otherKeys.userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
+ ?: return legacyFallbackTrust(
+ locallyTrusted,
+ DeviceTrustResult.MissingDeviceSignature(otherDevice.deviceId, otherKeys.selfSigningKey()
+ ?.unpaddedBase64PublicKey
+ ?: ""
+ )
+ )
+
+ // Check bob's device is signed by bob's SSK
+ try {
+ olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
+ } catch (e: Throwable) {
+ return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e))
+ }
+
+ return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
+ }
+
private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult {
return if (locallyTrusted == true) {
DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true))
@@ -675,36 +748,18 @@ internal class DefaultCrossSigningService @Inject constructor(
}
override fun onUsersDeviceUpdate(userIds: List) {
- cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
- Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users")
- userIds.forEach { otherUserId ->
- checkUserTrust(otherUserId).let {
- Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
- setUserKeysAsTrusted(otherUserId, it.isVerified())
- }
- }
- }
+ Timber.d("## CrossSigning - onUsersDeviceUpdate for $userIds")
+ val workerParams = UpdateTrustWorker.Params(sessionId = sessionId, updatedUserIds = userIds)
+ val workerData = WorkerParamsFactory.toData(workerParams)
- // now check device trust
- cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
- userIds.forEach { otherUserId ->
- // TODO if my keys have changes, i should recheck all devices of all users?
- val devices = cryptoStore.getUserDeviceList(otherUserId)
- devices?.forEach { device ->
- val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
- Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
- cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
- }
+ val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder()
+ .setInputData(workerData)
+ .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
+ .build()
- if (otherUserId == userId) {
- // It's me, i should check if a newly trusted device is signing my master key
- // In this case it will change my MSK trust, and should then re-trigger a check of all other user trust
- setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified())
- }
- }
-
- eventBus.post(CryptoToSessionUserTrustChange(userIds))
- }
+ workManagerProvider.workManager
+ .beginUniqueWork("TRUST_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
+ .enqueue()
}
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt
deleted file mode 100644
index 05ceba5965..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * 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.crypto.crosssigning
-
-import org.matrix.android.sdk.api.extensions.orFalse
-import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
-import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
-import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
-import org.matrix.android.sdk.internal.database.query.where
-import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.session.SessionLifecycleObserver
-import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
-import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
-import org.matrix.android.sdk.internal.task.TaskExecutor
-import org.matrix.android.sdk.internal.util.createBackgroundHandler
-import io.realm.Realm
-import io.realm.RealmConfiguration
-import kotlinx.coroutines.android.asCoroutineDispatcher
-import kotlinx.coroutines.launch
-import org.greenrobot.eventbus.EventBus
-import org.greenrobot.eventbus.Subscribe
-import timber.log.Timber
-import java.util.concurrent.atomic.AtomicBoolean
-import java.util.concurrent.atomic.AtomicReference
-import javax.inject.Inject
-
-internal class ShieldTrustUpdater @Inject constructor(
- private val eventBus: EventBus,
- private val computeTrustTask: ComputeTrustTask,
- private val taskExecutor: TaskExecutor,
- @SessionDatabase private val sessionRealmConfiguration: RealmConfiguration,
- private val roomSummaryUpdater: RoomSummaryUpdater
-) : SessionLifecycleObserver {
-
- companion object {
- private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD")
- private val BACKGROUND_HANDLER_DISPATCHER = BACKGROUND_HANDLER.asCoroutineDispatcher()
- }
-
- private val backgroundSessionRealm = AtomicReference()
-
- private val isStarted = AtomicBoolean()
-
- override fun onStart() {
- if (isStarted.compareAndSet(false, true)) {
- eventBus.register(this)
- BACKGROUND_HANDLER.post {
- backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration))
- }
- }
- }
-
- override fun onStop() {
- if (isStarted.compareAndSet(true, false)) {
- eventBus.unregister(this)
- BACKGROUND_HANDLER.post {
- backgroundSessionRealm.getAndSet(null).also {
- it?.close()
- }
- }
- }
- }
-
- @Subscribe
- fun onRoomMemberChange(update: SessionToCryptoRoomMembersUpdate) {
- if (!isStarted.get()) {
- return
- }
- taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) {
- val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds, update.isDirect))
- // We need to send that back to session base
- backgroundSessionRealm.get()?.executeTransaction { realm ->
- roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust)
- }
- }
- }
-
- @Subscribe
- fun onTrustUpdate(update: CryptoToSessionUserTrustChange) {
- if (!isStarted.get()) {
- return
- }
- onCryptoDevicesChange(update.userIds)
- }
-
- private fun onCryptoDevicesChange(users: List) {
- taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) {
- val realm = backgroundSessionRealm.get() ?: return@launch
- val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java)
- .`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray())
- .distinct(RoomMemberSummaryEntityFields.ROOM_ID)
- .findAll()
- .map { it.roomId }
-
- distinctRoomIds.forEach { roomId ->
- val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
- if (roomSummary?.isEncrypted.orFalse()) {
- val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
- try {
- val updatedTrust = computeTrustTask.execute(
- ComputeTrustTask.Params(allActiveRoomMembers, roomSummary?.isDirect == true)
- )
- realm.executeTransaction {
- roomSummaryUpdater.updateShieldTrust(it, roomId, updatedTrust)
- }
- } catch (failure: Throwable) {
- Timber.e(failure)
- }
- }
- }
- }
- }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
new file mode 100644
index 0000000000..f28fe7d642
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
@@ -0,0 +1,322 @@
+/*
+ * 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.crypto.crosssigning
+
+import android.content.Context
+import androidx.work.WorkerParameters
+import com.squareup.moshi.JsonClass
+import io.realm.Realm
+import io.realm.RealmConfiguration
+import io.realm.kotlin.where
+import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
+import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
+import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
+import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
+import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
+import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
+import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
+import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.di.CryptoDatabase
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.session.SessionComponent
+import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
+import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
+import org.matrix.android.sdk.internal.worker.SessionWorkerParams
+import timber.log.Timber
+import javax.inject.Inject
+
+internal class UpdateTrustWorker(context: Context,
+ params: WorkerParameters)
+ : SessionSafeCoroutineWorker(context, params, Params::class.java) {
+
+ @JsonClass(generateAdapter = true)
+ internal data class Params(
+ override val sessionId: String,
+ override val lastFailureMessage: String? = null,
+ val updatedUserIds: List
+ ) : SessionWorkerParams
+
+ @Inject lateinit var crossSigningService: DefaultCrossSigningService
+
+ // It breaks the crypto store contract, but we need to batch things :/
+ @CryptoDatabase @Inject lateinit var realmConfiguration: RealmConfiguration
+ @UserId @Inject lateinit var myUserId: String
+ @Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper
+ @SessionDatabase @Inject lateinit var sessionRealmConfiguration: RealmConfiguration
+
+ // @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater
+ @Inject lateinit var cryptoStore: IMXCryptoStore
+
+ override fun injectWith(injector: SessionComponent) {
+ injector.inject(this)
+ }
+
+ override suspend fun doSafeWork(params: Params): Result {
+ var userList = params.updatedUserIds
+ // Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
+ // or a new device?) So we check all again :/
+
+ Timber.d("## CrossSigning - Updating trust for $userList")
+
+ // First we check that the users MSK are trusted by mine
+ // After that we check the trust chain for each devices of each users
+ Realm.getInstance(realmConfiguration).use { realm ->
+ realm.executeTransaction {
+ // By mapping here to model, this object is not live
+ // I should update it if needed
+ var myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
+ .equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
+ .findFirst()?.let { mapCrossSigningInfoEntity(it) }
+
+ var myTrustResult: UserTrustResult? = null
+
+ if (userList.contains(myUserId)) {
+ Timber.d("## CrossSigning - Clear all trust as a change on my user was detected")
+ // i am in the list.. but i don't know exactly the delta of change :/
+ // If it's my cross signing keys we should refresh all trust
+ // do it anyway ?
+ userList = realm.where(CrossSigningInfoEntity::class.java)
+ .findAll().mapNotNull { it.userId }
+ Timber.d("## CrossSigning - Updating trust for all $userList")
+
+ // check right now my keys and mark it as trusted as other trust depends on it
+ val myDevices = realm.where()
+ .equalTo(UserEntityFields.USER_ID, myUserId)
+ .findFirst()
+ ?.devices
+ ?.map { deviceInfo ->
+ CryptoMapper.mapToModel(deviceInfo)
+ }
+ myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices).also {
+ updateCrossSigningKeysTrust(realm, myUserId, it.isVerified())
+ // update model reference
+ myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
+ .equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
+ .findFirst()?.let { mapCrossSigningInfoEntity(it) }
+ }
+ }
+
+ val otherInfos = userList.map {
+ it to realm.where(CrossSigningInfoEntity::class.java)
+ .equalTo(CrossSigningInfoEntityFields.USER_ID, it)
+ .findFirst()?.let { mapCrossSigningInfoEntity(it) }
+ }
+ .toMap()
+
+ val trusts = otherInfos.map { infoEntry ->
+ infoEntry.key to when (infoEntry.key) {
+ myUserId -> myTrustResult
+ else -> {
+ crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, infoEntry.value).also {
+ Timber.d("## CrossSigning - user:${infoEntry.key} result:$it")
+ }
+ }
+ }
+ }.toMap()
+
+ // TODO! if it's me and my keys has changed... I have to reset trust for everyone!
+ // i have all the new trusts, update DB
+ trusts.forEach {
+ val verified = it.value?.isVerified() == true
+ updateCrossSigningKeysTrust(realm, it.key, verified)
+ }
+
+ // Ok so now we have to check device trust for all these users..
+ Timber.v("## CrossSigning - Updating devices cross trust users ${trusts.keys}")
+ trusts.keys.forEach {
+ val devicesEntities = realm.where()
+ .equalTo(UserEntityFields.USER_ID, it)
+ .findFirst()
+ ?.devices
+
+ val trustMap = devicesEntities?.map { device ->
+ // get up to date from DB has could have been updated
+ val otherInfo = realm.where(CrossSigningInfoEntity::class.java)
+ .equalTo(CrossSigningInfoEntityFields.USER_ID, it)
+ .findFirst()?.let { mapCrossSigningInfoEntity(it) }
+ device to crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfo, CryptoMapper.mapToModel(device))
+ }?.toMap()
+
+ // Update trust if needed
+ devicesEntities?.forEach { device ->
+ val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
+ Timber.d("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
+ if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
+ Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
+ // need to save
+ val trustEntity = device.trustLevelEntity
+ if (trustEntity == null) {
+ realm.createObject(TrustLevelEntity::class.java).let {
+ it.locallyVerified = false
+ it.crossSignedVerified = crossSignedVerified
+ device.trustLevelEntity = it
+ }
+ } else {
+ trustEntity.crossSignedVerified = crossSignedVerified
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // So Cross Signing keys trust is updated, device trust is updated
+ // We can now update room shields? in the session DB?
+
+ Timber.d("## CrossSigning - Updating shields for impacted rooms...")
+ Realm.getInstance(sessionRealmConfiguration).use { it ->
+ it.executeTransaction { realm ->
+ val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java)
+ .`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray())
+ .distinct(RoomMemberSummaryEntityFields.ROOM_ID)
+ .findAll()
+ .map { it.roomId }
+ Timber.d("## CrossSigning - ... impacted rooms $distinctRoomIds")
+ distinctRoomIds.forEach { roomId ->
+ val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
+ if (roomSummary?.isEncrypted == true) {
+ Timber.d("## CrossSigning - Check shield state for room $roomId")
+ val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
+ try {
+ val updatedTrust = computeRoomShield(allActiveRoomMembers, roomSummary)
+ if (roomSummary.roomEncryptionTrustLevel != updatedTrust) {
+ Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust")
+ roomSummary.roomEncryptionTrustLevel = updatedTrust
+ }
+ } catch (failure: Throwable) {
+ Timber.e(failure)
+ }
+ }
+ }
+ }
+ }
+
+ return Result.success()
+ }
+
+ private fun updateCrossSigningKeysTrust(realm: Realm, userId: String, verified: Boolean) {
+ val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java)
+ .equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
+ .findFirst()
+ xInfoEntity?.crossSigningKeys?.forEach { info ->
+ // optimization to avoid trigger updates when there is no change..
+ if (info.trustLevelEntity?.isVerified() != verified) {
+ Timber.d("## CrossSigning - Trust change for $userId : $verified")
+ val level = info.trustLevelEntity
+ if (level == null) {
+ val newLevel = realm.createObject(TrustLevelEntity::class.java)
+ newLevel.locallyVerified = verified
+ newLevel.crossSignedVerified = verified
+ info.trustLevelEntity = newLevel
+ } else {
+ level.locallyVerified = verified
+ level.crossSignedVerified = verified
+ }
+ }
+ }
+ }
+
+ private fun computeRoomShield(activeMemberUserIds: List, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
+ Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> $activeMemberUserIds")
+ // The set of “all users” depends on the type of room:
+ // For regular / topic rooms, all users including yourself, are considered when decorating a room
+ // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
+ val listToCheck = if (roomSummaryEntity.isDirect) {
+ activeMemberUserIds.filter { it != myUserId }
+ } else {
+ activeMemberUserIds
+ }
+
+ val allTrustedUserIds = listToCheck
+ .filter { userId ->
+ Realm.getInstance(realmConfiguration).use {
+ it.where(CrossSigningInfoEntity::class.java)
+ .equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
+ .findFirst()?.let { mapCrossSigningInfoEntity(it) }?.isTrusted() == true
+ }
+ }
+ val myCrossKeys = Realm.getInstance(realmConfiguration).use {
+ it.where(CrossSigningInfoEntity::class.java)
+ .equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
+ .findFirst()?.let { mapCrossSigningInfoEntity(it) }
+ }
+
+ return if (allTrustedUserIds.isEmpty()) {
+ RoomEncryptionTrustLevel.Default
+ } else {
+ // If one of the verified user as an untrusted device -> warning
+ // If all devices of all verified users are trusted -> green
+ // else -> black
+ allTrustedUserIds
+ .mapNotNull { uid ->
+ Realm.getInstance(realmConfiguration).use {
+ it.where()
+ .equalTo(UserEntityFields.USER_ID, uid)
+ .findFirst()
+ ?.devices
+ ?.map {
+ CryptoMapper.mapToModel(it)
+ }
+ }
+ }
+ .flatten()
+ .let { allDevices ->
+ Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }}")
+ if (myCrossKeys != null) {
+ allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
+ } else {
+ // Legacy method
+ allDevices.any { !it.isVerified }
+ }
+ }
+ .let { hasWarning ->
+ if (hasWarning) {
+ RoomEncryptionTrustLevel.Warning
+ } else {
+ if (listToCheck.size == allTrustedUserIds.size) {
+ // all users are trusted and all devices are verified
+ RoomEncryptionTrustLevel.Trusted
+ } else {
+ RoomEncryptionTrustLevel.Default
+ }
+ }
+ }
+ }
+ }
+
+ private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo {
+ val userId = xsignInfo.userId ?: ""
+ return MXCrossSigningInfo(
+ userId = userId,
+ crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
+ crossSigningKeysMapper.map(userId, it)
+ }
+ )
+ }
+
+ override fun buildErrorParams(params: Params, message: String): Params {
+ return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
index 64579c1b67..fbcf5cfdeb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
@@ -30,7 +30,6 @@ import org.matrix.android.sdk.api.listeners.StepProgressListener
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
-import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.MegolmSessionData
@@ -85,6 +84,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData
import org.matrix.olm.OlmException
import org.matrix.olm.OlmPkDecryption
import org.matrix.olm.OlmPkEncryption
@@ -170,7 +170,7 @@ internal class DefaultKeysBackupService @Inject constructor(
runCatching {
withContext(coroutineDispatchers.crypto) {
val olmPkDecryption = OlmPkDecryption()
- val megolmBackupAuthData = if (password != null) {
+ val signalableMegolmBackupAuthData = if (password != null) {
// Generate a private key from the password
val backgroundProgressListener = if (progressListener == null) {
null
@@ -189,7 +189,7 @@ internal class DefaultKeysBackupService @Inject constructor(
}
val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
- MegolmBackupAuthData(
+ SignalableMegolmBackupAuthData(
publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey),
privateKeySalt = generatePrivateKeyResult.salt,
privateKeyIterations = generatePrivateKeyResult.iterations
@@ -197,14 +197,17 @@ internal class DefaultKeysBackupService @Inject constructor(
} else {
val publicKey = olmPkDecryption.generateKey()
- MegolmBackupAuthData(
+ SignalableMegolmBackupAuthData(
publicKey = publicKey
)
}
- val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, megolmBackupAuthData.signalableJSONDictionary())
+ val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableMegolmBackupAuthData.signalableJSONDictionary())
- val signedMegolmBackupAuthData = megolmBackupAuthData.copy(
+ val signedMegolmBackupAuthData = MegolmBackupAuthData(
+ publicKey = signalableMegolmBackupAuthData.publicKey,
+ privateKeySalt = signalableMegolmBackupAuthData.privateKeySalt,
+ privateKeyIterations = signalableMegolmBackupAuthData.privateKeyIterations,
signatures = objectSigner.signObject(canonicalJson)
)
@@ -223,8 +226,7 @@ internal class DefaultKeysBackupService @Inject constructor(
@Suppress("UNCHECKED_CAST")
val createKeysBackupVersionBody = CreateKeysBackupVersionBody(
algorithm = keysBackupCreationInfo.algorithm,
- authData = MoshiProvider.providesMoshi().adapter(Map::class.java)
- .fromJson(keysBackupCreationInfo.authData?.toJsonString() ?: "") as JsonDict?
+ authData = keysBackupCreationInfo.authData.toJsonDict()
)
keysBackupStateManager.state = KeysBackupState.Enabling
@@ -234,7 +236,10 @@ internal class DefaultKeysBackupService @Inject constructor(
this.callback = object : MatrixCallback {
override fun onSuccess(data: KeysVersion) {
// Reset backup markers.
- cryptoStore.resetBackupMarkers()
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+ // move tx out of UI thread
+ cryptoStore.resetBackupMarkers()
+ }
val keyBackupVersion = KeysVersionResult(
algorithm = createKeysBackupVersionBody.algorithm,
@@ -242,7 +247,7 @@ internal class DefaultKeysBackupService @Inject constructor(
version = data.version,
// We can consider that the server does not have keys yet
count = 0,
- hash = null
+ hash = ""
)
enableKeysBackup(keyBackupVersion)
@@ -264,7 +269,7 @@ internal class DefaultKeysBackupService @Inject constructor(
withContext(coroutineDispatchers.crypto) {
// If we're currently backing up to this backup... stop.
// (We start using it automatically in createKeysBackupVersion so this is symmetrical).
- if (keysBackupVersion != null && version == keysBackupVersion!!.version) {
+ if (keysBackupVersion != null && version == keysBackupVersion?.version) {
resetKeysBackupData()
keysBackupVersion = null
keysBackupStateManager.state = KeysBackupState.Unknown
@@ -405,10 +410,7 @@ internal class DefaultKeysBackupService @Inject constructor(
val keysBackupVersionTrust = KeysBackupVersionTrust()
val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
- if (keysBackupVersion.algorithm == null
- || authData == null
- || authData.publicKey.isEmpty()
- || authData.signatures.isNullOrEmpty()) {
+ if (authData == null || authData.publicKey.isEmpty() || authData.signatures.isEmpty()) {
Timber.v("getKeysBackupTrust: Key backup is absent or missing required data")
return keysBackupVersionTrust
}
@@ -476,7 +478,7 @@ internal class DefaultKeysBackupService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
// Get current signatures, or create an empty set
- val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap() ?: HashMap()
+ val myUserSignatures = authData.signatures[userId].orEmpty().toMutableMap()
if (trust) {
// Add current device signature
@@ -495,26 +497,23 @@ internal class DefaultKeysBackupService @Inject constructor(
// Create an updated version of KeysVersionResult
val newMegolmBackupAuthData = authData.copy()
- val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap()
+ val newSignatures = newMegolmBackupAuthData.signatures.toMutableMap()
newSignatures[userId] = myUserSignatures
val newMegolmBackupAuthDataWithNewSignature = newMegolmBackupAuthData.copy(
signatures = newSignatures
)
- val moshi = MoshiProvider.providesMoshi()
- val adapter = moshi.adapter(Map::class.java)
-
@Suppress("UNCHECKED_CAST")
UpdateKeysBackupVersionBody(
algorithm = keysBackupVersion.algorithm,
- authData = adapter.fromJson(newMegolmBackupAuthDataWithNewSignature.toJsonString()) as Map?,
- version = keysBackupVersion.version!!)
+ authData = newMegolmBackupAuthDataWithNewSignature.toJsonDict(),
+ version = keysBackupVersion.version)
}
// And send it to the homeserver
updateKeysBackupVersionTask
- .configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody)) {
+ .configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version, updateKeysBackupVersionBody)) {
this.callback = object : MatrixCallback {
override fun onSuccess(data: Unit) {
// Relaunch the state machine on this updated backup version
@@ -596,7 +595,9 @@ internal class DefaultKeysBackupService @Inject constructor(
val importResult = awaitCallback {
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it)
}
- cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
+ withContext(coroutineDispatchers.crypto) {
+ cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
+ }
Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}")
} else {
Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}")
@@ -683,9 +684,9 @@ internal class DefaultKeysBackupService @Inject constructor(
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
// Get backed up keys from the homeserver
- val data = getKeys(sessionId, roomId, keysVersionResult.version!!)
+ val data = getKeys(sessionId, roomId, keysVersionResult.version)
- withContext(coroutineDispatchers.crypto) {
+ withContext(coroutineDispatchers.computation) {
val sessionsData = ArrayList()
// Restore that data
var sessionsFromHsCount = 0
@@ -1018,19 +1019,10 @@ internal class DefaultKeysBackupService @Inject constructor(
* @return the authentication if found and valid, null in other case
*/
private fun getMegolmBackupAuthData(keysBackupData: KeysVersionResult): MegolmBackupAuthData? {
- if (keysBackupData.version.isNullOrBlank()
- || keysBackupData.algorithm != MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
- || keysBackupData.authData == null) {
- return null
- }
-
- val authData = keysBackupData.getAuthDataAsMegolmBackupAuthData()
-
- if (authData?.signatures == null || authData.publicKey.isBlank()) {
- return null
- }
-
- return authData
+ return keysBackupData
+ .takeIf { it.version.isNotEmpty() && it.algorithm == MXCRYPTO_ALGORITHM_MEGOLM_BACKUP }
+ ?.getAuthDataAsMegolmBackupAuthData()
+ ?.takeIf { it.publicKey.isNotEmpty() }
}
/**
@@ -1118,32 +1110,29 @@ internal class DefaultKeysBackupService @Inject constructor(
* @param keysVersionResult backup information object as returned by [getCurrentVersion].
*/
private fun enableKeysBackup(keysVersionResult: KeysVersionResult) {
- if (keysVersionResult.authData != null) {
- val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData()
+ val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData()
- if (retrievedMegolmBackupAuthData != null) {
- keysBackupVersion = keysVersionResult
+ if (retrievedMegolmBackupAuthData != null) {
+ keysBackupVersion = keysVersionResult
+ cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
cryptoStore.setKeyBackupVersion(keysVersionResult.version)
-
- onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash)
-
- try {
- backupOlmPkEncryption = OlmPkEncryption().apply {
- setRecipientKey(retrievedMegolmBackupAuthData.publicKey)
- }
- } catch (e: OlmException) {
- Timber.e(e, "OlmException")
- keysBackupStateManager.state = KeysBackupState.Disabled
- return
- }
-
- keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
-
- maybeBackupKeys()
- } else {
- Timber.e("Invalid authentication data")
- keysBackupStateManager.state = KeysBackupState.Disabled
}
+
+ onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash)
+
+ try {
+ backupOlmPkEncryption = OlmPkEncryption().apply {
+ setRecipientKey(retrievedMegolmBackupAuthData.publicKey)
+ }
+ } catch (e: OlmException) {
+ Timber.e(e, "OlmException")
+ keysBackupStateManager.state = KeysBackupState.Disabled
+ return
+ }
+
+ keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
+
+ maybeBackupKeys()
} else {
Timber.e("Invalid authentication data")
keysBackupStateManager.state = KeysBackupState.Disabled
@@ -1153,11 +1142,11 @@ internal class DefaultKeysBackupService @Inject constructor(
/**
* Update the DB with data fetch from the server
*/
- private fun onServerDataRetrieved(count: Int?, hash: String?) {
+ private fun onServerDataRetrieved(count: Int?, etag: String?) {
cryptoStore.setKeysBackupData(KeysBackupDataEntity()
.apply {
backupLastServerNumberOfKeys = count
- backupLastServerHash = hash
+ backupLastServerHash = etag
}
)
}
@@ -1172,6 +1161,7 @@ internal class DefaultKeysBackupService @Inject constructor(
cryptoStore.setKeyBackupVersion(null)
cryptoStore.setKeysBackupData(null)
+ backupOlmPkEncryption?.releaseEncryption()
backupOlmPkEncryption = null
// Reset backup markers
@@ -1222,22 +1212,19 @@ internal class DefaultKeysBackupService @Inject constructor(
// Gather data to send to the homeserver
// roomId -> sessionId -> MXKeyBackupData
- val keysBackupData = KeysBackupData(
- roomIdToRoomKeysBackupData = HashMap()
- )
+ val keysBackupData = KeysBackupData()
- for (olmInboundGroupSessionWrapper in olmInboundGroupSessionWrappers) {
- val keyBackupData = encryptGroupSession(olmInboundGroupSessionWrapper)
- if (keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId] == null) {
- val roomKeysBackupData = RoomKeysBackupData(
- sessionIdToKeyBackupData = HashMap()
- )
- keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId!!] = roomKeysBackupData
- }
+ olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
+ val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach
+ val olmInboundGroupSession = olmInboundGroupSessionWrapper.olmInboundGroupSession ?: return@forEach
try {
- keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId]!!
- .sessionIdToKeyBackupData[olmInboundGroupSessionWrapper.olmInboundGroupSession!!.sessionIdentifier()] = keyBackupData
+ encryptGroupSession(olmInboundGroupSessionWrapper)
+ ?.let {
+ keysBackupData.roomIdToRoomKeysBackupData
+ .getOrPut(roomId) { RoomKeysBackupData() }
+ .sessionIdToKeyBackupData[olmInboundGroupSession.sessionIdentifier()] = it
+ }
} catch (e: OlmException) {
Timber.e(e, "OlmException")
}
@@ -1245,71 +1232,71 @@ internal class DefaultKeysBackupService @Inject constructor(
Timber.v("backupKeys: 4 - Sending request")
- val sendingRequestCallback = object : MatrixCallback {
- override fun onSuccess(data: BackupKeysResult) {
- uiHandler.post {
- Timber.v("backupKeys: 5a - Request complete")
+ // Make the request
+ val version = keysBackupVersion?.version ?: return@withContext
- // Mark keys as backed up
- cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)
+ storeSessionDataTask
+ .configureWith(StoreSessionsDataTask.Params(version, keysBackupData)) {
+ this.callback = object : MatrixCallback {
+ override fun onSuccess(data: BackupKeysResult) {
+ uiHandler.post {
+ Timber.v("backupKeys: 5a - Request complete")
- if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) {
- Timber.v("backupKeys: All keys have been backed up")
- onServerDataRetrieved(data.count, data.hash)
+ // Mark keys as backed up
+ cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)
- // Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess()
- keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
- } else {
- Timber.v("backupKeys: Continue to back up keys")
- keysBackupStateManager.state = KeysBackupState.WillBackUp
+ if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) {
+ Timber.v("backupKeys: All keys have been backed up")
+ onServerDataRetrieved(data.count, data.hash)
- backupKeys()
- }
- }
- }
+ // Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess()
+ keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
+ } else {
+ Timber.v("backupKeys: Continue to back up keys")
+ keysBackupStateManager.state = KeysBackupState.WillBackUp
- override fun onFailure(failure: Throwable) {
- if (failure is Failure.ServerError) {
- uiHandler.post {
- Timber.e(failure, "backupKeys: backupKeys failed.")
-
- when (failure.error.code) {
- MatrixError.M_NOT_FOUND,
- MatrixError.M_WRONG_ROOM_KEYS_VERSION -> {
- // Backup has been deleted on the server, or we are not using the last backup version
- keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
- backupAllGroupSessionsCallback?.onFailure(failure)
- resetBackupAllGroupSessionsListeners()
- resetKeysBackupData()
- keysBackupVersion = null
-
- // Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver
- checkAndStartKeysBackup()
+ backupKeys()
+ }
+ }
+ }
+
+ override fun onFailure(failure: Throwable) {
+ if (failure is Failure.ServerError) {
+ uiHandler.post {
+ Timber.e(failure, "backupKeys: backupKeys failed.")
+
+ when (failure.error.code) {
+ MatrixError.M_NOT_FOUND,
+ MatrixError.M_WRONG_ROOM_KEYS_VERSION -> {
+ // Backup has been deleted on the server, or we are not using the last backup version
+ keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
+ backupAllGroupSessionsCallback?.onFailure(failure)
+ resetBackupAllGroupSessionsListeners()
+ resetKeysBackupData()
+ keysBackupVersion = null
+
+ // Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver
+ checkAndStartKeysBackup()
+ }
+ else ->
+ // Come back to the ready state so that we will retry on the next received key
+ keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
+ }
+ }
+ } else {
+ uiHandler.post {
+ backupAllGroupSessionsCallback?.onFailure(failure)
+ resetBackupAllGroupSessionsListeners()
+
+ Timber.e("backupKeys: backupKeys failed.")
+
+ // Retry a bit later
+ keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
+ maybeBackupKeys()
+ }
}
- else ->
- // Come back to the ready state so that we will retry on the next received key
- keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
}
}
- } else {
- uiHandler.post {
- backupAllGroupSessionsCallback?.onFailure(failure)
- resetBackupAllGroupSessionsListeners()
-
- Timber.e("backupKeys: backupKeys failed.")
-
- // Retry a bit later
- keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
- maybeBackupKeys()
- }
- }
- }
- }
-
- // Make the request
- storeSessionDataTask
- .configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData)) {
- this.callback = sendingRequestCallback
}
.executeBy(taskExecutor)
}
@@ -1318,47 +1305,45 @@ internal class DefaultKeysBackupService @Inject constructor(
@VisibleForTesting
@WorkerThread
- fun encryptGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2): KeyBackupData {
+ fun encryptGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2): KeyBackupData? {
// Gather information for each key
- val device = cryptoStore.deviceWithIdentityKey(olmInboundGroupSessionWrapper.senderKey!!)
+ val device = olmInboundGroupSessionWrapper.senderKey?.let { cryptoStore.deviceWithIdentityKey(it) }
// Build the m.megolm_backup.v1.curve25519-aes-sha2 data as defined at
// https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md#mmegolm_backupv1curve25519-aes-sha2-key-format
- val sessionData = olmInboundGroupSessionWrapper.exportKeys()
+ val sessionData = olmInboundGroupSessionWrapper.exportKeys() ?: return null
val sessionBackupData = mapOf(
- "algorithm" to sessionData!!.algorithm,
+ "algorithm" to sessionData.algorithm,
"sender_key" to sessionData.senderKey,
"sender_claimed_keys" to sessionData.senderClaimedKeys,
- "forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain
- ?: ArrayList()),
+ "forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain.orEmpty()),
"session_key" to sessionData.sessionKey)
- var encryptedSessionBackupData: OlmPkMessage? = null
+ val json = MoshiProvider.providesMoshi()
+ .adapter(Map::class.java)
+ .toJson(sessionBackupData)
- val moshi = MoshiProvider.providesMoshi()
- val adapter = moshi.adapter(Map::class.java)
-
- try {
- val json = adapter.toJson(sessionBackupData)
-
- encryptedSessionBackupData = backupOlmPkEncryption?.encrypt(json)
+ val encryptedSessionBackupData = try {
+ backupOlmPkEncryption?.encrypt(json)
} catch (e: OlmException) {
Timber.e(e, "OlmException")
+ null
}
+ ?: return null
// Build backup data for that key
return KeyBackupData(
firstMessageIndex = try {
- olmInboundGroupSessionWrapper.olmInboundGroupSession!!.firstKnownIndex
+ olmInboundGroupSessionWrapper.olmInboundGroupSession?.firstKnownIndex ?: 0
} catch (e: OlmException) {
Timber.e(e, "OlmException")
0L
},
- forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain!!.size,
+ forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain.orEmpty().size,
isVerified = device?.isVerified == true,
sessionData = mapOf(
- "ciphertext" to encryptedSessionBackupData!!.mCipherText,
+ "ciphertext" to encryptedSessionBackupData.mCipherText,
"mac" to encryptedSessionBackupData.mMac,
"ephemeral" to encryptedSessionBackupData.mEphemeralKey)
)
@@ -1371,9 +1356,9 @@ internal class DefaultKeysBackupService @Inject constructor(
val jsonObject = keyBackupData.sessionData
- val ciphertext = jsonObject?.get("ciphertext")?.toString()
- val mac = jsonObject?.get("mac")?.toString()
- val ephemeralKey = jsonObject?.get("ephemeral")?.toString()
+ val ciphertext = jsonObject["ciphertext"]?.toString()
+ val mac = jsonObject["mac"]?.toString()
+ val ephemeralKey = jsonObject["ephemeral"]?.toString()
if (ciphertext != null && mac != null && ephemeralKey != null) {
val encrypted = OlmPkMessage()
@@ -1418,8 +1403,7 @@ internal class DefaultKeysBackupService @Inject constructor(
@Suppress("UNCHECKED_CAST")
val createKeysBackupVersionBody = CreateKeysBackupVersionBody(
algorithm = keysBackupCreationInfo.algorithm,
- authData = MoshiProvider.providesMoshi().adapter(Map::class.java)
- .fromJson(keysBackupCreationInfo.authData?.toJsonString() ?: "") as JsonDict?
+ authData = keysBackupCreationInfo.authData.toJsonDict()
)
createKeysBackupVersionTask
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt
index ed5383d6eb..3f8333528f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt
@@ -35,7 +35,7 @@ import retrofit2.http.Path
import retrofit2.http.Query
/**
- * Ref: https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md
+ * Ref: https://matrix.org/docs/spec/client_server/unstable#server-side-key-backups
*/
internal interface RoomKeysApi {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt
index 9df5f29294..54b92546e9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.keysbackup.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.di.MoshiProvider
/**
@@ -30,7 +31,7 @@ data class MegolmBackupAuthData(
* The curve25519 public key used to encrypt the backups.
*/
@Json(name = "public_key")
- val publicKey: String = "",
+ val publicKey: String,
/**
* In case of a backup created from a password, the salt associated with the backup
@@ -50,20 +51,38 @@ data class MegolmBackupAuthData(
* userId -> (deviceSignKeyId -> signature)
*/
@Json(name = "signatures")
- val signatures: Map>? = null
+ val signatures: Map>
) {
- fun toJsonString(): String {
- return MoshiProvider.providesMoshi()
+ fun toJsonDict(): JsonDict {
+ val moshi = MoshiProvider.providesMoshi()
+ val adapter = moshi.adapter(Map::class.java)
+
+ return moshi
.adapter(MegolmBackupAuthData::class.java)
.toJson(this)
+ .let {
+ @Suppress("UNCHECKED_CAST")
+ adapter.fromJson(it) as JsonDict
+ }
}
- /**
- * Same as the parent [MXJSONModel JSONDictionary] but return only
- * data that must be signed.
- */
- fun signalableJSONDictionary(): Map = HashMap().apply {
+ fun signalableJSONDictionary(): JsonDict {
+ return SignalableMegolmBackupAuthData(
+ publicKey = publicKey,
+ privateKeySalt = privateKeySalt,
+ privateKeyIterations = privateKeyIterations
+ )
+ .signalableJSONDictionary()
+ }
+}
+
+internal data class SignalableMegolmBackupAuthData(
+ val publicKey: String,
+ val privateKeySalt: String? = null,
+ val privateKeyIterations: Int? = null
+) {
+ fun signalableJSONDictionary(): JsonDict = HashMap().apply {
put("public_key", publicKey)
privateKeySalt?.let {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt
index 1414d0e0d7..c668e78a9e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt
@@ -23,15 +23,15 @@ data class MegolmBackupCreationInfo(
/**
* The algorithm used for storing backups [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP].
*/
- val algorithm: String = "",
+ val algorithm: String,
/**
* Authentication data.
*/
- val authData: MegolmBackupAuthData? = null,
+ val authData: MegolmBackupAuthData,
/**
* The Base58 recovery key.
*/
- val recoveryKey: String = ""
+ val recoveryKey: String
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/BackupKeysResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/BackupKeysResult.kt
index a84ba7427b..3710a2d7d9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/BackupKeysResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/BackupKeysResult.kt
@@ -16,15 +16,16 @@
package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest
+import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
-data class BackupKeysResult(
-
+internal data class BackupKeysResult(
// The hash value which is an opaque string representing stored keys in the backup
- var hash: String? = null,
+ @Json(name = "etag")
+ val hash: String,
// The number of keys stored in the backup.
- var count: Int? = null
-
+ @Json(name = "count")
+ val count: Int
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt
index a7831b38f1..a6bd8f8aaa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt
@@ -21,17 +21,17 @@ import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.util.JsonDict
@JsonClass(generateAdapter = true)
-data class CreateKeysBackupVersionBody(
+internal data class CreateKeysBackupVersionBody(
/**
* The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined
*/
@Json(name = "algorithm")
- override val algorithm: String? = null,
+ override val algorithm: String,
/**
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2"
* see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData]
*/
@Json(name = "auth_data")
- override val authData: JsonDict? = null
+ override val authData: JsonDict
) : KeysAlgorithmAndData
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt
index 46eaa586a7..3f8129b8f6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
-import org.matrix.android.sdk.internal.di.MoshiProvider
+import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.network.parsing.ForceToBoolean
/**
@@ -30,13 +30,13 @@ data class KeyBackupData(
* Required. The index of the first message in the session that the key can decrypt.
*/
@Json(name = "first_message_index")
- val firstMessageIndex: Long = 0,
+ val firstMessageIndex: Long,
/**
* Required. The number of times this key has been forwarded.
*/
@Json(name = "forwarded_count")
- val forwardedCount: Int = 0,
+ val forwardedCount: Int,
/**
* Whether the device backing up the key has verified the device that the key is from.
@@ -44,16 +44,11 @@ data class KeyBackupData(
*/
@ForceToBoolean
@Json(name = "is_verified")
- val isVerified: Boolean = false,
+ val isVerified: Boolean,
/**
* Algorithm-dependent data.
*/
@Json(name = "session_data")
- val sessionData: Map? = null
-) {
-
- fun toJsonString(): String {
- return MoshiProvider.providesMoshi().adapter(KeyBackupData::class.java).toJson(this)
- }
-}
+ val sessionData: JsonDict
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt
index 117d4dce70..e098aa0440 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt
@@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest
import org.matrix.android.sdk.api.util.JsonDict
+import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
import org.matrix.android.sdk.internal.di.MoshiProvider
@@ -37,24 +38,25 @@ import org.matrix.android.sdk.internal.di.MoshiProvider
* }
*
*/
-interface KeysAlgorithmAndData {
+internal interface KeysAlgorithmAndData {
/**
* The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined
*/
- val algorithm: String?
+ val algorithm: String
/**
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData]
*/
- val authData: JsonDict?
+ val authData: JsonDict
/**
* Facility method to convert authData to a MegolmBackupAuthData object
*/
fun getAuthDataAsMegolmBackupAuthData(): MegolmBackupAuthData? {
return MoshiProvider.providesMoshi()
- .adapter(MegolmBackupAuthData::class.java)
- .fromJsonValue(authData)
+ .takeIf { algorithm == MXCRYPTO_ALGORITHM_MEGOLM_BACKUP }
+ ?.adapter(MegolmBackupAuthData::class.java)
+ ?.fromJsonValue(authData)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersion.kt
index 146c98b017..7a4c3415fc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersion.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersion.kt
@@ -23,5 +23,5 @@ import com.squareup.moshi.JsonClass
data class KeysVersion(
// the keys backup version
@Json(name = "version")
- val version: String? = null
+ val version: String
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt
index 0844c58d2e..485fd48a8c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt
@@ -26,24 +26,24 @@ data class KeysVersionResult(
* The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined
*/
@Json(name = "algorithm")
- override val algorithm: String? = null,
+ override val algorithm: String,
/**
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2"
* see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData]
*/
@Json(name = "auth_data")
- override val authData: JsonDict? = null,
+ override val authData: JsonDict,
// the backup version
@Json(name = "version")
- val version: String? = null,
+ val version: String,
// The hash value which is an opaque string representing stored keys in the backup
- @Json(name = "hash")
- val hash: String? = null,
+ @Json(name = "etag")
+ val hash: String,
// The number of keys stored in the backup.
@Json(name = "count")
- val count: Int? = null
+ val count: Int
) : KeysAlgorithmAndData
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt
index 65f0c1a845..4512ed7a55 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt
@@ -26,16 +26,16 @@ data class UpdateKeysBackupVersionBody(
* The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined
*/
@Json(name = "algorithm")
- override val algorithm: String? = null,
+ override val algorithm: String,
/**
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2"
* see [org.matrix.android.sdk.internal.crypto.keysbackup.MegolmBackupAuthData]
*/
@Json(name = "auth_data")
- override val authData: JsonDict? = null,
+ override val authData: JsonDict,
- // the backup version, mandatory
+ // Optional. The backup version. If present, must be the same as the path parameter.
@Json(name = "version")
- val version: String
+ val version: String? = null
) : KeysAlgorithmAndData
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt
index 478d55d077..1dc27c75ca 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt
@@ -48,15 +48,12 @@ class OlmInboundGroupSessionWrapper2 : Serializable {
*/
val firstKnownIndex: Long?
get() {
- if (null != olmInboundGroupSession) {
- try {
- return olmInboundGroupSession!!.firstKnownIndex
- } catch (e: Exception) {
- Timber.e(e, "## getFirstKnownIndex() : getFirstKnownIndex failed")
- }
+ return try {
+ olmInboundGroupSession?.firstKnownIndex
+ } catch (e: Exception) {
+ Timber.e(e, "## getFirstKnownIndex() : getFirstKnownIndex failed")
+ null
}
-
- return null
}
/**
@@ -90,11 +87,13 @@ class OlmInboundGroupSessionWrapper2 : Serializable {
@Throws(Exception::class)
constructor(megolmSessionData: MegolmSessionData) {
try {
- olmInboundGroupSession = OlmInboundGroupSession.importSession(megolmSessionData.sessionKey!!)
-
- if (olmInboundGroupSession!!.sessionIdentifier() != megolmSessionData.sessionId) {
- throw Exception("Mismatched group session Id")
- }
+ val safeSessionKey = megolmSessionData.sessionKey ?: throw Exception("invalid data")
+ olmInboundGroupSession = OlmInboundGroupSession.importSession(safeSessionKey)
+ .also {
+ if (it.sessionIdentifier() != megolmSessionData.sessionId) {
+ throw Exception("Mismatched group session Id")
+ }
+ }
senderKey = megolmSessionData.senderKey
keysClaimed = megolmSessionData.senderClaimedKeys
@@ -120,16 +119,18 @@ class OlmInboundGroupSessionWrapper2 : Serializable {
return null
}
- val wantedIndex = index ?: olmInboundGroupSession!!.firstKnownIndex
+ val safeOlmInboundGroupSession = olmInboundGroupSession ?: return null
+
+ val wantedIndex = index ?: safeOlmInboundGroupSession.firstKnownIndex
MegolmSessionData(
senderClaimedEd25519Key = keysClaimed?.get("ed25519"),
- forwardingCurve25519KeyChain = ArrayList(forwardingCurve25519KeyChain!!),
+ forwardingCurve25519KeyChain = forwardingCurve25519KeyChain?.toList().orEmpty(),
senderKey = senderKey,
senderClaimedKeys = keysClaimed,
roomId = roomId,
- sessionId = olmInboundGroupSession!!.sessionIdentifier(),
- sessionKey = olmInboundGroupSession!!.export(wantedIndex),
+ sessionId = safeOlmInboundGroupSession.sessionIdentifier(),
+ sessionKey = safeOlmInboundGroupSession.export(wantedIndex),
algorithm = MXCRYPTO_ALGORITHM_MEGOLM
)
} catch (e: Exception) {
@@ -145,14 +146,11 @@ class OlmInboundGroupSessionWrapper2 : Serializable {
* @return the exported data
*/
fun exportSession(messageIndex: Long): String? {
- if (null != olmInboundGroupSession) {
- try {
- return olmInboundGroupSession!!.export(messageIndex)
- } catch (e: Exception) {
- Timber.e(e, "## exportSession() : export failed")
- }
+ return try {
+ return olmInboundGroupSession?.export(messageIndex)
+ } catch (e: Exception) {
+ Timber.e(e, "## exportSession() : export failed")
+ null
}
-
- return null
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
index 0ae1e69124..9a9f645b49 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
@@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.crypto.store
import androidx.lifecycle.LiveData
+import androidx.paging.PagedList
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.util.Optional
@@ -126,7 +127,10 @@ internal interface IMXCryptoStore {
fun getPendingIncomingRoomKeyRequests(): List
fun getPendingIncomingGossipingRequests(): List
+
fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?)
+
+ fun storeIncomingGossipingRequests(requests: List)
// fun getPendingIncomingSecretShareRequests(): List
/**
@@ -364,6 +368,7 @@ internal interface IMXCryptoStore {
fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest?
fun saveGossipingEvent(event: Event)
+ fun saveGossipingEvents(events: List)
fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) {
updateGossipingRequestState(
@@ -441,11 +446,16 @@ internal interface IMXCryptoStore {
// Dev tools
fun getOutgoingRoomKeyRequests(): List
+ fun getOutgoingRoomKeyRequestsPaged(): LiveData>
fun getOutgoingSecretKeyRequests(): List
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
fun getIncomingRoomKeyRequests(): List
- fun getGossipingEventsTrail(): List
+ fun getIncomingRoomKeyRequestsPaged(): LiveData>
+ fun getGossipingEventsTrail(): LiveData>
+ fun getGossipingEvents(): List
fun setDeviceKeysUploaded(uploaded: Boolean)
fun getDeviceKeysUploaded(): Boolean
+ fun tidyUpDataBase()
+ fun logDbUsageInfo()
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
index b25349cba9..72274aa70a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
@@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto.store.db
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
+import androidx.paging.LivePagedListBuilder
+import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmConfiguration
@@ -62,6 +64,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFie
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
+import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
@@ -84,6 +87,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.query.get
import org.matrix.android.sdk.internal.crypto.store.db.query.getById
import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
+import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
import org.matrix.android.sdk.internal.di.CryptoDatabase
import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.MoshiProvider
@@ -998,7 +1002,50 @@ internal class RealmCryptoStore @Inject constructor(
}
}
- override fun getGossipingEventsTrail(): List {
+ override fun getIncomingRoomKeyRequestsPaged(): LiveData> {
+ val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
+ realm.where()
+ .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
+ .sort(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Sort.DESCENDING)
+ }
+ val dataSourceFactory = realmDataSourceFactory.map {
+ it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
+ ?: IncomingRoomKeyRequest(
+ requestBody = null,
+ deviceId = "",
+ userId = "",
+ requestId = "",
+ state = GossipingRequestState.NONE,
+ localCreationTimestamp = 0
+ )
+ }
+ return monarchy.findAllPagedWithChanges(realmDataSourceFactory,
+ LivePagedListBuilder(dataSourceFactory,
+ PagedList.Config.Builder()
+ .setPageSize(20)
+ .setEnablePlaceholders(false)
+ .setPrefetchDistance(1)
+ .build())
+ )
+ }
+
+ override fun getGossipingEventsTrail(): LiveData> {
+ val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
+ realm.where().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
+ }
+ val dataSourceFactory = realmDataSourceFactory.map { it.toModel() }
+ val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory,
+ LivePagedListBuilder(dataSourceFactory,
+ PagedList.Config.Builder()
+ .setPageSize(20)
+ .setEnablePlaceholders(false)
+ .setPrefetchDistance(1)
+ .build())
+ )
+ return trail
+ }
+
+ override fun getGossipingEvents(): List {
return monarchy.fetchAllCopiedSync { realm ->
realm.where()
}.map {
@@ -1066,24 +1113,43 @@ internal class RealmCryptoStore @Inject constructor(
return request
}
- override fun saveGossipingEvent(event: Event) {
+ override fun saveGossipingEvents(events: List) {
val now = System.currentTimeMillis()
- val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
- val entity = GossipingEventEntity(
- type = event.type,
- sender = event.senderId,
- ageLocalTs = ageLocalTs,
- content = ContentMapper.map(event.content)
- ).apply {
- sendState = SendState.SYNCED
- decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
- decryptionErrorCode = event.mCryptoError?.name
- }
- doRealmTransaction(realmConfiguration) { realm ->
- realm.insertOrUpdate(entity)
+ monarchy.writeAsync { realm ->
+ events.forEach { event ->
+ val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
+ val entity = GossipingEventEntity(
+ type = event.type,
+ sender = event.senderId,
+ ageLocalTs = ageLocalTs,
+ content = ContentMapper.map(event.content)
+ ).apply {
+ sendState = SendState.SYNCED
+ decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
+ decryptionErrorCode = event.mCryptoError?.name
+ }
+ realm.insertOrUpdate(entity)
+ }
}
}
+ override fun saveGossipingEvent(event: Event) {
+ monarchy.writeAsync { realm ->
+ val now = System.currentTimeMillis()
+ val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
+ val entity = GossipingEventEntity(
+ type = event.type,
+ sender = event.senderId,
+ ageLocalTs = ageLocalTs,
+ content = ContentMapper.map(event.content)
+ ).apply {
+ sendState = SendState.SYNCED
+ decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
+ decryptionErrorCode = event.mCryptoError?.name
+ }
+ realm.insertOrUpdate(entity)
+ }
+ }
// override fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? {
// val statesIndex = states.map { it.ordinal }.toTypedArray()
// return doRealmQueryAndCopy(realmConfiguration) { realm ->
@@ -1284,6 +1350,28 @@ internal class RealmCryptoStore @Inject constructor(
}
}
+ override fun storeIncomingGossipingRequests(requests: List) {
+ doRealmTransactionAsync(realmConfiguration) { realm ->
+ requests.forEach { request ->
+ // After a clear cache, we might have a
+ realm.createObject(IncomingGossipingRequestEntity::class.java).let {
+ it.otherDeviceId = request.deviceId
+ it.otherUserId = request.userId
+ it.requestId = request.requestId ?: ""
+ it.requestState = GossipingRequestState.PENDING
+ it.localCreationTimestamp = request.localCreationTimestamp ?: System.currentTimeMillis()
+ if (request is IncomingSecretShareRequest) {
+ it.type = GossipRequestType.SECRET
+ it.requestedInfoStr = request.secretName
+ } else if (request is IncomingRoomKeyRequest) {
+ it.type = GossipRequestType.KEY
+ it.requestedInfoStr = request.requestBody?.toJson()
+ }
+ }
+ }
+ }
+ }
+
// override fun getPendingIncomingSecretShareRequests(): List {
// return doRealmQueryAndCopyList(realmConfiguration) {
// it.where()
@@ -1417,6 +1505,27 @@ internal class RealmCryptoStore @Inject constructor(
.filterNotNull()
}
+ override fun getOutgoingRoomKeyRequestsPaged(): LiveData> {
+ val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
+ realm
+ .where(OutgoingGossipingRequestEntity::class.java)
+ .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
+ }
+ val dataSourceFactory = realmDataSourceFactory.map {
+ it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
+ ?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED)
+ }
+ val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory,
+ LivePagedListBuilder(dataSourceFactory,
+ PagedList.Config.Builder()
+ .setPageSize(20)
+ .setEnablePlaceholders(false)
+ .setPrefetchDistance(1)
+ .build())
+ )
+ return trail
+ }
+
override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? {
return doWithRealm(realmConfiguration) { realm ->
val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
@@ -1558,4 +1667,48 @@ internal class RealmCryptoStore @Inject constructor(
result
}
}
+
+ /**
+ * Some entries in the DB can get a bit out of control with time
+ * So we need to tidy up a bit
+ */
+ override fun tidyUpDataBase() {
+ val prevWeekTs = System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1_000
+ doRealmTransaction(realmConfiguration) { realm ->
+
+ // Only keep one week history
+ realm.where()
+ .lessThan(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, prevWeekTs)
+ .findAll().let {
+ Timber.i("## Crypto Clean up ${it.size} IncomingGossipingRequestEntity")
+ it.deleteAllFromRealm()
+ }
+
+ // Clean the cancelled ones?
+ realm.where()
+ .equalTo(OutgoingGossipingRequestEntityFields.REQUEST_STATE_STR, OutgoingGossipingRequestState.CANCELLED.name)
+ .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
+ .findAll().let {
+ Timber.i("## Crypto Clean up ${it.size} OutgoingGossipingRequestEntity")
+ it.deleteAllFromRealm()
+ }
+
+ // Only keep one week history
+ realm.where()
+ .lessThan(GossipingEventEntityFields.AGE_LOCAL_TS, prevWeekTs)
+ .findAll().let {
+ Timber.i("## Crypto Clean up ${it.size} GossipingEventEntityFields")
+ it.deleteAllFromRealm()
+ }
+
+ // Can we do something for WithHeldSessionEntity?
+ }
+ }
+
+ /**
+ * Prints out database info
+ */
+ override fun logDbUsageInfo() {
+ RealmDebugTools(realmConfiguration).logInfo("Crypto")
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
index c106c82538..08806b0627 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
@@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEnti
import org.matrix.android.sdk.internal.di.SerializeNulls
import io.realm.DynamicRealm
import io.realm.RealmMigration
+import io.realm.RealmObjectSchema
import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2
import timber.log.Timber
import javax.inject.Inject
@@ -57,6 +58,27 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
const val CRYPTO_STORE_SCHEMA_VERSION = 11L
}
+ private fun RealmObjectSchema.addFieldIfNotExists(fieldName: String, fieldType: Class<*>): RealmObjectSchema {
+ if (!hasField(fieldName)) {
+ addField(fieldName, fieldType)
+ }
+ return this
+ }
+
+ private fun RealmObjectSchema.removeFieldIfExists(fieldName: String): RealmObjectSchema {
+ if (hasField(fieldName)) {
+ removeField(fieldName)
+ }
+ return this
+ }
+
+ private fun RealmObjectSchema.setRequiredIfNotAlready(fieldName: String, isRequired: Boolean): RealmObjectSchema {
+ if (isRequired != isRequired(fieldName)) {
+ setRequired(fieldName, isRequired)
+ }
+ return this
+ }
+
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion")
@@ -89,13 +111,13 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
realm.schema.get("IncomingRoomKeyRequestEntity")
- ?.addField("requestBodyAlgorithm", String::class.java)
- ?.addField("requestBodyRoomId", String::class.java)
- ?.addField("requestBodySenderKey", String::class.java)
- ?.addField("requestBodySessionId", String::class.java)
+ ?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java)
+ ?.addFieldIfNotExists("requestBodyRoomId", String::class.java)
+ ?.addFieldIfNotExists("requestBodySenderKey", String::class.java)
+ ?.addFieldIfNotExists("requestBodySessionId", String::class.java)
?.transform { dynamicObject ->
- val requestBodyString = dynamicObject.getString("requestBodyString")
try {
+ val requestBodyString = dynamicObject.getString("requestBodyString")
// It was a map before
val map: Map? = deserializeFromRealm(requestBodyString)
@@ -109,18 +131,18 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
Timber.e(e, "Error")
}
}
- ?.removeField("requestBodyString")
+ ?.removeFieldIfExists("requestBodyString")
Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
realm.schema.get("OutgoingRoomKeyRequestEntity")
- ?.addField("requestBodyAlgorithm", String::class.java)
- ?.addField("requestBodyRoomId", String::class.java)
- ?.addField("requestBodySenderKey", String::class.java)
- ?.addField("requestBodySessionId", String::class.java)
+ ?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java)
+ ?.addFieldIfNotExists("requestBodyRoomId", String::class.java)
+ ?.addFieldIfNotExists("requestBodySenderKey", String::class.java)
+ ?.addFieldIfNotExists("requestBodySessionId", String::class.java)
?.transform { dynamicObject ->
- val requestBodyString = dynamicObject.getString("requestBodyString")
try {
+ val requestBodyString = dynamicObject.getString("requestBodyString")
// It was a map before
val map: Map? = deserializeFromRealm(requestBodyString)
@@ -134,16 +156,18 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
Timber.e(e, "Error")
}
}
- ?.removeField("requestBodyString")
+ ?.removeFieldIfExists("requestBodyString")
Timber.d("Create KeysBackupDataEntity")
- realm.schema.create("KeysBackupDataEntity")
- .addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java)
- .addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY)
- .setRequired(KeysBackupDataEntityFields.PRIMARY_KEY, true)
- .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java)
- .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java)
+ if (!realm.schema.contains("KeysBackupDataEntity")) {
+ realm.schema.create("KeysBackupDataEntity")
+ .addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java)
+ .addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY)
+ .setRequired(KeysBackupDataEntityFields.PRIMARY_KEY, true)
+ .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java)
+ .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java)
+ }
}
private fun migrateTo3RiotX(realm: DynamicRealm) {
@@ -151,8 +175,8 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
Timber.d("Migrate to RiotX model")
realm.schema.get("CryptoRoomEntity")
- ?.addField(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java)
- ?.setRequired(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false)
+ ?.addFieldIfNotExists(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java)
+ ?.setRequiredIfNotAlready(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false)
// Convert format of MXDeviceInfo, package has to be the same.
realm.schema.get("DeviceInfoEntity")
@@ -204,8 +228,13 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
// Version 4L added Cross Signing info persistence
private fun migrateTo4(realm: DynamicRealm) {
Timber.d("Step 3 -> 4")
- Timber.d("Create KeyInfoEntity")
+ if (realm.schema.contains("TrustLevelEntity")) {
+ Timber.d("Skipping Step 3 -> 4 because entities already exist")
+ return
+ }
+
+ Timber.d("Create KeyInfoEntity")
val trustLevelEntityEntitySchema = realm.schema.create("TrustLevelEntity")
.addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java)
.setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt
index 75f4c1730f..56b267decd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt
@@ -17,8 +17,13 @@ package org.matrix.android.sdk.internal.crypto.tasks
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
+import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitCallback
@@ -28,23 +33,23 @@ internal interface EncryptEventTask : Task {
data class Params(val roomId: String,
val event: Event,
/**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/
- val keepKeys: List? = null,
- val crypto: CryptoService
+ val keepKeys: List? = null
)
}
internal class DefaultEncryptEventTask @Inject constructor(
-// private val crypto: CryptoService
- private val localEchoRepository: LocalEchoRepository
+ private val localEchoRepository: LocalEchoRepository,
+ private val cryptoService: CryptoService
) : EncryptEventTask {
override suspend fun execute(params: EncryptEventTask.Params): Event {
- if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event
+ // don't want to wait for any query
+ // if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event
val localEvent = params.event
if (localEvent.eventId == null) {
throw IllegalArgumentException()
}
- localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING)
+ localEchoRepository.updateSendState(localEvent.eventId, localEvent.roomId, SendState.ENCRYPTING)
val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf()
params.keepKeys?.forEach {
@@ -52,8 +57,9 @@ internal class DefaultEncryptEventTask @Inject constructor(
}
// try {
+ // let it throws
awaitCallback {
- params.crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
+ cryptoService.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
}.let { result ->
val modifiedContent = HashMap(result.eventContent)
params.keepKeys?.forEach { toKeep ->
@@ -63,18 +69,34 @@ internal class DefaultEncryptEventTask @Inject constructor(
}
}
val safeResult = result.copy(eventContent = modifiedContent)
+ // Better handling of local echo, to avoid decrypting transition on remote echo
+ // Should I only do it for text messages?
+ val decryptionLocalEcho = if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) {
+ MXEventDecryptionResult(
+ clearEvent = Event(
+ type = localEvent.type,
+ content = localEvent.content,
+ roomId = localEvent.roomId
+ ).toContent(),
+ forwardingCurve25519KeyChain = emptyList(),
+ senderCurve25519Key = result.eventContent["sender_key"] as? String,
+ claimedEd25519Key = cryptoService.getMyDevice().fingerprint()
+ )
+ } else {
+ null
+ }
+
+ localEchoRepository.updateEcho(localEvent.eventId) { _, localEcho ->
+ localEcho.type = EventType.ENCRYPTED
+ localEcho.content = ContentMapper.map(modifiedContent)
+ decryptionLocalEcho?.also {
+ localEcho.setDecryptionResult(it)
+ }
+ }
return localEvent.copy(
type = safeResult.eventType,
content = safeResult.eventContent
)
}
-// } catch (throwable: Throwable) {
-// val sendState = when (throwable) {
-// is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES
-// else -> SendState.UNDELIVERED
-// }
-// localEchoUpdater.updateSendState(localEvent.eventId, sendState)
-// throw throwable
-// }
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt
new file mode 100644
index 0000000000..f35d1b63e8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/RedactEventTask.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.crypto.tasks
+
+import org.greenrobot.eventbus.EventBus
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.room.RoomAPI
+import org.matrix.android.sdk.internal.session.room.send.SendResponse
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface RedactEventTask : Task {
+ data class Params(
+ val txID: String,
+ val roomId: String,
+ val eventId: String,
+ val reason: String?
+ )
+}
+
+internal class DefaultRedactEventTask @Inject constructor(
+ private val roomAPI: RoomAPI,
+ private val eventBus: EventBus) : RedactEventTask {
+
+ override suspend fun execute(params: RedactEventTask.Params): String {
+ val executeRequest = executeRequest(eventBus) {
+ apiCall = roomAPI.redactEvent(
+ txId = params.txID,
+ roomId = params.roomId,
+ eventId = params.eventId,
+ reason = if (params.reason == null) emptyMap() else mapOf("reason" to params.reason)
+ )
+ }
+ return executeRequest.eventId
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt
index 10b0823c65..8b739c4b64 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt
@@ -15,7 +15,7 @@
*/
package org.matrix.android.sdk.internal.crypto.tasks
-import org.matrix.android.sdk.api.session.crypto.CryptoService
+import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.network.executeRequest
@@ -23,13 +23,12 @@ import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.session.room.send.SendResponse
import org.matrix.android.sdk.internal.task.Task
-import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface SendEventTask : Task {
data class Params(
val event: Event,
- val cryptoService: CryptoService?
+ val encrypt: Boolean
)
}
@@ -40,11 +39,11 @@ internal class DefaultSendEventTask @Inject constructor(
private val eventBus: EventBus) : SendEventTask {
override suspend fun execute(params: SendEventTask.Params): String {
- val event = handleEncryption(params)
- val localId = event.eventId!!
-
try {
- localEchoRepository.updateSendState(localId, SendState.SENDING)
+ val event = handleEncryption(params)
+ val localId = event.eventId!!
+
+ localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENDING)
val executeRequest = executeRequest(eventBus) {
apiCall = roomAPI.send(
localId,
@@ -53,26 +52,22 @@ internal class DefaultSendEventTask @Inject constructor(
eventType = event.type
)
}
- localEchoRepository.updateSendState(localId, SendState.SENT)
+ localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENT)
return executeRequest.eventId
} catch (e: Throwable) {
- localEchoRepository.updateSendState(localId, SendState.UNDELIVERED)
+// localEchoRepository.updateSendState(params.event.eventId!!, SendState.UNDELIVERED)
throw e
}
}
+ @Throws
private suspend fun handleEncryption(params: SendEventTask.Params): Event {
- if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) {
- try {
- return encryptEventTask.execute(EncryptEventTask.Params(
- params.event.roomId ?: "",
- params.event,
- listOf("m.relates_to"),
- params.cryptoService
- ))
- } catch (throwable: Throwable) {
- // We said it's ok to send verification request in clear
- }
+ if (params.encrypt && !params.event.isEncrypted()) {
+ return encryptEventTask.execute(EncryptEventTask.Params(
+ params.event.roomId ?: "",
+ params.event,
+ listOf("m.relates_to")
+ ))
}
return params.event
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt
index b48f84ac91..cedb7a6618 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt
@@ -15,21 +15,20 @@
*/
package org.matrix.android.sdk.internal.crypto.tasks
-import org.matrix.android.sdk.api.session.crypto.CryptoService
+import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.session.room.send.SendResponse
import org.matrix.android.sdk.internal.task.Task
-import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface SendVerificationMessageTask : Task {
data class Params(
- val event: Event,
- val cryptoService: CryptoService?
+ val event: Event
)
}
@@ -37,6 +36,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
private val localEchoRepository: LocalEchoRepository,
private val encryptEventTask: DefaultEncryptEventTask,
private val roomAPI: RoomAPI,
+ private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val eventBus: EventBus) : SendVerificationMessageTask {
override suspend fun execute(params: SendVerificationMessageTask.Params): String {
@@ -44,7 +44,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
val localId = event.eventId!!
try {
- localEchoRepository.updateSendState(localId, SendState.SENDING)
+ localEchoRepository.updateSendState(localId, event.roomId, SendState.SENDING)
val executeRequest = executeRequest(eventBus) {
apiCall = roomAPI.send(
localId,
@@ -53,22 +53,21 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
eventType = event.type
)
}
- localEchoRepository.updateSendState(localId, SendState.SENT)
+ localEchoRepository.updateSendState(localId, event.roomId, SendState.SENT)
return executeRequest.eventId
} catch (e: Throwable) {
- localEchoRepository.updateSendState(localId, SendState.UNDELIVERED)
+ localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED)
throw e
}
}
private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event {
- if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) {
+ if (cryptoSessionInfoProvider.isRoomEncrypted(params.event.roomId ?: "")) {
try {
return encryptEventTask.execute(EncryptEventTask.Params(
params.event.roomId ?: "",
params.event,
- listOf("m.relates_to"),
- params.cryptoService
+ listOf("m.relates_to")
))
} catch (throwable: Throwable) {
// We said it's ok to send verification request in clear
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
index c0f4671046..7f02750359 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
@@ -20,7 +20,6 @@ import android.os.Handler
import android.os.Looper
import dagger.Lazy
import org.matrix.android.sdk.api.MatrixCallback
-import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
@@ -111,9 +110,6 @@ internal class DefaultVerificationService @Inject constructor(
private val uiHandler = Handler(Looper.getMainLooper())
- // Cannot be injected in constructor as it creates a dependency cycle
- lateinit var cryptoService: CryptoService
-
// map [sender : [transaction]]
private val txMap = HashMap>()
@@ -129,7 +125,8 @@ internal class DefaultVerificationService @Inject constructor(
// Event received from the sync
fun onToDeviceEvent(event: Event) {
- cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+ Timber.d("## SAS onToDeviceEvent ${event.getClearType()}")
+ cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
when (event.getClearType()) {
EventType.KEY_VERIFICATION_START -> {
onStartRequestReceived(event)
@@ -163,7 +160,7 @@ internal class DefaultVerificationService @Inject constructor(
}
fun onRoomEvent(event: Event) {
- cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+ cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
when (event.getClearType()) {
EventType.KEY_VERIFICATION_START -> {
onRoomStartRequestReceived(event)
@@ -240,6 +237,7 @@ internal class DefaultVerificationService @Inject constructor(
}
private fun dispatchRequestAdded(tx: PendingVerificationRequest) {
+ Timber.v("## SAS dispatchRequestAdded txId:${tx.transactionId}")
uiHandler.post {
listeners.forEach {
try {
@@ -303,11 +301,14 @@ internal class DefaultVerificationService @Inject constructor(
// We don't want to block here
val otherDeviceId = validRequestInfo.fromDevice
+ Timber.v("## SAS onRequestReceived from $senderId and device $otherDeviceId, txId:${validRequestInfo.transactionId}")
+
cryptoCoroutineScope.launch {
if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) {
Timber.e("## Verification device $otherDeviceId is not known")
}
}
+ Timber.v("## SAS onRequestReceived .. checkKeysAreDownloaded launched")
// Remember this request
val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() }
@@ -1203,7 +1204,9 @@ internal class DefaultVerificationService @Inject constructor(
// TODO refactor this with the DM one
Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices")
- val targetDevices = otherDevices ?: cryptoService.getUserDevices(otherUserId).map { it.deviceId }
+ val targetDevices = otherDevices ?: cryptoStore.getUserDevices(otherUserId)
+ ?.values?.map { it.deviceId } ?: emptyList()
+
val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() }
val transport = verificationTransportToDeviceFactory.createTransport(null)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt
index fa7cd2e6f9..538d7b56e9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt
@@ -20,7 +20,6 @@ import androidx.work.Data
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.failure.shouldBeRetried
-import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
@@ -47,7 +46,6 @@ internal class SendVerificationMessageWorker(context: Context,
@Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask
@Inject lateinit var localEchoRepository: LocalEchoRepository
- @Inject lateinit var cryptoService: CryptoService
@Inject lateinit var cancelSendTracker: CancelSendTracker
override fun injectWith(injector: SessionComponent) {
@@ -70,8 +68,7 @@ internal class SendVerificationMessageWorker(context: Context,
return try {
val resultEventId = sendVerificationMessageTask.execute(
SendVerificationMessageTask.Params(
- event = localEvent,
- cryptoService = cryptoService
+ event = localEvent
)
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
index 4994325625..74827eeb2a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
@@ -15,7 +15,6 @@
*/
package org.matrix.android.sdk.internal.crypto.verification
-import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.events.model.Event
@@ -34,12 +33,13 @@ import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import io.realm.Realm
+import org.matrix.android.sdk.internal.crypto.EventDecryptor
import timber.log.Timber
import java.util.ArrayList
import javax.inject.Inject
internal class VerificationMessageProcessor @Inject constructor(
- private val cryptoService: CryptoService,
+ private val eventDecryptor: EventDecryptor,
private val verificationService: DefaultVerificationService,
@UserId private val userId: String,
@DeviceId private val deviceId: String?
@@ -82,7 +82,7 @@ internal class VerificationMessageProcessor @Inject constructor(
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
// for now decrypt sync
try {
- val result = cryptoService.decryptEvent(event, "")
+ val result = eventDecryptor.decryptEvent(event, "")
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
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 11a877e7c4..71f978c03c 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
@@ -17,10 +17,6 @@
package org.matrix.android.sdk.internal.database
import com.zhuinden.monarchy.Monarchy
-import org.matrix.android.sdk.api.session.crypto.CryptoService
-import org.matrix.android.sdk.api.session.crypto.MXCryptoError
-import org.matrix.android.sdk.api.session.events.model.Event
-import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertEntity
@@ -31,12 +27,13 @@ import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.launch
+import org.matrix.android.sdk.internal.crypto.EventDecryptor
import timber.log.Timber
import javax.inject.Inject
internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>,
- private val cryptoService: CryptoService)
+ private val eventDecryptor: EventDecryptor)
: RealmLiveEntityObserver(realmConfiguration) {
override val query = Monarchy.Query {
@@ -74,7 +71,7 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
return@forEach
}
val domainEvent = event.asDomain()
- decryptIfNeeded(domainEvent)
+// decryptIfNeeded(domainEvent)
processors.filter {
it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
}.forEach {
@@ -89,22 +86,22 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
}
}
- private fun decryptIfNeeded(event: Event) {
- if (event.isEncrypted() && event.mxDecryptionResult == null) {
- try {
- val result = cryptoService.decryptEvent(event, event.roomId ?: "")
- event.mxDecryptionResult = OlmDecryptionResult(
- payload = result.clearEvent,
- senderKey = result.senderCurve25519Key,
- keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
- forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
- )
- } catch (e: MXCryptoError) {
- Timber.v("Failed to decrypt event")
- // TODO -> we should keep track of this and retry, or some processing will never be handled
- }
- }
- }
+// private fun decryptIfNeeded(event: Event) {
+// if (event.isEncrypted() && event.mxDecryptionResult == null) {
+// try {
+// val result = eventDecryptor.decryptEvent(event, event.roomId ?: "")
+// event.mxDecryptionResult = OlmDecryptionResult(
+// payload = result.clearEvent,
+// senderKey = result.senderCurve25519Key,
+// keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
+// forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
+// )
+// } catch (e: MXCryptoError) {
+// Timber.v("Failed to decrypt event")
+// // TODO -> we should keep track of this and retry, or some processing will never be handled
+// }
+// }
+// }
private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
return processors.any {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/tools/RealmDebugTools.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/tools/RealmDebugTools.kt
new file mode 100644
index 0000000000..103e84dea6
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/tools/RealmDebugTools.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.database.tools
+
+import io.realm.Realm
+import io.realm.RealmConfiguration
+import org.matrix.android.sdk.BuildConfig
+import timber.log.Timber
+
+internal class RealmDebugTools(
+ private val realmConfiguration: RealmConfiguration
+) {
+ /**
+ * Log info about the DB
+ */
+ fun logInfo(baseName: String) {
+ buildString {
+ append("\n$baseName Realm located at : ${realmConfiguration.realmDirectory}/${realmConfiguration.realmFileName}")
+
+ if (BuildConfig.LOG_PRIVATE_DATA) {
+ val key = realmConfiguration.encryptionKey.joinToString("") { byte -> "%02x".format(byte) }
+ append("\n$baseName Realm encryption key : $key")
+ }
+
+ Realm.getInstance(realmConfiguration).use { realm ->
+ // Check if we have data
+ separator()
+ separator()
+ append("\n$baseName Realm is empty: ${realm.isEmpty}")
+ var total = 0L
+ val maxNameLength = realmConfiguration.realmObjectClasses.maxOf { it.simpleName.length }
+ realmConfiguration.realmObjectClasses.forEach { modelClazz ->
+ val count = realm.where(modelClazz).count()
+ total += count
+ append("\n$baseName Realm - count ${modelClazz.simpleName.padEnd(maxNameLength)} : $count")
+ }
+ separator()
+ append("\n$baseName Realm - total count: $total")
+ separator()
+ separator()
+ }
+ }
+ .let { Timber.i(it) }
+ }
+
+ private fun StringBuilder.separator() = append("\n==============================================")
+}
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 679a24be0c..25345e953c 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
@@ -59,12 +59,13 @@ import org.matrix.android.sdk.api.session.user.UserService
import org.matrix.android.sdk.api.session.widgets.WidgetService
import org.matrix.android.sdk.internal.auth.SessionParamsStore
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
+import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService
-import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
+import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
import org.matrix.android.sdk.internal.session.sync.job.SyncThread
import org.matrix.android.sdk.internal.session.sync.job.SyncWorker
@@ -114,14 +115,14 @@ internal class DefaultSession @Inject constructor(
private val accountDataService: Lazy,
private val _sharedSecretStorageService: Lazy,
private val accountService: Lazy,
- private val timelineEventDecryptor: TimelineEventDecryptor,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val defaultIdentityService: DefaultIdentityService,
private val integrationManagerService: IntegrationManagerService,
private val taskExecutor: TaskExecutor,
private val callSignalingService: Lazy,
@UnauthenticatedWithCertificate
- private val unauthenticatedWithCertificateOkHttpClient: Lazy
+ private val unauthenticatedWithCertificateOkHttpClient: Lazy,
+ private val eventSenderProcessor: EventSenderProcessor
) : Session,
RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(),
@@ -160,7 +161,7 @@ internal class DefaultSession @Inject constructor(
lifecycleObservers.forEach { it.onStart() }
}
eventBus.register(this)
- timelineEventDecryptor.start()
+ eventSenderProcessor.start()
}
override fun requireBackgroundSync() {
@@ -197,13 +198,14 @@ internal class DefaultSession @Inject constructor(
override fun close() {
assert(isOpen)
stopSync()
- timelineEventDecryptor.destroy()
+ // timelineEventDecryptor.destroy()
uiHandler.post {
lifecycleObservers.forEach { it.onStop() }
}
cryptoService.get().close()
isOpen = false
eventBus.unregister(this)
+ eventSenderProcessor.interrupt()
}
override fun getSyncStateLive() = getSyncThread().liveState()
@@ -283,4 +285,8 @@ internal class DefaultSession @Inject constructor(
override fun toString(): String {
return "$myUserId - ${sessionParams.deviceId}"
}
+
+ override fun logDbUsageInfo() {
+ RealmDebugTools(realmConfiguration).logInfo("Session")
+ }
}
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 ffa7c841cf..e6fd5a7a0c 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
@@ -24,6 +24,7 @@ import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker
import org.matrix.android.sdk.internal.crypto.CryptoModule
import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker
import org.matrix.android.sdk.internal.crypto.SendGossipWorker
+import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker
import org.matrix.android.sdk.internal.di.MatrixComponent
import org.matrix.android.sdk.internal.di.SessionAssistedInjectModule
@@ -45,7 +46,6 @@ import org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker
import org.matrix.android.sdk.internal.session.pushers.PushersModule
import org.matrix.android.sdk.internal.session.room.RoomModule
import org.matrix.android.sdk.internal.session.room.relation.SendRelationWorker
-import org.matrix.android.sdk.internal.session.room.send.EncryptEventWorker
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker
import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
@@ -109,8 +109,6 @@ internal interface SessionComponent {
fun inject(worker: SendRelationWorker)
- fun inject(worker: EncryptEventWorker)
-
fun inject(worker: MultipleEventSendingDispatcherWorker)
fun inject(worker: RedactEventWorker)
@@ -131,6 +129,8 @@ internal interface SessionComponent {
fun inject(worker: SendGossipWorker)
+ fun inject(worker: UpdateTrustWorker)
+
@Component.Factory
interface Factory {
fun create(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
index 355a152c82..32949d60c4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
@@ -41,8 +41,9 @@ import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
-import org.matrix.android.sdk.internal.crypto.crosssigning.ShieldTrustUpdater
import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
+import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask
+import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor
import org.matrix.android.sdk.internal.database.DatabaseCleaner
import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
@@ -331,10 +332,6 @@ internal abstract class SessionModule {
@IntoSet
abstract fun bindWidgetUrlFormatter(formatter: DefaultWidgetURLFormatter): SessionLifecycleObserver
- @Binds
- @IntoSet
- abstract fun bindShieldTrustUpdated(updater: ShieldTrustUpdater): SessionLifecycleObserver
-
@Binds
@IntoSet
abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver
@@ -367,4 +364,7 @@ internal abstract class SessionModule {
@Binds
abstract fun bindTypingUsersTracker(tracker: DefaultTypingUsersTracker): TypingUsersTracker
+
+ @Binds
+ abstract fun bindRedactEventTask(task: DefaultRedactEventTask): RedactEventTask
}
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 519beaf2ac..019da27d27 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
@@ -36,8 +36,8 @@ 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.session.room.send.RoomEventSender
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import timber.log.Timber
@@ -50,7 +50,7 @@ internal class DefaultCallSignalingService @Inject constructor(
private val userId: String,
private val activeCallHandler: ActiveCallHandler,
private val localEchoEventFactory: LocalEchoEventFactory,
- private val roomEventSender: RoomEventSender,
+ private val eventSenderProcessor: EventSenderProcessor,
private val taskExecutor: TaskExecutor,
private val turnServerTask: GetTurnServerTask
) : CallSignalingService {
@@ -103,7 +103,7 @@ internal class DefaultCallSignalingService @Inject constructor(
otherUserId = otherUserId,
isVideoCall = isVideoCall,
localEchoEventFactory = localEchoEventFactory,
- roomEventSender = roomEventSender
+ eventSenderProcessor = eventSenderProcessor
)
activeCallHandler.addCall(call).also {
return call
@@ -165,7 +165,7 @@ internal class DefaultCallSignalingService @Inject constructor(
otherUserId = event.senderId ?: return@let,
isVideoCall = content.isVideo(),
localEchoEventFactory = localEchoEventFactory,
- roomEventSender = roomEventSender
+ eventSenderProcessor = eventSenderProcessor
)
activeCallHandler.addCall(incomingCall)
onCallInvite(incomingCall, content)
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 7edb375d8e..6c0d437a60 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
@@ -29,8 +29,8 @@ 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.internal.session.call.DefaultCallSignalingService
+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.session.room.send.RoomEventSender
import org.webrtc.IceCandidate
import org.webrtc.SessionDescription
import timber.log.Timber
@@ -43,7 +43,7 @@ internal class MxCallImpl(
override val otherUserId: String,
override val isVideoCall: Boolean,
private val localEchoEventFactory: LocalEchoEventFactory,
- private val roomEventSender: RoomEventSender
+ private val eventSenderProcessor: EventSenderProcessor
) : MxCall {
override var state: CallState = CallState.Idle
@@ -91,7 +91,7 @@ internal class MxCallImpl(
offer = CallInviteContent.Offer(sdp = sdp.description)
)
.let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) }
- .also { roomEventSender.sendEvent(it) }
+ .also { eventSenderProcessor.postEvent(it) }
}
override fun sendLocalIceCandidates(candidates: List) {
@@ -106,7 +106,7 @@ internal class MxCallImpl(
}
)
.let { createEventAndLocalEcho(type = EventType.CALL_CANDIDATES, roomId = roomId, content = it.toContent()) }
- .also { roomEventSender.sendEvent(it) }
+ .also { eventSenderProcessor.postEvent(it) }
}
override fun sendLocalIceCandidateRemovals(candidates: List) {
@@ -119,7 +119,7 @@ internal class MxCallImpl(
callId = callId
)
.let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
- .also { roomEventSender.sendEvent(it) }
+ .also { eventSenderProcessor.postEvent(it) }
state = CallState.Terminated
}
@@ -132,7 +132,7 @@ internal class MxCallImpl(
answer = CallAnswerContent.Answer(sdp = sdp.description)
)
.let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) }
- .also { roomEventSender.sendEvent(it) }
+ .also { eventSenderProcessor.postEvent(it) }
}
private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
index 171e90703c..d49c2f120c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
@@ -61,7 +61,7 @@ internal class DefaultRoomService @Inject constructor(
return roomGetter.getRoom(roomId)
}
- override fun getExistingDirectRoomWithUser(otherUserId: String): Room? {
+ override fun getExistingDirectRoomWithUser(otherUserId: String): String? {
return roomGetter.getDirectRoomWith(otherUserId)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
index 9ff0deec89..d090ba5296 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
@@ -15,7 +15,7 @@
*/
package org.matrix.android.sdk.internal.session.room
-import org.matrix.android.sdk.api.session.crypto.CryptoService
+import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
@@ -47,7 +47,6 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where
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
@@ -78,9 +77,8 @@ private fun VerificationState?.toState(newState: VerificationState): Verificatio
return newState
}
-internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String,
- private val cryptoService: CryptoService
-) : EventInsertLiveProcessor {
+internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String)
+ : EventInsertLiveProcessor {
private val allowedTypes = listOf(
EventType.MESSAGE,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt
index 2947518605..eb9cd9fcba 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt
@@ -25,13 +25,12 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.session.SessionScope
-import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import javax.inject.Inject
internal interface RoomGetter {
fun getRoom(roomId: String): Room?
- fun getDirectRoomWith(otherUserId: String): Room?
+ fun getDirectRoomWith(otherUserId: String): String?
}
@SessionScope
@@ -46,16 +45,14 @@ internal class DefaultRoomGetter @Inject constructor(
}
}
- override fun getDirectRoomWith(otherUserId: String): Room? {
+ override fun getDirectRoomWith(otherUserId: String): String? {
return realmSessionProvider.withRealm { realm ->
RoomSummaryEntity.where(realm)
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
.findAll()
- .filter { dm -> dm.otherMemberIds.contains(otherUserId) }
- .map { it.roomId }
- .firstOrNull { roomId -> otherUserId in RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() }
- ?.let { roomId -> createRoom(realm, roomId) }
+ .firstOrNull { dm -> dm.otherMemberIds.size == 1 && dm.otherMemberIds.first() == otherUserId }
+ ?.roomId
}
}
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 30e3337a68..6381796ee0 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
@@ -101,6 +101,7 @@ internal abstract class RoomModule {
fun providesHtmlRenderer(): HtmlRenderer {
return HtmlRenderer
.builder()
+ .softbreak(" ")
.build()
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
index 7e28200ccd..632fcab70b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
@@ -16,10 +16,10 @@
package org.matrix.android.sdk.internal.session.room.create
+import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
-import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import org.matrix.android.sdk.api.session.identity.toMedium
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
@@ -27,11 +27,13 @@ import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
+import org.matrix.android.sdk.internal.session.content.FileUploader
import org.matrix.android.sdk.internal.session.identity.EnsureIdentityTokenTask
import org.matrix.android.sdk.internal.session.identity.data.IdentityStore
import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol
import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
import java.security.InvalidParameterException
+import java.util.UUID
import javax.inject.Inject
internal class CreateRoomBodyBuilder @Inject constructor(
@@ -39,6 +41,7 @@ internal class CreateRoomBodyBuilder @Inject constructor(
private val crossSigningService: CrossSigningService,
private val deviceListManager: DeviceListManager,
private val identityStore: IdentityStore,
+ private val fileUploader: FileUploader,
@AuthenticatedIdentity
private val accessTokenProvider: AccessTokenProvider
) {
@@ -66,7 +69,8 @@ internal class CreateRoomBodyBuilder @Inject constructor(
val initialStates = listOfNotNull(
buildEncryptionWithAlgorithmEvent(params),
- buildHistoryVisibilityEvent(params)
+ buildHistoryVisibilityEvent(params),
+ buildAvatarEvent(params)
)
.takeIf { it.isNotEmpty() }
@@ -85,15 +89,33 @@ internal class CreateRoomBodyBuilder @Inject constructor(
)
}
+ private suspend fun buildAvatarEvent(params: CreateRoomParams): Event? {
+ return params.avatarUri?.let { avatarUri ->
+ // First upload the image, ignoring any error
+ tryOrNull {
+ fileUploader.uploadFromUri(
+ uri = avatarUri,
+ filename = UUID.randomUUID().toString(),
+ mimeType = "image/jpeg")
+ }
+ ?.let { response ->
+ Event(
+ type = EventType.STATE_ROOM_AVATAR,
+ stateKey = "",
+ content = mapOf("url" to response.contentUri)
+ )
+ }
+ }
+ }
+
private fun buildHistoryVisibilityEvent(params: CreateRoomParams): Event? {
return params.historyVisibility
?.let {
- val contentMap = mapOf("history_visibility" to it)
-
Event(
type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
stateKey = "",
- content = contentMap.toContent())
+ content = mapOf("history_visibility" to it)
+ )
}
}
@@ -111,12 +133,10 @@ internal class CreateRoomBodyBuilder @Inject constructor(
if (it != MXCRYPTO_ALGORITHM_MEGOLM) {
throw InvalidParameterException("Unsupported algorithm: $it")
}
- val contentMap = mapOf("algorithm" to it)
-
Event(
type = EventType.STATE_ROOM_ENCRYPTION,
stateKey = "",
- content = contentMap.toContent()
+ content = mapOf("algorithm" to it)
)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
index a151a16383..a7f3f83980 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
@@ -17,12 +17,10 @@ package org.matrix.android.sdk.internal.session.room.relation
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
-import androidx.work.OneTimeWorkRequest
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixCallback
-import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageType
@@ -32,30 +30,25 @@ import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
+import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.di.SessionId
-import org.matrix.android.sdk.internal.session.room.send.EncryptEventWorker
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
-import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker
-import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
-import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon
+import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.fetchCopyMap
-import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import timber.log.Timber
internal class DefaultRelationService @AssistedInject constructor(
@Assisted private val roomId: String,
- @SessionId private val sessionId: String,
- private val timeLineSendEventWorkCommon: TimelineSendEventWorkCommon,
+ private val eventSenderProcessor: EventSenderProcessor,
private val eventFactory: LocalEchoEventFactory,
- private val cryptoService: CryptoService,
+ private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask,
private val timelineEventMapper: TimelineEventMapper,
@@ -83,8 +76,7 @@ internal class DefaultRelationService @AssistedInject constructor(
.none { it.addedByMe && it.key == reaction }) {
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
.also { saveLocalEcho(it) }
- val sendRelationWork = createSendEventWork(event, true)
- timeLineSendEventWorkCommon.postWork(roomId, sendRelationWork)
+ return eventSenderProcessor.postEvent(event, false /* reaction are not encrypted*/)
} else {
Timber.w("Reaction already added")
NoOpCancellable
@@ -107,9 +99,7 @@ internal class DefaultRelationService @AssistedInject constructor(
data.redactEventId?.let { toRedact ->
val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null)
.also { saveLocalEcho(it) }
- val redactWork = createRedactEventWork(redactEvent, toRedact, null)
-
- timeLineSendEventWorkCommon.postWork(roomId, redactWork)
+ eventSenderProcessor.postRedaction(redactEvent, null)
}
}
}
@@ -121,18 +111,6 @@ internal class DefaultRelationService @AssistedInject constructor(
.executeBy(taskExecutor)
}
- // TODO duplicate with send service?
- private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest {
- val sendContentWorkerParams = RedactEventWorker.Params(
- sessionId,
- localEvent.eventId!!,
- roomId,
- eventId,
- reason)
- val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
- return timeLineSendEventWorkCommon.createWork(redactWorkData, true)
- }
-
override fun editTextMessage(targetEventId: String,
msgType: String,
newBodyText: CharSequence,
@@ -141,14 +119,7 @@ internal class DefaultRelationService @AssistedInject constructor(
val event = eventFactory
.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
.also { saveLocalEcho(it) }
- return if (cryptoService.isRoomEncrypted(roomId)) {
- val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
- val workRequest = createSendEventWork(event, false)
- timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest)
- } else {
- val workRequest = createSendEventWork(event, true)
- timeLineSendEventWorkCommon.postWork(roomId, workRequest)
- }
+ return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
}
override fun editReply(replyToEdit: TimelineEvent,
@@ -165,18 +136,11 @@ internal class DefaultRelationService @AssistedInject constructor(
compatibilityBodyText
)
.also { saveLocalEcho(it) }
- return if (cryptoService.isRoomEncrypted(roomId)) {
- val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
- val workRequest = createSendEventWork(event, false)
- timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest)
- } else {
- val workRequest = createSendEventWork(event, true)
- timeLineSendEventWorkCommon.postWork(roomId, workRequest)
- }
+ return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
}
override fun fetchEditHistory(eventId: String, callback: MatrixCallback>) {
- val params = FetchEditHistoryTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), eventId)
+ val params = FetchEditHistoryTask.Params(roomId, cryptoSessionInfoProvider.isRoomEncrypted(roomId), eventId)
fetchEditHistoryTask
.configureWith(params) {
this.callback = callback
@@ -189,27 +153,7 @@ internal class DefaultRelationService @AssistedInject constructor(
?.also { saveLocalEcho(it) }
?: return null
- return if (cryptoService.isRoomEncrypted(roomId)) {
- val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
- val workRequest = createSendEventWork(event, false)
- timeLineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, workRequest)
- } else {
- val workRequest = createSendEventWork(event, true)
- timeLineSendEventWorkCommon.postWork(roomId, workRequest)
- }
- }
-
- private fun createEncryptEventWork(event: Event, keepKeys: List?): OneTimeWorkRequest {
- // Same parameter
- val params = EncryptEventWorker.Params(sessionId, event.eventId!!, keepKeys)
- val sendWorkData = WorkerParamsFactory.toData(params)
- return timeLineSendEventWorkCommon.createWork(sendWorkData, true)
- }
-
- private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
- val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = event.eventId!!)
- val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
- return timeLineSendEventWorkCommon.createWork(sendWorkData, startChain)
+ return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
}
override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index ec366cb6aa..b13ce15da6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -25,7 +25,6 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
-import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.isTextMessage
@@ -45,13 +44,13 @@ import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.CancelableBag
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.NoOpCancellable
+import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
-import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon
+import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.CancelableWork
-import org.matrix.android.sdk.internal.worker.AlwaysSuccessfulWorker
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.startChain
import timber.log.Timber
@@ -63,13 +62,12 @@ private const val UPLOAD_WORK = "UPLOAD_WORK"
internal class DefaultSendService @AssistedInject constructor(
@Assisted private val roomId: String,
private val workManagerProvider: WorkManagerProvider,
- private val timelineSendEventWorkCommon: TimelineSendEventWorkCommon,
@SessionId private val sessionId: String,
private val localEchoEventFactory: LocalEchoEventFactory,
- private val cryptoService: CryptoService,
+ private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val taskExecutor: TaskExecutor,
private val localEchoRepository: LocalEchoRepository,
- private val roomEventSender: RoomEventSender,
+ private val eventSenderProcessor: EventSenderProcessor,
private val cancelSendTracker: CancelSendTracker
) : SendService {
@@ -92,19 +90,6 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) }
}
- // For test only
- private fun sendTextMessages(text: CharSequence, msgType: String, autoMarkdown: Boolean, times: Int): Cancelable {
- return CancelableBag().apply {
- // Send the event several times
- repeat(times) { i ->
- localEchoEventFactory.createTextEvent(roomId, msgType, "$text - $i", autoMarkdown)
- .also { createLocalEcho(it) }
- .let { sendEvent(it) }
- .also { add(it) }
- }
- }
- }
-
override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType)
.also { createLocalEcho(it) }
@@ -133,13 +118,14 @@ internal class DefaultSendService @AssistedInject constructor(
override fun redactEvent(event: Event, reason: String?): Cancelable {
// TODO manage media/attachements?
- return createRedactEventWork(event, reason)
- .let { timelineSendEventWorkCommon.postWork(roomId, it) }
+ val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)
+ .also { createLocalEcho(it) }
+ return eventSenderProcessor.postRedaction(redactionEcho, reason)
}
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable {
if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) {
- localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
+ localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
return sendEvent(localEcho.root)
}
return NoOpCancellable
@@ -153,7 +139,7 @@ internal class DefaultSendService @AssistedInject constructor(
val url = messageContent.getFileUrl() ?: return NoOpCancellable
if (url.startsWith("mxc://")) {
// We need to resend only the message as the attachment is ok
- localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
+ localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
return sendEvent(localEcho.root)
}
@@ -170,7 +156,7 @@ internal class DefaultSendService @AssistedInject constructor(
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.IMAGE
)
- localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
+ localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true)
}
is MessageVideoContent -> {
@@ -184,7 +170,7 @@ internal class DefaultSendService @AssistedInject constructor(
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.VIDEO
)
- localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
+ localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true)
}
is MessageFileContent -> {
@@ -195,7 +181,7 @@ internal class DefaultSendService @AssistedInject constructor(
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.FILE
)
- localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
+ localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true)
}
is MessageAudioContent -> {
@@ -207,7 +193,7 @@ internal class DefaultSendService @AssistedInject constructor(
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.AUDIO
)
- localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
+ localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true)
}
else -> NoOpCancellable
@@ -222,25 +208,6 @@ internal class DefaultSendService @AssistedInject constructor(
}
}
- override fun clearSendingQueue() {
- timelineSendEventWorkCommon.cancelAllWorks(roomId)
- workManagerProvider.workManager.cancelUniqueWork(buildWorkName(UPLOAD_WORK))
-
- // Replace the worker chains with a AlwaysSuccessfulWorker, to ensure the queues are well emptied
- workManagerProvider.matrixOneTimeWorkRequestBuilder()
- .build().let {
- timelineSendEventWorkCommon.postWork(roomId, it, ExistingWorkPolicy.REPLACE)
-
- // need to clear also image sending queue
- workManagerProvider.workManager
- .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it)
- .enqueue()
- }
- taskExecutor.executorScope.launch {
- localEchoRepository.clearSendingQueue(roomId)
- }
- }
-
override fun cancelSend(eventId: String) {
cancelSendTracker.markLocalEchoForCancel(eventId, roomId)
taskExecutor.executorScope.launch {
@@ -262,13 +229,6 @@ internal class DefaultSendService @AssistedInject constructor(
}
}
-// override fun failAllPendingMessages() {
-// taskExecutor.executorScope.launch {
-// val eventsToResend = localEchoRepository.getAllEventsWithStates(roomId, SendState.PENDING_STATES)
-// localEchoRepository.updateSendState(roomId, eventsToResend.map { it.eventId }, SendState.UNDELIVERED)
-// }
-// }
-
override fun sendMedia(attachment: ContentAttachmentData,
compressBeforeSending: Boolean,
roomIds: Set): Cancelable {
@@ -291,7 +251,7 @@ internal class DefaultSendService @AssistedInject constructor(
private fun internalSendMedia(allLocalEchoes: List, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable {
val cancelableBag = CancelableBag()
- allLocalEchoes.groupBy { cryptoService.isRoomEncrypted(it.roomId!!) }
+ allLocalEchoes.groupBy { cryptoSessionInfoProvider.isRoomEncrypted(it.roomId!!) }
.apply {
keys.forEach { isRoomEncrypted ->
// Should never be empty
@@ -301,7 +261,7 @@ internal class DefaultSendService @AssistedInject constructor(
val dispatcherWork = createMultipleEventDispatcherWork(isRoomEncrypted)
workManagerProvider.workManager
- .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
+ .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND_OR_REPLACE, uploadWork)
.then(dispatcherWork)
.enqueue()
.also { operation ->
@@ -322,7 +282,7 @@ internal class DefaultSendService @AssistedInject constructor(
}
private fun sendEvent(event: Event): Cancelable {
- return roomEventSender.sendEvent(event)
+ return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(event.roomId!!))
}
private fun createLocalEcho(event: Event) {
@@ -333,28 +293,6 @@ internal class DefaultSendService @AssistedInject constructor(
return "${roomId}_$identifier"
}
- private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
- // Same parameter
- return EncryptEventWorker.Params(sessionId, event.eventId ?: "")
- .let { WorkerParamsFactory.toData(it) }
- .let {
- workManagerProvider.matrixOneTimeWorkRequestBuilder()
- .setConstraints(WorkManagerProvider.workConstraints)
- .setInputData(it)
- .startChain(startChain)
- .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
- .build()
- }
- }
-
- private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
- return localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)
- .also { createLocalEcho(it) }
- .let { RedactEventWorker.Params(sessionId, it.eventId!!, roomId, event.eventId, reason) }
- .let { WorkerParamsFactory.toData(it) }
- .let { timelineSendEventWorkCommon.createWork(it, true) }
- }
-
private fun createUploadMediaWork(allLocalEchos: List,
attachment: ContentAttachmentData,
isRoomEncrypted: Boolean,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt
deleted file mode 100644
index 73b4c48e14..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * 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.room.send
-
-import android.content.Context
-import androidx.work.WorkerParameters
-import com.squareup.moshi.JsonClass
-import org.matrix.android.sdk.api.failure.Failure
-import org.matrix.android.sdk.api.session.crypto.CryptoService
-import org.matrix.android.sdk.api.session.events.model.Event
-import org.matrix.android.sdk.api.session.events.model.EventType
-import org.matrix.android.sdk.api.session.events.model.toContent
-import org.matrix.android.sdk.api.session.room.send.SendState
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
-import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
-import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
-import org.matrix.android.sdk.internal.database.mapper.ContentMapper
-import org.matrix.android.sdk.internal.session.SessionComponent
-import org.matrix.android.sdk.internal.util.awaitCallback
-import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
-import org.matrix.android.sdk.internal.worker.SessionWorkerParams
-import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
-import timber.log.Timber
-import javax.inject.Inject
-
-/**
- * Possible previous worker: None
- * Possible next worker : Always [SendEventWorker]
- */
-internal class EncryptEventWorker(context: Context, params: WorkerParameters)
- : SessionSafeCoroutineWorker(context, params, Params::class.java) {
-
- @JsonClass(generateAdapter = true)
- internal data class Params(
- override val sessionId: String,
- val eventId: String,
- /** Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to) */
- val keepKeys: List? = null,
- override val lastFailureMessage: String? = null
- ) : SessionWorkerParams
-
- @Inject lateinit var crypto: CryptoService
- @Inject lateinit var localEchoRepository: LocalEchoRepository
- @Inject lateinit var cancelSendTracker: CancelSendTracker
-
- override fun injectWith(injector: SessionComponent) {
- injector.inject(this)
- }
-
- override suspend fun doSafeWork(params: Params): Result {
- Timber.v("## SendEvent: Start Encrypt work for event ${params.eventId}")
-
- val localEvent = localEchoRepository.getUpToDateEcho(params.eventId)
- if (localEvent?.eventId == null) {
- return Result.success()
- }
-
- if (cancelSendTracker.isCancelRequestedFor(localEvent.eventId, localEvent.roomId)) {
- return Result.success()
- .also { Timber.e("## SendEvent: Event sending has been cancelled ${localEvent.eventId}") }
- }
-
- localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING)
-
- val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf()
- params.keepKeys?.forEach {
- localMutableContent.remove(it)
- }
-
- var error: Throwable? = null
- var result: MXEncryptEventContentResult? = null
- try {
- result = awaitCallback {
- crypto.encryptEventContent(localMutableContent, localEvent.type, localEvent.roomId!!, it)
- }
- } catch (throwable: Throwable) {
- error = throwable
- }
- if (result != null) {
- val modifiedContent = HashMap(result.eventContent)
- params.keepKeys?.forEach { toKeep ->
- localEvent.content?.get(toKeep)?.let {
- // put it back in the encrypted thing
- modifiedContent[toKeep] = it
- }
- }
- // Better handling of local echo, to avoid decrypting transition on remote echo
- // Should I only do it for text messages?
- val decryptionLocalEcho = if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) {
- MXEventDecryptionResult(
- clearEvent = Event(
- type = localEvent.type,
- content = localEvent.content,
- roomId = localEvent.roomId
- ).toContent(),
- forwardingCurve25519KeyChain = emptyList(),
- senderCurve25519Key = result.eventContent["sender_key"] as? String,
- claimedEd25519Key = crypto.getMyDevice().fingerprint()
- )
- } else {
- null
- }
- localEchoRepository.updateEcho(localEvent.eventId) { _, localEcho ->
- localEcho.type = EventType.ENCRYPTED
- localEcho.content = ContentMapper.map(modifiedContent)
- decryptionLocalEcho?.also {
- localEcho.setDecryptionResult(it)
- }
- }
-
- val nextWorkerParams = SendEventWorker.Params(sessionId = params.sessionId, eventId = params.eventId)
- return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
- } else {
- val sendState = when (error) {
- is Failure.CryptoError -> SendState.FAILED_UNKNOWN_DEVICES
- else -> SendState.UNDELIVERED
- }
- localEchoRepository.updateSendState(localEvent.eventId, sendState)
- // always return success, or the chain will be stuck for ever!
- val nextWorkerParams = SendEventWorker.Params(
- sessionId = params.sessionId,
- eventId = localEvent.eventId,
- lastFailureMessage = error?.localizedMessage ?: "Error"
- )
- return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
- }
- }
-
- override fun buildErrorParams(params: Params, message: String): Params {
- return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
- }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
index da3e0429b0..c01923055b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
@@ -90,8 +90,7 @@ internal class LocalEchoEventFactory @Inject constructor(
private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent {
if (autoMarkdown) {
- val source = textPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString()
- return markdownParser.parse(source)
+ return markdownParser.parse(text)
} else {
// Try to detect pills
textPillsUtils.processSpecialSpansToHtml(text)?.let {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt
index 9e1de291c4..f4871ab35d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt
@@ -88,8 +88,9 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
}
}
- fun updateSendState(eventId: String, sendState: SendState) {
+ fun updateSendState(eventId: String, roomId: String?, sendState: SendState) {
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}")
+ eventBus.post(DefaultTimeline.OnLocalEchoUpdated(roomId ?: "", eventId, sendState))
updateEchoAsync(eventId) { realm, sendingEventEntity ->
if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) {
// If already synced, do not put as sent
@@ -137,6 +138,14 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
}
}
+ fun deleteFailedEchoAsync(roomId: String, eventId: String?) {
+ monarchy.runTransactionSync { realm ->
+ TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
+ EventEntity.where(realm, eventId = eventId ?: "").findFirst()?.deleteFromRealm()
+ roomSummaryUpdater.updateSendingInformation(realm, roomId)
+ }
+ }
+
suspend fun clearSendingQueue(roomId: String) {
monarchy.awaitTransaction { realm ->
TimelineEventEntity
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt
index 030c9deb6a..c99d482300 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.send
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
+import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils
import javax.inject.Inject
/**
@@ -27,18 +28,21 @@ import javax.inject.Inject
*/
internal class MarkdownParser @Inject constructor(
private val parser: Parser,
- private val htmlRenderer: HtmlRenderer
+ private val htmlRenderer: HtmlRenderer,
+ private val textPillsUtils: TextPillsUtils
) {
private val mdSpecialChars = "[`_\\-*>.\\[\\]#~]".toRegex()
- fun parse(text: String): TextContent {
+ fun parse(text: CharSequence): TextContent {
+ val source = textPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString()
+
// If no special char are detected, just return plain text
- if (text.contains(mdSpecialChars).not()) {
- return TextContent(text)
+ if (source.contains(mdSpecialChars).not()) {
+ return TextContent(source)
}
- val document = parser.parse(text)
+ val document = parser.parse(source)
val htmlText = htmlRenderer.render(document)
// Cleanup extra paragraph
@@ -48,13 +52,14 @@ internal class MarkdownParser @Inject constructor(
htmlText
}
- return if (isFormattedTextPertinent(text, cleanHtmlText)) {
+ return if (isFormattedTextPertinent(source, cleanHtmlText)) {
// According to https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes:
// The plain text version of the HTML should be provided in the body.
// But it caused too many problems so it has been removed in #2002
- TextContent(text, cleanHtmlText.postTreatment())
+ // See #739
+ TextContent(text.toString(), cleanHtmlText.postTreatment())
} else {
- TextContent(text)
+ TextContent(source)
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt
index ba69a8751b..bc307bc74f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt
@@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.session.room.send
import android.content.Context
-import androidx.work.BackoffPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
@@ -31,7 +30,6 @@ import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.startChain
import timber.log.Timber
-import java.util.concurrent.TimeUnit
import javax.inject.Inject
/**
@@ -57,7 +55,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
override fun doOnError(params: Params): Result {
params.localEchoIds.forEach { localEchoIds ->
- localEchoRepository.updateSendState(localEchoIds.eventId, SendState.UNDELIVERED)
+ localEchoRepository.updateSendState(localEchoIds.eventId, localEchoIds.roomId, SendState.UNDELIVERED)
}
return super.doOnError(params)
@@ -73,19 +71,10 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
params.localEchoIds.forEach { localEchoIds ->
val roomId = localEchoIds.roomId
val eventId = localEchoIds.eventId
- if (params.isEncrypted) {
- localEchoRepository.updateSendState(eventId, SendState.ENCRYPTING)
- Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event $eventId")
- val encryptWork = createEncryptEventWork(params.sessionId, eventId, true)
- // Note that event will be replaced by the result of the previous work
- val sendWork = createSendEventWork(params.sessionId, eventId, false)
- timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork)
- } else {
- localEchoRepository.updateSendState(eventId, SendState.SENDING)
+ localEchoRepository.updateSendState(eventId, roomId, SendState.SENDING)
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event $eventId")
val sendWork = createSendEventWork(params.sessionId, eventId, true)
timelineSendEventWorkCommon.postWork(roomId, sendWork)
- }
}
return Result.success()
@@ -95,18 +84,6 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
- private fun createEncryptEventWork(sessionId: String, eventId: String, startChain: Boolean): OneTimeWorkRequest {
- val params = EncryptEventWorker.Params(sessionId, eventId)
- val sendWorkData = WorkerParamsFactory.toData(params)
-
- return workManagerProvider.matrixOneTimeWorkRequestBuilder()
- .setConstraints(WorkManagerProvider.workConstraints)
- .setInputData(sendWorkData)
- .startChain(startChain)
- .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
- .build()
- }
-
private fun createSendEventWork(sessionId: String, eventId: String, startChain: Boolean): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = eventId)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt
deleted file mode 100644
index 8f783d7478..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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.room.send
-
-import androidx.work.BackoffPolicy
-import androidx.work.OneTimeWorkRequest
-import org.matrix.android.sdk.api.session.crypto.CryptoService
-import org.matrix.android.sdk.api.session.events.model.Event
-import org.matrix.android.sdk.api.util.Cancelable
-import org.matrix.android.sdk.internal.di.SessionId
-import org.matrix.android.sdk.internal.di.WorkManagerProvider
-import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon
-import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
-import org.matrix.android.sdk.internal.worker.startChain
-import timber.log.Timber
-import java.util.concurrent.TimeUnit
-import javax.inject.Inject
-
-internal class RoomEventSender @Inject constructor(
- private val workManagerProvider: WorkManagerProvider,
- private val timelineSendEventWorkCommon: TimelineSendEventWorkCommon,
- @SessionId private val sessionId: String,
- private val cryptoService: CryptoService
-) {
- fun sendEvent(event: Event): Cancelable {
- // Encrypted room handling
- return if (cryptoService.isRoomEncrypted(event.roomId ?: "")
- && !event.isEncrypted() // In case of resend where it's already encrypted so skip to send
- ) {
- Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event ${event.eventId}")
- val encryptWork = createEncryptEventWork(event, true)
- // Note that event will be replaced by the result of the previous work
- val sendWork = createSendEventWork(event, false)
- timelineSendEventWorkCommon.postSequentialWorks(event.roomId ?: "", encryptWork, sendWork)
- } else {
- Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event ${event.eventId}")
- val sendWork = createSendEventWork(event, true)
- timelineSendEventWorkCommon.postWork(event.roomId ?: "", sendWork)
- }
- }
-
- private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
- // Same parameter
- val params = EncryptEventWorker.Params(sessionId, event.eventId!!)
- val sendWorkData = WorkerParamsFactory.toData(params)
-
- return workManagerProvider.matrixOneTimeWorkRequestBuilder()
- .setConstraints(WorkManagerProvider.workConstraints)
- .setInputData(sendWorkData)
- .startChain(startChain)
- .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
- .build()
- }
-
- private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
- val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = event.eventId!!)
- val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
-
- return timelineSendEventWorkCommon.createWork(sendWorkData, startChain)
- }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt
index 0014213b3f..37a429d242 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt
@@ -22,12 +22,11 @@ import com.squareup.moshi.JsonClass
import io.realm.RealmConfiguration
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.shouldBeRetried
-import org.matrix.android.sdk.api.session.events.model.Content
+import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask
import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.SessionComponent
-import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber
@@ -47,11 +46,14 @@ internal class SendEventWorker(context: Context,
internal data class Params(
override val sessionId: String,
override val lastFailureMessage: String? = null,
- val eventId: String
+ val eventId: String,
+ // use this as an override if you want to send in clear in encrypted room
+ val isEncrypted: Boolean? = null
) : SessionWorkerParams
@Inject lateinit var localEchoRepository: LocalEchoRepository
- @Inject lateinit var roomAPI: RoomAPI
+ @Inject lateinit var sendEventTask: SendEventTask
+ @Inject lateinit var cryptoService: CryptoService
@Inject lateinit var eventBus: EventBus
@Inject lateinit var cancelSendTracker: CancelSendTracker
@SessionDatabase @Inject lateinit var realmConfiguration: RealmConfiguration
@@ -63,7 +65,7 @@ internal class SendEventWorker(context: Context,
override suspend fun doSafeWork(params: Params): Result {
val event = localEchoRepository.getUpToDateEcho(params.eventId)
if (event?.eventId == null || event.roomId == null) {
- localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED)
+ localEchoRepository.updateSendState(params.eventId, event?.roomId, SendState.UNDELIVERED)
return Result.success()
.also { Timber.e("Work cancelled due to bad input data") }
}
@@ -77,7 +79,7 @@ internal class SendEventWorker(context: Context,
}
if (params.lastFailureMessage != null) {
- localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED)
+ localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED)
// Transmit the error
return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") }
@@ -85,12 +87,12 @@ internal class SendEventWorker(context: Context,
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}")
return try {
- sendEvent(event.eventId, event.roomId, event.type, event.content)
+ sendEventTask.execute(SendEventTask.Params(event, params.isEncrypted ?: cryptoService.isRoomEncrypted(event.roomId)))
Result.success()
} catch (exception: Throwable) {
if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) {
Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}")
- localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED)
+ localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED)
return Result.success()
} else {
Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}")
@@ -102,12 +104,4 @@ internal class SendEventWorker(context: Context,
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
-
- private suspend fun sendEvent(eventId: String, roomId: String, type: String, content: Content?) {
- localEchoRepository.updateSendState(eventId, SendState.SENDING)
- executeRequest(eventBus) {
- apiCall = roomAPI.send(eventId, roomId, type, content)
- }
- localEchoRepository.updateSendState(eventId, SendState.SENT)
- }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt
new file mode 100644
index 0000000000..62e225c624
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt
@@ -0,0 +1,239 @@
+/*
+ * 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.room.send.queue
+
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.matrix.android.sdk.api.auth.data.SessionParams
+import org.matrix.android.sdk.api.auth.data.sessionId
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.failure.Failure
+import org.matrix.android.sdk.api.failure.MatrixError
+import org.matrix.android.sdk.api.failure.isTokenError
+import org.matrix.android.sdk.api.session.crypto.CryptoService
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.sync.SyncState
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.internal.session.SessionScope
+import org.matrix.android.sdk.internal.task.TaskExecutor
+import timber.log.Timber
+import java.io.IOException
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.net.Socket
+import java.util.Timer
+import java.util.TimerTask
+import java.util.concurrent.LinkedBlockingQueue
+import javax.inject.Inject
+import kotlin.concurrent.schedule
+
+/**
+ * A simple ever running thread unique for that session responsible of sending events in order.
+ * Each send is retried 3 times, if there is no network (e.g if cannot ping home server) it will wait and
+ * periodically test reachability before resume (does not count as a retry)
+ *
+ * If the app is killed before all event were sent, on next wakeup the scheduled events will be re posted
+ */
+@SessionScope
+internal class EventSenderProcessor @Inject constructor(
+ private val cryptoService: CryptoService,
+ private val sessionParams: SessionParams,
+ private val queuedTaskFactory: QueuedTaskFactory,
+ private val taskExecutor: TaskExecutor,
+ private val memento: QueueMemento
+) : Thread("SENDER_THREAD_SID_${sessionParams.credentials.sessionId()}") {
+
+ private fun markAsManaged(task: QueuedTask) {
+ memento.track(task)
+ }
+
+ private fun markAsFinished(task: QueuedTask) {
+ memento.unTrack(task)
+ }
+
+ // API
+ fun postEvent(event: Event): Cancelable {
+ return postEvent(event, event.roomId?.let { cryptoService.isRoomEncrypted(it) } ?: false)
+ }
+
+ override fun start() {
+ super.start()
+ // We should check for sending events not handled because app was killed
+ // But we should be careful of only took those that was submitted to us, because if it's
+ // for example it's a media event it is handled by some worker and he will handle it
+ // This is a bit fragile :/
+ // also some events cannot be retried manually by users, e.g reactions
+ // they were previously relying on workers to do the work :/ and was expected to always finally succeed
+ // Also some echos are not to be resent like redaction echos (fake event created for aggregation)
+
+ tryOrNull {
+ taskExecutor.executorScope.launch {
+ Timber.d("## Send relaunched pending events on restart")
+ memento.restoreTasks(this@EventSenderProcessor)
+ }
+ }
+ }
+
+ fun postEvent(event: Event, encrypt: Boolean): Cancelable {
+ val task = queuedTaskFactory.createSendTask(event, encrypt)
+ return postTask(task)
+ }
+
+ fun postRedaction(redactionLocalEcho: Event, reason: String?): Cancelable {
+ return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason)
+ }
+
+ fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?): Cancelable {
+ val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason)
+ return postTask(task)
+ }
+
+ fun postTask(task: QueuedTask): Cancelable {
+ // non blocking add to queue
+ sendingQueue.add(task)
+ markAsManaged(task)
+ return object : Cancelable {
+ override fun cancel() {
+ task.cancel()
+ }
+ }
+ }
+
+ companion object {
+ private const val RETRY_WAIT_TIME_MS = 10_000L
+ }
+
+ private var sendingQueue = LinkedBlockingQueue()
+
+ private var networkAvailableLock = Object()
+ private var canReachServer = true
+ private var retryNoNetworkTask: TimerTask? = null
+
+ override fun run() {
+ Timber.v("## SendThread started ts:${System.currentTimeMillis()}")
+ try {
+ while (!isInterrupted) {
+ Timber.v("## SendThread wait for task to process")
+ val task = sendingQueue.take()
+ Timber.v("## SendThread Found task to process $task")
+
+ if (task.isCancelled()) {
+ Timber.v("## SendThread send cancelled for $task")
+ // we do not execute this one
+ continue
+ }
+ // we check for network connectivity
+ while (!canReachServer) {
+ Timber.v("## SendThread cannot reach server, wait ts:${System.currentTimeMillis()}")
+ // schedule to retry
+ waitForNetwork()
+ // if thread as been killed meanwhile
+// if (state == State.KILLING) break
+ }
+ Timber.v("## Server is Reachable")
+ // so network is available
+
+ runBlocking {
+ retryLoop@ while (task.retryCount < 3) {
+ try {
+ // SendPerformanceProfiler.startStage(task.event.eventId!!, SendPerformanceProfiler.Stages.SEND_WORKER)
+ Timber.v("## SendThread retryLoop for $task retryCount ${task.retryCount}")
+ task.execute()
+ // sendEventTask.execute(SendEventTask.Params(task.event, task.encrypt, cryptoService))
+ // SendPerformanceProfiler.stopStage(task.event.eventId, SendPerformanceProfiler.Stages.SEND_WORKER)
+ break@retryLoop
+ } catch (exception: Throwable) {
+ when {
+ exception is IOException || exception is Failure.NetworkConnection -> {
+ canReachServer = false
+ task.retryCount++
+ if (task.retryCount >= 3) task.onTaskFailed()
+ while (!canReachServer) {
+ Timber.v("## SendThread retryLoop cannot reach server, wait ts:${System.currentTimeMillis()}")
+ // schedule to retry
+ waitForNetwork()
+ }
+ }
+ (exception is Failure.ServerError && exception.error.code == MatrixError.M_LIMIT_EXCEEDED) -> {
+ task.retryCount++
+ if (task.retryCount >= 3) task.onTaskFailed()
+ Timber.v("## SendThread retryLoop retryable error for $task reason: ${exception.localizedMessage}")
+ // wait a bit
+ // Todo if its a quota exception can we get timout?
+ sleep(3_000)
+ continue@retryLoop
+ }
+ exception.isTokenError() -> {
+ Timber.v("## SendThread retryLoop retryable TOKEN error, interrupt")
+ // we can exit the loop
+ task.onTaskFailed()
+ throw InterruptedException()
+ }
+ else -> {
+ Timber.v("## SendThread retryLoop Un-Retryable error, try next task")
+ // this task is in error, check next one?
+ break@retryLoop
+ }
+ }
+ }
+ }
+ }
+ markAsFinished(task)
+ }
+ } catch (interruptionException: InterruptedException) {
+ // will be thrown is thread is interrupted while seeping
+ interrupt()
+ Timber.v("## InterruptedException!! ${interruptionException.localizedMessage}")
+ }
+// state = State.KILLED
+ // is this needed?
+ retryNoNetworkTask?.cancel()
+ Timber.w("## SendThread finished ${System.currentTimeMillis()}")
+ }
+
+ private fun waitForNetwork() {
+ retryNoNetworkTask = Timer(SyncState.NoNetwork.toString(), false).schedule(RETRY_WAIT_TIME_MS) {
+ synchronized(networkAvailableLock) {
+ canReachServer = checkHostAvailable().also {
+ Timber.v("## SendThread checkHostAvailable $it")
+ }
+ networkAvailableLock.notify()
+ }
+ }
+ synchronized(networkAvailableLock) { networkAvailableLock.wait() }
+ }
+
+ /**
+ * Check if homeserver is reachable.
+ */
+ private fun checkHostAvailable(): Boolean {
+ val host = sessionParams.homeServerConnectionConfig.homeServerUri.host ?: return false
+ val port = sessionParams.homeServerConnectionConfig.homeServerUri.port.takeIf { it != -1 } ?: 80
+ val timeout = 30_000
+ try {
+ Socket().use { socket ->
+ val inetAddress: InetAddress = InetAddress.getByName(host)
+ val inetSocketAddress = InetSocketAddress(inetAddress, port)
+ socket.connect(inetSocketAddress, timeout)
+ return true
+ }
+ } catch (e: IOException) {
+ Timber.v("## EventSender isHostAvailable failure ${e.localizedMessage}")
+ return false
+ }
+ }
+}
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
new file mode 100644
index 0000000000..e69c65ec4c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueueMemento.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.room.send.queue
+
+import android.content.Context
+import org.matrix.android.sdk.api.auth.data.sessionId
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.crypto.CryptoService
+import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.internal.di.SessionId
+import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * Simple lightweight persistence
+ * Don't want to go in DB due to current issues
+ * Will never manage lots of events, it simply uses sharedPreferences.
+ * It is just used to remember what events/localEchos was managed by the event sender in order to
+ * reschedule them (and only them) on next restart
+ */
+internal class QueueMemento @Inject constructor(context: Context,
+ @SessionId sessionId: String,
+ private val queuedTaskFactory: QueuedTaskFactory,
+ private val localEchoRepository: LocalEchoRepository,
+ private val cryptoService: CryptoService) {
+
+ private val storage = context.getSharedPreferences("QueueMemento_$sessionId", Context.MODE_PRIVATE)
+ private val managedTaskInfos = mutableListOf()
+
+ fun track(task: QueuedTask) {
+ synchronized(managedTaskInfos) {
+ managedTaskInfos.add(task)
+ persist()
+ }
+ }
+
+ fun unTrack(task: QueuedTask) {
+ managedTaskInfos.remove(task)
+ persist()
+ }
+
+ private fun persist() {
+ managedTaskInfos.mapIndexedNotNull { index, queuedTask ->
+ toTaskInfo(queuedTask, index)?.let { TaskInfo.map(it) }
+ }.toSet().let { set ->
+ storage.edit()
+ .putStringSet("ManagedBySender", set)
+ .apply()
+ }
+ }
+
+ 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
+ }
+ }
+ }
+
+ suspend fun restoreTasks(eventProcessor: EventSenderProcessor) {
+ // events should be restarted in correct order
+ storage.getStringSet("ManagedBySender", null)?.let { pending ->
+ Timber.d("## Send - Recovering unsent events $pending")
+ pending.mapNotNull { tryOrNull { TaskInfo.map(it) } }
+ }
+ ?.sortedBy { it.order }
+ ?.forEach { info ->
+ try {
+ when (info) {
+ 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)
+ Timber.d("## Send -Reschedule send $info")
+ eventProcessor.postTask(queuedTaskFactory.createSendTask(it, info.encrypt ?: cryptoService.isRoomEncrypted(it.roomId)))
+ }
+ }
+ }
+ is RedactEventTaskInfo -> {
+ info.redactionLocalEcho?.let { localEchoRepository.getUpToDateEcho(it) }?.let {
+ localEchoRepository.updateSendState(it.eventId!!, it.roomId, SendState.UNSENT)
+ // try to get reason
+ val reason = it.content?.get("reason") as? String
+ if (it.redacts != null && it.roomId != null) {
+ Timber.d("## Send -Reschedule redact $info")
+ eventProcessor.postTask(queuedTaskFactory.createRedactTask(it.eventId, it.redacts, it.roomId, reason))
+ }
+ }
+ // postTask(queuedTaskFactory.createRedactTask(info.eventToRedactId, info.)
+ }
+ }
+ } catch (failure: Throwable) {
+ Timber.e("failed to restore task $info")
+ }
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt
similarity index 63%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt
index 271b9e52d3..bccbc97ff4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTask.kt
@@ -5,7 +5,7 @@
* 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
+ * 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,
@@ -13,14 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.matrix.android.sdk.internal.crypto.crosssigning
-data class SessionToCryptoRoomMembersUpdate(
- val roomId: String,
- val isDirect: Boolean,
- val userIds: List
-)
+package org.matrix.android.sdk.internal.session.room.send.queue
-data class CryptoToSessionUserTrustChange(
- val userIds: List
-)
+abstract class QueuedTask {
+ var retryCount = 0
+
+ abstract suspend fun execute()
+
+ abstract fun onTaskFailed()
+
+ abstract fun isCancelled() : Boolean
+
+ abstract fun cancel()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt
new file mode 100644
index 0000000000..90bb47c435
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/QueuedTaskFactory.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.room.send.queue
+
+import org.matrix.android.sdk.api.session.crypto.CryptoService
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
+import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask
+import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
+import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
+import javax.inject.Inject
+
+internal class QueuedTaskFactory @Inject constructor(
+ private val sendEventTask: SendEventTask,
+ private val cryptoService: CryptoService,
+ private val localEchoRepository: LocalEchoRepository,
+ private val redactEventTask: RedactEventTask,
+ private val cancelSendTracker: CancelSendTracker
+) {
+
+ fun createSendTask(event: Event, encrypt: Boolean): QueuedTask {
+ return SendEventQueuedTask(
+ event = event,
+ encrypt = encrypt,
+ cryptoService = cryptoService,
+ localEchoRepository = localEchoRepository,
+ sendEventTask = sendEventTask,
+ cancelSendTracker = cancelSendTracker
+ )
+ }
+
+ fun createRedactTask(redactionLocalEcho: String, eventId: String, roomId: String, reason: String?): QueuedTask {
+ return RedactQueuedTask(
+ redactionLocalEchoId = redactionLocalEcho,
+ toRedactEventId = eventId,
+ roomId = roomId,
+ reason = reason,
+ redactEventTask = redactEventTask,
+ localEchoRepository = localEchoRepository,
+ cancelSendTracker = cancelSendTracker
+ )
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt
new file mode 100644
index 0000000000..a3c19a1f7c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/RedactQueuedTask.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.room.send.queue
+
+import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
+import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
+import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
+
+internal class RedactQueuedTask(
+ val toRedactEventId: String,
+ val redactionLocalEchoId: String,
+ val roomId: String,
+ val reason: String?,
+ val redactEventTask: RedactEventTask,
+ val localEchoRepository: LocalEchoRepository,
+ val cancelSendTracker: CancelSendTracker
+) : QueuedTask() {
+
+ private var _isCancelled: Boolean = false
+
+ override fun toString() = "[RedactEventRunnableTask $redactionLocalEchoId]"
+
+ override suspend fun execute() {
+ redactEventTask.execute(RedactEventTask.Params(redactionLocalEchoId, roomId, toRedactEventId, reason))
+ }
+
+ override fun onTaskFailed() {
+ localEchoRepository.updateSendState(redactionLocalEchoId, roomId, SendState.UNDELIVERED)
+ }
+
+ override fun isCancelled(): Boolean {
+ return _isCancelled || cancelSendTracker.isCancelRequestedFor(redactionLocalEchoId, roomId)
+ }
+
+ override fun cancel() {
+ _isCancelled = true
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt
new file mode 100644
index 0000000000..21a4145a9d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.room.send.queue
+
+import org.matrix.android.sdk.api.session.crypto.CryptoService
+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.room.send.SendState
+import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask
+import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
+import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
+
+internal class SendEventQueuedTask(
+ val event: Event,
+ val encrypt: Boolean,
+ val sendEventTask: SendEventTask,
+ val cryptoService: CryptoService,
+ val localEchoRepository: LocalEchoRepository,
+ val cancelSendTracker: CancelSendTracker
+) : QueuedTask() {
+
+ private var _isCancelled: Boolean = false
+
+ override fun toString() = "[SendEventRunnableTask ${event.eventId}]"
+
+ override suspend fun execute() {
+ sendEventTask.execute(SendEventTask.Params(event, encrypt))
+ }
+
+ override fun onTaskFailed() {
+ when (event.getClearType()) {
+ EventType.REDACTION,
+ EventType.REACTION -> {
+ // we just delete? it will not be present on timeline and no ux to retry
+ localEchoRepository.deleteFailedEchoAsync(eventId = event.eventId, roomId = event.roomId ?: "")
+ // TODO update aggregation :/ or it will stay locally
+ }
+ else -> {
+ localEchoRepository.updateSendState(event.eventId!!, event.roomId, SendState.UNDELIVERED)
+ }
+ }
+ }
+
+ override fun isCancelled(): Boolean {
+ return _isCancelled || cancelSendTracker.isCancelRequestedFor(event.eventId, event.roomId)
+ }
+
+ override fun cancel() {
+ _isCancelled = true
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/TaskInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/TaskInfo.kt
new file mode 100644
index 0000000000..87c6299c4d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/TaskInfo.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.room.send.queue
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import com.squareup.moshi.Moshi
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.internal.di.SerializeNulls
+import org.matrix.android.sdk.internal.network.parsing.RuntimeJsonAdapterFactory
+
+/**
+ * Info that need to be persisted by the sender thread
+ * With polymorphic moshi parsing
+ */
+internal interface TaskInfo {
+ val type: String
+ val order: Int
+
+ companion object {
+ const val TYPE_UNKNOWN = "TYPE_UNKNOWN"
+ const val TYPE_SEND = "TYPE_SEND"
+ const val TYPE_REDACT = "TYPE_REDACT"
+
+ val moshi = Moshi.Builder()
+ .add(RuntimeJsonAdapterFactory.of(TaskInfo::class.java, "type", FallbackTaskInfo::class.java)
+ .registerSubtype(SendEventTaskInfo::class.java, TYPE_SEND)
+ .registerSubtype(RedactEventTaskInfo::class.java, TYPE_REDACT)
+ )
+ .add(SerializeNulls.JSON_ADAPTER_FACTORY)
+ .build()
+
+ fun map(info: TaskInfo): String {
+ return moshi.adapter(TaskInfo::class.java).toJson(info)
+ }
+
+ fun map(string: String): TaskInfo? {
+ return tryOrNull { moshi.adapter(TaskInfo::class.java).fromJson(string) }
+ }
+ }
+}
+
+@JsonClass(generateAdapter = true)
+internal data class SendEventTaskInfo(
+ @Json(name = "type") override val type: String = TaskInfo.TYPE_SEND,
+ @Json(name = "localEchoId") val localEchoId: String,
+ @Json(name = "encrypt") val encrypt: Boolean?,
+ @Json(name = "order") override val order: Int
+) : TaskInfo
+
+@JsonClass(generateAdapter = true)
+internal data class RedactEventTaskInfo(
+ @Json(name = "type") override val type: String = TaskInfo.TYPE_REDACT,
+ @Json(name = "redactionLocalEcho") val redactionLocalEcho: String?,
+ @Json(name = "order") override val order: Int
+) : TaskInfo
+
+@JsonClass(generateAdapter = true)
+internal data class FallbackTaskInfo(
+ @Json(name = "type") override val type: String = TaskInfo.TYPE_REDACT,
+ @Json(name = "order") override val order: Int
+) : TaskInfo
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
index d21805f4f3..65d375e176 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
@@ -140,4 +140,15 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
)
}
}
+
+ override fun deleteAvatar(callback: MatrixCallback): Cancelable {
+ return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
+ sendStateEvent(
+ eventType = EventType.STATE_ROOM_AVATAR,
+ body = emptyMap(),
+ callback = callback,
+ stateKey = null
+ )
+ }
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
index f9a27c367c..8c71604183 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
@@ -16,10 +16,8 @@
package org.matrix.android.sdk.internal.session.room.summary
-import dagger.Lazy
import io.realm.Realm
-import org.greenrobot.eventbus.EventBus
-import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
+import org.matrix.android.sdk.api.extensions.tryOrNull
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.Membership
@@ -28,9 +26,11 @@ import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
-import org.matrix.android.sdk.internal.crypto.crosssigning.SessionToCryptoRoomMembersUpdate
+import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
+import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields
@@ -46,7 +46,6 @@ import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver
import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
-import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications
import timber.log.Timber
@@ -56,8 +55,8 @@ internal class RoomSummaryUpdater @Inject constructor(
@UserId private val userId: String,
private val roomDisplayNameResolver: RoomDisplayNameResolver,
private val roomAvatarResolver: RoomAvatarResolver,
- private val timelineEventDecryptor: Lazy,
- private val eventBus: EventBus) {
+ private val eventDecryptor: EventDecryptor,
+ private val crossSigningService: DefaultCrossSigningService) {
fun update(realm: Realm,
roomId: String,
@@ -126,9 +125,14 @@ internal class RoomSummaryUpdater @Inject constructor(
}
roomSummaryEntity.updateHasFailedSending()
- if (latestPreviewableEvent?.root?.type == EventType.ENCRYPTED && latestPreviewableEvent.root?.decryptionResultJson == null) {
+ val root = latestPreviewableEvent?.root
+ if (root?.type == EventType.ENCRYPTED && root.decryptionResultJson == null) {
Timber.v("Should decrypt ${latestPreviewableEvent.eventId}")
- timelineEventDecryptor.get().requestDecryption(TimelineEventDecryptor.DecryptionRequest(latestPreviewableEvent.eventId, ""))
+ // mmm i want to decrypt now or is it ok to do it async?
+ tryOrNull {
+ eventDecryptor.decryptEvent(root.asDomain(), "")
+ // eventDecryptor.decryptEventAsync(root.asDomain(), "", NoOpMatrixCallback())
+ }
}
if (updateMembers) {
@@ -142,7 +146,8 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
if (roomSummaryEntity.isEncrypted) {
- eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, roomSummaryEntity.isDirect, roomSummaryEntity.otherMemberIds.toList() + userId))
+ // mmm maybe we could only refresh shield instead of checking trust also?
+ crossSigningService.onUsersDeviceUpdate(roomSummaryEntity.otherMemberIds.toList())
}
}
}
@@ -156,13 +161,4 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.updateHasFailedSending()
roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
}
-
- fun updateShieldTrust(realm: Realm,
- roomId: String,
- trust: RoomEncryptionTrustLevel?) {
- val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
- if (roomSummaryEntity.isEncrypted) {
- roomSummaryEntity.roomEncryptionTrustLevel = trust
- }
- }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 9178759bcc..995a21aa23 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -32,8 +32,11 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
+import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@@ -83,6 +86,7 @@ internal class DefaultTimeline(
data class OnNewTimelineEvents(val roomId: String, val eventIds: List)
data class OnLocalEchoCreated(val roomId: String, val timelineEvent: TimelineEvent)
+ data class OnLocalEchoUpdated(val roomId: String, val eventId: String, val sendState: SendState)
companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
@@ -102,7 +106,9 @@ internal class DefaultTimeline(
private var prevDisplayIndex: Int? = null
private var nextDisplayIndex: Int? = null
- private val inMemorySendingEvents = Collections.synchronizedList(ArrayList())
+
+ private val uiEchoManager = UIEchoManager()
+
private val builtEvents = Collections.synchronizedList(ArrayList())
private val builtEventsIdMap = Collections.synchronizedMap(HashMap())
private val backwardsState = AtomicReference(State())
@@ -161,14 +167,14 @@ internal class DefaultTimeline(
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
?: throw IllegalStateException("Can't open a timeline without a room")
- sendingEvents = roomEntity.sendingTimelineEvents.where().filterEventsWithSettings().findAll()
+ // We don't want to filter here because some sending events that are not displayed
+ // are still used for ui echo (relation like reaction)
+ sendingEvents = roomEntity.sendingTimelineEvents.where()/*.filterEventsWithSettings()*/.findAll()
sendingEvents.addChangeListener { events ->
- // Remove in memory as soon as they are known by database
- events.forEach { te ->
- inMemorySendingEvents.removeAll { te.eventId == it.eventId }
- }
+ uiEchoManager.sentEventsUpdated(events)
postSnapshot()
}
+
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
filteredEvents = nonFilteredEvents.where()
.filterEventsWithSettings()
@@ -318,16 +324,15 @@ internal class DefaultTimeline(
@Subscribe(threadMode = ThreadMode.MAIN)
fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated) {
- if (isLive && onLocalEchoCreated.roomId == roomId) {
- // do not add events that would have been filtered
- if (listOf(onLocalEchoCreated.timelineEvent).filterEventsWithSettings().isNotEmpty()) {
- listeners.forEach {
- it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId))
- }
- Timber.v("On local echo created: ${onLocalEchoCreated.timelineEvent.eventId}")
- inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent)
- postSnapshot()
- }
+ if (uiEchoManager.onLocalEchoCreated(onLocalEchoCreated)) {
+ postSnapshot()
+ }
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ fun onLocalEchoUpdated(onLocalEchoUpdated: OnLocalEchoUpdated) {
+ if (uiEchoManager.onLocalEchoUpdated(onLocalEchoUpdated)) {
+ postSnapshot()
}
}
@@ -407,12 +412,17 @@ internal class DefaultTimeline(
private fun buildSendingEvents(): List {
val builtSendingEvents = ArrayList()
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
- builtSendingEvents.addAll(inMemorySendingEvents.filterEventsWithSettings())
- sendingEvents.forEach { timelineEventEntity ->
- if (builtSendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) {
- builtSendingEvents.add(timelineEventMapper.map(timelineEventEntity))
- }
- }
+ builtSendingEvents.addAll(uiEchoManager.getInMemorySendingEvents().filterEventsWithSettings())
+ sendingEvents
+ .map { timelineEventMapper.map(it) }
+ // Filter out sending event that are not displayable!
+ .filterEventsWithSettings()
+ .forEach { timelineEvent ->
+ if (builtSendingEvents.find { it.eventId == timelineEvent.eventId } == null) {
+ uiEchoManager.updateSentStateWithUiEcho(timelineEvent)
+ builtSendingEvents.add(timelineEvent)
+ }
+ }
}
return builtSendingEvents
}
@@ -622,14 +632,11 @@ internal class DefaultTimeline(
val timelineEvent = buildTimelineEvent(eventEntity)
val transactionId = timelineEvent.root.unsignedData?.transactionId
- val sendingEvent = inMemorySendingEvents.find {
- it.eventId == transactionId
- }
- inMemorySendingEvents.remove(sendingEvent)
+ uiEchoManager.onSyncedEvent(transactionId)
if (timelineEvent.isEncrypted()
&& timelineEvent.root.mxDecryptionResult == null) {
- timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(it, timelineID)) }
+ timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineID)) }
}
val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
@@ -649,7 +656,10 @@ internal class DefaultTimeline(
timelineEventEntity = eventEntity,
buildReadReceipts = settings.buildReadReceipts,
correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId)
- )
+ ).let {
+ // eventually enhance with ui echo?
+ (uiEchoManager.decorateEventWithReactionUiEcho(it) ?: it)
+ }
/**
* This has to be called on TimelineThread as it accesses realm live results
@@ -778,8 +788,8 @@ internal class DefaultTimeline(
val filterType = !settings.filters.filterTypes || settings.filters.allowedTypes.contains(it.root.type)
if (!filterType) return@filter false
- val filterEdits = if (settings.filters.filterEdits && it.root.type == EventType.MESSAGE) {
- val messageContent = it.root.content.toModel()
+ val filterEdits = if (settings.filters.filterEdits && it.root.getClearType() == EventType.MESSAGE) {
+ val messageContent = it.root.getClearContent().toModel()
messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE
} else {
true
@@ -797,4 +807,161 @@ internal class DefaultTimeline(
val isPaginating: Boolean = false,
val requestedPaginationCount: Int = 0
)
+
+ private data class ReactionUiEchoData(
+ val localEchoId: String,
+ val reactedOnEventId: String,
+ val reaction: String
+ )
+
+ inner class UIEchoManager {
+
+ private val inMemorySendingEvents = Collections.synchronizedList(ArrayList())
+
+ fun getInMemorySendingEvents(): List {
+ return inMemorySendingEvents.toList()
+ }
+
+ /**
+ * Due to lag of DB updates, we keep some UI echo of some properties to update timeline faster
+ */
+ private val inMemorySendingStates = Collections.synchronizedMap(HashMap())
+
+ private val inMemoryReactions = Collections.synchronizedMap>(HashMap())
+
+ fun sentEventsUpdated(events: RealmResults) {
+ // Remove in memory as soon as they are known by database
+ events.forEach { te ->
+ inMemorySendingEvents.removeAll { te.eventId == it.eventId }
+ }
+ inMemorySendingStates.keys.removeAll { key ->
+ events.find { it.eventId == key } == null
+ }
+
+ inMemoryReactions.forEach { (_, uiEchoData) ->
+ uiEchoData.removeAll { data ->
+ // I remove the uiEcho, when the related event is not anymore in the sending list
+ // (means that it is synced)!
+ events.find { it.eventId == data.localEchoId } == null
+ }
+ }
+ }
+
+ fun onLocalEchoUpdated(onLocalEchoUpdated: OnLocalEchoUpdated): Boolean {
+ if (isLive && onLocalEchoUpdated.roomId == roomId) {
+ val existingState = inMemorySendingStates[onLocalEchoUpdated.eventId]
+ inMemorySendingStates[onLocalEchoUpdated.eventId] = onLocalEchoUpdated.sendState
+ if (existingState != onLocalEchoUpdated.sendState) {
+ return true
+ }
+ }
+ return false
+ }
+
+ // return true if should update
+ fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated): Boolean {
+ var postSnapshot = false
+ if (isLive && onLocalEchoCreated.roomId == roomId) {
+ // Manage some ui echos (do it before filter because actual event could be filtered out)
+ when (onLocalEchoCreated.timelineEvent.root.getClearType()) {
+ EventType.REDACTION -> {
+ }
+ EventType.REACTION -> {
+ val content = onLocalEchoCreated.timelineEvent.root.content?.toModel()
+ if (RelationType.ANNOTATION == content?.relatesTo?.type) {
+ val reaction = content.relatesTo.key
+ val relatedEventID = content.relatesTo.eventId
+ inMemoryReactions.getOrPut(relatedEventID) { mutableListOf() }
+ .add(
+ ReactionUiEchoData(
+ localEchoId = onLocalEchoCreated.timelineEvent.eventId,
+ reactedOnEventId = relatedEventID,
+ reaction = reaction
+ )
+ )
+ postSnapshot = rebuildEvent(relatedEventID) {
+ decorateEventWithReactionUiEcho(it)
+ } || postSnapshot
+ }
+ }
+ }
+
+ // do not add events that would have been filtered
+ if (listOf(onLocalEchoCreated.timelineEvent).filterEventsWithSettings().isNotEmpty()) {
+ listeners.forEach {
+ it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId))
+ }
+ Timber.v("On local echo created: ${onLocalEchoCreated.timelineEvent.eventId}")
+ inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent)
+ postSnapshot = true
+ }
+ }
+ return postSnapshot
+ }
+
+ fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? {
+ val relatedEventID = timelineEvent.eventId
+ val contents = inMemoryReactions[relatedEventID] ?: return null
+
+ var existingAnnotationSummary = timelineEvent.annotations ?: EventAnnotationsSummary(
+ relatedEventID
+ )
+ val updateReactions = existingAnnotationSummary.reactionsSummary.toMutableList()
+
+ contents.forEach { uiEchoReaction ->
+ val existing = updateReactions.firstOrNull { it.key == uiEchoReaction.reaction }
+ if (existing == null) {
+ // just add the new key
+ ReactionAggregatedSummary(
+ key = uiEchoReaction.reaction,
+ count = 1,
+ addedByMe = true,
+ firstTimestamp = System.currentTimeMillis(),
+ sourceEvents = emptyList(),
+ localEchoEvents = listOf(uiEchoReaction.localEchoId)
+ ).let { updateReactions.add(it) }
+ } else {
+ // update Existing Key
+ if (!existing.localEchoEvents.contains(uiEchoReaction.localEchoId)) {
+ updateReactions.remove(existing)
+ // only update if echo is not yet there
+ ReactionAggregatedSummary(
+ key = existing.key,
+ count = existing.count + 1,
+ addedByMe = true,
+ firstTimestamp = existing.firstTimestamp,
+ sourceEvents = existing.sourceEvents,
+ localEchoEvents = existing.localEchoEvents + uiEchoReaction.localEchoId
+
+ ).let { updateReactions.add(it) }
+ }
+ }
+ }
+
+ existingAnnotationSummary = existingAnnotationSummary.copy(
+ reactionsSummary = updateReactions
+ )
+ return timelineEvent.copy(
+ annotations = existingAnnotationSummary
+ )
+ }
+
+ fun updateSentStateWithUiEcho(element: TimelineEvent) {
+ inMemorySendingStates[element.eventId]?.let {
+ // Timber.v("## ${System.currentTimeMillis()} Send event refresh echo with live state ${it} from state ${element.root.sendState}")
+ element.root.sendState = element.root.sendState.takeIf { it == SendState.SENT } ?: it
+ }
+ }
+
+ fun onSyncedEvent(transactionId: String?) {
+ val sendingEvent = inMemorySendingEvents.find {
+ it.eventId == transactionId
+ }
+ inMemorySendingEvents.remove(sendingEvent)
+ // Is it too early to clear it? will be done when removed from sending anyway?
+ inMemoryReactions.forEach { (_, u) ->
+ u.filterNot { it.localEchoId == transactionId }
+ }
+ }
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
index e91487eab0..3517f26c5d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
@@ -15,24 +15,22 @@
*/
package org.matrix.android.sdk.internal.session.room.timeline
+import io.realm.Realm
+import io.realm.RealmConfiguration
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+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.internal.crypto.NewSessionListener
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
-import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.session.SessionScope
-import io.realm.Realm
-import io.realm.RealmConfiguration
import timber.log.Timber
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import javax.inject.Inject
-@SessionScope
internal class TimelineEventDecryptor @Inject constructor(
@SessionDatabase
private val realmConfiguration: RealmConfiguration,
@@ -83,14 +81,14 @@ internal class TimelineEventDecryptor @Inject constructor(
synchronized(unknownSessionsFailure) {
for (requests in unknownSessionsFailure.values) {
if (request in requests) {
- Timber.d("Skip Decryption request for event ${request.eventId}, unknown session")
+ Timber.d("Skip Decryption request for event ${request.event.eventId}, unknown session")
return
}
}
}
synchronized(existingRequests) {
if (!existingRequests.add(request)) {
- Timber.d("Skip Decryption request for event ${request.eventId}, already requested")
+ Timber.d("Skip Decryption request for event ${request.event.eventId}, already requested")
return
}
}
@@ -101,25 +99,29 @@ internal class TimelineEventDecryptor @Inject constructor(
}
}
- private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) = realm.executeTransaction {
- val eventId = request.eventId
+ private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
+ val event = request.event
val timelineId = request.timelineId
- Timber.v("Decryption request for event $eventId")
- val eventEntity = EventEntity.where(realm, eventId = eventId).findFirst()
- ?: return@executeTransaction Unit.also {
- Timber.d("Decryption request for unknown message")
- }
- val event = eventEntity.asDomain()
try {
- val result = cryptoService.decryptEvent(event, timelineId)
- Timber.v("Successfully decrypted event $eventId")
- eventEntity.setDecryptionResult(result)
+ val result = cryptoService.decryptEvent(request.event, timelineId)
+ Timber.v("Successfully decrypted event ${event.eventId}")
+ realm.executeTransaction {
+ EventEntity.where(it, eventId = event.eventId ?: "")
+ .findFirst()
+ ?.setDecryptionResult(result)
+ }
} catch (e: MXCryptoError) {
- Timber.v(e, "Failed to decrypt event $eventId")
+ Timber.v("Failed to decrypt event ${event.eventId} : ${e.localizedMessage}")
if (e is MXCryptoError.Base /*&& e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID*/) {
// Keep track of unknown sessions to automatically try to decrypt on new session
- eventEntity.decryptionErrorCode = e.errorType.name
- eventEntity.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
+ realm.executeTransaction {
+ EventEntity.where(it, eventId = event.eventId ?: "")
+ .findFirst()
+ ?.let {
+ it.decryptionErrorCode = e.errorType.name
+ it.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
+ }
+ }
event.content?.toModel()?.let { content ->
content.sessionId?.let { sessionId ->
synchronized(unknownSessionsFailure) {
@@ -130,7 +132,7 @@ internal class TimelineEventDecryptor @Inject constructor(
}
}
} catch (t: Throwable) {
- Timber.e("Failed to decrypt event $eventId, ${t.localizedMessage}")
+ Timber.e("Failed to decrypt event ${event.eventId}, ${t.localizedMessage}")
} finally {
synchronized(existingRequests) {
existingRequests.remove(request)
@@ -139,7 +141,7 @@ internal class TimelineEventDecryptor @Inject constructor(
}
data class DecryptionRequest(
- val eventId: String,
+ val event: Event,
val timelineId: String
)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt
index e30d1b5b44..bfd4e22cc2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt
@@ -21,7 +21,6 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.ListenableWorker
import androidx.work.OneTimeWorkRequest
import org.matrix.android.sdk.api.util.Cancelable
-import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.util.CancelableWork
import org.matrix.android.sdk.internal.worker.startChain
@@ -38,24 +37,6 @@ internal class TimelineSendEventWorkCommon @Inject constructor(
private val workManagerProvider: WorkManagerProvider
) {
- fun postSequentialWorks(roomId: String, vararg workRequests: OneTimeWorkRequest): Cancelable {
- return when {
- workRequests.isEmpty() -> NoOpCancellable
- workRequests.size == 1 -> postWork(roomId, workRequests.first())
- else -> {
- val firstWork = workRequests.first()
- var continuation = workManagerProvider.workManager
- .beginUniqueWork(buildWorkName(roomId), ExistingWorkPolicy.APPEND, firstWork)
- for (i in 1 until workRequests.size) {
- val workRequest = workRequests[i]
- continuation = continuation.then(workRequest)
- }
- continuation.enqueue()
- CancelableWork(workManagerProvider.workManager, firstWork.id)
- }
- }
- }
-
fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND_OR_REPLACE): Cancelable {
workManagerProvider.workManager
.beginUniqueWork(buildWorkName(roomId), policy, workRequest)
@@ -77,11 +58,6 @@ internal class TimelineSendEventWorkCommon @Inject constructor(
return "${roomId}_$SEND_WORK"
}
- fun cancelAllWorks(roomId: String) {
- workManagerProvider.workManager
- .cancelUniqueWork(buildWorkName(roomId))
- }
-
companion object {
private const val SEND_WORK = "SEND_WORK"
private const val BACKOFF_DELAY = 10_000L
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt
index 8589889b30..b1b2f65dc2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt
@@ -16,6 +16,9 @@
package org.matrix.android.sdk.internal.session.sync
+import io.realm.Realm
+import io.realm.kotlin.createObject
+import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.R
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Event
@@ -54,16 +57,12 @@ import org.matrix.android.sdk.internal.session.room.read.FullyReadContent
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
-import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent
import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync
import org.matrix.android.sdk.internal.session.sync.model.RoomSync
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral
import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse
-import io.realm.Realm
-import io.realm.kotlin.createObject
-import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject
@@ -76,8 +75,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val roomTypingUsersHandler: RoomTypingUsersHandler,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
@UserId private val userId: String,
- private val eventBus: EventBus,
- private val timelineEventDecryptor: TimelineEventDecryptor) {
+ private val eventBus: EventBus) {
sealed class HandlingStrategy {
data class JOINED(val data: Map) : HandlingStrategy()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
index cfd7865269..74cba5e796 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
@@ -54,7 +54,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
private val networkConnectivityChecker: NetworkConnectivityChecker,
private val backgroundDetectionObserver: BackgroundDetectionObserver,
private val activeCallHandler: ActiveCallHandler
-) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
+) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
private var state: SyncState = SyncState.Idle
private var liveState = MutableLiveData(state)
diff --git a/matrix-sdk-android/src/main/res/values-bg/strings.xml b/matrix-sdk-android/src/main/res/values-bg/strings.xml
index 9654fd00b5..c3a5f3be82 100644
--- a/matrix-sdk-android/src/main/res/values-bg/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-bg/strings.xml
@@ -1,9 +1,7 @@
-
+
-
%1$s: %2$s
%1$s изпрати снимка.
-
Поканата на %s
%1$s покани %2$s
%1$s Ви покани
@@ -22,7 +20,7 @@
%1$s смени името на стаята на: %2$s
%s започна видео разговор.
%s започна гласов разговор.
- %s отговори на повикването.
+ %s отговори на обаждането.
%s прекрати разговора.
%1$s направи бъдещата история на стаята видима за %2$s
всички членове, от момента на поканването им в нея.
@@ -31,54 +29,39 @@
всеки.
непозната (%s).
%1$s включи шифроване от край до край (%2$s)
-
%1$s заяви VoIP групов разговор
Започна VoIP групов разговор
Груповият разговор приключи
-
(профилната снимка също беше сменена)
%1$s премахна името на стаята
%1$s премахна темата на стаята
%1$s обнови своя профил %2$s
%1$s изпрати покана на %2$s да се присъедини към стаята
%1$s прие поканата за %2$s
-
** Неуспешно разшифроване: %s **
Неуспешно премахване
Неуспешно изпращане на съобщението
-
Неуспешно качване на снимката
-
Грешка в мрежата
Matrix грешка
-
В момента не е възможно да се присъедините отново към празна стая.
-
Шифровано съобщение
-
Имейл адрес
Телефонен номер
-
Устройството на подателя не изпрати ключовете за това съобщение.
-
%1$s изпрати стикер.
-
Покана от %s
Покана за стая
%1$s и %2$s
-
- %1$s и 1 друг
- %1$s и %2$d други
-
Празна стая
-
Премахнато съобщение
Съобщение премахнато от %1$s
Премахнато съобщение [причина: %1$s]
Съобщение премахнато от %1$s [причина: %2$s]
-
Начална синхронизация:
\nИмпортиране на профил…
Начална синхронизация:
@@ -95,12 +78,9 @@
\nИмпортиране на общности
Начална синхронизация:
\nИмпортиране на данни за профила
-
%s обнови тази стая.
-
Изпращане на съобщение…
Изчисти опашката за изпращане
-
%1$s оттегли поканата за присъединяване на %2$s към стаята
поканата на %1$s. Причина: %2$s
%1$s покани %2$s. Причина: %3$s
@@ -115,34 +95,25 @@
%1$s премахна поканата за присъединяване на %2$s в стаята. Причина: %3$s
%1$s прие поканата за %2$s. Причина: %3$s
%1$s оттегли поканата на %2$s. Причина: %3$s
-
- %1$s добави %2$s като адрес за тази стая.
- %1$s добави %2$s като адреси за тази стая.
-
- %1$s премахна %2$s като адрес за тази стая.
- %1$s премахна %2$s като адреси за тази стая.
-
%1$s добави %2$s и премахна %3$s като адреси за тази стая.
-
%1$s настрой %2$s като основен адрес за тази стая.
%1$s премахна основния адрес за тази стая.
-
%1$s разреши на гости да се присъединяват в стаята.
%1$s предотврати присъединяването на гости в стаята.
-
%1$s включи шифроване от-край-до-край.
%1$s включи шифроване от-край-до-край (неразпознат алгоритъм %2$s).
-
%s изпрати запитване за потвърждение на ключа ви, но клиентът ви не поддържа верифициране посредством чат. Ще трябва да използвате стария метод за верифициране на ключове.
-
%1$s създаде стаята
Изпратихте снимка.
Изпратихте стикер.
-
Ваша покана
Създадохте стаята
Поканихте %1$s
@@ -152,4 +123,94 @@
Изгонихте %1$s
Отблокирахте %1$s
Блокирахте %1$s
-
+ Включихте шифроване от-край-до-край (непознат алгоритъм: %1$s).
+ Включихте шифроване от-край-до-край.
+ Спряхте възможността гости да се присъединяват в стаята.
+ %1$s спря възможността гости да се присъединяват в стаята.
+ Спряхте възможността гости да се присъединяват в стаята.
+ Позволихте на гости да се присъединяват тук.
+ %1$s позволи на гости да се присъединяват тук.
+ Позволихте на гости да се присъединяват към стаята.
+ Премахнахте основния адрес на стаята.
+ Зададохте %1$s като основен адрес на стаята.
+ Добавихте %1$s и премахнахте %2$s от адресите за стаята.
+
+ - Премахнахте %1$s от адресите на стаята.
+ - Премахнахте %1$s от адресите на стаята.
+
+
+ - Добавихте %1$s като адрес за тази стая.
+ - Добавихте %1$s като адреси за тази стая.
+
+ Оттеглихте поканата на %1$s. Причина: %2$s
+ Приехте поканата за %1$s. Причина: %2$s
+ Оттеглихте поканата за присъединяване в стаята от %1$s. Причина: %2$s
+ Изпратихте покана към %1$s за присъединяване в стаята. Причина: %2$s
+ Блокирахте %1$s. Причина: %2$s
+ Отблокирахте %1$s. Причина: %2$s
+ Изгонихте %1$s. Причина: %2$s
+ Отхвърлихте поканата. Причина: %1$s
+ Напуснахте. Причина: %1$s
+ %1$s напусна. Причина: %2$s
+ Напуснахте стаята. Причина: %1$s
+ Присъединихте се. Причина: %1$s
+ %1$s се присъедини. Причина: %2$s
+ Присъединихте се в стаята. Причина: %1$s
+ Поканихте %1$s. Причина: %2$s
+ Ваша покана. Причина: %1$s
+ %1$s от %2$s на %3$s
+ %1$s промени нивото на достъп на %2$s.
+ Променихте нивото на достъп на %1$s.
+ Собствено ниво
+ Собствено ниво (%1$d)
+ По подразбиране
+ Модератор
+ Администратор
+ Променихте %1$s приспособлението
+ %1$s промени %2$s приспособлението
+ Премахнахте %1$s приспособлението
+ %1$s премахна %2$s приспособлението
+ Добавихте %1$s приспособление
+ %1$s добави %2$s приспособление
+ Приехте поканата за %1$s
+ Оттеглихте поканата от %1$s
+ %1$s оттегли поканата от %2$s
+ Оттеглихте поканата за присъединяване в стаята от %1$s
+ Поканихте %1$s
+ %1$s покани %2$s
+ Изпратихте покана към %1$s за присъединяване в стаята
+ Обновихте профила си %1$s
+ Премахнахте снимката на стаята
+ %1$s премахна снимката на стаята
+ Премахнахте темата на стаята
+ Премахнахте името на стаята
+ Заявихте VoIP конференция
+ Обновихте чата.
+ %s обнови чата.
+ Обновихте стаята.
+ Включихте шифроване от-край-до-край (%1$s)
+ Направихте бъдещите съобщения видими за %1$s
+ %1$s направи бъдещите съобщения видими за %2$s
+ Направихте бъдещата история на стаята видима за %1$s
+ Прекратихте разговора.
+ Започнахте видео разговор.
+ Отговорихте на обаждането.
+ Изпратихте данни за настройка на разговора.
+ %s изпрати данни за настройка на разговора.
+ Започнахте гласов разговор.
+ Променихте името на стаята на: %1$s
+ Променихте снимката на стаята
+ %1$s промени снимката на стаята
+ Променихте темата на: %1$s
+ Премахнахте името си (%1$s)
+ Променихте името си от %1$s на %2$s
+ Променихте името си на %1$s
+ Променихте снимката си
+ Оттеглихте поканата от %1$s
+ Напуснахте стаята
+ %1$s напусна стаята
+ Присъединихте се
+ %1$s се присъедини
+ Създадохте дискусията
+ %1$s създаде дискусията
+
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml b/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml
index 8d31488283..35f8feaf0f 100644
--- a/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml
@@ -174,7 +174,7 @@
- আপনি এই ঘরের ঠিকানা হিসাবে %1$s সরিয়েছেন।
- - আপনি এই ঘরের ঠিকানা হিসাবে %2$s গুলি সরিয়েছেন।
+ - আপনি এই ঘরের ঠিকানা হিসাবে %1$s গুলি সরিয়েছেন।
%1$s %2$s যোগ করেছে এবং %3$s গুলি এই ঘরের ঠিকানা হিসাবে সরানো হয়েছে।
আপনি %1$s যোগ করেছেন এবং %2$s কে এই ঘরের ঠিকানা হিসাবে সরিয়ে দিয়েছেন।
diff --git a/matrix-sdk-android/src/main/res/values-cs/strings.xml b/matrix-sdk-android/src/main/res/values-cs/strings.xml
index 2ea2112b45..ebf7590596 100644
--- a/matrix-sdk-android/src/main/res/values-cs/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-cs/strings.xml
@@ -1,9 +1,8 @@
-
+
%1$s: %2$s
Uživatel %1$s poslal obrázek.
Uživatel %1$s poslal nálepku.
-
Pozvání od uživatele %s
Uživatel %1$s pozval uživatele %2$s
Uživatel %1$s vás pozval
@@ -31,51 +30,36 @@
kohokoliv.
neznámým (%s).
%1$s zapnuli end-to-end šifrování (%2$s)
-
%1$s požádali o VoIP konferenci
Začala VoIP konference
VoIP konference skončila
-
(profilový obrázek byl také změněn)
%1$s odstranili název místnosti
%1$s odstranili téma místnosti
%1$s aktualizovali svůj profil %2$s
%1$s do této místnosti pozvali %2$s
%1$s přijali pozvání pro %2$s
-
** Nelze dešifrovat: %s **
Odesílatelovo zařízení nám neposlalo klíče pro tuto zprávu.
-
Nelze vymazat
Zprávu nelze odeslat
-
Obrázek nelze nahrát
-
Chyba sítě
Chyba v Matrixu
-
V současnosti není možné znovu vstoupit do prázdné místnosti.
-
Šifrovaná zpráva
-
E-mailová adresa
Telefonní číslo
-
Pozvání od %s
Pozvání do místnosti
-
%1$s a %2$s
-
- %1$s a jeden další
- %1$s a %2$d další
- %1$s a %2$d dalších
-
Prázdná místnost
-
%s povýšili tuto místnost.
-
Zpráva byla smazána [důvod: %1$s]
Zpráva smazána uživatelem %1$s [důvod: %2$s]
%1$s zrušili pozvánku do místnosti pro %2$s
@@ -93,13 +77,10 @@
\nImportuji komunity
Úvodní synchronizace:
\nImportuji data účtu
-
Odesílám zprávu…
-
Úvodní synchronizace:
\nImportuji pozvání
Vymazat frontu neodeslaných zpráv
-
%1$s pozvali %2$s. Důvod: %3$s
%1$s vás pozvali. Důvod: %2$s
%1$s opustil místnost. Důvod: %2$s
@@ -107,7 +88,6 @@
Zprávu odstranil/a %1$s
Poslali jste obrázek.
Poslali jste nálepku.
-
Vaše pozvání
%1$s založil místnost
Vy jste založili místnost
@@ -136,7 +116,6 @@
Učinili jste budoucí historii místnosti viditelnou pro %1$s
Zapnuli jste end-to-end šifrování (%1$s)
Povýšili jste tuto místnost.
-
Požádali jste o VoIP konferenci
Odstranili jste jméno místnosti
Odstranili jste téma místnosti
@@ -146,24 +125,20 @@
Poslali jste %1$s pozvání ke vstupu do místnosti
Zrušili jste pozvánku ke vstupu do místnosti pro %1$s
Přijali jste pozvání pro %1$s
-
%1$s přidali widget %2$s
Přidali jste widget %1$s
%1$s odstranili widget %2$s
Odstranili jste widget %1$s
%1$s změnil widget %2$s
Změnili jste widget %1$s
-
Správce
Moderátor
Výchozí
Vlastní (%1$d)
Vlastní
-
Změnili jste %1$s stupeň oprávnění.
%1$s změnili %2$s stupeň oprávnění.
%1$s z %2$s na %3$s
-
Pozvání od %1$s. Důvod: %2$s
Vaše pozvání. Důvod: %1$s
Pozvali jste %1$s. Důvod: %2$s
@@ -179,56 +154,68 @@
%1$s vykázali %2$s. Důvod: %3$s
Vykázali jste %1$s. Důvod: %2$s
%1$s poslali %2$s pozvání, aby vstoupili do místnosti. Důvod: %3$s
- "Poslali jste %1$s pozvání, aby vstoupili do místnosti. Důvod: %2$s"
+ Poslali jste %1$s pozvání, aby vstoupili do místnosti. Důvod: %2$s
%1$s zrušili pozvání do místnosti pro %2$s. Důvod: %3$s
Zrušili jste pozvání do místnosti pro %1$s. Důvod: %2$s
%1$s přijali pozvání pro %2$s. Důvod: %3$s
Přijali jste pozvání pro %1$s. Důvod: %2$s
%1$s zrušili pozvání pro %2$s. Důvod: %3$s
Zrušili jste pozvání od %1$s. Důvod: %2$s
-
- %1$s přidali %2$s jako adresu pro tuto místnost.
- %1$s přidali %2$s jako adresy pro tuto místnost.
- %1$s přidali %2$s jako adresy pro tuto místnost.
-
- Přidali jste %1$s jako adresu pro tuto místnost.
- Přidali jste %1$s jako adresy pro tuto místnost.
- Přidali jste %1$s jako adresy pro tuto místnost.
-
- %1$s odstranili %2$s jako adresu pro tuto místnost.
- %1$s odstranili %2$s jako adresy pro tuto místnost.
- %1$s odstranili %2$s jako adresy pro tuto místnost.
-
- - Odstranili jste %2$s jako adresu pro tuto místnost.
- - Odstranili jste %2$s jako adresuy pro tuto místnost.
- - Odstranili jste %2$s jako adresy pro tuto místnost.
+ - Odstranili jste %1$s jako adresu pro tuto místnost.
+ - Odstranili jste %1$s jako adresuy pro tuto místnost.
+ - Odstranili jste %1$s jako adresy pro tuto místnost.
-
- %1$s přidali %2$ a odstranili %3$s jako adresy pro tuto místnost.
+ %1$s přidali %2$s a odstranili %3$s jako adresy pro tuto místnost.
Přidali jste %1$s a odstranili %2$s jako adresy pro tuto místnost.
-
%1$s nastavili hlavní adresu této místnosti na %2$s.
Nastavili jste %1$s na hlavní adresu této místnosti.
%1$s odstranili hlavní adresu této místnosti.
Odstranili jste hlavní adresu této místnosti.
-
- "%1$s povolili hostům vstoupit do místnosti."
+ %1$s povolili hostům vstoupit do místnosti.
Povolili jste hostům vstoupit do místnosti.
%1$s zamezili hostům vstoupit do místnosti.
Zamezili jste hostům vstoupit do místnosti.
-
%1$s zapnuli end-to-end šifrování.
Zapnuli jste end-to-end šifrování.
%1$s zapnuli end-to-end šifrování (neznámý algoritmus %2$s).
- Zapnuli jste end-to-end šifrování (neznámý algoritmus %2$s).
-
+ Zapnuli jste end-to-end šifrování (neznámý algoritmus %1$s).
%s žádá ověření Vašeho klíče, ale Váš klient nepodporuje ověření klíče v chatu. Budete muset k ověření klíčů použít zastaralý způsob ověření.
-
-
+ Zamezili jste hostům vstoupit do této místnosti.
+ %1$s zamezil hostům vstoupit do této místnosti.
+ Povolili jste hostům vstoupit.
+ %1$s povolil hostům vstoupit.
+ Odešli jste. Důvod: %1$s
+ %1$s odešli. Důvod: %2$s
+ Vstoupili jste. Důvod: %1$s
+ %1$s vstoupili. Důvod: %2$s
+ Vy jste zrušili pozvání pro %1$s
+ %1$s zrušili pozvání pro %2$s
+ Vy jste pozvali %1$s
+ %1$s pozvali %2$s
+ Vy jste tady provedli upgrade.
+ %s tady provedli upgrade.
+ Učinili jste budoucí zprávy viditelné pro %1$s
+ %1$s učinili budoucí zprávy viditelné pro %2$s
+ Odešli jste z místnosti
+ %1$s odešli z místnosti
+ Vstoupili jste
+ %1$s vstoupili
+ Založili jste diskusi
+ %1$s založil diskusi
+
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/res/values-de/strings.xml b/matrix-sdk-android/src/main/res/values-de/strings.xml
index 9b10aae10d..4c574d578a 100644
--- a/matrix-sdk-android/src/main/res/values-de/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-de/strings.xml
@@ -73,14 +73,22 @@
%s hat diesen Raum aufgewertet.
Sende eine Nachricht…
Sendewarteschlange leeren
- Erste Synchronisation: Importiere Benutzerkonto…
- Erste Synchronisation: Importiere Cryptoschlüssel
- Erste Synchronisation: Importiere Räume
- Erste Synchronisation: Importiere betretene Räume
- Erste Synchronisation: Importiere eingeladene Räume
- Erste Synchronisation: Importiere verlassene Räume
- Erste Synchronisation: Importiere Gemeinschaften
- Erste Synchronisation: Importiere Benutzerdaten
+ Erste Synchronisation:
+\nImportiere Benutzerkonto…
+ Erste Synchronisation:
+\nImportiere Cryptoschlüssel
+ Erste Synchronisation:
+\nImportiere Räume
+ Erste Synchronisation:
+\nImportiere betretene Räume
+ Erste Synchronisation:
+\nImportiere eingeladene Räume
+ Erste Synchronisation:
+\nImportiere verlassene Räume
+ Erste Synchronisation:
+\nImportiere Communities
+ Erste Synchronisation:
+\nImportiere Benutzerdaten
%1$s hat die Einladung an %2$s, den Raum zu betreten, zurückgezogen
%1$s\'s Einladung. Grund: %2$s
%1$s hat %2$s eingeladen. Grund: %3$s
@@ -107,7 +115,7 @@
%1$s legt die Hauptadresse fest für diesen Raum als %2$s fest.
%1$s entfernt die Hauptadresse für diesen Raum.
%1$s hat Gästen erlaubt den Raum zu betreten.
- %1$s hat Gäste unterbunden den Raum zu betreten.
+ %1$s hat Gästen untersagt den Raum zu betreten.
%1$s aktivierte Ende-zu-Ende-Verschlüsselung.
%1$s aktivierte Ende-zu-Ende-Verschlüsselung (unbekannter Algorithmus %2$s).
%s fordert zur Überprüfung deines Schlüssels auf, jedoch unterstützt dein Client nicht die Schlüsselüberprüfung im Chat. Du musst die herkömmliche Schlüsselüberprüfung verwenden, um die Schlüssel zu überprüfen.
@@ -199,7 +207,7 @@
%1$s hat die Einladung für %2$s zurückgezogen
Du hast %1$s eingeladen
%1$s hat %2$s eingeladen
- Du hast zukünftige Nachrichten für %2$s sichtbar gemacht
+ Du hast zukünftige Nachrichten für %1$s sichtbar gemacht
%1$s hat zukünftige Nachrichten für %2$s sichtbar gemacht
Du hast den Raum verlassen
%1$s hat den Raum verlassen
@@ -207,4 +215,10 @@
%1$s ist beigetreten
Du hast eine Diskussion erstellt
%1$s hat eine Diskussion erstellt
+ %s hat hier ein Upgrade durchgeführt.
+ Du hast hier ein Upgrade durchgeführt.
+ Du hast Gästen untersagt den Raum zu betreten.
+ %1$s hat Gästen untersagt den Raum zu betreten.
+ Du bist gegangen. Grund: %1$s
+ %1$s ist gegangen. Grund: %2$s
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/res/values-eo/strings.xml b/matrix-sdk-android/src/main/res/values-eo/strings.xml
index 69b009ca7e..10be2103cf 100644
--- a/matrix-sdk-android/src/main/res/values-eo/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-eo/strings.xml
@@ -1,26 +1,24 @@
-
+
%1$s sendis bildon.
%1$s sendis glumarkon.
-
Invito de %s
%1$s invitis uzanton %2$s
%1$s invitis vin
- %1$s alvenis
- %1$s foriris
- %1$s malakceptis la inviton
+ %1$s envenis
+ %1$s foriris de la ĉambro
+ %1$s rifuzis la inviton
%1$s forpelis uzanton %2$s
%1$s malforbaris uzanton %2$s
%1$s forbaris uzanton %2$s
%1$s nuligis inviton por %2$s
%1$s ŝanĝis sian profilbildon
** Ne eblas malĉifri: %s **
- La aparato de la sendanto ne sendis al ni la ŝlosilojn por tiu mesaĝo.
-
+ La aparato de la sendinto ne sendis al ni la ŝlosilojn por tiu mesaĝo.
%1$s: %2$s
- %1$s ŝanĝis sian vidigan nomon al %2$s
- %1$s ŝanĝis sian vidigan nomon de %2$s al %3$s
- %1$s forigis sian vidigan nomon (%2$s)
+ %1$s ŝanĝis sian prezentan nomon al %2$s
+ %1$s ŝanĝis sian prezentan nomon de %2$s al %3$s
+ %1$s forigis sian prezentan nomon (%2$s)
%1$s ŝanĝis la temon al: %2$s
%1$s ŝanĝis nomon de la ĉambro al: %2$s
%s vidvokis.
@@ -28,50 +26,38 @@
%s respondis la vokon.
%s finis la vokon.
%1$s videbligis estontan historion de ĉambro al %2$s
- ĉiuj ĉambranoj, ekde iliaj invitoj.
- ĉiuj ĉambranoj, ekde iliaj aliĝoj.
+ ĉiuj ĉambranoj, ekde siaj invitoj.
+ ĉiuj ĉambranoj, ekde siaj aliĝoj.
ĉiuj ĉambranoj.
ĉiu ajn.
nekonata (%s).
%1$s ŝaltis tutvojan ĉifradon (%2$s)
%s gradaltigis la ĉambron.
-
Mesaĝo foriĝis
- Mesaĝo foriĝis de %1$s
+ Mesaĝon forigis %1$s
Mesaĝo foriĝis [kialo: %1$s]
- Mesaĝo foriĝis de %1$s [kialo: %2$s]
+ Mesaĝon forigis %1$s [kialo: %2$s]
%1$s ĝisdatigis sian profilon %2$s
%1$s sendis aliĝan inviton al %2$s
%1$s nuligis la aliĝan inviton por %2$s
%1$s akceptis la inviton por %2$s
-
Ne povis redakti
Ne povas sendi mesaĝon
-
Malsukcesis alŝuti bildon
-
Reta eraro
Matrix-eraro
-
- Nun ne eblas re-aliĝi al malplena ĉambro
-
+ Nun ne eblas re-aliĝi al malplena ĉambro.
Ĉifrita mesaĝo
-
Retpoŝtadreso
Telefonnumero
-
Invito de %s
- Ĉambra invito
-
+ Invito al ĉambro
%1$s kaj %2$s
-
- %1$s kaj 1 alia
- %1$s kaj %2$d aliaj
-
Malplena ĉambro
-
Komenca spegulado:
\nEnportante konton…
Komenca spegulado:
@@ -88,52 +74,143 @@
\nEnportante komunumojn
Komenca spegulado:
\nEnportante datumojn de konto
-
Sendante mesaĝon…
Vakigi sendan atendovicon
-
%1$s petis grupan vokon
Grupa voko komenciĝis
Grupa voko finiĝis
-
(ankaŭ profilbildo ŝanĝiĝis)
%1$s forigis nomon de la ĉambro
%1$s forigis temon de la ĉambro
Invito de %1$s. Kialo: %2$s
%1$s invitis uzanton %2$s. Kialo: %3$s
%1$s invitis vin. Kialo: %2$s
- %1$s aliĝis al la ĉambro. Kialo: %2$s
+ %1$s envenis. Kialo: %2$s
%1$s foriris de la ĉambro. Kialo: %2$s
%1$s rifuzis la inviton. Kialo: %2$s
%1$s forpelis uzanton %2$s. Kialo: %3$s
%1$s malforbaris uzanton %2$s. Kialo: %3$s
%1$s forbaris uzanton %2$s. Kialo: %3$s
- %1$s sendis inviton al la ĉambro al %2$s. Kialo: %3$s
- %1$s nuligis la inviton al la ĉambro al %2$s. Kialo: %3$s
+ %1$s sendis al %2$s inviton al la ĉambro. Kialo: %3$s
+ %1$s nuligis la inviton al la ĉambro por %2$s. Kialo: %3$s
%1$s akceptis la inviton por %2$s. Kialo: %3$s
- %1$s nuligis la inviton al %2$s. Kialo: %3$s
-
+ %1$s nuligis la inviton por %2$s. Kialo: %3$s
- %1$s aldonis %2$s kiel adreson por ĉi tiu ĉambro.
- %1$s aldonis %2$s kiel adresojn por ĉi tiu ĉambro.
-
- %1$s forigis %2$s kiel adreson por ĉi tiu ĉambro.
- %1$s forigis %2$s kiel adresojn por ĉi tiu ĉambro.
-
%1$s aldonis %2$s kaj forigis %3$s kiel adresojn por ĉi tiu ĉambro.
-
- %1$s agordis la ĉefadreson por ĉi tiu ĉambro al %2$s.
+ %1$s agordis la ĉefadreson de ĉi tiu ĉambro al %2$s.
%1$s forigis la ĉefadreson de ĉi tiu ĉambro.
-
- %1$s permesis al gastoj aliĝi al la ĉambro.
- %1$s malpermesis al gastoj aliĝi al la ĉambro.
-
+ %1$s permesis al gastoj enveni.
+ %1$s malpermesis al gastoj enveni.
%1$s ŝaltis tutvojan ĉifradon.
%1$s ŝaltis tutvojan ĉifradon (kun nerekonita algoritmo %2$s).
-
- %s petas kontrolon de via ŝlosilo, sed via kliento ne subtenas kontrolon de ŝlosiloj en la babilujo. Vi devos uzi malnovecan kontrolon de ŝlosiloj.
-
-
+ %s petas kontrolon de via ŝlosilo, sed via kliento ne subtenas kontrolon de ŝlosiloj en la babilujo. Vi devos uzi malnovan kontrolmanieron de ŝlosiloj.
+ Vi ŝanĝis la povnivelon de %1$s.
+ %1$s sanĝis la povnivelon de %2$s.
+ Vi ŝaltis tutvojan ĉifradon (kun nerekonita algoritmo %1$s).
+ Vi ŝaltis tutvojan ĉifradon.
+ Vi malpermesis al gastoj aliĝi.
+ %1$s malpermesis al gastoj aliĝi.
+ Vi malpermesis al gastoj enveni.
+ Vi permesis al gastoj aliĝi.
+ %1$s permesis al gastoj aliĝi.
+ Vi permesis al gastoj enveni.
+ Vi forigis la ĉefadreson de ĉi tiu ĉambro.
+ Vi agordis al ĉefadreson de ĉi tiu ĉambro al %1$s.
+ Vi aldonis %1$s kaj forigis %2$s kiel adresojn por ĉi tiu ĉambro.
+
+ - Vi forigis %1$s kiel adreson por ĉi tiu ĉambro.
+ - Vi forigis %1$s kiel adresojn por ĉi tiu ĉambro.
+
+
+ - Vi aldonis %1$s kiel adreson por ĉi tiu ĉambro.
+ - Vi aldonis %1$s kiel adresojn por ĉi tiu ĉambro.
+
+ Vi nuligis la inviton por %1$s. Kialo: %2$s
+ Vi akceptis la inviton por %1$s. Kialo: %2$s
+ Vi nuligis inviton al la ĉambro por %1$s. Kialo: %2$s
+ Vi sendis al %1$s inviton al la ĉambro. Kialo: %2$s
+ Vi forbaris uzanton %1$s. Kialo: %2$s
+ Vi malforbaris uzanton %1$s. Kialo: %2$s
+ Vi forpelis uzanton %1$s. Kialo: %2$s
+ Vi rifuzis la inviton. Kialo: %1$s
+ Vi foriris. Kialo: %1$s
+ %1$s foriris. Kialo: %2$s
+ Vi foriris de la ĉambro. Kialo: %1$s
+ Vi envenis. Kialo: %1$s
+ Vi aliĝis. Kialo: %1$s
+ %1$s aliĝis. Kialo: %2$s
+ Vi invitis uzanton %1$s. Kialo: %2$s
+ Via invito. Kialo: %1$s
+ %1$s de %2$s al %3$s
+ Propra
+ Ordinara
+ Propra (%1$d)
+ Reguligisto
+ Administranto
+ Vi ŝanĝis la fenestraĵon %1$s
+ %1$s ŝanĝis la fenestraĵon %2$s
+ Vi forigis la fenestraĵon %1$s
+ %1$s forigis la fenestraĵon %2$s
+ Vi aldonis la fenestraĵon %1$s
+ %1$s aldonis la fenestraĵon %2$s
+ Vi akceptis la inviton por %1$s
+ Vi nuligis la inviton por %1$s
+ %1$s nuligis la inviton por %2$s
+ Vi nuligis la aliĝan inviton por %1$s
+ Vi invitis uzanton %1$s
+ %1$s invitis uzanton %2$s
+ Vi sendis aliĝan inviton al %1$s
+ Vi ĝisdatigis vian profilon %1$s
+ Vi forigis bildon de la ĉambro
+ %1$s forigis bildon de la ĉambro
+ Vi forigis temon de la ĉambro
+ Vi forigis nomon de la ĉambro
+ Vi petis grupan vokon
+ Vi gradaltigis la interparolon.
+ %s gradaltigis la interparolon.
+ Vi gradaltigis la ĉambron.
+ Vi ŝaltis tutvojan ĉifradon (%1$s)
+ %1$s videbligis al %2$s estontajn mesaĝojn
+ Vi videbligis al %1$s estontajn mesaĝojn
+ Vi videbligis estontan historion de ĉambro al %1$s
+ Vi finis la vokon.
+ Vi respondis la vokon.
+ Vi sendis datumojn por prepari la vokon.
+ %s sendis datumojn por prepari la vokon.
+ Vi voĉvokis.
+ Vi vidvokis.
+ Vi ŝanĝis la nomon de la ĉambro al: %1$s
+ Vi ŝanĝis la bildon de la ĉambro
+ %1$s ŝanĝis la bildon de la ĉambro
+ Vi ŝanĝis la temon al: %1$s
+ Vi forigis vian prezentan nomon (%1$s)
+ Vi ŝanĝis vian prezentan nomon de %1$s al %2$s
+ Vi ŝanĝis vian prezentan nomon al %1$s
+ Vi ŝanĝis vian profilbildon
+ Vi nuligis inviton por %1$s
+ Vi forbaris uzanton %1$s
+ Vi malforbaris uzanton %1$s
+ Vi forpelis uzanton %1$s
+ Vi rifuzis la inviton
+ Vi foriris de la ĉambro
+ %1$s foriris de la ĉambro
+ Vi foriris de la ĉambro
+ Vi envenis
+ %1$s envenis
+ Vi envenis
+ Vi invitis uzanton %1$s
+ Vi kreis la diskuton
+ %1$s kreis la diskuton
+ Vi kreis la ĉambron
+ %1$s kreis la ĉambron
+ Via invito
+ Vi sendis glumarkon.
+ Vi sendis bildon.
+
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/res/values-es/strings.xml b/matrix-sdk-android/src/main/res/values-es/strings.xml
index 1b1935602c..e2d09c7857 100644
--- a/matrix-sdk-android/src/main/res/values-es/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-es/strings.xml
@@ -226,7 +226,7 @@
- Quitaste %1$s como dirección para esta sala.
- - Quitaste %2$s como direcciones para esta sala.
+ - Quitaste %1$s como direcciones para esta sala.
"%1$s agregó %2$s y eliminó %3$s como direcciones para esta sala."
diff --git a/matrix-sdk-android/src/main/res/values-fa/strings.xml b/matrix-sdk-android/src/main/res/values-fa/strings.xml
index 25d92b4abe..042fda7ddd 100644
--- a/matrix-sdk-android/src/main/res/values-fa/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-fa/strings.xml
@@ -1,9 +1,8 @@
-
+
%1$s: %2$s
%1$s تصویری فرستاد.
%1$s برچسبی فرستاد.
-
دعوت %s
%1$s، %2$s را دعوت کرد
%1$s دعوتتان کرد
@@ -32,11 +31,9 @@
ناشناخته (%s).
%1$s رمزنگاری سرتاسری را روشن کرد (%2$s)
%s این اتاق را ارتقا داد.
-
%1$s درخواست یک گردهمایی صوتی داد
گردهمایی صوتی آغاز شد
گردهمایی صوتی پایان یافت
-
(تصویر هم عوض شد)
%1$s نام اتاق را پاک کرد
%1$s موضوع اتاق را پاک کرد
@@ -47,36 +44,24 @@
%1$s دعوتی برای پیوستن %2$s به اتاق فرستاد
%1$s دعوت پیوستن به اتاق %2$s را باطل کرد
%1$s دعوت برای %2$s را پذیرفت
-
** ناتوان در رمزگشایی: %s **
دستگاه فرستنده، کلیدهای این پیام را برایمان نفرستاده است.
-
ناتوان در فرستادن پیام
-
شکست در بارگذاری تصویر
-
خطای شبکه
خطای ماتریکس
-
در حال حاضر امکان بازپیوست به اتاقی خالی وجود ندارد.
-
پیام رمزنگاری شده
-
نشانی رایانامه
شماره تلفن
-
دعوت از %s
دعوت اتاق
-
%1$s و %2$s
-
- %1$s و ۱ نفر دیگر
- %1$s و %2$d نفر دیگر
-
اتاق خالی
-
همگامسازی نخستین:
\nدر حال درونریزی حساب…
همگامسازی نخستین:
@@ -93,10 +78,8 @@
\nدر حال درونریزی انجمنها
همگامسازی نخستین:
\nدر حال درونریزی دادههای حساب
-
در حال فرستادن پیام…
پاکسازی صفِ در حال ارسال
-
دعوت %1$s. دلیل: %2$s
%1$s، %2$s را دعوت کرد. دلیل: %3$s
%1$s دعوتتان کرد. دلیل: %2$s
@@ -110,36 +93,27 @@
%1$s دعوت %2$s برای پیوستن به اتاق را باطل کرد. دلیل: %3$s
%1$s دعوت برای %2$s را پذیرفت. دلیل: %3$s
%1$s دعوت %2$s را نپذیرفت. دلیل: %3$s
-
- %1$s، %2$s را به عنوان نشانیای برای این اتاق افزود.
- %1$s، %2$s را به عنوان نشانیهایی برای این اتاق افزود.
-
- %1$s، %2$s را به عنوان نشانیای برای این اتاق پاک کرد.
- %1$s، %3$s را به عنوان نشانیهایی برای این اتاق پاک کرد.
-
%1$s برای نشانی این اتاق، %2$s را افزود و %3$s را پاک کرد.
-
%1$s نشانی اصلی این اتاق را به %2$s تنظیم کرد.
%1$s نشانی اصلی را برای این اتاق پاک کرد.
-
%1$s اجازه داد میمهانان به گروه بپیوندند.
%1$s جلوی پیوستن میمهانان به گروه را گرفت.
-
%1$s رمزنگاری سرتاسری را روشن کرد.
%1$s رمزنگاری سرتاسری را روشن کرد (الگوریتم تشخیصدادهنشده %2$s ).
-
%s درخواست تأیید کلیدتان را دارد، ولی کارخواهتان تأیید کلید درون گپ را پشتیبانی نمیکند. برای تأیید کلیدها لازم است از تأییدیهٔ کلید قدیمی استفاده کنید.
-
%1$s اتاق را ایجاد کرد
%1$s نمایهاش را بهروز کرد %2$s
نمیتوان ویرایش کرد
تصویری فرستادید.
برچسبی فرستادید.
-
دعوتتان
اتاق را ایجاد کردید
از %1$s دعوت کردید
@@ -167,7 +141,6 @@
تاریخچهٔ آتی اتاق را برای %1$s نمایان کردید
رمزنگاری سرتاسری را روشن کردید (%1$s)
این اتاق را ارتقا دادید.
-
دارخواست کنفرانس ویپ دادید
نام اتاق را برداشتید
موضوع اتاق را برداشتید
@@ -177,24 +150,20 @@
برای %1$s دعوت پیوستن به اتاق فرستادید
دعوت پیوستن %1$s به اتاق را پس گرفتید
دعوت برای %1$s را پذیرفتید
-
%1$s ابزارک %2$s را افزود
ابزارک %1$s را افزودید
%1$s ابزارک %2$s را برداشت
ابزارک %1$s را برداشتید
%1$s ابزارک %2$s را دستکاری کرد
ابزارک %1$s را دستکاری کردید
-
مدیر
ناظم
پیشگزیده
سفارشی (%1$d)
سفارشی
-
سطح قدرت %1$s را تغییر دادید.
%1$s سطح قدرت %2$s را تغییر داد.
%1$s از %2$s به %3$s
-
دعوتتان. دلیل: %1$s
%1$s را دعوت کردید. دلیل: %2$s
به اتاق پیوستید. دلیل: %1$s
@@ -207,26 +176,41 @@
دعوت %1$s برای پیوستن به اتاق را پس گرفتید. دلیل: %2$s
دعوت برای %1$s را پذیرفتید. دلیل: %2$s
دعوت %1$s را رد کردید. دلیل: %2$s
-
- نشانی %1$s را به این اتاق افزودید.
- نشانیهای %1$s را به این اتاق افزودید.
-
- نشانی %1$s ار از این اتاق برداشتید.
- نشانیهای %1$s ار از این اتاق برداشتید.
-
نشانی %1$s ار افزوده و %2$s را از این اتاق برداشتید.
-
نشانی اصلی این اتاق را به %1$s تنظیم کردید.
نشانی اصلی این اتاق را برداشتید.
-
به میهمانان اجازهٔ پیوستن به گروه دادید.
میمهانان را از پیوستن به گروه بازداشتید.
-
رمزنگاری سرتاسری را روشن کردید.
رمزنگاری سرتاسری را روشن کردید (الگوریتم ناشناخته %1$s).
-
-
+ مهمانها را از پیوستن به اتاق بازداشتید.
+ %1$s مهمانها را از پیوستن به اتاق بازداشت.
+ به مهمانها اجازه دادید به اینجا بپیوندند.
+ %1$s به مهمانها اجازه داد به اینجا بپیوندند.
+ رفتید. دلیل: %1$s
+ %1$s رفت. دلیل: %2$s
+ پیوستید. دلیل: %1$s
+ %1$sپیوست. دلیل: %2$s
+ دعوت %1$s را پس گرفتید
+ %1$s دعوت %2$s را پس گرفت
+ %1$s را دعوت کردید
+ %1$s، %2$s را دعوت کرد
+ اینجا را ارتقا دادید.
+ %s اینجا را ارتقا داد.
+ پیامهای آینده را برای %1$s نمایان کردید
+ %1$s پیامهای آینده را برای %2$s نمایان کرد
+ اتاق را ترک کردید
+ %1$s اتاق را ترک کرد
+ پیوستید
+ %1$s پیوست
+ گفتوگو را ایجاد کردید
+ %1$s گفتوگو را ایجاد کرد
+
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/res/values-fr/strings.xml b/matrix-sdk-android/src/main/res/values-fr/strings.xml
index 71b956a7e7..9c5fa7d3b1 100644
--- a/matrix-sdk-android/src/main/res/values-fr/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-fr/strings.xml
@@ -1,9 +1,7 @@
-
+
-
%1$s : %2$s
%1$s a envoyé une image.
-
invitation de %s
%1$s a invité %2$s
%1$s vous a invité
@@ -11,13 +9,13 @@
%1$s est parti du salon
%1$s a rejeté l’invitation
%1$s a expulsé %2$s
- %1$s a révoqué le bannissement de %2$s
- %1$s a banni %2$s
+ %1$s a révoqué l\'exclusion de %2$s
+ %1$s a exclus %2$s
%1$s a annulé l’invitation de %2$s
%1$s a changé d’avatar
%1$s a modifié son nom affiché en %2$s
- %1$s a modifié son nom affiché %2$s en %3$s
- %1$s a supprimé son nom affiché (%2$s)
+ %1$s a modifié son nom affiché de %2$s en %3$s
+ %1$s a supprimé son nom affiché (précédemment %2$s)
%1$s a changé le sujet en : %2$s
%1$s a changé le nom du salon en : %2$s
%s a passé un appel vidéo.
@@ -31,54 +29,39 @@
n’importe qui.
inconnu (%s).
%1$s a activé le chiffrement de bout en bout (%2$s)
-
%1$s a demandé une téléconférence VoIP
Téléconférence VoIP démarrée
Téléconférence VoIP terminée
-
(l’avatar a aussi changé)
%1$s a supprimé le nom du salon
%1$s a supprimé le sujet du salon
%1$s a mis à jour son profil %2$s
%1$s a envoyé une invitation à %2$s pour rejoindre le salon
%1$s a accepté l’invitation pour %2$s
-
** Déchiffrement impossible : %s **
L’appareil de l’expéditeur ne nous a pas envoyé les clés pour ce message.
-
Effacement impossible
Envoi du message impossible
-
L’envoi de l’image a échoué
-
Erreur de réseau
Erreur de Matrix
-
Il est impossible pour le moment de revenir dans un salon vide.
-
Message chiffré
-
Adresse e-mail
Numéro de téléphone
-
%1$s a envoyé un sticker.
-
Invitation de %s
Invitation au salon
Salon vide
%1$s et %2$s
-
- %1$s et 1 autre
- %1$s et %2$d autres
-
-
Message supprimé
Message supprimé par %1$s
Message supprimé [motif : %1$s]
Message supprimé par %1$s [motif : %2$s]
-
Synchronisation initiale :
\nImportation du compte…
Synchronisation initiale :
@@ -95,12 +78,9 @@
\nImportation des communautés
Synchronisation initiale :
\nImportation des données du compte
-
%s a mis à niveau ce salon.
-
Envoi du message…
Vider la file d’envoi
-
%1$s a révoqué l’invitation pour %2$s à rejoindre le salon
Invitation de %1$s. Raison : %2$s
%1$s a invité %2$s. Raison : %3$s
@@ -109,35 +89,128 @@
%1$s est parti du salon. Raison : %2$s
%1$s a refusé l’invitation. Raison : %2$s
%1$s a expulsé %2$s. Raison : %3$s
- %1$s a révoqué le bannissement de %2$s. Raison : %3$s
- %1$s a banni %2$s. Raison : %3$s
+ %1$s a révoqué l\'exclusion de %2$s. Raison : %3$s
+ %1$s a exclus %2$s. Raison : %3$s
%1$s a envoyé une invitation à %2$s pour rejoindre le salon. Raison : %3$s
%1$s a révoqué l’invitation de %2$s à rejoindre le salon. Raison : %3$s
%1$s a accepté l’invitation pour %2$s. Raison : %3$s
%1$s a annulé l’invitation de %2$s. Raison : %3$s
-
- %1$s a ajouté %2$s comme adresse pour ce salon.
- %1$s a ajouté %2$s comme adresses pour ce salon.
-
- %1$s a supprimé %2$s comme adresse pour ce salon.
- %1$s a supprimé %3$s comme adresses pour ce salon.
-
%1$s a ajouté %2$s et supprimé %3$s comme adresses pour ce salon.
-
%1$s a défini %2$s comme adresse principale pour ce salon.
%1$s a supprimé l’adresse principale de ce salon.
-
%1$s a autorisé les visiteurs à rejoindre le salon.
%1$s a empêché les visiteurs de rejoindre le salon.
-
%1$s a activé le chiffrement de bout en bout.
%1$s a activé le chiffrement de bout en bout (algorithme %2$s inconnu).
-
%s demande à vérifier votre clé, mais votre client ne supporte pas la vérification de clés dans les discussions. Vous devrez utiliser l’ancienne vérification de clés pour vérifier les clés.
-
%1$s a créé le salon
-
+ Vous avez mis cet endroit à niveau.
+ %s a mis cet endroit à niveau.
+ Vous avez mis à niveau ce salon.
+ Vous avez expulsé %1$s
+ Vous avez rejeté l\'invitation
+ Vous avez quitté le salon
+ %1$s a quitté le salon
+ Vous avez quitté le salon
+ Vous avez rejoint le salon
+ %1$s a rejoint le salon
+ Vous avez rejoint le salon
+ Vous avez invité %1$s
+ Vous avez créé la discussion
+ %1$s a créé la discussion
+ Vous avez créé le salon
+ Votre invitation
+ Vous avez envoyé un autocollant.
+ Vous avez envoyé une image.
+ Vous avez activé le chiffrement de bout en bout (algorithme %1$s inconnu).
+ Vous avez activé le chiffrement de bout en bout.
+ Vous avez empêché les visiteurs de rejoindre le salon.
+ %1$s a empêché les visiteurs de rejoindre le salon.
+ Vous avez empêché les visiteurs de rejoindre le salon.
+ Vous avez autorisé les visiteurs à venir ici.
+ %1$s a autorisé les visiteurs à venir ici.
+ Vous avez autorisé les visiteurs à rejoindre le salon.
+ Vous avez supprimé l’adresse principale de ce salon.
+ Vous avez défini %1$s comme adresse principale pour ce salon.
+ Vous avez ajouté %1$s et supprimé %2$s comme adresses pour ce salon.
+
+ - Vous avez supprimé %1$s comme adresse pour ce salon.
+ - Vous avez supprimé %1$s comme adresses pour ce salon.
+
+
+ - Vous avez ajouté %1$s comme adresse pour ce salon.
+ - Vous avez ajouté %1$s comme adresses pour ce salon.
+
+ Vous avez annulé l’invitation de %1$s. Raison : %2$s
+ Vous avez accepté l’invitation pour %1$s. Raison : %2$s
+ Vous avez révoqué l’invitation de %1$s à rejoindre le salon. Raison : %2$s
+ Vous avez envoyé une invitation à %1$s pour rejoindre le salon. Raison : %2$s
+ Vous avez refusé l’invitation. Raison : %1$s
+ Vous êtes parti. Raison : %1$s
+ %1$s est parti. Raison : %2$s
+ Vous êtes parti du salon. Raison : %1$s
+ %1$s rejoint. Raison : %2$s
+ Vous avez rejoint. Raison : %1$s
+ Vous avez rejoint le salon. Raison : %1$s
+ Vous avez invité %1$s. Raison : %2$s
+ Votre invitation. Raison %1$s
+ %1$s de %2$s à %3$s
+ %1$s a modifié le niveau de pouvoir de %2$s.
+ Vous avez modifié le niveau de pouvoir de %1$s.
+ Personnalisé
+ Personnalisé (%1$d)
+ Défaut
+ Modérateur
+ Admin
+ Vous avez modifié le widget %1$s
+ %1$s a modifié le widget %2$s
+ Vous avez supprimé le widget %1$s
+ %1$s a supprimé le widget %2$s
+ Vous avez ajouté le widget %1$s
+ %1$s a ajouté le widget %2$s
+ Vous avez accepté l’invitation pour %1$s
+ Vous avez révoqué l\'invitation pour %1$s
+ %1$s a révoqué l\'invitation pour %2$s
+ Vous avez rendu les futurs messages visible pour %1$s
+ %1$s a rendu les futurs messages visible pour %2$s
+ Vous avez supprimé l\'avatar du salon
+ %1$s a supprimé l\'avatar du salon
+ Vous avez supprimé le nom du salon
+ Vous avez demandé une téléconférence VoIP
+ Vous avez activé le chiffrement de bout en bout (%1$s)
+ Vous avez rendu l’historique futur du salon visible pour %1$s
+ Vous avez raccroché.
+ Vous avez répondu à l’appel.
+ Vous avez envoyé les données pour configurer l\'appel.
+ %s a envoyé les données pour configurer l\'appel.
+ Vous avez passé un appel vocal.
+ Vous avez passé un appel vidéo.
+ Vous avez changé le nom du salon en : %1$s
+ Vous avez modifié l\'avatar du salon
+ %1$s a modifié l\'avatar du salon
+ Vous avez modifié votre nom affiché de %1$s en %2$s
+ Vous avez modifié votre nom affiché en %1$s
+ Vous avez changé votre avatar
+ Vous avez annulé l’invitation de %1$s
+ Vous avez changé le sujet en : %1$s
+ Vous avez supprimé votre nom affiché (précédemment %1$s)
+ Vous avez révoqué l’invitation pour %1$s à rejoindre le salon
+ Vous avez invité %1$s
+ %1$s a invité %2$s
+ Vous avez envoyé une invitation à %1$s pour rejoindre le salon
+ Vous avez mis à jour votre profile %1$s
+ Vous avez supprimé le sujet du salon
+ Vous avez exclus %1$s. Raison : %2$s
+ Vous avez révoqué l\'exclusion de %1$s. Raison : %2$s
+ Vous avez exclus %1$s
+ Vous avez révoqué l\'exclusion de %1$s
+ Vous avez expulsé %1$s. Raison : %2$s
+
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/res/values-hu/strings.xml b/matrix-sdk-android/src/main/res/values-hu/strings.xml
index 896a97b023..4aade76c55 100644
--- a/matrix-sdk-android/src/main/res/values-hu/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-hu/strings.xml
@@ -1,8 +1,7 @@
-
+
%1$s: %2$s
%1$s küldött egy képet.
-
%s meghívója
%1$s meghívta: %2$s
%1$s meghívott
@@ -20,7 +19,7 @@
%1$s megváltoztatta a témát erre: %2$s
%1$s megváltoztatta a szoba nevét erre: %2$s
%s videóhívást kezdeményezett.
- %s hanghívást kezdeményezett.
+ %s hanghívást indított.
%s fogadta a hívást.
%s befejezte a hívást.
%1$s láthatóvá tette a jövőbeli előzményeket %2$s számára
@@ -30,54 +29,39 @@
bárki.
ismeretlen (%s).
%1$s bekapcsolta a végpontok közötti titkosítást (%2$s)
-
%1$s hanghívás konferenciát kérelmezett
Hanghívás konferencia elindult
Hanghívás konferencia befejeződött
-
(a profilkép is megváltozott)
%1$s eltávolította a szoba nevét
%1$s eltávolította a szoba témáját
%1$s megváltoztatta a(z) %2$s profilját
%1$s meghívót küldött %2$s számára, hogy csatlakozzon a szobához
%1$s elfogadta a meghívót ebbe: %2$s
-
** Visszafejtés sikertelen: %s **
A küldő eszköze nem küldte el a kulcsokat ehhez az üzenethez.
-
Kitakarás sikertelen
Üzenet küldése sikertelen
-
Kép feltöltése sikertelen
-
Hálózati hiba
Matrix hiba
-
Jelenleg nem lehetséges újracsatlakozni egy üres szobához.
-
Titkosított üzenet
-
E-mail cím
Telefonszám
-
%1$s küldött egy matricát.
-
Meghívó tőle: %s
Meghívó egy szobába
%1$s és %2$s
Üres szoba
-
- %1$s és 1 másik
- %1$s és %2$d másik
-
-
Üzenet eltávolítva
Üzenetet eltávolította: %1$s
Üzenet eltávolítva [ok: %1$s]
Üzenetet eltávolította: %1$s [ok: %2$s]
-
Induló szinkronizáció:
\nFiók betöltése…
Induló szinkronizáció:
@@ -94,12 +78,9 @@
\nKözösségek betöltése
Induló szinkronizáció:
\nFiók adatok betöltése
-
%s frissítette ezt a szobát.
-
Üzenet küldése…
Küldő sor ürítése
-
%1$s visszavonta %2$s meghívását, hogy csatlakozzon a szobához
%1$s meghívója. Ok: %2$s
%1$s meghívta őt: %2$s. Ok: %3$s
@@ -114,29 +95,48 @@
%1$s visszavonta %2$s meghívóját a szobába való belépéshez. Ok: %3$s
%1$s elfogadta a meghívót ide: %2$s. Ok: %3$s
%1$s visszavonta %2$s meghívóját. Ok: %3$s
-
- %1$s ezt a címet adta a szobához: %2$s.
- %1$s ezeket a címeket adta a szobához: %2$s.
-
- %1$s ezt a címet törölte a szobából: %3$s.
- %1$s ezeket a címeket törölte a szobából: %3$s.
-
%1$s a szobához adta ezeket:%2$s és törölte ezeket: %3$s.
-
%1$s a szoba elsődleges címét erre állította be: %2$s.
%1$s eltávolította a szoba elsődleges címét.
-
%1$s megengedte a vendégeknek, hogy belépjenek ebbe a szobába.
%1$s megtiltotta a vendégeknek, hogy belépjenek ebbe a szobába.
-
%1$s bekapcsolta a végpontok közötti titkosítást.
%1$s bekapcsolta a végpontok közötti titkosítást (ismeretlen algoritmus %2$s).
-
%s kéri a kulcsok ellenőrzését de a kliens nem támogatja a szobán belüli kulcs ellenőrzést. A hagyományos módon kell ellenőrizned a kulcsokat.
-
%1$s szobát készített
-
+ Fogadtad a hívást.
+ Befejezted a hívást.
+ Hívási adatokat küldtél.
+ %s hívási adatokat küldött.
+ Hanghívást indítottál.
+ Videóhívást indítottál.
+ Megváltoztattad a szoba képét
+ %1$s megváltoztatta a szoba képét
+ Megváltoztattad a témát erre: %1$s
+ Megváltoztattad a profilképed
+ Visszavontad %1$s meghívóját
+ Kitiltottad %1$s felhasználót
+ Visszaengedted %1$s felhasználót
+ Kirúgtad %1$s felhasználót
+ Visszautasítottad a meghívót
+ Elhagytad a szobát
+ %1$s elhagyta a szobát
+ Elhagytad a szobát
+ Csatlakoztál
+ %1$s csatlakozott
+ Beléptél a szobába
+ Meghívtad: %1$s
+ Létrehoztad a beszélgetést
+ %1$s létrehozta a beszélgetést
+ Létrehoztad a szobát
+ Matricát küldtél.
+ Képet küldtél.
+
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/res/values-it/strings.xml b/matrix-sdk-android/src/main/res/values-it/strings.xml
index d4479be674..5eab8c57df 100644
--- a/matrix-sdk-android/src/main/res/values-it/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-it/strings.xml
@@ -183,7 +183,7 @@
- Hai rimosso %1$s come indirizzo per questa stanza.
- - Hai rimosso %2$s come indirizzi per questa stanza.
+ - Hai rimosso %1$s come indirizzi per questa stanza.
Hai aggiunto %1$s e rimosso %2$s come indirizzi per questa stanza.
Hai impostato l\'indirizzo principale per questa stanza a %1$s.
diff --git a/matrix-sdk-android/src/main/res/values-kab/strings.xml b/matrix-sdk-android/src/main/res/values-kab/strings.xml
index e557a7c824..8b94fad9eb 100644
--- a/matrix-sdk-android/src/main/res/values-kab/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-kab/strings.xml
@@ -172,7 +172,7 @@
- Tekkseḍ %1$s am tansa i texxamt-a.
- - Tekkseḍ %2$s am tansiwin i texxamt-a.
+ - Tekkseḍ %1$s am tansiwin i texxamt-a.
%1$s yerna %2$s terniḍ tekkseḍ %3$s am tansiwin i texxamt-a.
Terniḍ %1$s terniḍ tekkseḍ %2$s am tansiwin i texxamt-a.
diff --git a/matrix-sdk-android/src/main/res/values-lt/strings.xml b/matrix-sdk-android/src/main/res/values-lt/strings.xml
index b867219408..4b07e1ec88 100644
--- a/matrix-sdk-android/src/main/res/values-lt/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-lt/strings.xml
@@ -1,8 +1,19 @@
-
+
%1$s: %2$s
- %1$s išsiuntė atvaizdą.
+ %1$s išsiuntė vaizdą.
%1$s išsiuntė lipduką.
-
%s pakvietimas
-
+ Jūs prisijungėte prie kambario
+ %1$s prisijungė prie kambario
+ %1$s pakvietė jus
+ Jūs pakvietėte %1$s
+ %1$s pakvietė %2$s
+ Jūs sukūrėte diskusiją
+ %1$s sukūrė diskusiją
+ Jūs sukūrėte kambarį
+ %1$s sukūrė kambarį
+ Jūsų pakvietimas
+ Jūs išsiuntėte lipduką.
+ Jūs išsiuntėte vaizdą.
+
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml
index 4e62a21c0e..ed9f91cdb3 100644
--- a/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml
@@ -182,7 +182,7 @@
- Você removeu %1$s como um endereço desta sala.
- - Você removeu %2$s como endereços desta sala.
+ - Você removeu %1$s como endereços desta sala.
%1$s adicionou %2$s e removeu %3$s como endereços desta sala.
Você adicionou %1$s e removeu %2$s como endereços desta sala.
diff --git a/matrix-sdk-android/src/main/res/values-ru/strings.xml b/matrix-sdk-android/src/main/res/values-ru/strings.xml
index 97643a34fe..ef9cda1dc2 100644
--- a/matrix-sdk-android/src/main/res/values-ru/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-ru/strings.xml
@@ -225,4 +225,6 @@
%1$s вошел(ла)
Вы создали обсуждение
%1$s создал(а) обсуждение
+ Вы обновили.
+ %s обновлена.
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/res/values-sk/strings.xml b/matrix-sdk-android/src/main/res/values-sk/strings.xml
index c75c8b4832..15924d02e1 100644
--- a/matrix-sdk-android/src/main/res/values-sk/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-sk/strings.xml
@@ -183,8 +183,8 @@
- Odstránili ste adresu %1$s pre túto miestnosť.
- - Odstránili ste adresy %2$s pre túto miestnosť.
- - Odstránili ste adresy %2$s pre túto miestnosť.
+ - Odstránili ste adresy %1$s pre túto miestnosť.
+ - Odstránili ste adresy %1$s pre túto miestnosť.
Pridali ste %1$s a odstránili adresy %2$s pre túto miestnosť.
Nastavili ste hlavnú adresu tejto miestnosti %1$s.
diff --git a/matrix-sdk-android/src/main/res/values-sq/strings.xml b/matrix-sdk-android/src/main/res/values-sq/strings.xml
index 9756a11762..4055b35025 100644
--- a/matrix-sdk-android/src/main/res/values-sq/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml
@@ -1,4 +1,4 @@
-
+
%1$s: %2$s
%1$s dërgoi një figurë.
@@ -25,39 +25,26 @@
%1$s kërkoi një konferencë VoIP
Konferenca VoIP filloi
Konferenca VoIP përfundoi
-
(u ndryshua edhe avatari)
%1$s hoqi emrin e dhomës
%1$s përditësoi profilin e tij %2$s
%1$s pranoi ftesën tuaj për %2$s
-
** S’arrihet të shfshehtëzohet: %s **
Pajisja e dërguesit nuk na ka dërguar kyçet për këtë mesazh.
-
S’u redaktua dot
S’arrihet të dërgohet mesazh
-
Ngarkimi i figurës dështoi
-
Gabim rrjeti
Gabim Matrix
-
Hëpërhë s’është e mundur të rihyhet në një dhomë të zbrazët.
-
- U fshehtëzua mesazhi
-
+ Mesazh i fshehtëzuar
Adresë email
Numër telefoni
-
Ftesë nga %s
Ftesë Dhome
-
%1$s dhe %2$s
-
Dhomë e zbrazët
-
%1$s dërgoi një ngjitës.
-
Ftesë e %s
%1$s hoqi dëbimin për %2$s
%1$s tërhoqi mbrapsht ftesën për %2$s
@@ -65,20 +52,17 @@
%1$s ndryshoi emrin e tyre në ekran nga %2$s në %3$s
%1$s hoqi emrin e tij në ekran (%2$s)
%1$s aktivizoi fshehtëzim skaj-më-skaj (%2$s)
-
%1$s hoqi temën e dhomës
%1$s dërgoi një ftesë për %2$s që të marrë pjesë në dhomë
- %1$s dhe 1 tjetër
- %1$s dhe %2$d të tjerë
-
Mesazhi u hoq
Mesazhi u hoq nga %1$s
Mesazh i hequr [arsye: %1$s]
Mesazh i hequr nga %1$s [arsye: %2$s]
%s e përmirësoi këtë dhomë.
-
Njëkohësimi Fillestar:
\nPo importohet llogaria…
Njëkohësimi Fillestar:
@@ -95,10 +79,8 @@
\nPo importohen Bashkësi
Njëkohësimi Fillestar:
\nPo importohet të Dhëna Llogarie
-
Po dërgohet mesazh…
Spastro radhë pritjeje
-
%1$s shfuqizoi ftesën për %2$s për pjesëmarrje te dhoma
Ftesë e %1$s. Arsye: %2$s
%1$s ftoi %2$s. Arsye: %3$s
@@ -113,34 +95,25 @@
%1$s shfuqizoi ftesën për %2$s për të ardhur në dhomë. Arsye: %3$s
%1$s pranoi ftesën për %2$s. Arsye: %3$s
%1$s tërhoqi mbrapsht ftesën për %2$s. Arsye: %3$s
-
- %1$s shtoi %2$s si një adresë për këtë dhomë.
- %1$s shtoi %2$s si adresa për këtë dhomë.
-
- %1$s hoqi %2$s si adresë për këtë dhomë.
- %1$s hoqi %3$s si adresa për këtë dhomë.
-
%1$s shtoi %2$s dhe hoqi %3$s si adresa për këtë dhomë.
-
%1$s caktoi %2$s si adresë kryesore për këtë dhomë.
%1$s hoqi adresën kryesore për këtë dhomë.
-
%1$s ka lejuar vizitorë të marrin pjesë në dhomë.
%1$s ka penguar vizitorë të marrin pjesë në dhomë.
-
%1$s aktivizoi fshehtëzim skaj-më-skaj.
%1$s aktivizoi fshehtëzim skaj-më-skaj (algoritëm i papranuar %2$s).
-
%s po kërkon të verifikojë kyçin tuaj, por klienti juaj nuk mbulon verifikim kyçesh brenda fjalosjeje. Që të verifikoni kyça, do t’ju duhet të përdorni verifikim të dikurshëm kyçesh.
-
%1$s krijo dhomën
Dërguat një figurë.
Dërguat një ngjitës.
-
Ftesa juaj
Krijuat dhomën
Ftuat %1$s
@@ -168,7 +141,6 @@
E bëtë historikun e ardhshëm të dhomë të dukshëm për %1$s
Aktivizuat fshehtëzim skaj-më-skaj (%1$s)
Përmirësuat këtë dhomë.
-
Kërkuat një konferencë VoIP
Hoqët emrin e dhomës
Hoqët temën e dhomës
@@ -178,24 +150,20 @@
Dërguat një ftesë te %1$s për të ardhur te dhoma
Shfuqizuat ftesën për ardhjen në dhomë të %1$s
Pranuat ftesën për %1$s
-
%1$s shtoi widget-in %2$s
Shtuat widget-in %1$s
%1$s hoqi widget-in %2$s
Hoqët widget-in %1$s
%1$s ndryshoi widget-in %2$s
Ndryshuat widget-in %1$s
-
Përgjegjës
Moderator
Parazgjedhje
Vetjake (%1$d)
Vetjake
-
Ndryshuat shkallën e pushtetit për %1$s.
%1$s ndryshoi shkallën e pushtetit për %2$s.
%1$s nga %2$s në %3$s
-
Ftesa juaj. Arsye: %1$s
Ftuat %1$s. Arsye: %2$s
Erdhët në dhomë, Arsye: %1$s
@@ -208,26 +176,41 @@
Shfuqizuat ftesën për ardhjen në dhomë të %1$s. Arsye: %2$s
Pranuat ftesën për %1$s. Arsye: %2$s
Tërhoqët mbrapsht ftesën për %1$s. Arsye: %2$s
-
- Shtuat %1$s si një adresë për këtë dhomë.
- Shtuat %1$s si adresa për këtë dhomë.
-
- Hoqët %1$s si një adresë për këtë dhomë.
- Hoqët %1$s si adresa për këtë dhomë.
-
Shtuat %1$s dhe hoqët %2$s si adresa për këtë dhomë.
-
Caktuat si adresë kryesore për këtë dhomë %1$s.
Hoqët adresën kryesore për këtë dhomë.
-
Keni lejuar të vijnë mysafirë në dhomë.
U keni penguar mysafirëve të vijnë në dhomë.
-
Aktivizuat fshehtëzimin skaj-më-skaj.
Aktivizuat fshehtëzimin skaj-më-skaj (algoritëm %1$s i panjohur).
-
-
+ Keni penguar të vijnë në dhomë mysafirë.
+ %1$s ka penguar të vijnë në dhomë mysafirë.
+ Keni lejuar të vijnë mysafirë këtu.
+ %1$s ka lejuar të vijnë këtu mysafirë.
+ Dolët. Arsye: %1$s
+ %1$s doli. Arsye: %2$s
+ Erdhët. Arsye: %1$s
+ %1$s erdhi. Arsye: %2$s
+ Shfuqizuat ftesën për %1$s
+ %1$s shfuqizoi ftesën për %2$s
+ Ftuat %1$s
+ %1$s ftoi %2$s
+ U përmirësuat këtu.
+ %s këtu u përmirësua.
+ I bëtë mesazhet e ardhshëm të dukshëm për %1$s
+ %1$s i bëri mesazhet e ardhshëm të dukshëm për %2$s
+ Dolët nga dhoma
+ %1$s doli nga dhoma
+ Erdhët
+ %1$s erdhi
+ Krijuat diskutimin
+ %1$s krijoi diskutimin
+
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/res/values-sv/strings.xml b/matrix-sdk-android/src/main/res/values-sv/strings.xml
index 25e51b69e5..d42c6ba2ca 100644
--- a/matrix-sdk-android/src/main/res/values-sv/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-sv/strings.xml
@@ -174,7 +174,7 @@
- Du tog bort %1$s som en adress för det här rummet.
- - Du tog bort %2$s som adresser för det här rummet.
+ - Du tog bort %1$s som adresser för det här rummet.
%1$s lade till %2$s och tog bort %3$s som adresser för det här rummet.
Du lade till %1$s och tog bort %2$s som adresser för det här rummet.
diff --git a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml
index 496bbe6bf8..5c5da36c26 100644
--- a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml
@@ -177,7 +177,7 @@
- 您新增了 %1$s 为此聊天室的地址。
- - 您移除了此聊天室的 %2$s 地址。
+ - 您移除了此聊天室的 %1$s 地址。
您为此聊天室新增了 %1$s 并移除了 %2$s 地址。
您将此聊天室的主地址设为了 %1$s。
@@ -196,7 +196,7 @@
%1$s 邀请了 %2$s
您在此处升级。
%s 在此处升级。
- 您使未来的消息对 %2$s 可见
+ 您使未来的消息对 %1$s 可见
%1$s 使未来的消息对 %2$s 可见
您离开了聊天室
%1$s 离开了聊天室
@@ -204,4 +204,8 @@
%1$s 已加入
您创建了讨论
%1$s 创建了讨论
+ 你已阻止客人加入房间。
+ %1$s已阻止客人加入房间。
+ 你已允许客人加入这里。
+ %1$s 已允许客人加入这里。
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml
index 4a3293b195..b3de5910a5 100644
--- a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml
+++ b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml
@@ -177,7 +177,7 @@
- 您為此聊天室新增了 %1$s 作為地址。
- - 您為此聊天室移除了 %2$s 作為地址。
+ - 您為此聊天室移除了 %1$s 作為地址。
您為此聊天室新增了 %1$s 並移除了 %2$s 作為地址。
您將此聊天室的主要地址設定為 %1$s。
diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml
index 3f75b715f6..27f083269f 100644
--- a/matrix-sdk-android/src/main/res/values/strings.xml
+++ b/matrix-sdk-android/src/main/res/values/strings.xml
@@ -225,7 +225,7 @@
- You removed %1$s as an address for this room.
- - You removed %2$s as addresses for this room.
+ - You removed %1$s as addresses for this room.
%1$s added %2$s and removed %3$s as addresses for this room.
diff --git a/tools/check/check_code_quality.sh b/tools/check/check_code_quality.sh
index e855440e81..0b4272cbfe 100755
--- a/tools/check/check_code_quality.sh
+++ b/tools/check/check_code_quality.sh
@@ -91,7 +91,6 @@ ${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_resources.txt
./vector/src/main/res/color \
./vector/src/main/res/layout \
./vector/src/main/res/values \
- ./vector/src/main/res/values-v21 \
./vector/src/main/res/xml
resultForbiddenStringInResource=$?
diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt
index 6e879df7ab..63a3fad109 100644
--- a/tools/check/forbidden_strings_in_code.txt
+++ b/tools/check/forbidden_strings_in_code.txt
@@ -164,7 +164,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===82
+enum class===83
### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3
diff --git a/tools/release/download_buildkite_artifacts.py b/tools/release/download_buildkite_artifacts.py
index 4439c2fb8c..067a1a4dfe 100755
--- a/tools/release/download_buildkite_artifacts.py
+++ b/tools/release/download_buildkite_artifacts.py
@@ -45,6 +45,10 @@ parser.add_argument('-e',
'--expecting',
type=int,
help='the expected number of artifacts. If omitted, no check will be done.')
+parser.add_argument('-i',
+ '--ignoreErrors',
+ help='Ignore errors that can be ignored. Build state and number of artifacts.',
+ action="store_true")
parser.add_argument('-d',
'--directory',
default="",
@@ -91,9 +95,14 @@ print(" git commit : \"%s\"" % data0.get('commit'))
print(" git commit message : \"%s\"" % data0.get('message'))
print(" build state : %s" % data0.get('state'))
+error = False
+
if data0.get('state') != 'passed':
print("❌ Error, the build is in state '%s', and not 'passed'" % data0.get('state'))
- exit(1)
+ if args.ignoreErrors:
+ error = True
+ else:
+ exit(1)
### Fetch artifacts list
@@ -110,8 +119,11 @@ data = json.loads(r.content.decode())
print(" %d artifact(s) found." % len(data))
if args.expecting is not None and args.expecting != len(data):
- print("Error, expecting %d artifacts and found %d." % (args.expecting, len(data)))
- exit(1)
+ print("❌ Error, expecting %d artifacts and found %d." % (args.expecting, len(data)))
+ if args.ignoreErrors:
+ error = True
+ else:
+ exit(1)
if args.verbose:
print("Json data:")
@@ -128,8 +140,6 @@ else:
if not args.simulate:
os.mkdir(targetDir)
-error = False
-
for elt in data:
if args.verbose:
print()
@@ -157,7 +167,7 @@ for elt in data:
print("❌ Checksum mismatch: expecting %s and get %s" % (elt.get("sha1sum"), hash))
if error:
- print("❌ Error(s) occurred, check the log")
+ print("❌ Error(s) occurred, please check the log")
exit(1)
else:
print("Done!")
diff --git a/vector/build.gradle b/vector/build.gradle
index 4fc2f831da..96b4994a7a 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -17,7 +17,7 @@ androidExtensions {
// Note: 2 digits max for each value
ext.versionMajor = 1
ext.versionMinor = 0
-ext.versionPatch = 9
+ext.versionPatch = 10
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'
@@ -39,9 +39,9 @@ def generateVersionCodeFromVersionName() {
def getVersionCode() {
if (gitBranchName() == "develop") {
- return generateVersionCodeFromTimestamp() * 10
+ return generateVersionCodeFromTimestamp()
} else {
- return generateVersionCodeFromVersionName() * 10
+ return generateVersionCodeFromVersionName()
}
}
@@ -166,13 +166,14 @@ android {
}
applicationVariants.all { variant ->
+ // assign different version code for each output
+ def baseVariantVersion = variant.versionCode * 10
variant.outputs.each { output ->
def baseAbiVersionCode = project.ext.abiVersionCodes.get(output.getFilter(OutputFile.ABI))
// Known limitation: it does not modify the value in the BuildConfig.java generated file
- print "ABI " + output.getFilter(OutputFile.ABI) + " \tvariant.versionCode " + variant.versionCode
// See https://issuetracker.google.com/issues/171133218
- output.versionCodeOverride = variant.versionCode + baseAbiVersionCode
- print " \t-> VersionCode = " + output.versionCodeOverride + "\n"
+ output.versionCodeOverride = baseVariantVersion + baseAbiVersionCode
+ print "ABI " + output.getFilter(OutputFile.ABI) + " \t-> VersionCode = " + output.versionCodeOverride + "\n"
}
}
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 643d5599de..fb4764b3be 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -63,7 +63,6 @@
diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt
index 4f89763cda..5be313d719 100644
--- a/vector/src/main/java/im/vector/app/VectorApplication.kt
+++ b/vector/src/main/java/im/vector/app/VectorApplication.kt
@@ -42,6 +42,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.configuration.VectorConfiguration
import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog
import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks
@@ -89,6 +90,7 @@ class VectorApplication :
@Inject lateinit var rxConfig: RxConfig
@Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var pinLocker: PinLocker
+ @Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
lateinit var vectorComponent: VectorComponent
@@ -173,6 +175,7 @@ class VectorApplication :
})
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker)
+ ProcessLifecycleOwner.get().lifecycle.addObserver(webRtcPeerConnectionManager)
// This should be done as early as possible
// initKnownEmojiHashSet(appContext)
diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
index 014a244bf8..acdad5407c 100644
--- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
@@ -87,6 +87,7 @@ import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment
import im.vector.app.features.roomprofile.uploads.files.RoomUploadsFilesFragment
import im.vector.app.features.roomprofile.uploads.media.RoomUploadsMediaFragment
import im.vector.app.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
+import im.vector.app.features.settings.VectorSettingsGeneralFragment
import im.vector.app.features.settings.VectorSettingsHelpAboutFragment
import im.vector.app.features.settings.VectorSettingsLabsFragment
import im.vector.app.features.settings.VectorSettingsNotificationPreferenceFragment
@@ -292,6 +293,11 @@ interface FragmentModule {
@FragmentKey(VectorSettingsPinFragment::class)
fun bindVectorSettingsPinFragment(fragment: VectorSettingsPinFragment): Fragment
+ @Binds
+ @IntoMap
+ @FragmentKey(VectorSettingsGeneralFragment::class)
+ fun bindVectorSettingsGeneralFragment(fragment: VectorSettingsGeneralFragment): Fragment
+
@Binds
@IntoMap
@FragmentKey(PushRulesFragment::class)
diff --git a/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt
new file mode 100644
index 0000000000..7198cdb4a2
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.core.dialogs
+
+import android.app.Activity
+import android.net.Uri
+import androidx.appcompat.app.AlertDialog
+import androidx.core.net.toUri
+import androidx.fragment.app.Fragment
+import com.yalantis.ucrop.UCrop
+import im.vector.app.R
+import im.vector.app.core.extensions.registerStartForActivityResult
+import im.vector.app.core.resources.ColorProvider
+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.features.media.createUCropWithDefaultSettings
+import im.vector.lib.multipicker.MultiPicker
+import im.vector.lib.multipicker.entity.MultiPickerImageType
+import java.io.File
+
+/**
+ * Use to let the user choose between Camera (with permission handling) and Gallery (with single image selection),
+ * then edit the image
+ * [Listener.onImageReady] will be called with an uri of a square image store in the cache of the application.
+ * It's up to the caller to delete the file.
+ */
+class GalleryOrCameraDialogHelper(
+ // must implement GalleryOrCameraDialogHelper.Listener
+ private val fragment: Fragment,
+ private val colorProvider: ColorProvider
+) {
+ interface Listener {
+ fun onImageReady(uri: Uri?)
+ }
+
+ private val activity
+ get() = fragment.requireActivity()
+
+ private val listener = fragment as? Listener ?: error("Fragment must implement GalleryOrCameraDialogHelper.Listener")
+
+ private val takePhotoPermissionActivityResultLauncher = fragment.registerForPermissionsResult { allGranted ->
+ if (allGranted) {
+ doOpenCamera()
+ }
+ }
+
+ private val takePhotoActivityResultLauncher = fragment.registerStartForActivityResult { activityResult ->
+ if (activityResult.resultCode == Activity.RESULT_OK) {
+ avatarCameraUri?.let { uri ->
+ MultiPicker.get(MultiPicker.CAMERA)
+ .getTakenPhoto(activity, uri)
+ ?.let { startUCrop(it) }
+ }
+ }
+ }
+
+ private val pickImageActivityResultLauncher = fragment.registerStartForActivityResult { activityResult ->
+ if (activityResult.resultCode == Activity.RESULT_OK) {
+ MultiPicker
+ .get(MultiPicker.IMAGE)
+ .getSelectedFiles(activity, activityResult.data)
+ .firstOrNull()
+ ?.let { startUCrop(it) }
+ }
+ }
+
+ private val uCropActivityResultLauncher = fragment.registerStartForActivityResult { activityResult ->
+ if (activityResult.resultCode == Activity.RESULT_OK) {
+ activityResult.data?.let { listener.onImageReady(UCrop.getOutput(it)) }
+ }
+ }
+
+ private fun startUCrop(image: MultiPickerImageType) {
+ val destinationFile = File(activity.cacheDir, "${image.displayName}_e_${System.currentTimeMillis()}")
+ val uri = image.contentUri
+ createUCropWithDefaultSettings(colorProvider, uri, destinationFile.toUri(), fragment.getString(R.string.rotate_and_crop_screen_title))
+ .withAspectRatio(1f, 1f)
+ .getIntent(activity)
+ .let { uCropActivityResultLauncher.launch(it) }
+ }
+
+ private enum class Type {
+ Camera,
+ Gallery
+ }
+
+ fun show() {
+ AlertDialog.Builder(activity)
+ .setTitle(R.string.attachment_type_dialog_title)
+ .setItems(arrayOf(
+ fragment.getString(R.string.attachment_type_camera),
+ fragment.getString(R.string.attachment_type_gallery)
+ )) { _, which ->
+ onAvatarTypeSelected(if (which == 0) Type.Camera else Type.Gallery)
+ }
+ .setPositiveButton(R.string.cancel, null)
+ .show()
+ }
+
+ private fun onAvatarTypeSelected(type: Type) {
+ when (type) {
+ Type.Camera ->
+ if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, activity, takePhotoPermissionActivityResultLauncher)) {
+ avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(activity, takePhotoActivityResultLauncher)
+ }
+ Type.Gallery ->
+ MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher)
+ }
+ }
+
+ private var avatarCameraUri: Uri? = null
+ private fun doOpenCamera() {
+ avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(activity, takePhotoActivityResultLauncher)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt
index a495f14b6e..1c80e6a85c 100644
--- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt
+++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt
@@ -44,8 +44,10 @@ abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel(R.id.bottomSheetRoomPreviewAvatar)
val roomName by bind(R.id.bottomSheetRoomPreviewName)
+ val roomLowPriority by bind(R.id.bottomSheetRoomPreviewLowPriority)
val roomFavorite by bind(R.id.bottomSheetRoomPreviewFavorite)
val roomSettings by bind(R.id.bottomSheetRoomPreviewSettings)
}
diff --git a/vector/src/main/java/im/vector/app/core/qrcode/QrCode.kt b/vector/src/main/java/im/vector/app/core/qrcode/QrCode.kt
index f79ae7afd9..170baa04fe 100644
--- a/vector/src/main/java/im/vector/app/core/qrcode/QrCode.kt
+++ b/vector/src/main/java/im/vector/app/core/qrcode/QrCode.kt
@@ -34,12 +34,15 @@ fun String.toBitMatrix(size: Int): BitMatrix {
fun BitMatrix.toBitmap(@ColorInt backgroundColor: Int = Color.WHITE,
@ColorInt foregroundColor: Int = Color.BLACK): Bitmap {
- val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
- for (x in 0 until width) {
- for (y in 0 until height) {
- bmp.setPixel(x, y, if (get(x, y)) foregroundColor else backgroundColor)
+ val colorBuffer = IntArray(width * height)
+ var rowOffset = 0
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ val arrayIndex = x + rowOffset
+ colorBuffer[arrayIndex] = if (get(x, y)) foregroundColor else backgroundColor
}
+ rowOffset += width
}
- return bmp
+ return Bitmap.createBitmap(colorBuffer, width, height, Bitmap.Config.ARGB_8888)
}
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 b14a097eb6..d5d8bb14dd 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
@@ -17,6 +17,8 @@
package im.vector.app.core.services
import android.content.Context
+import android.media.Ringtone
+import android.media.RingtoneManager
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.MediaPlayer
@@ -25,7 +27,26 @@ import androidx.core.content.getSystemService
import im.vector.app.R
import timber.log.Timber
-class CallRingPlayer(
+class CallRingPlayerIncoming(
+ context: Context
+) {
+
+ private val applicationContext = context.applicationContext
+ private var r: Ringtone? = null
+
+ fun start() {
+ val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
+ r = RingtoneManager.getRingtone(applicationContext, notification)
+ Timber.v("## VOIP Starting ringing incomming")
+ r?.play()
+ }
+
+ fun stop() {
+ r?.stop()
+ }
+}
+
+class CallRingPlayerOutgoing(
context: Context
) {
@@ -44,12 +65,12 @@ class CallRingPlayer(
try {
if (player?.isPlaying == false) {
player?.start()
- Timber.v("## VOIP Starting ringing")
+ Timber.v("## VOIP Starting ringing outgoing")
} else {
Timber.v("## VOIP already playing")
}
} catch (failure: Throwable) {
- Timber.e(failure, "## VOIP Failed to start ringing")
+ Timber.e(failure, "## VOIP Failed to start ringing outgoing")
player = null
}
} else {
@@ -74,7 +95,7 @@ class CallRingPlayer(
} else {
mediaPlayer.setAudioAttributes(AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
- .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
+ .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.build())
}
return mediaPlayer
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 1362c20be1..075b237be2 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
@@ -40,7 +40,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
private lateinit var notificationUtils: NotificationUtils
private lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
- private var callRingPlayer: CallRingPlayer? = null
+ private var callRingPlayerIncoming: CallRingPlayerIncoming? = null
+ private var callRingPlayerOutgoing: CallRingPlayerOutgoing? = null
private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null
private var bluetoothHeadsetStateReceiver: BluetoothHeadsetReceiver? = null
@@ -63,14 +64,16 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
super.onCreate()
notificationUtils = vectorComponent().notificationUtils()
webRtcPeerConnectionManager = vectorComponent().webRtcPeerConnectionManager()
- callRingPlayer = CallRingPlayer(applicationContext)
+ callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext)
+ callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext)
wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this)
bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(this, this)
}
override fun onDestroy() {
super.onDestroy()
- callRingPlayer?.stop()
+ callRingPlayerIncoming?.stop()
+ callRingPlayerOutgoing?.stop()
wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(this, it) }
wiredHeadsetStateReceiver = null
bluetoothHeadsetStateReceiver?.let { BluetoothHeadsetReceiver.unRegister(this, it) }
@@ -100,16 +103,17 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
when (intent.action) {
ACTION_INCOMING_RINGING_CALL -> {
mediaSession?.isActive = true
- callRingPlayer?.start()
+ callRingPlayerIncoming?.start()
displayIncomingCallNotification(intent)
}
ACTION_OUTGOING_RINGING_CALL -> {
mediaSession?.isActive = true
- callRingPlayer?.start()
+ callRingPlayerOutgoing?.start()
displayOutgoingRingingCallNotification(intent)
}
ACTION_ONGOING_CALL -> {
- callRingPlayer?.stop()
+ callRingPlayerIncoming?.stop()
+ callRingPlayerOutgoing?.stop()
displayCallInProgressNotification(intent)
}
ACTION_NO_ACTIVE_CALL -> hideCallNotifications()
@@ -117,7 +121,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
// lower notification priority
displayCallInProgressNotification(intent)
// stop ringing
- callRingPlayer?.stop()
+ callRingPlayerIncoming?.stop()
+ callRingPlayerOutgoing?.stop()
}
ACTION_ONGOING_CALL_BG -> {
// there is an ongoing call but call activity is in background
@@ -125,7 +130,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
}
else -> {
// Should not happen
- callRingPlayer?.stop()
+ callRingPlayerIncoming?.stop()
+ callRingPlayerOutgoing?.stop()
myStopSelf()
}
}
diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt
index b5552e4d62..e553b5e0d3 100644
--- a/vector/src/main/java/im/vector/app/features/MainActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt
@@ -21,6 +21,7 @@ import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import androidx.appcompat.app.AlertDialog
+import androidx.lifecycle.Lifecycle
import com.bumptech.glide.Glide
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
@@ -205,13 +206,15 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity {
}
private fun displayError(failure: Throwable) {
- AlertDialog.Builder(this)
- .setTitle(R.string.dialog_title_error)
- .setMessage(errorFormatter.toHumanReadable(failure))
- .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() }
- .setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() }
- .setCancelable(false)
- .show()
+ if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
+ AlertDialog.Builder(this)
+ .setTitle(R.string.dialog_title_error)
+ .setMessage(errorFormatter.toHumanReadable(failure))
+ .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() }
+ .setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() }
+ .setCancelable(false)
+ .show()
+ }
}
private fun startNextActivityAndFinish() {
diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt
index b040101c84..9f3ba39bbe 100644
--- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt
@@ -19,7 +19,6 @@ package im.vector.app.features.attachments.preview
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
-import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.Menu
@@ -39,6 +38,7 @@ import com.airbnb.mvrx.withState
import com.yalantis.ucrop.UCrop
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
+import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.OnSnapPositionChangeListener
@@ -49,7 +49,6 @@ import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_attachments_preview.*
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
-import timber.log.Timber
import java.io.File
import javax.inject.Inject
@@ -80,20 +79,15 @@ class AttachmentsPreviewFragment @Inject constructor(
}
}
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- // TODO handle this one (Ucrop lib)
- @Suppress("DEPRECATION")
- super.onActivityResult(requestCode, resultCode, data)
-
- if (resultCode == RESULT_OK) {
- if (requestCode == UCrop.REQUEST_CROP && data != null) {
- Timber.v("Crop success")
- handleCropResult(data)
+ private val uCropActivityResultLauncher = registerStartForActivityResult { activityResult ->
+ if (activityResult.resultCode == RESULT_OK) {
+ val resultUri = activityResult.data?.let { UCrop.getOutput(it) }
+ if (resultUri != null) {
+ viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultUri))
+ } else {
+ Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
}
}
- if (resultCode == UCrop.RESULT_ERROR) {
- Timber.v("Crop error")
- }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -170,15 +164,6 @@ class AttachmentsPreviewFragment @Inject constructor(
}
}
- private fun handleCropResult(result: Intent) {
- val resultUri = UCrop.getOutput(result)
- if (resultUri != null) {
- viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultUri))
- } else {
- Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
- }
- }
-
private fun handleRemoveAction() {
viewModel.handle(AttachmentsPreviewAction.RemoveCurrentAttachment)
}
@@ -187,8 +172,9 @@ class AttachmentsPreviewFragment @Inject constructor(
val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState
val destinationFile = File(requireContext().cacheDir, "${currentAttachment.name}_edited_image_${System.currentTimeMillis()}")
val uri = currentAttachment.queryUri
- createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), currentAttachment.name)
- .start(requireContext(), this)
+ createUCropWithDefaultSettings(colorProvider, uri, destinationFile.toUri(), currentAttachment.name)
+ .getIntent(requireContext())
+ .let { intent -> uCropActivityResultLauncher.launch(intent) }
}
private fun setupRecyclerViews() {
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
index 9b3c42ab5d..3a24cf6d48 100644
--- a/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt
+++ b/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt
@@ -93,6 +93,10 @@ class CallAudioManager(
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
@@ -150,7 +154,7 @@ class CallAudioManager(
fun onCallConnected(mxCall: MxCall) {
Timber.v("##VOIP: AudioManager call answered, adjusting current sound device")
- adjustCurrentSoundDevice(mxCall)
+ setupAudioManager(mxCall)
}
fun getAvailableSoundDevices(): List {
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 edb75441c8..445f40e5b1 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
@@ -88,6 +88,11 @@ class VectorCallViewModel @AssistedInject constructor(
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)
+ }
}
override fun onCaptureStateChanged() {
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
index c70b52b09b..86b38c1158 100644
--- a/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt
+++ b/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt
@@ -19,10 +19,14 @@ 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
@@ -72,7 +76,7 @@ import javax.inject.Singleton
class WebRtcPeerConnectionManager @Inject constructor(
private val context: Context,
private val activeSessionDataSource: ActiveSessionDataSource
-) : CallsListener {
+) : CallsListener, LifecycleObserver {
private val currentSession: Session?
get() = activeSessionDataSource.currentValue?.orNull()
@@ -170,6 +174,8 @@ class WebRtcPeerConnectionManager @Inject constructor(
private var currentCaptureMode: CaptureFormat = CaptureFormat.HD
+ private var isInBackground: Boolean = true
+
var capturerIsInError = false
set(value) {
field = value
@@ -201,6 +207,16 @@ class WebRtcPeerConnectionManager @Inject constructor(
}
}
+ @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
@@ -702,6 +718,18 @@ class WebRtcPeerConnectionManager @Inject constructor(
)
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() {
@@ -849,6 +877,16 @@ class WebRtcPeerConnectionManager @Inject constructor(
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 {
diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt
index 72b767e12f..2e5097fdb7 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt
@@ -103,7 +103,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
} else {
// we need to get existing backup passphrase/key and convert to SSSS
val keyVersion = awaitCallback {
- session.cryptoService().keysBackupService().getVersion(version.version ?: "", it)
+ session.cryptoService().keysBackupService().getVersion(version.version, it)
}
if (keyVersion == null) {
// strange case... just finish?
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 0a1b631344..e42eb6de6f 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
@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import org.matrix.android.sdk.api.util.toMatrixItem
+import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@@ -116,6 +117,7 @@ class IncomingVerificationRequestHandler @Inject constructor(
}
override fun verificationRequestCreated(pr: PendingVerificationRequest) {
+ 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
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt
index 2720c20fb0..2d09974687 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt
@@ -49,7 +49,6 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import org.matrix.android.sdk.api.session.events.model.LocalEcho
-import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
@@ -233,7 +232,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
override fun handle(action: VerificationAction) = withState { state ->
val otherUserId = state.otherUserMxItem?.id ?: return@withState
val roomId = state.roomId
- ?: session.getExistingDirectRoomWithUser(otherUserId)?.roomId
+ ?: session.getExistingDirectRoomWithUser(otherUserId)
when (action) {
is VerificationAction.RequestVerificationByDM -> {
@@ -245,14 +244,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
pendingRequest = Loading()
)
}
- val roomParams = CreateRoomParams()
- .apply {
- invitedUserIds.add(otherUserId)
- setDirectMessage()
- enableEncryptionIfInvitedUsersSupportIt = true
- }
-
- session.createRoom(roomParams, object : MatrixCallback {
+ session.createDirectRoom(otherUserId, object : MatrixCallback {
override fun onSuccess(data: String) {
setState {
copy(
diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt
new file mode 100644
index 0000000000..c5a45d8f1b
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.form
+
+import android.net.Uri
+import android.view.View
+import android.widget.ImageView
+import androidx.core.view.isVisible
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import com.airbnb.epoxy.EpoxyModelWithHolder
+import com.bumptech.glide.request.RequestOptions
+import im.vector.app.R
+import im.vector.app.core.epoxy.ClickListener
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.onClick
+import im.vector.app.core.glide.GlideApp
+import im.vector.app.features.home.AvatarRenderer
+import org.matrix.android.sdk.api.util.MatrixItem
+
+@EpoxyModelClass(layout = R.layout.item_editable_avatar)
+abstract class FormEditableAvatarItem : EpoxyModelWithHolder() {
+
+ @EpoxyAttribute
+ var avatarRenderer: AvatarRenderer? = null
+
+ @EpoxyAttribute
+ var matrixItem: MatrixItem? = null
+
+ @EpoxyAttribute
+ var enabled: Boolean = true
+
+ @EpoxyAttribute
+ var imageUri: Uri? = null
+
+ @EpoxyAttribute
+ var clickListener: ClickListener? = null
+
+ @EpoxyAttribute
+ var deleteListener: ClickListener? = null
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ holder.imageContainer.onClick(clickListener?.takeIf { enabled })
+ if (matrixItem != null) {
+ avatarRenderer?.render(matrixItem!!, holder.image)
+ } else {
+ GlideApp.with(holder.image)
+ .load(imageUri)
+ .apply(RequestOptions.circleCropTransform())
+ .into(holder.image)
+ }
+ holder.delete.isVisible = enabled && (imageUri != null || matrixItem?.avatarUrl?.isNotEmpty() == true)
+ holder.delete.onClick(deleteListener?.takeIf { enabled })
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val imageContainer by bind(R.id.itemEditableAvatarImageContainer)
+ val image by bind(R.id.itemEditableAvatarImage)
+ val delete by bind(R.id.itemEditableAvatarDelete)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/form/FormSubmitButtonItem.kt b/vector/src/main/java/im/vector/app/features/form/FormSubmitButtonItem.kt
new file mode 100644
index 0000000000..2d2a5e7aec
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/form/FormSubmitButtonItem.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.form
+
+import android.widget.Button
+import androidx.annotation.StringRes
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import com.airbnb.epoxy.EpoxyModelWithHolder
+import im.vector.app.R
+import im.vector.app.core.epoxy.ClickListener
+import im.vector.app.core.epoxy.VectorEpoxyHolder
+import im.vector.app.core.epoxy.onClick
+import im.vector.app.core.extensions.setTextOrHide
+
+@EpoxyModelClass(layout = R.layout.item_form_submit_button)
+abstract class FormSubmitButtonItem : EpoxyModelWithHolder() {
+
+ @EpoxyAttribute
+ var enabled: Boolean = true
+
+ @EpoxyAttribute
+ var buttonTitle: String? = null
+
+ @EpoxyAttribute
+ @StringRes
+ var buttonTitleId: Int? = null
+
+ @EpoxyAttribute
+ var buttonClickListener: ClickListener? = null
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ if (buttonTitleId != null) {
+ holder.button.setText(buttonTitleId!!)
+ } else {
+ holder.button.setTextOrHide(buttonTitle)
+ }
+
+ holder.button.isEnabled = enabled
+ holder.button.onClick(buttonClickListener)
+ }
+
+ class Holder : VectorEpoxyHolder() {
+ val button by bind(R.id.form_submit_button)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt
index 12689cd983..e267248fc3 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeDrawerFragment.kt
@@ -19,6 +19,7 @@ package im.vector.app.features.home
import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
+import im.vector.app.BuildConfig
import im.vector.app.R
import im.vector.app.core.extensions.observeK
import im.vector.app.core.extensions.replaceChildFragment
@@ -75,7 +76,7 @@ class HomeDrawerFragment @Inject constructor(
}
// Debug menu
- homeDrawerHeaderDebugView.isVisible = vectorPreferences.developerMode()
+ homeDrawerHeaderDebugView.isVisible = BuildConfig.DEBUG && vectorPreferences.developerMode()
homeDrawerHeaderDebugView.debouncedClicks {
sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer)
navigator.openDebug(requireActivity())
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 88eb1b5109..99adc0bf83 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
@@ -68,7 +68,6 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class IgnoreUser(val userId: String?) : RoomDetailAction()
- object ClearSendQueue : RoomDetailAction()
object ResendAll : RoomDetailAction()
data class StartCall(val isVideo: Boolean) : RoomDetailAction()
object EndCall : RoomDetailAction()
@@ -89,5 +88,6 @@ sealed class RoomDetailAction : VectorViewModelAction {
val userJustAccepted: Boolean,
val grantedEvents: RoomDetailViewEvents) : RoomDetailAction()
+ data class OpenOrCreateDm(val userId: String) : RoomDetailAction()
data class JumpToReadReceipt(val userId: String) : RoomDetailAction()
}
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 92c3499e5e..9c6c473a7f 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
@@ -20,6 +20,7 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.content.DialogInterface
import android.content.Intent
+import android.content.res.Configuration
import android.graphics.Typeface
import android.net.Uri
import android.os.Build
@@ -27,11 +28,13 @@ import android.os.Bundle
import android.os.Parcelable
import android.text.Spannable
import android.view.HapticFeedbackConstants
+import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
+import android.view.inputmethod.EditorInfo
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
@@ -360,6 +363,7 @@ class RoomDetailFragment @Inject constructor(
RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView()
RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
+ is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it)
}.exhaustive
}
@@ -368,6 +372,10 @@ class RoomDetailFragment @Inject constructor(
}
}
+ private fun handleOpenRoom(openRoom: RoomDetailViewEvents.OpenRoom) {
+ navigator.openRoom(requireContext(), openRoom.roomId, null)
+ }
+
private fun requestNativeWidgetPermission(it: RoomDetailViewEvents.RequestNativeWidgetPermission) {
val tag = RoomWidgetPermissionBottomSheet::class.java.name
val dFrag = childFragmentManager.findFragmentByTag(tag) as? RoomWidgetPermissionBottomSheet
@@ -645,13 +653,6 @@ class RoomDetailFragment @Inject constructor(
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
- R.id.clear_message_queue -> {
- // This a temporary option during dev as it is not super stable
- // Cancel all pending actions in room queue and post a dummy
- // Then mark all sending events as undelivered
- roomDetailViewModel.handle(RoomDetailAction.ClearSendQueue)
- true
- }
R.id.invite -> {
navigator.openInviteUsersToRoom(requireActivity(), roomDetailArgs.roomId)
true
@@ -890,6 +891,8 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.JumpToReadReceipt(roomDetailPendingAction.userId))
is RoomDetailPendingAction.MentionUser ->
insertUserDisplayNameInTextEditor(roomDetailPendingAction.userId)
+ is RoomDetailPendingAction.OpenOrCreateDm ->
+ roomDetailViewModel.handle(RoomDetailAction.OpenOrCreateDm(roomDetailPendingAction.userId))
}.exhaustive
}
@@ -1065,10 +1068,33 @@ class RoomDetailFragment @Inject constructor(
}
private fun setupComposer() {
- autoCompleter.setup(composerLayout.composerEditText)
+ val composerEditText = composerLayout.composerEditText
+ autoCompleter.setup(composerEditText)
observerUserTyping()
+ if (vectorPreferences.sendMessageWithEnter()) {
+ // imeOptions="actionSend" only works with single line, so we remove multiline inputType
+ composerEditText.inputType = composerEditText.inputType and EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE.inv()
+ composerEditText.imeOptions = EditorInfo.IME_ACTION_SEND
+ }
+
+ composerEditText.setOnEditorActionListener { v, actionId, keyEvent ->
+ val imeActionId = actionId and EditorInfo.IME_MASK_ACTION
+ if (EditorInfo.IME_ACTION_DONE == imeActionId || EditorInfo.IME_ACTION_SEND == imeActionId) {
+ sendTextMessage(v.text)
+ true
+ }
+ // Add external keyboard functionality (to send messages)
+ else if (null != keyEvent
+ && !keyEvent.isShiftPressed
+ && keyEvent.keyCode == KeyEvent.KEYCODE_ENTER
+ && resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS) {
+ sendTextMessage(v.text)
+ true
+ } else false
+ }
+
composerLayout.callback = object : TextComposerView.Callback {
override fun onAddAttachment() {
if (!::attachmentTypeSelector.isInitialized) {
@@ -1078,16 +1104,7 @@ class RoomDetailFragment @Inject constructor(
}
override fun onSendMessage(text: CharSequence) {
- if (lockSendButton) {
- Timber.w("Send button is locked")
- return
- }
- if (text.isNotBlank()) {
- // We collapse ASAP, if not there will be a slight anoying delay
- composerLayout.collapse(true)
- lockSendButton = true
- roomDetailViewModel.handle(RoomDetailAction.SendMessage(text, vectorPreferences.isMarkdownEnabled()))
- }
+ sendTextMessage(text)
}
override fun onCloseRelatedMessage() {
@@ -1107,6 +1124,19 @@ class RoomDetailFragment @Inject constructor(
}
}
+ private fun sendTextMessage(text: CharSequence) {
+ if (lockSendButton) {
+ Timber.w("Send button is locked")
+ return
+ }
+ if (text.isNotBlank()) {
+ // We collapse ASAP, if not there will be a slight anoying delay
+ composerLayout.collapse(true)
+ lockSendButton = true
+ roomDetailViewModel.handle(RoomDetailAction.SendMessage(text, vectorPreferences.isMarkdownEnabled()))
+ }
+ }
+
private fun observerUserTyping() {
composerLayout.composerEditText.textChanges()
.skipInitialValue()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingAction.kt
index 394d46ef8d..598ab9d056 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingAction.kt
@@ -17,6 +17,7 @@
package im.vector.app.features.home.room.detail
sealed class RoomDetailPendingAction {
+ data class OpenOrCreateDm(val userId: String) : RoomDetailPendingAction()
data class JumpToReadReceipt(val userId: String) : RoomDetailPendingAction()
data class MentionUser(val userId: String) : RoomDetailPendingAction()
}
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 ee2d193473..b9e3e6b31d 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
@@ -38,6 +38,8 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class ShowInfoOkDialog(val message: String) : RoomDetailViewEvents()
data class ShowE2EErrorMessage(val withHeldCode: WithHeldCode?) : RoomDetailViewEvents()
+ data class OpenRoom(val roomId: String) : RoomDetailViewEvents()
+
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : 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 19cc27a510..9efad1081f 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
@@ -251,7 +251,6 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
- is RoomDetailAction.ClearSendQueue -> handleClearSendQueue()
is RoomDetailAction.ResendAll -> handleResendAll()
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
is RoomDetailAction.ReportContent -> handleReportContent(action)
@@ -274,10 +273,28 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
is RoomDetailAction.CancelSend -> handleCancel(action)
+ is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
}.exhaustive
}
+ 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 {
+ session.createDirectRoom(action.userId, it)
+ }
+ _viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId))
+ }
+ } else {
+ if (existingDmRoomId != initialState.roomId) {
+ _viewEvents.post(RoomDetailViewEvents.OpenRoom(existingDmRoomId))
+ }
+ }
+ }
+
private fun handleJumpToReadReceipt(action: RoomDetailAction.JumpToReadReceipt) {
room.getUserReadReceipt(action.userId)
?.let { handleNavigateToEvent(RoomDetailAction.NavigateToEvent(it, true)) }
@@ -542,9 +559,6 @@ class RoomDetailViewModel @AssistedInject constructor(
return@withState false
}
when (itemId) {
- R.id.clear_message_queue ->
- // For now always disable when not in developer mode, worker cancellation is not working properly
- timeline.pendingEventCount() > 0 && vectorPreferences.developerMode()
R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.timeline_setting -> true
R.id.invite -> state.canInvite
@@ -1065,10 +1079,6 @@ class RoomDetailViewModel @AssistedInject constructor(
}
}
- private fun handleClearSendQueue() {
- room.clearSendingQueue()
- }
-
private fun handleResendAll() {
room.resendAllFailedMessages()
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt
index 10dc9254d8..201e9a4f82 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt
@@ -52,8 +52,6 @@ class SearchFragment @Inject constructor(
private val fragmentArgs: SearchArgs by args()
private val searchViewModel: SearchViewModel by fragmentViewModel()
- private var pendingScrollToPosition: Int? = null
-
override fun getLayoutResId() = R.layout.fragment_search
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -70,12 +68,6 @@ class SearchFragment @Inject constructor(
searchResultRecycler.configureWith(controller, showDivider = false)
(searchResultRecycler.layoutManager as? LinearLayoutManager)?.stackFromEnd = true
controller.listener = this
-
- controller.addModelBuildListener {
- pendingScrollToPosition?.let {
- searchResultRecycler.smoothScrollToPosition(it)
- }
- }
}
override fun onDestroy() {
@@ -100,10 +92,8 @@ class SearchFragment @Inject constructor(
}
}
} else {
- pendingScrollToPosition = (state.lastBatchSize - 1).coerceAtLeast(0)
-
- stateView.state = StateView.State.Content
controller.setData(state)
+ stateView.state = StateView.State.Content
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt
index c917c4557d..b927fb5ff3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt
@@ -16,16 +16,24 @@
package im.vector.app.features.home.room.detail.search
+import android.graphics.Typeface
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.style.StyleSpan
+import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.epoxy.VisibilityState
+import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.loadingItem
-import im.vector.app.core.ui.list.genericItemHeader
+import im.vector.app.core.epoxy.noResultItem
+import im.vector.app.core.resources.StringProvider
+import im.vector.app.core.ui.list.GenericItemHeader_
import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.Session
+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.search.EventAndSender
import org.matrix.android.sdk.api.util.toMatrixItem
import java.util.Calendar
import javax.inject.Inject
@@ -33,6 +41,7 @@ import javax.inject.Inject
class SearchResultController @Inject constructor(
private val session: Session,
private val avatarRenderer: AvatarRenderer,
+ private val stringProvider: StringProvider,
private val dateFormatter: VectorDateFormatter
) : TypedEpoxyController() {
@@ -52,6 +61,8 @@ class SearchResultController @Inject constructor(
override fun buildModels(data: SearchViewState?) {
data ?: return
+ val searchItems = buildSearchResultItems(data)
+
if (data.hasMoreResult) {
loadingItem {
// Always use a different id, because we can be notified several times of visibility state changed
@@ -62,35 +73,85 @@ class SearchResultController @Inject constructor(
}
}
}
+ } else {
+ if (searchItems.isEmpty()) {
+ // All returned results by the server has been filtered out and there is no more result
+ noResultItem {
+ id("noResult")
+ text(stringProvider.getString(R.string.no_result_placeholder))
+ }
+ } else {
+ noResultItem {
+ id("noMoreResult")
+ text(stringProvider.getString(R.string.no_more_results))
+ }
+ }
}
- buildSearchResultItems(data.searchResult)
+ searchItems.forEach { add(it) }
}
- private fun buildSearchResultItems(events: List) {
+ /**
+ * @return the list of EpoxyModel (date items and search result items), or an empty list if all items have been filtered out
+ */
+ private fun buildSearchResultItems(data: SearchViewState): List> {
var lastDate: Calendar? = null
+ val result = mutableListOf>()
+
+ data.searchResult.forEach { eventAndSender ->
+ val event = eventAndSender.event
+
+ @Suppress("UNCHECKED_CAST")
+ // Take new content first
+ val text = ((event.content?.get("m.new_content") as? Content) ?: event.content)?.get("body") as? String ?: return@forEach
+ val spannable = setHighLightedText(text, data.highlights) ?: return@forEach
- events.forEach { eventAndSender ->
val eventDate = Calendar.getInstance().apply {
timeInMillis = eventAndSender.event.originServerTs ?: System.currentTimeMillis()
}
if (lastDate?.get(Calendar.DAY_OF_YEAR) != eventDate.get(Calendar.DAY_OF_YEAR)) {
- genericItemHeader {
- id(eventDate.hashCode())
- text(dateFormatter.format(eventDate.timeInMillis, DateFormatKind.EDIT_HISTORY_HEADER))
- }
+ GenericItemHeader_()
+ .id(eventDate.hashCode())
+ .text(dateFormatter.format(eventDate.timeInMillis, DateFormatKind.EDIT_HISTORY_HEADER))
+ .let { result.add(it) }
}
lastDate = eventDate
- searchResultItem {
- id(eventAndSender.event.eventId)
- avatarRenderer(avatarRenderer)
- dateFormatter(dateFormatter)
- event(eventAndSender.event)
- sender(eventAndSender.sender
- ?: eventAndSender.event.senderId?.let { session.getUser(it) }?.toMatrixItem())
- listener { listener?.onItemClicked(eventAndSender.event) }
+ SearchResultItem_()
+ .id(eventAndSender.event.eventId)
+ .avatarRenderer(avatarRenderer)
+ .formattedDate(dateFormatter.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE))
+ .spannable(spannable)
+ .sender(eventAndSender.sender
+ ?: eventAndSender.event.senderId?.let { session.getUser(it) }?.toMatrixItem())
+ .listener { listener?.onItemClicked(eventAndSender.event) }
+ .let { result.add(it) }
+ }
+
+ return result
+ }
+
+ /**
+ * Highlight the text. If the text is not found, return null to ignore this result
+ * See https://github.com/matrix-org/synapse/issues/8686
+ */
+ private fun setHighLightedText(text: String, highlights: List): Spannable? {
+ val wordToSpan: Spannable = SpannableString(text)
+ var found = false
+ highlights.forEach { highlight ->
+ var searchFromIndex = 0
+ while (searchFromIndex < text.length) {
+ val indexOfHighlight = text.indexOf(highlight, searchFromIndex, ignoreCase = true)
+ searchFromIndex = if (indexOfHighlight == -1) {
+ Integer.MAX_VALUE
+ } else {
+ // bold
+ found = true
+ wordToSpan.setSpan(StyleSpan(Typeface.BOLD), indexOfHighlight, indexOfHighlight + highlight.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+ indexOfHighlight + 1
+ }
}
}
+ return wordToSpan.takeIf { found }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
index 10407c64e0..a3e5983c3a 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt
@@ -21,23 +21,20 @@ import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
-import im.vector.app.core.date.DateFormatKind
-import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.home.AvatarRenderer
-import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass(layout = R.layout.item_search_result)
abstract class SearchResultItem : VectorEpoxyModel() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
- @EpoxyAttribute var dateFormatter: VectorDateFormatter? = null
- @EpoxyAttribute lateinit var event: Event
+ @EpoxyAttribute var formattedDate: String? = null
+ @EpoxyAttribute lateinit var spannable: CharSequence
@EpoxyAttribute var sender: MatrixItem? = null
@EpoxyAttribute var listener: ClickListener? = null
@@ -47,9 +44,8 @@ abstract class SearchResultItem : VectorEpoxyModel() {
holder.view.onClick(listener)
sender?.let { avatarRenderer.render(it, holder.avatarImageView) }
holder.memberNameView.setTextOrHide(sender?.getBestName())
- holder.timeView.text = dateFormatter?.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
- // TODO Improve that (use formattedBody, etc.)
- holder.contentView.text = event.content?.get("body") as? String
+ holder.timeView.text = formattedDate
+ holder.contentView.text = spannable
}
class Holder : VectorEpoxyHolder() {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt
index f61bcbd029..ab440f6b5f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt
@@ -145,6 +145,7 @@ class SearchViewModel @AssistedInject constructor(
setState {
copy(
searchResult = accumulatedResult,
+ highlights = searchResult.highlights.orEmpty(),
hasMoreResult = !nextBatch.isNullOrEmpty(),
lastBatchSize = searchResult.results.orEmpty().size,
asyncSearchRequest = Success(Unit)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt
index 9f700b6e31..41fecbb5e2 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt
@@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.search.EventAndSender
data class SearchViewState(
// Accumulated search result
val searchResult: List = emptyList(),
+ val highlights: List = emptyList(),
val hasMoreResult: Boolean = false,
// Last batch size, will help RecyclerView to position itself
val lastBatchSize: Int = 0,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt
index 791a3c388f..feba62dea3 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt
@@ -41,23 +41,27 @@ abstract class MessageTextItem : AbsMessageItem() {
var movementMethod: MovementMethod? = null
override fun bind(holder: Holder) {
- super.bind(holder)
- holder.messageView.movementMethod = movementMethod
if (useBigFont) {
holder.messageView.textSize = 44F
} else {
holder.messageView.textSize = 14F
}
- renderSendState(holder.messageView, holder.messageView)
- holder.messageView.setOnClickListener(attributes.itemClickListener)
- holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
if (searchForPills) {
- message?.findPillsAndProcess(coroutineScope) { it.bind(holder.messageView) }
+ message?.findPillsAndProcess(coroutineScope) {
+ // mmm.. not sure this is so safe in regards to cell reuse
+ it.bind(holder.messageView)
+ }
}
val textFuture = PrecomputedTextCompat.getTextFuture(
message ?: "",
TextViewCompat.getTextMetricsParams(holder.messageView),
null)
+ super.bind(holder)
+ holder.messageView.movementMethod = movementMethod
+
+ renderSendState(holder.messageView, holder.messageView)
+ holder.messageView.setOnClickListener(attributes.itemClickListener)
+ holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
holder.messageView.setTextFuture(textFuture)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt
index 1b05c3f2fd..4a6c1c16fc 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt
@@ -27,7 +27,7 @@ sealed class RoomListAction : VectorViewModelAction {
data class RejectInvitation(val roomSummary: RoomSummary) : RoomListAction()
data class FilterWith(val filter: String) : RoomListAction()
data class ChangeRoomNotificationState(val roomId: String, val notificationState: RoomNotificationState) : RoomListAction()
- data class ToggleFavorite(val roomId: String) : RoomListAction()
+ data class ToggleTag(val roomId: String, val tag: String) : RoomListAction()
data class LeaveRoom(val roomId: String) : RoomListAction()
object MarkAllRoomsRead : RoomListAction()
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
index aa10196956..f1d35a74d5 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt
@@ -52,6 +52,7 @@ import kotlinx.android.synthetic.main.fragment_room_list.*
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
import javax.inject.Inject
@@ -233,7 +234,10 @@ class RoomListFragment @Inject constructor(
navigator.openRoomProfile(requireActivity(), quickAction.roomId)
}
is RoomListQuickActionsSharedAction.Favorite -> {
- roomListViewModel.handle(RoomListAction.ToggleFavorite(quickAction.roomId))
+ roomListViewModel.handle(RoomListAction.ToggleTag(quickAction.roomId, RoomTag.ROOM_TAG_FAVOURITE))
+ }
+ is RoomListQuickActionsSharedAction.LowPriority -> {
+ roomListViewModel.handle(RoomListAction.ToggleTag(quickAction.roomId, RoomTag.ROOM_TAG_LOW_PRIORITY))
}
is RoomListQuickActionsSharedAction.Leave -> {
AlertDialog.Builder(requireContext())
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
index d94a8010ba..c32629d6ae 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt
@@ -16,6 +16,7 @@
package im.vector.app.features.home.room.list
+import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
@@ -23,6 +24,8 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.utils.DataSource
import io.reactivex.schedulers.Schedulers
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.extensions.orFalse
@@ -30,6 +33,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
+import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx
import timber.log.Timber
import javax.inject.Inject
@@ -70,7 +74,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
is RoomListAction.MarkAllRoomsRead -> handleMarkAllRoomsRead()
is RoomListAction.LeaveRoom -> handleLeaveRoom(action)
is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
- is RoomListAction.ToggleFavorite -> handleToggleFavorite(action)
+ is RoomListAction.ToggleTag -> handleToggleTag(action)
}.exhaustive
}
@@ -172,19 +176,39 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
})
}
- private fun handleToggleFavorite(action: RoomListAction.ToggleFavorite) {
- session.getRoom(action.roomId)?.let {
- val callback = object : MatrixCallback {
- override fun onFailure(failure: Throwable) {
+ private fun handleToggleTag(action: RoomListAction.ToggleTag) {
+ session.getRoom(action.roomId)?.let { room ->
+ viewModelScope.launch(Dispatchers.IO) {
+ try {
+ if (room.roomSummary()?.hasTag(action.tag) == false) {
+ // Favorite and low priority tags are exclusive, so maybe delete the other tag first
+ action.tag.otherTag()
+ ?.takeIf { room.roomSummary()?.hasTag(it).orFalse() }
+ ?.let { tagToRemove ->
+ awaitCallback { room.deleteTag(tagToRemove, it) }
+ }
+
+ // Set the tag. We do not handle the order for the moment
+ awaitCallback {
+ room.addTag(action.tag, 0.5, it)
+ }
+ } else {
+ awaitCallback {
+ room.deleteTag(action.tag, it)
+ }
+ }
+ } catch (failure: Throwable) {
_viewEvents.post(RoomListViewEvents.Failure(failure))
}
}
- if (it.roomSummary()?.isFavorite == false) {
- // Set favorite tag. We do not handle the order for the moment
- it.addTag(RoomTag.ROOM_TAG_FAVOURITE, 0.5, callback)
- } else {
- it.deleteTag(RoomTag.ROOM_TAG_FAVOURITE, callback)
- }
+ }
+ }
+
+ private fun String.otherTag(): String? {
+ return when (this) {
+ RoomTag.ROOM_TAG_FAVOURITE -> RoomTag.ROOM_TAG_LOW_PRIORITY
+ RoomTag.ROOM_TAG_LOW_PRIORITY -> RoomTag.ROOM_TAG_FAVOURITE
+ else -> null
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt
index ccd38125f9..e3a5db4b97 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt
@@ -89,8 +89,9 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R
sharedActionViewModel.post(quickAction)
// Do not dismiss for all the actions
when (quickAction) {
- is RoomListQuickActionsSharedAction.Favorite -> Unit
- else -> dismiss()
+ is RoomListQuickActionsSharedAction.LowPriority -> Unit
+ is RoomListQuickActionsSharedAction.Favorite -> Unit
+ else -> dismiss()
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt
index ed52dcfdad..ebacdbd1eb 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt
@@ -47,9 +47,11 @@ class RoomListQuickActionsEpoxyController @Inject constructor(
avatarRenderer(avatarRenderer)
matrixItem(roomSummary.toMatrixItem())
stringProvider(stringProvider)
+ izLowPriority(roomSummary.isLowPriority)
izFavorite(roomSummary.isFavorite)
settingsClickListener { listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.Settings(roomSummary.roomId)) }
favoriteClickListener { listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.Favorite(roomSummary.roomId)) }
+ lowPriorityClickListener { listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.LowPriority(roomSummary.roomId)) }
}
// Notifications
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt
index 5ef705d5b0..075dca0c52 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt
@@ -52,6 +52,10 @@ sealed class RoomListQuickActionsSharedAction(
R.drawable.ic_room_actions_settings
)
+ data class LowPriority(val roomId: String) : RoomListQuickActionsSharedAction(
+ R.string.room_list_quick_actions_low_priority_add,
+ R.drawable.ic_low_priority_24)
+
data class Favorite(val roomId: String) : RoomListQuickActionsSharedAction(
R.string.room_list_quick_actions_favorite_add,
R.drawable.ic_star_24dp)
diff --git a/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt b/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt
index 81d6f1f996..195421ff58 100644
--- a/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/media/BigImageViewerActivity.kt
@@ -16,42 +16,22 @@
package im.vector.app.features.media
-import android.app.Activity
import android.content.Context
import android.content.Intent
-import android.net.Uri
import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
-import android.widget.Toast
-import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri
-import com.yalantis.ucrop.UCrop
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.ScreenComponent
-import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseActivity
-import im.vector.app.core.resources.ColorProvider
-import im.vector.app.core.resources.StringProvider
-import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
-import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
-import im.vector.app.core.utils.allGranted
-import im.vector.app.core.utils.checkPermissions
-import im.vector.lib.multipicker.MultiPicker
-import im.vector.lib.multipicker.entity.MultiPickerImageType
import kotlinx.android.synthetic.main.activity_big_image_viewer.*
-import java.io.File
import javax.inject.Inject
+/**
+ * Simple Activity to display an avatar in fullscreen
+ */
class BigImageViewerActivity : VectorBaseActivity() {
@Inject lateinit var sessionHolder: ActiveSessionHolder
- @Inject lateinit var colorProvider: ColorProvider
- @Inject lateinit var stringProvider: StringProvider
-
- private var uri: Uri? = null
-
- override fun getMenuRes() = R.menu.vector_big_avatar_viewer
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
@@ -68,7 +48,7 @@ class BigImageViewerActivity : VectorBaseActivity() {
setDisplayHomeAsUpEnabled(true)
}
- uri = sessionHolder.getSafeActiveSession()
+ val uri = sessionHolder.getSafeActiveSession()
?.contentUrlResolver()
?.resolveFullSize(intent.getStringExtra(EXTRA_IMAGE_URL))
?.toUri()
@@ -80,117 +60,14 @@ class BigImageViewerActivity : VectorBaseActivity() {
}
}
- override fun onPrepareOptionsMenu(menu: Menu): Boolean {
- menu.findItem(R.id.bigAvatarEditAction).isVisible = shouldShowEditAction()
- return super.onPrepareOptionsMenu(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- if (item.itemId == R.id.bigAvatarEditAction) {
- showAvatarSelector()
- return true
- }
- return super.onOptionsItemSelected(item)
- }
-
- private fun shouldShowEditAction(): Boolean {
- return uri != null && intent.getBooleanExtra(EXTRA_CAN_EDIT_IMAGE, false)
- }
-
- private fun showAvatarSelector() {
- AlertDialog.Builder(this)
- .setItems(arrayOf(
- stringProvider.getString(R.string.attachment_type_camera),
- stringProvider.getString(R.string.attachment_type_gallery)
- )) { dialog, which ->
- dialog.cancel()
- onAvatarTypeSelected(isCamera = (which == 0))
- }
- .show()
- }
-
- private var avatarCameraUri: Uri? = null
- private fun onAvatarTypeSelected(isCamera: Boolean) {
- if (isCamera) {
- if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
- avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(this, takePhotoActivityResultLauncher)
- }
- } else {
- MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher)
- }
- }
-
- private fun onRoomAvatarSelected(image: MultiPickerImageType) {
- val destinationFile = File(cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")
- val uri = image.contentUri
- createUCropWithDefaultSettings(this, uri, destinationFile.toUri(), image.displayName)
- .apply { withAspectRatio(1f, 1f) }
- .start(this)
- }
-
- private val takePhotoActivityResultLauncher = registerStartForActivityResult { activityResult ->
- if (activityResult.resultCode == Activity.RESULT_OK) {
- avatarCameraUri?.let { uri ->
- MultiPicker.get(MultiPicker.CAMERA)
- .getTakenPhoto(this, uri)
- ?.let {
- onRoomAvatarSelected(it)
- }
- }
- }
- }
-
- private val pickImageActivityResultLauncher = registerStartForActivityResult { activityResult ->
- if (activityResult.resultCode == Activity.RESULT_OK) {
- MultiPicker
- .get(MultiPicker.IMAGE)
- .getSelectedFiles(this, activityResult.data)
- .firstOrNull()?.let {
- onRoomAvatarSelected(it)
- }
- }
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- // TODO handle this one (Ucrop lib)
- @Suppress("DEPRECATION")
- super.onActivityResult(requestCode, resultCode, data)
-
- if (resultCode == Activity.RESULT_OK) {
- when (requestCode) {
- UCrop.REQUEST_CROP -> data?.let { onAvatarCropped(UCrop.getOutput(it)) }
- }
- }
- }
-
- override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- if (allGranted(grantResults)) {
- when (requestCode) {
- PERMISSION_REQUEST_CODE_LAUNCH_CAMERA -> onAvatarTypeSelected(true)
- }
- }
- }
-
- private fun onAvatarCropped(uri: Uri?) {
- if (uri != null) {
- setResult(Activity.RESULT_OK, Intent().setData(uri))
- this@BigImageViewerActivity.finish()
- } else {
- Toast.makeText(this, "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
- }
- }
-
companion object {
private const val EXTRA_TITLE = "EXTRA_TITLE"
private const val EXTRA_IMAGE_URL = "EXTRA_IMAGE_URL"
- private const val EXTRA_CAN_EDIT_IMAGE = "EXTRA_CAN_EDIT_IMAGE"
- fun newIntent(context: Context, title: String?, imageUrl: String, canEditImage: Boolean = false): Intent {
+ fun newIntent(context: Context, title: String?, imageUrl: String): Intent {
return Intent(context, BigImageViewerActivity::class.java).apply {
putExtra(EXTRA_TITLE, title)
putExtra(EXTRA_IMAGE_URL, imageUrl)
- putExtra(EXTRA_CAN_EDIT_IMAGE, canEditImage)
}
}
}
diff --git a/vector/src/main/java/im/vector/app/features/media/UCropHelper.kt b/vector/src/main/java/im/vector/app/features/media/UCropHelper.kt
index 8c8c8f22f1..191571959b 100644
--- a/vector/src/main/java/im/vector/app/features/media/UCropHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/media/UCropHelper.kt
@@ -16,16 +16,17 @@
package im.vector.app.features.media
-import android.content.Context
import android.graphics.Color
import android.net.Uri
-import androidx.core.content.ContextCompat
import com.yalantis.ucrop.UCrop
import com.yalantis.ucrop.UCropActivity
import im.vector.app.R
-import im.vector.app.features.themes.ThemeUtils
+import im.vector.app.core.resources.ColorProvider
-fun createUCropWithDefaultSettings(context: Context, source: Uri, destination: Uri, toolbarTitle: String?): UCrop {
+fun createUCropWithDefaultSettings(colorProvider: ColorProvider,
+ source: Uri,
+ destination: Uri,
+ toolbarTitle: String?): UCrop {
return UCrop.of(source, destination)
.withOptions(
UCrop.Options()
@@ -39,15 +40,15 @@ fun createUCropWithDefaultSettings(context: Context, source: Uri, destination: U
// Disable freestyle crop, usability was not easy
// setFreeStyleCropEnabled(true)
// Color used for toolbar icon and text
- setToolbarColor(ThemeUtils.getColor(context, R.attr.riotx_background))
- setToolbarWidgetColor(ThemeUtils.getColor(context, R.attr.vctr_toolbar_primary_text_color))
+ setToolbarColor(colorProvider.getColorFromAttribute(R.attr.riotx_background))
+ setToolbarWidgetColor(colorProvider.getColorFromAttribute(R.attr.vctr_toolbar_primary_text_color))
// Background
- setRootViewBackgroundColor(ThemeUtils.getColor(context, R.attr.riotx_background))
+ setRootViewBackgroundColor(colorProvider.getColorFromAttribute(R.attr.riotx_background))
// Status bar color (pb in dark mode, icon of the status bar are dark)
- setStatusBarColor(ThemeUtils.getColor(context, R.attr.riotx_header_panel_background))
+ setStatusBarColor(colorProvider.getColorFromAttribute(R.attr.riotx_header_panel_background))
// Known issue: there is still orange color used by the lib
// https://github.com/Yalantis/uCrop/issues/602
- setActiveControlsWidgetColor(ContextCompat.getColor(context, R.color.riotx_accent))
+ setActiveControlsWidgetColor(colorProvider.getColor(R.color.riotx_accent))
// Hide the logo (does not work)
setLogoColor(Color.TRANSPARENT)
}
diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt
index e26736de7e..3f38e4ef15 100755
--- a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt
@@ -69,6 +69,9 @@ class BugReportActivity : VectorBaseActivity() {
bug_report_button_include_crash_logs.isChecked = false
bug_report_button_include_crash_logs.isVisible = false
+ bug_report_button_include_key_share_history.isChecked = false
+ bug_report_button_include_key_share_history.isVisible = false
+
// Keep the screenshot
} else {
supportActionBar?.setTitle(R.string.title_activity_bug_report)
@@ -121,6 +124,7 @@ class BugReportActivity : VectorBaseActivity() {
forSuggestion,
bug_report_button_include_logs.isChecked,
bug_report_button_include_crash_logs.isChecked,
+ bug_report_button_include_key_share_history.isChecked,
bug_report_button_include_screenshot.isChecked,
bug_report_edit_text.text.toString(),
object : BugReporter.IMXBugReportListener {
diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt
index e7382a17e9..96248187aa 100755
--- a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt
+++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt
@@ -33,6 +33,7 @@ import im.vector.app.core.extensions.getAllChildFragments
import im.vector.app.core.extensions.toOnOff
import im.vector.app.features.settings.VectorLocale
import im.vector.app.features.settings.VectorPreferences
+import im.vector.app.features.settings.devtools.GossipingEventsSerializer
import im.vector.app.features.settings.locale.SystemLocaleProvider
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.version.VersionProvider
@@ -74,6 +75,7 @@ class BugReporter @Inject constructor(
private const val LOG_CAT_FILENAME = "logcat.log"
private const val LOG_CAT_SCREENSHOT_FILENAME = "screenshot.png"
private const val CRASH_FILENAME = "crash.log"
+ private const val KEY_REQUESTS_FILENAME = "keyRequests.log"
private const val BUFFER_SIZE = 1024 * 1024 * 50
}
@@ -143,6 +145,7 @@ class BugReporter @Inject constructor(
* @param forSuggestion true to send a suggestion
* @param withDevicesLogs true to include the device log
* @param withCrashLogs true to include the crash logs
+ * @param withKeyRequestHistory true to include the crash logs
* @param withScreenshot true to include the screenshot
* @param theBugDescription the bug description
* @param listener the listener
@@ -152,6 +155,7 @@ class BugReporter @Inject constructor(
forSuggestion: Boolean,
withDevicesLogs: Boolean,
withCrashLogs: Boolean,
+ withKeyRequestHistory: Boolean,
withScreenshot: Boolean,
theBugDescription: String,
listener: IMXBugReportListener?) {
@@ -207,6 +211,22 @@ class BugReporter @Inject constructor(
}
}
+ activeSessionHolder.getSafeActiveSession()
+ ?.takeIf { !mIsCancelled && withKeyRequestHistory }
+ ?.cryptoService()
+ ?.getGossipingEvents()
+ ?.let { GossipingEventsSerializer().serialize(it) }
+ ?.toByteArray()
+ ?.let { rawByteArray ->
+ File(context.cacheDir.absolutePath, KEY_REQUESTS_FILENAME)
+ .also {
+ it.outputStream()
+ .use { os -> os.write(rawByteArray) }
+ }
+ }
+ ?.let { compressFile(it) }
+ ?.let { gzippedFiles.add(it) }
+
var deviceId = "undefined"
var userId = "undefined"
var olmVersion = "undefined"
@@ -426,6 +446,10 @@ class BugReporter @Inject constructor(
*/
fun openBugReportScreen(activity: FragmentActivity, forSuggestion: Boolean = false) {
screenshot = takeScreenshot(activity)
+ activeSessionHolder.getSafeActiveSession()?.let {
+ it.logDbUsageInfo()
+ it.cryptoService().logDbUsageInfo()
+ }
val intent = Intent(activity, BugReportActivity::class.java)
intent.putExtra("FOR_SUGGESTION", forSuggestion)
diff --git a/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt b/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt
index f26ffdd700..a279c16cb1 100644
--- a/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt
+++ b/vector/src/main/java/im/vector/app/features/rageshake/VectorFileLogger.kt
@@ -102,8 +102,8 @@ class VectorFileLogger @Inject constructor(val context: Context, private val vec
return if (vectorPreferences.labAllowedExtendedLogging()) {
false
} else {
- // Exclude debug and verbose logs
- priority <= Log.DEBUG
+ // Exclude verbose logs
+ priority < Log.DEBUG
}
}
diff --git a/vector/src/main/java/im/vector/app/features/reactions/widget/ReactionButton.kt b/vector/src/main/java/im/vector/app/features/reactions/widget/ReactionButton.kt
index 364ac999c1..331c3e81da 100644
--- a/vector/src/main/java/im/vector/app/features/reactions/widget/ReactionButton.kt
+++ b/vector/src/main/java/im/vector/app/features/reactions/widget/ReactionButton.kt
@@ -112,8 +112,8 @@ class ReactionButton @JvmOverloads constructor(context: Context,
// emojiView?.typeface = this.emojiTypeFace ?: Typeface.DEFAULT
context.withStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr) {
- onDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape)
- offDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape_off)
+ onDrawable = ContextCompat.getDrawable(context, R.drawable.reaction_rounded_rect_shape)
+ offDrawable = ContextCompat.getDrawable(context, R.drawable.reaction_rounded_rect_shape_off)
circleStartColor = getColor(R.styleable.ReactionButton_circle_start_color, 0)
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt
index 6be810d0be..9dc41cbc21 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt
@@ -20,6 +20,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.airbnb.mvrx.viewModel
+import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.addFragment
@@ -58,19 +59,19 @@ class RoomDirectoryActivity : VectorBaseActivity() {
.subscribe { sharedAction ->
when (sharedAction) {
is RoomDirectorySharedAction.Back -> onBackPressed()
- is RoomDirectorySharedAction.CreateRoom ->
+ is RoomDirectorySharedAction.CreateRoom -> {
addFragmentToBackstack(R.id.simpleFragmentContainer, CreateRoomFragment::class.java)
+ // Transmit the filter to the createRoomViewModel
+ withState(roomDirectoryViewModel) {
+ createRoomViewModel.handle(CreateRoomAction.SetName(it.currentFilter))
+ }
+ }
is RoomDirectorySharedAction.ChangeProtocol ->
addFragmentToBackstack(R.id.simpleFragmentContainer, RoomDirectoryPickerFragment::class.java)
is RoomDirectorySharedAction.Close -> finish()
}
}
.disposeOnDestroy()
-
- roomDirectoryViewModel.selectSubscribe(this, PublicRoomsViewState::currentFilter) { currentFilter ->
- // Transmit the filter to the createRoomViewModel
- createRoomViewModel.handle(CreateRoomAction.SetName(currentFilter))
- }
}
override fun initUiAndData() {
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt
index 3b687395fd..4b3eacffaa 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt
@@ -16,12 +16,17 @@
package im.vector.app.features.roomdirectory.createroom
+import android.net.Uri
import im.vector.app.core.platform.VectorViewModelAction
sealed class CreateRoomAction : VectorViewModelAction {
+ data class SetAvatar(val imageUri: Uri?) : CreateRoomAction()
data class SetName(val name: String) : CreateRoomAction()
+ data class SetTopic(val topic: String) : CreateRoomAction()
data class SetIsPublic(val isPublic: Boolean) : CreateRoomAction()
data class SetIsInRoomDirectory(val isInRoomDirectory: Boolean) : CreateRoomAction()
data class SetIsEncrypted(val isEncrypted: Boolean) : CreateRoomAction()
+
object Create : CreateRoomAction()
+ object Reset : CreateRoomAction()
}
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt
index ba7c5ca083..d1cc884336 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt
@@ -26,7 +26,10 @@ import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
+import im.vector.app.features.discovery.settingsSectionTitleItem
import im.vector.app.features.form.formEditTextItem
+import im.vector.app.features.form.formEditableAvatarItem
+import im.vector.app.features.form.formSubmitButtonItem
import im.vector.app.features.form.formSwitchItem
import javax.inject.Inject
@@ -67,6 +70,17 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
}
private fun buildForm(viewState: CreateRoomViewState, enableFormElement: Boolean) {
+ formEditableAvatarItem {
+ id("avatar")
+ enabled(enableFormElement)
+ imageUri(viewState.avatarUri)
+ clickListener { listener?.onAvatarChange() }
+ deleteListener { listener?.onAvatarDelete() }
+ }
+ settingsSectionTitleItem {
+ id("nameSection")
+ titleResId(R.string.create_room_name_section)
+ }
formEditTextItem {
id("name")
enabled(enableFormElement)
@@ -77,6 +91,24 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
listener?.onNameChange(text)
}
}
+ settingsSectionTitleItem {
+ id("topicSection")
+ titleResId(R.string.create_room_topic_section)
+ }
+ formEditTextItem {
+ id("topic")
+ enabled(enableFormElement)
+ value(viewState.roomTopic)
+ hint(stringProvider.getString(R.string.create_room_topic_hint))
+
+ onTextChange { text ->
+ listener?.onTopicChange(text)
+ }
+ }
+ settingsSectionTitleItem {
+ id("settingsSection")
+ titleResId(R.string.create_room_settings_section)
+ }
formSwitchItem {
id("public")
enabled(enableFormElement)
@@ -116,13 +148,23 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
listener?.setIsEncrypted(value)
}
}
+ formSubmitButtonItem {
+ id("submit")
+ enabled(enableFormElement)
+ buttonTitleId(R.string.create_room_action_create)
+ buttonClickListener { listener?.submit() }
+ }
}
interface Listener {
+ fun onAvatarDelete()
+ fun onAvatarChange()
fun onNameChange(newName: String)
+ fun onTopicChange(newTopic: String)
fun setIsPublic(isPublic: Boolean)
fun setIsInRoomDirectory(isInRoomDirectory: Boolean)
fun setIsEncrypted(isEncrypted: Boolean)
fun retry()
+ fun submit()
}
}
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt
index f3be178e64..88b8a65a1c 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt
@@ -16,30 +16,41 @@
package im.vector.app.features.roomdirectory.createroom
+import android.net.Uri
import android.os.Bundle
-import android.view.MenuItem
import android.view.View
+import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
+import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
+import im.vector.app.core.extensions.exhaustive
+import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.core.resources.ColorProvider
import im.vector.app.features.roomdirectory.RoomDirectorySharedAction
import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel
import kotlinx.android.synthetic.main.fragment_create_room.*
import timber.log.Timber
import javax.inject.Inject
-class CreateRoomFragment @Inject constructor(private val createRoomController: CreateRoomController) : VectorBaseFragment(), CreateRoomController.Listener {
+class CreateRoomFragment @Inject constructor(
+ private val createRoomController: CreateRoomController,
+ colorProvider: ColorProvider
+) : VectorBaseFragment(),
+ CreateRoomController.Listener,
+ GalleryOrCameraDialogHelper.Listener,
+ OnBackPressed {
private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel
private val viewModel: CreateRoomViewModel by activityViewModel()
- override fun getLayoutResId() = R.layout.fragment_create_room
+ private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider)
- override fun getMenuRes() = R.menu.vector_room_creation
+ override fun getLayoutResId() = R.layout.fragment_create_room
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -49,6 +60,11 @@ class CreateRoomFragment @Inject constructor(private val createRoomController: C
createRoomClose.debouncedClicks {
sharedActionViewModel.post(RoomDirectorySharedAction.Back)
}
+ viewModel.observeViewEvents {
+ when (it) {
+ CreateRoomViewEvents.Quit -> vectorBaseActivity.onBackPressed()
+ }.exhaustive
+ }
}
override fun onDestroyView() {
@@ -57,26 +73,31 @@ class CreateRoomFragment @Inject constructor(private val createRoomController: C
super.onDestroyView()
}
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
- R.id.action_create_room -> {
- viewModel.handle(CreateRoomAction.Create)
- true
- }
- else ->
- super.onOptionsItemSelected(item)
- }
- }
-
private fun setupRecyclerView() {
createRoomForm.configureWith(createRoomController)
createRoomController.listener = this
}
+ override fun onAvatarDelete() {
+ viewModel.handle(CreateRoomAction.SetAvatar(null))
+ }
+
+ override fun onAvatarChange() {
+ galleryOrCameraDialogHelper.show()
+ }
+
+ override fun onImageReady(uri: Uri?) {
+ viewModel.handle(CreateRoomAction.SetAvatar(uri))
+ }
+
override fun onNameChange(newName: String) {
viewModel.handle(CreateRoomAction.SetName(newName))
}
+ override fun onTopicChange(newTopic: String) {
+ viewModel.handle(CreateRoomAction.SetTopic(newTopic))
+ }
+
override fun setIsPublic(isPublic: Boolean) {
viewModel.handle(CreateRoomAction.SetIsPublic(isPublic))
}
@@ -89,11 +110,33 @@ class CreateRoomFragment @Inject constructor(private val createRoomController: C
viewModel.handle(CreateRoomAction.SetIsEncrypted(isEncrypted))
}
+ override fun submit() {
+ viewModel.handle(CreateRoomAction.Create)
+ }
+
override fun retry() {
Timber.v("Retry")
viewModel.handle(CreateRoomAction.Create)
}
+ override fun onBackPressed(toolbarButton: Boolean): Boolean {
+ return withState(viewModel) {
+ return@withState if (!it.isEmpty()) {
+ AlertDialog.Builder(requireContext())
+ .setTitle(R.string.dialog_title_warning)
+ .setMessage(R.string.warning_room_not_created_yet)
+ .setPositiveButton(R.string.yes) { _, _ ->
+ viewModel.handle(CreateRoomAction.Reset)
+ }
+ .setNegativeButton(R.string.no, null)
+ .show()
+ true
+ } else {
+ false
+ }
+ }
+ }
+
override fun invalidate() = withState(viewModel) { state ->
val async = state.asyncCreateRoomRequest
if (async is Success) {
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewEvents.kt
new file mode 100644
index 0000000000..4ff4ee4bdf
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewEvents.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.roomdirectory.createroom
+
+import im.vector.app.core.platform.VectorViewEvents
+
+/**
+ * Transient events for room creation screen
+ */
+sealed class CreateRoomViewEvents : VectorViewEvents {
+ object Quit : CreateRoomViewEvents()
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt
index c213992258..57af95b107 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt
@@ -16,6 +16,7 @@
package im.vector.app.features.roomdirectory.createroom
+import androidx.core.net.toFile
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
@@ -26,7 +27,7 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
-import im.vector.app.core.platform.EmptyViewEvents
+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
@@ -44,7 +45,7 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: CreateRoomViewState,
private val session: Session,
private val rawService: RawService
-) : VectorViewModel(initialState) {
+) : VectorViewModel(initialState) {
@AssistedInject.Factory
interface Factory {
@@ -90,16 +91,37 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
override fun handle(action: CreateRoomAction) {
when (action) {
+ is CreateRoomAction.SetAvatar -> setAvatar(action)
is CreateRoomAction.SetName -> setName(action)
+ is CreateRoomAction.SetTopic -> setTopic(action)
is CreateRoomAction.SetIsPublic -> setIsPublic(action)
is CreateRoomAction.SetIsInRoomDirectory -> setIsInRoomDirectory(action)
is CreateRoomAction.SetIsEncrypted -> setIsEncrypted(action)
is CreateRoomAction.Create -> doCreateRoom()
- }
+ CreateRoomAction.Reset -> doReset()
+ }.exhaustive
}
+ private fun doReset() {
+ setState {
+ // Delete temporary file with the avatar
+ avatarUri?.let { tryOrNull { it.toFile().delete() } }
+
+ CreateRoomViewState(
+ isEncrypted = adminE2EByDefault,
+ hsAdminHasDisabledE2E = !adminE2EByDefault
+ )
+ }
+
+ _viewEvents.post(CreateRoomViewEvents.Quit)
+ }
+
+ private fun setAvatar(action: CreateRoomAction.SetAvatar) = setState { copy(avatarUri = action.imageUri) }
+
private fun setName(action: CreateRoomAction.SetName) = setState { copy(roomName = action.name) }
+ private fun setTopic(action: CreateRoomAction.SetTopic) = setState { copy(roomTopic = action.topic) }
+
private fun setIsPublic(action: CreateRoomAction.SetIsPublic) = setState {
copy(
isPublic = action.isPublic,
@@ -123,6 +145,8 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
val createRoomParams = CreateRoomParams()
.apply {
name = state.roomName.takeIf { it.isNotBlank() }
+ topic = state.roomTopic.takeIf { it.isNotBlank() }
+ avatarUri = state.avatarUri
// Directory visibility
visibility = if (state.isInRoomDirectory) RoomDirectoryVisibility.PUBLIC else RoomDirectoryVisibility.PRIVATE
// Public room
diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt
index a49473b16e..433cc02cc9 100644
--- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt
@@ -16,15 +16,24 @@
package im.vector.app.features.roomdirectory.createroom
+import android.net.Uri
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
data class CreateRoomViewState(
+ val avatarUri: Uri? = null,
val roomName: String = "",
+ val roomTopic: String = "",
val isPublic: Boolean = false,
val isInRoomDirectory: Boolean = false,
val isEncrypted: Boolean = false,
val hsAdminHasDisabledE2E: Boolean = false,
val asyncCreateRoomRequest: Async = Uninitialized
-) : MvRxState
+) : MvRxState {
+
+ /**
+ * Return true if there is not important input from user
+ */
+ fun isEmpty() = avatarUri == null && roomName.isEmpty() && roomTopic.isEmpty()
+}
diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt
index a3ffd80ade..2e91091443 100644
--- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt
+++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt
@@ -45,6 +45,7 @@ class RoomMemberProfileController @Inject constructor(
fun onTapVerify()
fun onShowDeviceList()
fun onShowDeviceListNoCrossSigning()
+ fun onOpenDmClicked()
fun onJumpToReadReceiptClicked()
fun onMentionClicked()
fun onEditPowerLevel(currentRole: Role)
@@ -173,6 +174,14 @@ class RoomMemberProfileController @Inject constructor(
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
+ buildProfileAction(
+ id = "direct",
+ editable = false,
+ title = stringProvider.getString(R.string.room_member_open_or_create_dm),
+ dividerColor = dividerColor,
+ action = { callback?.onOpenDmClicked() }
+ )
+
if (state.hasReadReceipt) {
buildProfileAction(
id = "read_receipt",
diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt
index 2f5b2d5387..d60b5580fa 100644
--- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt
@@ -278,6 +278,11 @@ class RoomMemberProfileFragment @Inject constructor(
DeviceListBottomSheet.newInstance(it.userId).show(parentFragmentManager, "DEV_LIST")
}
+ override fun onOpenDmClicked() {
+ roomDetailPendingActionStore.data = RoomDetailPendingAction.OpenOrCreateDm(fragmentArgs.userId)
+ vectorBaseActivity.finish()
+ }
+
override fun onJumpToReadReceiptClicked() {
roomDetailPendingActionStore.data = RoomDetailPendingAction.JumpToReadReceipt(fragmentArgs.userId)
vectorBaseActivity.finish()
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt
index ece221d884..85bc8773a5 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt
@@ -17,14 +17,12 @@
package im.vector.app.features.roomprofile
-import android.net.Uri
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
sealed class RoomProfileAction : VectorViewModelAction {
object LeaveRoom : RoomProfileAction()
data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction()
- data class ChangeRoomAvatar(val uri: Uri, val fileName: String?) : RoomProfileAction()
object ShareRoomProfile : RoomProfileAction()
object CreateShortcut : RoomProfileAction()
}
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 c2f25c08d3..5bd121d49b 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
@@ -17,24 +17,18 @@
package im.vector.app.features.roomprofile
-import android.app.Activity
-import android.content.Intent
-import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.view.MenuItem
import android.view.View
-import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.pm.ShortcutManagerCompat
-import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
-import com.yalantis.ucrop.UCrop
import im.vector.app.R
import im.vector.app.core.animations.AppBarStateChangeListener
import im.vector.app.core.animations.MatrixItemAppBarStateChangeListener
@@ -42,14 +36,9 @@ import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.copyOnLongClick
import im.vector.app.core.extensions.exhaustive
-import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.setTextOrHide
-import im.vector.app.core.intent.getFilenameFromUri
import im.vector.app.core.platform.VectorBaseFragment
-import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
-import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.copyToClipboard
-import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.features.crypto.util.toImageRes
import im.vector.app.features.home.AvatarRenderer
@@ -58,9 +47,6 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomS
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.app.features.media.BigImageViewerActivity
-import im.vector.app.features.media.createUCropWithDefaultSettings
-import im.vector.lib.multipicker.MultiPicker
-import im.vector.lib.multipicker.entity.MultiPickerImageType
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_matrix_profile.*
import kotlinx.android.synthetic.main.view_stub_room_profile_header.*
@@ -68,7 +54,6 @@ import org.matrix.android.sdk.api.session.room.notification.RoomNotificationStat
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import timber.log.Timber
-import java.io.File
import javax.inject.Inject
@Parcelize
@@ -80,7 +65,8 @@ class RoomProfileFragment @Inject constructor(
private val roomProfileController: RoomProfileController,
private val avatarRenderer: AvatarRenderer,
val roomProfileViewModelFactory: RoomProfileViewModel.Factory
-) : VectorBaseFragment(), RoomProfileController.Callback {
+) : VectorBaseFragment(),
+ RoomProfileController.Callback {
private val roomProfileArgs: RoomProfileArgs by args()
private lateinit var roomListQuickActionsSharedActionViewModel: RoomListQuickActionsSharedActionViewModel
@@ -112,11 +98,10 @@ class RoomProfileFragment @Inject constructor(
matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener)
roomProfileViewModel.observeViewEvents {
when (it) {
- is RoomProfileViewEvents.Loading -> showLoading(it.message)
- is RoomProfileViewEvents.Failure -> showFailure(it.throwable)
- is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink)
- RoomProfileViewEvents.OnChangeAvatarSuccess -> dismissLoadingDialog()
- is RoomProfileViewEvents.OnShortcutReady -> addShortcut(it)
+ is RoomProfileViewEvents.Loading -> showLoading(it.message)
+ is RoomProfileViewEvents.Failure -> showFailure(it.throwable)
+ is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink)
+ is RoomProfileViewEvents.OnShortcutReady -> addShortcut(it)
}.exhaustive
}
roomListQuickActionsSharedActionViewModel
@@ -157,14 +142,6 @@ class RoomProfileFragment @Inject constructor(
else -> Timber.v("$action not handled")
}
- private fun onLeaveRoom() {
- vectorBaseActivity.finish()
- }
-
- private fun showError(throwable: Throwable) {
- showErrorInSnackbar(throwable)
- }
-
private fun setupRecyclerView() {
roomProfileController.callback = this
matrixProfileRecyclerView.configureWith(roomProfileController, hasFixedSize = true, disableItemAnimation = true)
@@ -267,98 +244,12 @@ class RoomProfileFragment @Inject constructor(
}
private fun onAvatarClicked(view: View, matrixItem: MatrixItem.RoomItem) = withState(roomProfileViewModel) {
- if (matrixItem.avatarUrl?.isNotEmpty() == true) {
- val intent = BigImageViewerActivity.newIntent(requireContext(), matrixItem.getBestName(), matrixItem.avatarUrl!!, it.canChangeAvatar)
- val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, ViewCompat.getTransitionName(view) ?: "")
- bigImageStartForActivityResult.launch(intent, options)
- } else if (it.canChangeAvatar) {
- showAvatarSelector()
- }
- }
-
- private fun showAvatarSelector() {
- AlertDialog.Builder(requireContext())
- .setItems(arrayOf(
- getString(R.string.attachment_type_camera),
- getString(R.string.attachment_type_gallery)
- )) { dialog, which ->
- dialog.cancel()
- onAvatarTypeSelected(isCamera = (which == 0))
+ matrixItem.avatarUrl
+ ?.takeIf { it.isNotEmpty() }
+ ?.let { avatarUrl ->
+ val intent = BigImageViewerActivity.newIntent(requireContext(), matrixItem.getBestName(), avatarUrl)
+ val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, ViewCompat.getTransitionName(view) ?: "")
+ startActivity(intent, options.toBundle())
}
- .show()
- }
-
- private val takePhotoPermissionActivityResultLauncher = registerForPermissionsResult { allGranted ->
- if (allGranted) {
- onAvatarTypeSelected(true)
- }
- }
-
- private var avatarCameraUri: Uri? = null
- private fun onAvatarTypeSelected(isCamera: Boolean) {
- if (isCamera) {
- if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), takePhotoPermissionActivityResultLauncher)) {
- avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(requireActivity(), takePhotoActivityResultLauncher)
- }
- } else {
- MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher)
- }
- }
-
- private fun onRoomAvatarSelected(image: MultiPickerImageType) {
- val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")
- val uri = image.contentUri
- createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), image.displayName)
- .apply { withAspectRatio(1f, 1f) }
- .start(requireContext(), this)
- }
-
- private val takePhotoActivityResultLauncher = registerStartForActivityResult { activityResult ->
- if (activityResult.resultCode == Activity.RESULT_OK) {
- avatarCameraUri?.let { uri ->
- MultiPicker.get(MultiPicker.CAMERA)
- .getTakenPhoto(requireContext(), uri)
- ?.let {
- onRoomAvatarSelected(it)
- }
- }
- }
- }
-
- private val pickImageActivityResultLauncher = registerStartForActivityResult { activityResult ->
- if (activityResult.resultCode == Activity.RESULT_OK) {
- MultiPicker
- .get(MultiPicker.IMAGE)
- .getSelectedFiles(requireContext(), activityResult.data)
- .firstOrNull()?.let {
- onRoomAvatarSelected(it)
- }
- }
- }
-
- private val bigImageStartForActivityResult = registerStartForActivityResult { activityResult ->
- if (activityResult.resultCode == Activity.RESULT_OK) {
- activityResult.data?.let { onAvatarCropped(it.data) }
- }
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- // TODO handle this one (Ucrop lib)
- @Suppress("DEPRECATION")
- super.onActivityResult(requestCode, resultCode, data)
-
- if (resultCode == Activity.RESULT_OK) {
- when (requestCode) {
- UCrop.REQUEST_CROP -> data?.let { onAvatarCropped(UCrop.getOutput(it)) }
- }
- }
- }
-
- private fun onAvatarCropped(uri: Uri?) {
- if (uri != null) {
- roomProfileViewModel.handle(RoomProfileAction.ChangeRoomAvatar(uri, getFilenameFromUri(context, uri)))
- } else {
- Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show()
- }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt
index 380efd6fcd..237df0bed5 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt
@@ -26,7 +26,6 @@ sealed class RoomProfileViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : RoomProfileViewEvents()
data class Failure(val throwable: Throwable) : RoomProfileViewEvents()
- object OnChangeAvatarSuccess : RoomProfileViewEvents()
data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents()
data class OnShortcutReady(val shortcutInfo: ShortcutInfoCompat) : RoomProfileViewEvents()
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt
index 922dd995e9..e927ec9876 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt
@@ -28,18 +28,15 @@ 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.home.ShortcutCreator
-import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.Session
-import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
-import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
+import org.matrix.android.sdk.rx.RxRoom
import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
-import java.util.UUID
class RoomProfileViewModel @AssistedInject constructor(
@Assisted private val initialState: RoomProfileViewState,
@@ -65,33 +62,23 @@ class RoomProfileViewModel @AssistedInject constructor(
private val room = session.getRoom(initialState.roomId)!!
init {
- observeRoomSummary()
+ val rxRoom = room.rx()
+ observeRoomSummary(rxRoom)
+ observeBannedRoomMembers(rxRoom)
}
- private fun observeRoomSummary() {
- val rxRoom = room.rx()
+ private fun observeRoomSummary(rxRoom: RxRoom) {
rxRoom.liveRoomSummary()
.unwrap()
.execute {
copy(roomSummary = it)
}
+ }
- val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable()
-
- powerLevelsContentLive
- .subscribe {
- val powerLevelsHelper = PowerLevelsHelper(it)
- setState {
- copy(canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR))
- }
- }
- .disposeOnClear()
-
+ private fun observeBannedRoomMembers(rxRoom: RxRoom) {
rxRoom.liveRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.BAN) })
.execute {
- copy(
- bannedMembership = it
- )
+ copy(bannedMembership = it)
}
}
@@ -100,7 +87,6 @@ class RoomProfileViewModel @AssistedInject constructor(
RoomProfileAction.LeaveRoom -> handleLeaveRoom()
is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile()
- is RoomProfileAction.ChangeRoomAvatar -> handleChangeAvatar(action)
RoomProfileAction.CreateShortcut -> handleCreateShortcut()
}.exhaustive
}
@@ -142,18 +128,4 @@ class RoomProfileViewModel @AssistedInject constructor(
_viewEvents.post(RoomProfileViewEvents.ShareRoomProfile(permalink))
}
}
-
- private fun handleChangeAvatar(action: RoomProfileAction.ChangeRoomAvatar) {
- _viewEvents.post(RoomProfileViewEvents.Loading())
- room.rx().updateAvatar(action.uri, action.fileName ?: UUID.randomUUID().toString())
- .subscribe(
- {
- _viewEvents.post(RoomProfileViewEvents.OnChangeAvatarSuccess)
- },
- {
- _viewEvents.post(RoomProfileViewEvents.Failure(it))
- }
- )
- .disposeOnClear()
- }
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt
index 10d35db36e..50723655bc 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewState.kt
@@ -26,8 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
data class RoomProfileViewState(
val roomId: String,
val roomSummary: Async = Uninitialized,
- val bannedMembership: Async> = Uninitialized,
- val canChangeAvatar: Boolean = false
+ val bannedMembership: Async> = Uninitialized
) : MvRxState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt
index 5d35586cce..80bb8813cf 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsAction.kt
@@ -20,10 +20,12 @@ import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
sealed class RoomSettingsAction : VectorViewModelAction {
+ data class SetAvatarAction(val avatarAction: RoomSettingsViewState.AvatarAction) : RoomSettingsAction()
data class SetRoomName(val newName: String) : RoomSettingsAction()
data class SetRoomTopic(val newTopic: String) : RoomSettingsAction()
data class SetRoomHistoryVisibility(val visibility: RoomHistoryVisibility) : RoomSettingsAction()
data class SetRoomCanonicalAlias(val newCanonicalAlias: String) : RoomSettingsAction()
object EnableEncryption : RoomSettingsAction()
object Save : RoomSettingsAction()
+ object Cancel : RoomSettingsAction()
}
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 f680e28aa8..5231cc6b06 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
@@ -23,20 +23,27 @@ import im.vector.app.core.epoxy.profiles.buildProfileSection
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.form.formEditTextItem
+import im.vector.app.features.form.formEditableAvatarItem
+import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter
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.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class RoomSettingsController @Inject constructor(
private val stringProvider: StringProvider,
+ private val avatarRenderer: AvatarRenderer,
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
colorProvider: ColorProvider
) : TypedEpoxyController() {
interface Callback {
+ // Delete the avatar, or cancel an avatar change
+ fun onAvatarDelete()
+ fun onAvatarChange()
fun onEnableEncryptionClicked()
fun onNameChanged(name: String)
fun onTopicChanged(topic: String)
@@ -58,6 +65,25 @@ class RoomSettingsController @Inject constructor(
val historyVisibility = data.historyVisibilityEvent?.let { formatRoomHistoryVisibilityEvent(it) } ?: ""
val newHistoryVisibility = data.newHistoryVisibility?.let { roomHistoryVisibilityFormatter.format(it) }
+ formEditableAvatarItem {
+ id("avatar")
+ enabled(data.actionPermissions.canChangeAvatar)
+ when (val avatarAction = data.avatarAction) {
+ RoomSettingsViewState.AvatarAction.None -> {
+ // Use the current value
+ avatarRenderer(avatarRenderer)
+ // We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar.
+ matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl))
+ }
+ RoomSettingsViewState.AvatarAction.DeleteAvatar ->
+ imageUri(null)
+ is RoomSettingsViewState.AvatarAction.UpdateAvatar ->
+ imageUri(avatarAction.newAvatarUri)
+ }
+ clickListener { callback?.onAvatarChange() }
+ deleteListener { callback?.onAvatarDelete() }
+ }
+
buildProfileSection(
stringProvider.getString(R.string.settings)
)
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 68c631b391..57521f7d80 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
@@ -16,6 +16,7 @@
package im.vector.app.features.roomprofile.settings
+import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
@@ -26,10 +27,14 @@ import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
+import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.exhaustive
+import im.vector.app.core.intent.getFilenameFromUri
+import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.toast
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter
@@ -40,17 +45,24 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.util.toMatrixItem
+import java.util.UUID
import javax.inject.Inject
class RoomSettingsFragment @Inject constructor(
val viewModelFactory: RoomSettingsViewModel.Factory,
private val controller: RoomSettingsController,
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
+ colorProvider: ColorProvider,
private val avatarRenderer: AvatarRenderer
-) : VectorBaseFragment(), RoomSettingsController.Callback {
+) :
+ VectorBaseFragment(),
+ RoomSettingsController.Callback,
+ OnBackPressed,
+ GalleryOrCameraDialogHelper.Listener {
private val viewModel: RoomSettingsViewModel by fragmentViewModel()
private val roomProfileArgs: RoomProfileArgs by args()
+ private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider)
override fun getLayoutResId() = R.layout.fragment_room_setting_generic
@@ -67,7 +79,11 @@ class RoomSettingsFragment @Inject constructor(
viewModel.observeViewEvents {
when (it) {
is RoomSettingsViewEvents.Failure -> showFailure(it.throwable)
- is RoomSettingsViewEvents.Success -> showSuccess()
+ RoomSettingsViewEvents.Success -> showSuccess()
+ RoomSettingsViewEvents.GoBack -> {
+ ignoreChanges = true
+ vectorBaseActivity.onBackPressed()
+ }
}.exhaustive
}
}
@@ -161,4 +177,59 @@ class RoomSettingsFragment @Inject constructor(
override fun onAliasChanged(alias: String) {
viewModel.handle(RoomSettingsAction.SetRoomCanonicalAlias(alias))
}
+
+ override fun onImageReady(uri: Uri?) {
+ uri ?: return
+ viewModel.handle(
+ RoomSettingsAction.SetAvatarAction(
+ RoomSettingsViewState.AvatarAction.UpdateAvatar(
+ newAvatarUri = uri,
+ newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString())
+ )
+ )
+ }
+
+ override fun onAvatarDelete() {
+ withState(viewModel) {
+ when (it.avatarAction) {
+ RoomSettingsViewState.AvatarAction.None -> {
+ viewModel.handle(RoomSettingsAction.SetAvatarAction(RoomSettingsViewState.AvatarAction.DeleteAvatar))
+ }
+ RoomSettingsViewState.AvatarAction.DeleteAvatar -> {
+ /* Should not happen */
+ Unit
+ }
+ is RoomSettingsViewState.AvatarAction.UpdateAvatar -> {
+ // Cancel the update of the avatar
+ viewModel.handle(RoomSettingsAction.SetAvatarAction(RoomSettingsViewState.AvatarAction.None))
+ }
+ }
+ }
+ }
+
+ override fun onAvatarChange() {
+ galleryOrCameraDialogHelper.show()
+ }
+
+ private var ignoreChanges = false
+
+ override fun onBackPressed(toolbarButton: Boolean): Boolean {
+ if (ignoreChanges) return false
+
+ return withState(viewModel) {
+ return@withState if (it.showSaveAction) {
+ AlertDialog.Builder(requireContext())
+ .setTitle(R.string.dialog_title_warning)
+ .setMessage(R.string.warning_unsaved_change)
+ .setPositiveButton(R.string.warning_unsaved_change_discard) { _, _ ->
+ viewModel.handle(RoomSettingsAction.Cancel)
+ }
+ .setNegativeButton(R.string.cancel, null)
+ .show()
+ true
+ } else {
+ false
+ }
+ }
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewEvents.kt
index 952ca791c9..83a768fb34 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewEvents.kt
@@ -25,4 +25,5 @@ import im.vector.app.core.platform.VectorViewEvents
sealed class RoomSettingsViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : RoomSettingsViewEvents()
object Success : RoomSettingsViewEvents()
+ object GoBack : RoomSettingsViewEvents()
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt
index 0fdb6139c2..32d8f043c3 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt
@@ -16,6 +16,7 @@
package im.vector.app.features.roomprofile.settings
+import androidx.core.net.toFile
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
@@ -27,9 +28,14 @@ import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import io.reactivex.Completable
import io.reactivex.Observable
import org.matrix.android.sdk.api.MatrixCallback
+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.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
+import org.matrix.android.sdk.rx.mapOptional
import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
@@ -55,16 +61,19 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
init {
observeRoomSummary()
+ observeRoomAvatar()
observeState()
}
private fun observeState() {
selectSubscribe(
+ RoomSettingsViewState::avatarAction,
RoomSettingsViewState::newName,
RoomSettingsViewState::newCanonicalAlias,
RoomSettingsViewState::newTopic,
RoomSettingsViewState::newHistoryVisibility,
- RoomSettingsViewState::roomSummary) { newName,
+ RoomSettingsViewState::roomSummary) { avatarAction,
+ newName,
newCanonicalAlias,
newTopic,
newHistoryVisibility,
@@ -72,7 +81,8 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
val summary = asyncSummary()
setState {
copy(
- showSaveAction = summary?.name != newName
+ showSaveAction = avatarAction !is RoomSettingsViewState.AvatarAction.None
+ || summary?.name != newName
|| summary?.topic != newTopic
|| summary?.canonicalAlias != newCanonicalAlias?.takeIf { it.isNotEmpty() }
|| newHistoryVisibility != null
@@ -101,6 +111,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
.subscribe {
val powerLevelsHelper = PowerLevelsHelper(it)
val permissions = RoomSettingsViewState.ActionPermissions(
+ canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR),
canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME),
canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC),
canChangeCanonicalAlias = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
@@ -114,17 +125,52 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
.disposeOnClear()
}
+ /**
+ * We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar.
+ */
+ private fun observeRoomAvatar() {
+ room.rx()
+ .liveStateEvent(EventType.STATE_ROOM_AVATAR, QueryStringValue.NoCondition)
+ .mapOptional { it.content.toModel() }
+ .unwrap()
+ .subscribe {
+ setState { copy(currentRoomAvatarUrl = it.avatarUrl) }
+ }
+ .disposeOnClear()
+ }
+
override fun handle(action: RoomSettingsAction) {
when (action) {
is RoomSettingsAction.EnableEncryption -> handleEnableEncryption()
+ is RoomSettingsAction.SetAvatarAction -> handleSetAvatarAction(action)
is RoomSettingsAction.SetRoomName -> setState { copy(newName = action.newName) }
is RoomSettingsAction.SetRoomTopic -> setState { copy(newTopic = action.newTopic) }
is RoomSettingsAction.SetRoomHistoryVisibility -> setState { copy(newHistoryVisibility = action.visibility) }
is RoomSettingsAction.SetRoomCanonicalAlias -> setState { copy(newCanonicalAlias = action.newCanonicalAlias) }
is RoomSettingsAction.Save -> saveSettings()
+ is RoomSettingsAction.Cancel -> cancel()
}.exhaustive
}
+ private fun handleSetAvatarAction(action: RoomSettingsAction.SetAvatarAction) {
+ deletePendingAvatar()
+ setState { copy(avatarAction = action.avatarAction) }
+ }
+
+ private fun deletePendingAvatar() {
+ // Maybe delete the pending avatar
+ withState {
+ (it.avatarAction as? RoomSettingsViewState.AvatarAction.UpdateAvatar)
+ ?.let { tryOrNull { it.newAvatarUri.toFile().delete() } }
+ }
+ }
+
+ private fun cancel() {
+ deletePendingAvatar()
+
+ _viewEvents.post(RoomSettingsViewEvents.GoBack)
+ }
+
private fun saveSettings() = withState { state ->
postLoading(true)
@@ -132,6 +178,15 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
val summary = state.roomSummary.invoke()
+ when (val avatarAction = state.avatarAction) {
+ RoomSettingsViewState.AvatarAction.None -> Unit
+ RoomSettingsViewState.AvatarAction.DeleteAvatar -> {
+ operationList.add(room.rx().deleteAvatar())
+ }
+ is RoomSettingsViewState.AvatarAction.UpdateAvatar -> {
+ operationList.add(room.rx().updateAvatar(avatarAction.newAvatarUri, avatarAction.newAvatarFileName))
+ }
+ }
if (summary?.name != state.newName) {
operationList.add(room.rx().updateName(state.newName ?: ""))
}
@@ -155,6 +210,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
{
postLoading(false)
setState { copy(newHistoryVisibility = null) }
+ deletePendingAvatar()
_viewEvents.post(RoomSettingsViewEvents.Success)
},
{
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt
index fe04c8b508..f913bed382 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt
@@ -16,6 +16,7 @@
package im.vector.app.features.roomprofile.settings
+import android.net.Uri
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
@@ -29,6 +30,8 @@ data class RoomSettingsViewState(
val historyVisibilityEvent: Event? = null,
val roomSummary: Async = Uninitialized,
val isLoading: Boolean = false,
+ val currentRoomAvatarUrl: String? = null,
+ val avatarAction: AvatarAction = AvatarAction.None,
val newName: String? = null,
val newTopic: String? = null,
val newHistoryVisibility: RoomHistoryVisibility? = null,
@@ -40,10 +43,18 @@ data class RoomSettingsViewState(
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
data class ActionPermissions(
+ val canChangeAvatar: Boolean = false,
val canChangeName: Boolean = false,
val canChangeTopic: Boolean = false,
val canChangeCanonicalAlias: Boolean = false,
val canChangeHistoryReadability: Boolean = false,
val canEnableEncryption: Boolean = false
)
+
+ sealed class AvatarAction {
+ object None : AvatarAction()
+ object DeleteAvatar : AvatarAction()
+ data class UpdateAvatar(val newAvatarUri: Uri,
+ val newAvatarFileName: String) : AvatarAction()
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt
index dfae073e7e..d8171bd30d 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsGeneralFragment.kt
@@ -18,8 +18,6 @@
package im.vector.app.features.settings
-import android.app.Activity
-import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.Editable
@@ -28,7 +26,6 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
-import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.preference.EditTextPreference
import androidx.preference.Preference
@@ -38,28 +35,22 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.cache.DiskCache
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
-import com.yalantis.ucrop.UCrop
import im.vector.app.R
+import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.extensions.hideKeyboard
-import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.showPassword
import im.vector.app.core.intent.getFilenameFromUri
import im.vector.app.core.platform.SimpleTextWatcher
import im.vector.app.core.preference.UserAvatarPreference
import im.vector.app.core.preference.VectorPreference
import im.vector.app.core.preference.VectorSwitchPreference
-import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
+import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.TextUtils
-import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.getSizeOfFiles
-import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.toast
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
-import im.vector.app.features.media.createUCropWithDefaultSettings
import im.vector.app.features.workers.signout.SignOutUiWorker
-import im.vector.lib.multipicker.MultiPicker
-import im.vector.lib.multipicker.entity.MultiPickerImageType
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -74,13 +65,18 @@ import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
import java.io.File
import java.util.UUID
+import javax.inject.Inject
-class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
+class VectorSettingsGeneralFragment @Inject constructor(
+ colorProvider: ColorProvider
+):
+ VectorSettingsBaseFragment(),
+ GalleryOrCameraDialogHelper.Listener {
override var titleRes = R.string.settings_general_title
override val preferenceXmlRes = R.xml.vector_settings_general
- private var avatarCameraUri: Uri? = null
+ private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider)
private val mUserSettingsCategory by lazy {
findPreference(VectorPreferences.SETTINGS_USER_SETTINGS_PREFERENCE_KEY)!!
@@ -154,7 +150,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
// Avatar
mUserAvatarPreference.let {
it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
- onUpdateAvatarClick()
+ galleryOrCameraDialogHelper.show()
false
}
}
@@ -279,42 +275,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
session.integrationManagerService().removeListener(integrationServiceListener)
}
- private val attachmentPhotoActivityResultLauncher = registerStartForActivityResult { activityResult ->
- if (activityResult.resultCode == Activity.RESULT_OK) {
- avatarCameraUri?.let { uri ->
- MultiPicker.get(MultiPicker.CAMERA)
- .getTakenPhoto(requireContext(), uri)
- ?.let {
- onAvatarSelected(it)
- }
- }
- }
- }
-
- private val attachmentImageActivityResultLauncher = registerStartForActivityResult { activityResult ->
- val data = activityResult.data ?: return@registerStartForActivityResult
- if (activityResult.resultCode == Activity.RESULT_OK) {
- MultiPicker
- .get(MultiPicker.IMAGE)
- .getSelectedFiles(requireContext(), data)
- .firstOrNull()?.let {
- onAvatarSelected(it)
- }
- }
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- // TODO handle this one (Ucrop lib)
- @Suppress("DEPRECATION")
- super.onActivityResult(requestCode, resultCode, data)
-
- if (resultCode == Activity.RESULT_OK) {
- when (requestCode) {
- UCrop.REQUEST_CROP -> data?.let { onAvatarCropped(UCrop.getOutput(it)) }
- }
- }
- }
-
private fun refreshIntegrationManagerSettings() {
val integrationAllowed = session.integrationManagerService().isIntegrationEnabled()
(findPreference(VectorPreferences.SETTINGS_ALLOW_INTEGRATIONS_KEY))!!.let {
@@ -334,46 +294,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
}
}
- /**
- * Update the avatar.
- */
- private fun onUpdateAvatarClick() {
- AlertDialog
- .Builder(requireContext())
- .setItems(arrayOf(
- getString(R.string.attachment_type_camera),
- getString(R.string.attachment_type_gallery)
- )) { dialog, which ->
- dialog.cancel()
- onAvatarTypeSelected(isCamera = (which == 0))
- }.show()
- }
-
- private val takePhotoActivityResultLauncher = registerForPermissionsResult { allGranted ->
- if (allGranted) {
- onAvatarTypeSelected(true)
- }
- }
-
- private fun onAvatarTypeSelected(isCamera: Boolean) {
- if (isCamera) {
- if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), takePhotoActivityResultLauncher)) {
- avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(requireActivity(), attachmentPhotoActivityResultLauncher)
- }
- } else {
- MultiPicker.get(MultiPicker.IMAGE).single().startWith(attachmentImageActivityResultLauncher)
- }
- }
-
- private fun onAvatarSelected(image: MultiPickerImageType) {
- val destinationFile = File(requireContext().cacheDir, "${image.displayName}_edited_image_${System.currentTimeMillis()}")
- val uri = image.contentUri
- createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), image.displayName)
- .apply { withAspectRatio(1f, 1f) }
- .start(requireContext(), this)
- }
-
- private fun onAvatarCropped(uri: Uri?) {
+ override fun onImageReady(uri: Uri?) {
if (uri != null) {
uploadAvatar(uri)
} else {
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsEpoxyController.kt
deleted file mode 100644
index cf93bc14e7..0000000000
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsEpoxyController.kt
+++ /dev/null
@@ -1,235 +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.settings.devtools
-
-import com.airbnb.epoxy.TypedEpoxyController
-import com.airbnb.mvrx.Fail
-import com.airbnb.mvrx.Loading
-import com.airbnb.mvrx.Success
-import com.airbnb.mvrx.Uninitialized
-import im.vector.app.R
-import im.vector.app.core.date.DateFormatKind
-import im.vector.app.core.date.VectorDateFormatter
-import im.vector.app.core.epoxy.loadingItem
-import im.vector.app.core.extensions.exhaustive
-import im.vector.app.core.resources.ColorProvider
-import im.vector.app.core.resources.StringProvider
-import im.vector.app.core.ui.list.GenericItem
-import im.vector.app.core.ui.list.genericFooterItem
-import im.vector.app.core.ui.list.genericItem
-import im.vector.app.core.ui.list.genericItemHeader
-import me.gujun.android.span.span
-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.internal.crypto.model.event.OlmEventContent
-import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
-import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
-import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
-import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
-import javax.inject.Inject
-
-class GossipingEventsEpoxyController @Inject constructor(
- private val stringProvider: StringProvider,
- private val vectorDateFormatter: VectorDateFormatter,
- private val colorProvider: ColorProvider
-) : TypedEpoxyController() {
-
- interface InteractionListener {
- fun didTap(event: Event)
- }
-
- var interactionListener: InteractionListener? = null
-
- override fun buildModels(data: GossipingEventsPaperTrailState?) {
- when (val async = data?.events) {
- is Uninitialized,
- is Loading -> {
- loadingItem {
- id("loadingOutgoing")
- loadingText(stringProvider.getString(R.string.loading))
- }
- }
- is Fail -> {
- genericItem {
- id("failOutgoing")
- title(async.error.localizedMessage)
- }
- }
- is Success -> {
- val eventList = async.invoke()
- if (eventList.isEmpty()) {
- genericFooterItem {
- id("empty")
- text(stringProvider.getString(R.string.no_result_placeholder))
- }
- return
- }
-
- eventList.forEachIndexed { _, event ->
- genericItem {
- id(event.hashCode())
- itemClickAction(GenericItem.Action("view").apply { perform = Runnable { interactionListener?.didTap(event) } })
- title(
- if (event.isEncrypted()) {
- "${event.getClearType()} [encrypted]"
- } else {
- event.type
- }
- )
- description(
- span {
- +vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
- span("\nfrom: ") {
- textStyle = "bold"
- }
- +"${event.senderId}"
- apply {
- if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
- val content = event.getClearContent().toModel()
- span("\nreqId:") {
- textStyle = "bold"
- }
- +" ${content?.requestId}"
- span("\naction:") {
- textStyle = "bold"
- }
- +" ${content?.action}"
- if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
- span("\nsessionId:") {
- textStyle = "bold"
- }
- +" ${content.body?.sessionId}"
- }
- span("\nrequestedBy: ") {
- textStyle = "bold"
- }
- +"${content?.requestingDeviceId}"
- } else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
- val encryptedContent = event.content.toModel()
- val content = event.getClearContent().toModel()
- if (event.mxDecryptionResult == null) {
- span("**Failed to Decrypt** ${event.mCryptoError}") {
- textColor = colorProvider.getColor(R.color.vector_error_color)
- }
- }
- span("\nsessionId:") {
- textStyle = "bold"
- }
- +" ${content?.sessionId}"
- span("\nFrom Device (sender key):") {
- textStyle = "bold"
- }
- +" ${encryptedContent?.senderKey}"
- } else if (event.getClearType() == EventType.SEND_SECRET) {
- val content = event.getClearContent().toModel()
-
- span("\nrequestId:") {
- textStyle = "bold"
- }
- +" ${content?.requestId}"
- span("\nFrom Device:") {
- textStyle = "bold"
- }
- +" ${event.mxDecryptionResult?.payload?.get("sender_device")}"
- } else if (event.getClearType() == EventType.REQUEST_SECRET) {
- val content = event.getClearContent().toModel()
- span("\nreqId:") {
- textStyle = "bold"
- }
- +" ${content?.requestId}"
- span("\naction:") {
- textStyle = "bold"
- }
- +" ${content?.action}"
- if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
- span("\nsecretName:") {
- textStyle = "bold"
- }
- +" ${content.secretName}"
- }
- span("\nrequestedBy: ") {
- textStyle = "bold"
- }
- +"${content?.requestingDeviceId}"
- }
- }
- }
- )
- }
- }
- }
- }
- }
-
- private fun buildOutgoing(data: KeyRequestListViewState?) {
- data?.outgoingRoomKeyRequests?.let { async ->
- when (async) {
- is Uninitialized,
- is Loading -> {
- loadingItem {
- id("loadingOutgoing")
- loadingText(stringProvider.getString(R.string.loading))
- }
- }
- is Fail -> {
- genericItem {
- id("failOutgoing")
- title(async.error.localizedMessage)
- }
- }
- is Success -> {
- if (async.invoke().isEmpty()) {
- genericFooterItem {
- id("empty")
- text(stringProvider.getString(R.string.no_result_placeholder))
- }
- return
- }
-
- val requestList = async.invoke().groupBy { it.roomId }
-
- requestList.forEach {
- genericItemHeader {
- id(it.key)
- text("roomId: ${it.key}")
- }
- it.value.forEach { roomKeyRequest ->
- genericItem {
- id(roomKeyRequest.requestId)
- title(roomKeyRequest.requestId)
- description(
- span {
- span("sessionId:\n") {
- textStyle = "bold"
- }
- +"${roomKeyRequest.sessionId}"
- span("\nstate:") {
- textStyle = "bold"
- }
- +"\n${roomKeyRequest.state.name}"
- }
- )
- }
- }
- }
- }
- }.exhaustive
- }
- }
-}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt
index e2c855a9e3..0ceb8e148d 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt
@@ -33,16 +33,19 @@ import javax.inject.Inject
class GossipingEventsPaperTrailFragment @Inject constructor(
val viewModelFactory: GossipingEventsPaperTrailViewModel.Factory,
- private val epoxyController: GossipingEventsEpoxyController,
+ private val epoxyController: GossipingTrailPagedEpoxyController,
private val colorProvider: ColorProvider
-) : VectorBaseFragment(), GossipingEventsEpoxyController.InteractionListener {
+) : VectorBaseFragment(), GossipingTrailPagedEpoxyController.InteractionListener {
override fun getLayoutResId() = R.layout.fragment_generic_recycler
private val viewModel: GossipingEventsPaperTrailViewModel by fragmentViewModel(GossipingEventsPaperTrailViewModel::class)
override fun invalidate() = withState(viewModel) { state ->
- epoxyController.setData(state)
+ state.events.invoke()?.let {
+ epoxyController.submitList(it)
+ }
+ Unit
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt
index d903725b22..4249ef09fa 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt
@@ -16,13 +16,12 @@
package im.vector.app.features.settings.devtools
-import androidx.lifecycle.viewModelScope
+import androidx.paging.PagedList
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
-import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
@@ -30,12 +29,12 @@ import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
-import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.rx.asObservable
data class GossipingEventsPaperTrailState(
- val events: Async> = Uninitialized
+ val events: Async> = Uninitialized
) : MvRxState
class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState,
@@ -50,14 +49,10 @@ class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted i
setState {
copy(events = Loading())
}
- viewModelScope.launch {
- session.cryptoService().getGossipingEventsTrail().let {
- val sorted = it.sortedByDescending { it.ageLocalTs }
- setState {
- copy(events = Success(sorted))
+ session.cryptoService().getGossipingEventsTrail().asObservable()
+ .execute {
+ copy(events = it)
}
- }
- }
}
override fun handle(action: EmptyAction) {}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt
new file mode 100644
index 0000000000..d18a6c2ba8
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.settings.devtools
+
+import im.vector.app.core.resources.DateProvider
+import me.gujun.android.span.span
+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.internal.crypto.model.event.OlmEventContent
+import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
+import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
+import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
+import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
+import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
+import org.threeten.bp.format.DateTimeFormatter
+
+class GossipingEventsSerializer {
+ private val full24DateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
+
+ fun serialize(eventList: List): String {
+ return buildString {
+ eventList.forEach {
+ val clearType = it.getClearType()
+ append("[${getFormattedDate(it.ageLocalTs)}] $clearType from:${it.senderId} - ")
+ when (clearType) {
+ EventType.ROOM_KEY_REQUEST -> {
+ val content = it.getClearContent().toModel()
+ append("reqId:${content?.requestId} action:${content?.action} ")
+ if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
+ append("sessionId: ${content.body?.sessionId} ")
+ }
+ append("requestedBy: ${content?.requestingDeviceId}")
+ }
+ EventType.FORWARDED_ROOM_KEY -> {
+ val encryptedContent = it.content.toModel()
+ val content = it.getClearContent().toModel()
+
+ append("sessionId:${content?.sessionId} From Device (sender key):${encryptedContent?.senderKey}")
+ span("\nFrom Device (sender key):") {
+ textStyle = "bold"
+ }
+ }
+ EventType.ROOM_KEY -> {
+ val content = it.getClearContent()
+ append("sessionId:${content?.get("session_id")} roomId:${content?.get("room_id")} dest:${content?.get("_dest") ?: "me"}")
+ }
+ EventType.SEND_SECRET -> {
+ val content = it.getClearContent().toModel()
+ append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}")
+ }
+ EventType.REQUEST_SECRET -> {
+ val content = it.getClearContent().toModel()
+ append("reqId:${content?.requestId} action:${content?.action} ")
+ if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
+ append("secretName:${content.secretName} ")
+ }
+ append("requestedBy:${content?.requestingDeviceId}")
+ }
+ EventType.ENCRYPTED -> {
+ append("Failed to Decrypt")
+ }
+ else -> {
+ append("??")
+ }
+ }
+ append("\n")
+ }
+ }
+ }
+
+ private fun getFormattedDate(ageLocalTs: Long?): String {
+ return ageLocalTs
+ ?.let { DateProvider.toLocalDateTime(it) }
+ ?.let { full24DateFormatter.format(it) }
+ ?: "?"
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt
new file mode 100644
index 0000000000..603c67d074
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt
@@ -0,0 +1,168 @@
+/*
+ * 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.settings.devtools
+
+import com.airbnb.epoxy.EpoxyModel
+import com.airbnb.epoxy.paging.PagedListEpoxyController
+import im.vector.app.R
+import im.vector.app.core.date.DateFormatKind
+import im.vector.app.core.date.VectorDateFormatter
+import im.vector.app.core.resources.ColorProvider
+import im.vector.app.core.resources.StringProvider
+import im.vector.app.core.ui.list.GenericItem
+import im.vector.app.core.ui.list.GenericItem_
+import im.vector.app.core.utils.createUIHandler
+import me.gujun.android.span.span
+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.internal.crypto.model.event.OlmEventContent
+import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
+import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
+import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
+import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
+import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
+import javax.inject.Inject
+
+class GossipingTrailPagedEpoxyController @Inject constructor(
+ private val stringProvider: StringProvider,
+ private val vectorDateFormatter: VectorDateFormatter,
+ private val colorProvider: ColorProvider
+) : PagedListEpoxyController(
+ // Important it must match the PageList builder notify Looper
+ modelBuildingHandler = createUIHandler()
+) {
+
+ interface InteractionListener {
+ fun didTap(event: Event)
+ }
+
+ var interactionListener: InteractionListener? = null
+
+ override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> {
+ val event = item ?: return GenericItem_().apply { id(currentPosition) }
+ return GenericItem_().apply {
+ id(event.hashCode())
+ itemClickAction(GenericItem.Action("view").apply { perform = Runnable { interactionListener?.didTap(event) } })
+ title(
+ if (event.isEncrypted()) {
+ "${event.getClearType()} [encrypted]"
+ } else {
+ event.type
+ }
+ )
+ description(
+ span {
+ +vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
+ span("\nfrom: ") {
+ textStyle = "bold"
+ }
+ +"${event.senderId}"
+ apply {
+ if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
+ val content = event.getClearContent().toModel()
+ span("\nreqId:") {
+ textStyle = "bold"
+ }
+ +" ${content?.requestId}"
+ span("\naction:") {
+ textStyle = "bold"
+ }
+ +" ${content?.action}"
+ if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
+ span("\nsessionId:") {
+ textStyle = "bold"
+ }
+ +" ${content.body?.sessionId}"
+ }
+ span("\nrequestedBy: ") {
+ textStyle = "bold"
+ }
+ +"${content?.requestingDeviceId}"
+ } else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
+ val encryptedContent = event.content.toModel()
+ val content = event.getClearContent().toModel