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") + expectedFormattedText = "<$htmlExpectedTag>$name$softBreak$name") } // With new line between two blocks @@ -348,7 +354,7 @@ class MarkdownParserTest : InstrumentedTest { .let { markdownParser.parse(it) .expect(expectedText = it, - expectedFormattedText = "<$htmlExpectedTag>$name<$htmlExpectedTag>$name") + expectedFormattedText = "<$htmlExpectedTag>$name
<$htmlExpectedTag>$name") } } 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