Merge branch 'develop' into feature/fga/voip_v1_start

This commit is contained in:
ganfra 2021-01-29 18:32:00 +01:00
commit f4fd8af3b4
261 changed files with 7945 additions and 1813 deletions

View File

@ -1,12 +1,46 @@
Changes in Element 1.0.14 (2020-XX-XX) Changes in Element 1.0.15 (2020-XX-XX)
===================================================
Features ✨:
-
Improvements 🙌:
-
Bugfix 🐛:
- Fix clear cache issue: sometimes, after a clear cache, there is still a token, so the init sync service is not started.
- Sidebar too large in horizontal orientation or tablets (#475)
- UrlPreview should be updated when the url is edited and changed (#2678)
- When receiving a new pepper from identity server, use it on the next hash lookup (#2708)
- Crashes reported by PlayStore (new in 1.0.14) (#2707)
Translations 🗣:
-
SDK API changes ⚠️:
- Increase targetSdkVersion to 30 (#2600)
Build 🧱:
- Compile with Android SDK 30 (Android 11)
Test:
-
Other changes:
- Update Dagger to 2.31 version so we can use the embedded AssistedInject feature
Changes in Element 1.0.14 (2020-01-15)
=================================================== ===================================================
Features ✨: Features ✨:
- Enable url previews for notices (#2562) - Enable url previews for notices (#2562)
- Edit room permissions (#2471)
Improvements 🙌: Improvements 🙌:
- Add System theme option and set as default (#904, #2387) - Add System theme option and set as default (#904, #2387)
- Warn user when he is leaving a not public room (#1460) - Store megolm outbound session to improve send time of first message after app launch.
- Warn user when they are leaving a not public room (#1460)
- Option to disable emoji keyboard (#2563)
Bugfix 🐛: Bugfix 🐛:
- Unspecced msgType field in m.sticker (#2580) - Unspecced msgType field in m.sticker (#2580)
@ -15,19 +49,17 @@ Bugfix 🐛:
- Room Topic not displayed correctly after visiting a link (#2551) - Room Topic not displayed correctly after visiting a link (#2551)
- Hiding membership events works the exact opposite (#2603) - Hiding membership events works the exact opposite (#2603)
- Tapping drawer having more than 1 room in notifications gives "malformed link" error (#2605) - Tapping drawer having more than 1 room in notifications gives "malformed link" error (#2605)
- Sent image not displayed when opened immediately after sending (#409)
- Initial sync is not retried correctly when there is some network error. (#2632) - Initial sync is not retried correctly when there is some network error. (#2632)
Translations 🗣: - Fix switch theme issue, and white field issue (#2599, #2528)
- - Fix request too large Uri error when joining a room
SDK API changes ⚠️: Translations 🗣:
- - New language supported: Hebrew
Build 🧱: Build 🧱:
- Remove dependency to org.greenrobot.eventbus library - Remove dependency to org.greenrobot.eventbus library
Test:
-
Other changes: Other changes:
- Migrate to ViewBindings (#1072) - Migrate to ViewBindings (#1072)

View File

@ -32,11 +32,11 @@ buildscript {
} }
android { android {
compileSdkVersion 29 compileSdkVersion 30
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 30
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
} }

View File

@ -18,15 +18,19 @@
package im.vector.lib.attachmentviewer package im.vector.lib.attachmentviewer
import android.graphics.Color import android.graphics.Color
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.GestureDetector import android.view.GestureDetector
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ScaleGestureDetector import android.view.ScaleGestureDetector
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager import android.view.WindowManager
import android.widget.ImageView import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.GestureDetectorCompat import androidx.core.view.GestureDetectorCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -94,14 +98,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// This is important for the dispatchTouchEvent, if not we must correct setDecorViewFullScreen()
// the touch coordinates
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_IMMERSIVE)
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
views = ActivityAttachmentViewerBinding.inflate(layoutInflater) views = ActivityAttachmentViewerBinding.inflate(layoutInflater)
setContentView(views.root) setContentView(views.root)
@ -134,6 +131,29 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
} }
} }
@Suppress("DEPRECATION")
private fun setDecorViewFullScreen() {
// This is important for the dispatchTouchEvent, if not we must correct
// the touch coordinates
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
window.setDecorFitsSystemWindows(false)
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
// New API instead of FLAG_TRANSLUCENT_STATUS
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
// new API instead of FLAG_TRANSLUCENT_NAVIGATION
window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
} else {
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_IMMERSIVE)
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
}
}
fun onSelectedPositionChanged(position: Int) { fun onSelectedPositionChanged(position: Int) {
attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition)?.let { attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition)?.let {
(it as? BaseViewHolder)?.onSelected(false) (it as? BaseViewHolder)?.onSelected(false)
@ -313,28 +333,48 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
?.handleCommand(commands) ?.handleCommand(commands)
} }
@Suppress("DEPRECATION")
private fun hideSystemUI() { private fun hideSystemUI() {
systemUiVisibility = false systemUiVisibility = false
// Enables regular immersive mode. // Enables regular immersive mode.
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE. // For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY // Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Set the content to appear under the system bars so that the // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
// content doesn't resize when the system bars hide and show. window.setDecorFitsSystemWindows(false)
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE // new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars())
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN // New API instead of SYSTEM_UI_FLAG_IMMERSIVE
// Hide the nav bar and status bar window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // New API instead of FLAG_TRANSLUCENT_STATUS
or View.SYSTEM_UI_FLAG_FULLSCREEN) window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
} else {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
// Set the content to appear under the system bars so that the
// content doesn't resize when the system bars hide and show.
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
// Hide the nav bar and status bar
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN)
}
} }
// Shows the system bars by removing all the flags // Shows the system bars by removing all the flags
// except for the ones that make the content appear under the system bars. // except for the ones that make the content appear under the system bars.
@Suppress("DEPRECATION")
private fun showSystemUI() { private fun showSystemUI() {
systemUiVisibility = true systemUiVisibility = true
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION // New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) window.setDecorFitsSystemWindows(false)
} else {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
}
} }
} }

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<color name="half_transparent_status_bar">#80000000</color>
</resources>

View File

@ -1 +1,2 @@
// TODO Diese neue Version enthält hauptsächlich Fehlerkorrekturen und Verbesserungen. Nachrichten verschicken geht jetzt viel schneller.
Vollständige Versionshinweise: https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -0,0 +1,2 @@
Diese neue Version enthält hauptsächlich Verbesserungen der Benutzer*innenoberfläche und der Handhabung. Du kannst jetzt ganz schnell Freund*innen einladen und DMs erstellen, indem du schlicht einen QR-Code scannst.
Vollständige Versionshinweise: https://github.com/vector-im/element-android/releases/tag/v1.0.11

View File

@ -1,2 +1,2 @@
Main changes in this version: URL Preview, new Emoji keyboard, new room settings capabilities, and snow for Christmas! Main changes in this version: URL Preview, new Emoji keyboard, new room settings capabilities, and snow for Christmas!
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.0.12 Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.0.13

View File

@ -0,0 +1,2 @@
Main changes in this version: Edit room permissions, automatic light/dark theme, and a bunch of bug fixes.
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.0.14

View File

@ -0,0 +1,2 @@
Selles uues versioonis leidub põhiliselt veaparandusi ja pisikohendusi. Sõnumite saatmine on nüüd märkatavalt kiirem.
Kogu ingliskeelne muudatuste logi: https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -0,0 +1,2 @@
Uues versioonis leidub põhiliselt kasutajaliidese ning kasutajakogemuse parandusi. Nüüd saad sõpradele kutseid saata ning otsevestlusi alustada QR-koodi lugemise abil.
Kõik muudatused: https://github.com/vector-im/element-android/releases/tag/v1.0.11

View File

@ -1 +1,2 @@
// برای انجام این نگارش جدید به طور عمده شامل رفع اشکال‌ها و بهبودها است. ارسال پیام اکنون بسیار سریعتر است.
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -0,0 +1,2 @@
این نگارش جدید به طور عمده شامل رابط کاربری و بهبود تجربه کاربر است. اکنون می‌توانید با پویش کدهای QR دوستانتان را دعوت کرده و بسیار سریع پیام مستقیم ایجاد کنید.
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.0.11

View File

@ -1 +1 @@
گپ و تماس نامتمرکز امن. داده‌هایتان را از شرکت‌ها امن نگه دارید. گپ و تماس نامتمرکز امن. داده‌هایتان را از اشخاص سوم امن نگه دارید.

View File

@ -1 +1 @@
المنت (ریوت سابق) Element (پیشتر Riot.im)

View File

@ -0,0 +1,2 @@
Tämä versio sisältää pääosin käyttöliittymä- ja käyttökokemusparannuksia. Voit nyt kutsua kavereita ja luoda yksityisviestejä nopeasti QR-koodeja lukemalla.
Täysi muutosloki: https://github.com/vector-im/element-android/releases/tag/v1.0.11

View File

@ -1,30 +1,30 @@
Element on uudenlainen viestinsovellus, joka: Element on uudenlainen viestinsovellus, joka:
1. Antaa sinun päättää yksityisyydestäsi. 1. Antaa sinun päättää yksityisyydestäsi
2. Antaa sinun kommunikoida kenen tahansa kanssa Matrix-verkossa ja jopa sen ulkopuolella siltaamalla sovelluksiin, kuten Slack 2. Antaa sinun kommunikoida kenen tahansa kanssa Matrix-verkossa ja jopa sen ulkopuolella siltaamalla sovelluksiin, kuten Slack
3. Suojaa sinua mainonnalta, tietojen keräämiseltä ja suljetuilta alustoilta 3. Suojaa sinua mainonnalta, tietojen keräämiseltä ja suljetuilta alustoilta
4. Suojaa sinut päästä päähän -salauksella sekä ristiin varmentamisella muiden todentamiseksi 4. Suojaa sinut päästä päähän -salauksella sekä ristiin varmentamisella muiden todentamiseksi
Element eroaa täysin muista viestintäsovelluksista, koska se on hajautettu ja avointa lähdekoodia. Element eroaa täysin muista viestintäsovelluksista, koska se on hajautettu ja avointa lähdekoodia.
Element antaa sinun isännöidä itse - valita isännän - jotta sinulla on yksityisyys ja voit hallita tietojasi sekä keskustelujasi. Se antaa sinulle pääsyn avoimeen verkkoon; joten et ole jumissa Elementin käyttäjissä. Element antaa sinun isännöidä itse - tai valita palveluntarjoajan - jotta sinulla on yksityisyys ja voit hallita tietojasi sekä keskustelujasi. Se antaa sinulle pääsyn avoimeen verkkoon, joten et jää juttelemaan vain toisten Elementin käyttäjien kanssa. Se on myös hyvin turvallinen.
Element pystyy tekemään kaiken tämän, koska se toimii Matrixilla - avoimella, hajautetun viestinnän standardilla. Element pystyy tekemään kaiken tämän, koska se toimii Matrixilla - avoimella, hajautetun viestinnän standardilla.
Element antaa sinulle hallinnan antamalla sinun valita, kuka isännöi keskustelujasi. Element-sovelluksessa voit valita isännän eri tavoin: Element antaa sinulle päätösvallan antamalla sinun valita, kuka isännöi keskustelujasi. Element-sovelluksessa voit valita isännän eri tavoin:
1. Hanki ilmainen tili Matrix-kehittäjien ylläpitämällä matrix.org-palvelimella tai valitse tuhansista vapaaehtoisten ylläpitämistä julkisista palvelimista. 1. Hanki ilmainen tili Matrix-kehittäjien ylläpitämällä matrix.org-palvelimella tai valitse tuhansista vapaaehtoisten ylläpitämistä julkisista palvelimista.
2. Isännöi tiliäsi itse suorittamalla palvelinta omalla laitteellasi 2. Isännöi tiliäsi itse ylläpitämällä palvelinta omalla laitteellasi
3. Luo tili mukautetulla palvelimella yksinkertaisesti tilaamalla Element Matrix Services -palvelu 3. Luo tili sinua varten tehdyllä palvelimella tilaamalla Element Matrix Services -palvelu
<b>Miksi valita Element?</b> <b>Miksi valita Element?</b>
<b>OMAT TIEDOT</b>: Sinä päätät, missä tietosi ja viestisi säilytetään. Hallitset sitä itse, eikä jokin MEGAYHTIÖ, joka tutkii tietojasi tai antaa niitä kolmansille osapuolille. <b>OMAT TIEDOT</b>: Sinä päätät, missä tietosi ja viestisi säilytetään. Sinä määräät, ei jokin jättiyhtiö, joka tutkii tietojasi tai antaa niitä kolmansille osapuolille.
<b>AVOIN KOMMUNIKOINYI JA YHTEISTYÖ</b>: Voit keskustella kaikkien muiden Matrix-verkon käyttäjien kanssa, riippumatta siitä käyttävätkö he Elementiä tai muuta Matrix-sovellusta, ja vaikka he käyttäisivät eri viestijärjestelmiä, kuten Slack, IRC tai XMPP. <b>AVOINTA VIESTINTÄÄ JA YHTEISTYÖTÄ</b>: Voit keskustella kaikkien muiden Matrix-verkon käyttäjien kanssa, riippumatta siitä käyttävätkö he Elementiä tai muuta Matrix-sovellusta, ja vaikka he käyttäisivät eri viestijärjestelmiä, kuten Slack, IRC tai XMPP.
<b>ERITTÄIN TURVALLINEN</b>: Vahva päästä päähän -salaus (vain keskustelussa olevat voivat purkaa viestien salauksen), ja ristiin varmentaminen keskustelun osallistujien laitteiden tarkistamiseksi. <b>ERITTÄIN TURVALLINEN</b>: Vahva päästä päähän -salaus (vain keskustelussa olevat voivat purkaa viestien salauksen), ja ristiin varmentaminen keskustelun osallistujien laitteiden tarkistamiseksi.
<b>TÄYDELLISTÄ VIESTINTÄÄ</b>: Viestit, ääni- ja videopuhelut, tiedostojen jakaminen, näytön jakaminen ja koko joukko integraatioita, botteja ja widgettejä. Rakenna huoneita, yhteisöjä, pidä yhteyttä ja tee asioita. <b>KATTAVAA VIESTINTÄÄ</b>: Viestit, ääni- ja videopuhelut, tiedostojen jakaminen, näytön jakaminen ja koko joukko integraatioita, botteja ja sovelmia. Rakenna huoneita ja yhteisöjä, pidä yhteyttä ja hoida asiasi.
<b>MISSÄ TAHANSA OLETKIN</b>: Pidä yhteyttä missä tahansa, täysin synkronoidun viestihistorian kautta kaikilla laitteillasi ja verkossa osoitteessa https://app.element.io. <b>MISSÄ TAHANSA OLETKIN</b>: Pidä yhteyttä missä tahansa, täysin synkronoidun viestihistorian kautta kaikilla laitteillasi ja verkossa osoitteessa https://app.element.io.

View File

@ -1 +1 @@
Turvallista, hajautettua, keskusteluja ja VoIP-puheluita. Pidä tietosi turvassa. Turvallista, hajautettua keskustelua ja VoIP-puheluita. Pidä tietosi turvassa.

View File

@ -0,0 +1,2 @@
Cette nouvelle version contient principalement des corrections de bogues et des améliorations. Envoyer un message est maintenant plus rapide.
Liste complète des changements : https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -0,0 +1,2 @@
Ez az új verzió főképp hibajavításokat, és teljesítménybeli fejlesztéseket tartalmaz. Most már sokkal gyorsabb az üzenetek elküldése.
A változtatások teljes listája itt található: https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -0,0 +1,2 @@
Ez az új verzió főleg a felhasználói felülettel és a felhasználói élménnyel kapcsolatos javításokat tartalmaz. Mostantól már sokkal gyorsabban hívhatsz meg új ismerősöket a QR kód beolvasás által.
A változtatások teljes listája itt található: https://github.com/vector-im/element-android/releases/tag/v1.0.11

View File

@ -17,7 +17,7 @@ Az Element a te kezedbe adja az irányítást azáltal, hogy eldöntheted, ki t
2. A saját számítógépeden is futtathatsz szervert 2. A saját számítógépeden is futtathatsz szervert
3. Előfizethetsz egy saját szerverre az Element Matrix Szolgáltatások platformon 3. Előfizethetsz egy saját szerverre az Element Matrix Szolgáltatások platformon
<b>Miért válaszd az Element-et?</b> <b>Miért jó az Element-et választani?</b>
<b>ADATAID MEGVÉDÉSE</b>: Eldöntheted, hol tárold az adataid és üzeneteid. A te tulajdonodban van, nem valami megacégnél, ami bányássza az adataid, vagy továbbadja másoknak. <b>ADATAID MEGVÉDÉSE</b>: Eldöntheted, hol tárold az adataid és üzeneteid. A te tulajdonodban van, nem valami megacégnél, ami bányássza az adataid, vagy továbbadja másoknak.

View File

@ -0,0 +1,2 @@
Questa nuova versione contiene principalmente miglioramenti di interfaccia ed esperienza utente. Ora puoi invitare amici e iniziare messaggi diretti rapidamente tramite codici QR.
Cronologia completa: https://github.com/vector-im/element-android/releases/tag/v1.0.11

View File

@ -0,0 +1,2 @@
Denne nye versjonen inneholder hovedsakelig feilrettinger og forbedringer. Å sende en melding er nå mye raskere.
Full endringslogg: https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -0,0 +1,2 @@
Denne nye versjonen inneholder hovedsakelig forbedringer av brukergrensesnittet og brukeropplevelsen. Nå kan du invitere venner og opprette DM veldig raskt ved å skanne QR-koder.
Full endringslogg: https://github.com/vector-im/element-android/releases/tag/v1.0.11

View File

@ -0,0 +1 @@
Sikker desentralisert chat & VoIP. Beskytt dataene dine fra tredjeparter.

View File

@ -0,0 +1 @@
Element (tidligere Riot.im)

View File

@ -0,0 +1,2 @@
Esta nova versão contém principalmente melhorias na interface do usuário e na experiência do usuário. Agora você pode convidar amigos e criar conversas rapidamente, digitalizando códigos QR.
Registro completo de alterações: https://github.com/vector-im/element-android/releases/tag/v1.0.11

View File

@ -0,0 +1,2 @@
Эта новая версия в основном содержит исправления ошибок и улучшения. Отправка сообщения стала намного быстрее.
Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -0,0 +1,2 @@
Эта новая версия в основном содержит улучшения пользовательского интерфейса и взаимодействия с пользователем. Теперь вы можете приглашать друзей и очень быстро создавать чаты, сканируя QR-коды.
Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.0.11

View File

@ -0,0 +1,2 @@
Táto verzia obsahuje predovšetkým opravy chýb. Odosielanie správ je odteraz omnoho rýchlejšie.
Kompletný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -0,0 +1,2 @@
Táto verzia obsahuje najmä vylepšenia používateľského rozhrania. Pozývať priateľov alebo vytvárať priame konverzácie môžete veľmi rýchlo naskenovaním QR kódov.
Kompletný zoznam zmien: https://github.com/vector-im/element-android/releases/tag/v1.0.11

View File

@ -0,0 +1,2 @@
Den här nya versionen innehåller mest förbättringar för användargränssnittet och användarupplevelsen. Du kan nu bjuda in vänner och skapa direktmeddelanden väldigt snabbt genom att skanna QR-koder.
Full ändringslogg: https://github.com/vector-im/element-android/releases/tag/v1.0.11

View File

@ -0,0 +1,2 @@
Ця нова версія містить переважно поліпшення інтерфейсу та зручності користування. Тепер ви можете запросити друзів і створити прямі повідомлення дуже швидко, скануючи QR-коди.
Повний перелік змін: https://github.com/vector-im/element-android/releases/tag/v1.0.11

View File

@ -0,0 +1,2 @@
Phiên bản mới này chủ yếu bao gồm sửa lỗi và một số cải thiện. Gửi tin nhắn trở nên nhanh chóng hơn trước.
Danh sách đầy đủ các thay đổi: https://github.com/vector-im/element-android/releases/tag/v1.0.10

View File

@ -0,0 +1,2 @@
Phiên bản mới này chủ yếu bao gồm các cải thiện về giao diện và trải nghiệm người dùng. Bây giờ bạn có thể mời bạn bè và bắt đầu nói chuyện nhanh chóng bằng cách quét mã QR.
Danh sách đầy đủ các thay đổi: https://github.com/vector-im/element-android/releases/tag/v1.0.11

View File

@ -0,0 +1 @@
Ứng dụng chat và gọi phân tán bảo mật. Bảo vệ dữ liệu của bạn khỏi bên thứ ba.

View File

@ -0,0 +1 @@
Element (trước là Riot.im)

View File

@ -0,0 +1,2 @@
這個新版本主要包含使用者介面與使用者體驗改善。現在您可以邀請朋友,並透過掃描 QR code 來快速建立直接訊息了。
完整變更紀錄https://github.com/vector-im/element-android/releases/tag/v1.0.11

View File

@ -0,0 +1,2 @@
此版本中的主要變更URL 預覽、新的表情符號鍵盤、新的聊天室設定功能以及聖誕節降雪!
完整變更紀錄https://github.com/vector-im/element-android/releases/tag/v1.0.12

View File

@ -0,0 +1,2 @@
此版本中的主要變更URL 預覽、新的表情符號鍵盤、新的聊天室設定功能以及聖誕節降雪!
完整變更紀錄https://github.com/vector-im/element-android/releases/tag/v1.0.12

View File

@ -1,6 +1,6 @@
#Mon Dec 07 18:05:35 CET 2020 #Fri Jan 29 18:05:42 CET 2021
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-all.zip

View File

@ -3,11 +3,11 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
android { android {
compileSdkVersion 29 compileSdkVersion 30
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 30
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"

View File

@ -14,12 +14,12 @@ buildscript {
} }
android { android {
compileSdkVersion 29 compileSdkVersion 30
testOptions.unitTests.includeAndroidResources = true testOptions.unitTests.includeAndroidResources = true
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 30
versionCode 1 versionCode 1
versionName "0.0.1" versionName "0.0.1"
// Multidex is useful for tests // Multidex is useful for tests
@ -112,7 +112,7 @@ dependencies {
def lifecycle_version = '2.2.0' def lifecycle_version = '2.2.0'
def arch_version = '2.1.0' def arch_version = '2.1.0'
def markwon_version = '3.1.0' def markwon_version = '3.1.0'
def daggerVersion = '2.29.1' def daggerVersion = '2.31'
def work_version = '2.4.0' def work_version = '2.4.0'
def retrofit_version = '2.6.2' def retrofit_version = '2.6.2'
@ -160,8 +160,6 @@ dependencies {
// DI // DI
implementation "com.google.dagger:dagger:$daggerVersion" implementation "com.google.dagger:dagger:$daggerVersion"
kapt "com.google.dagger:dagger-compiler:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion"
compileOnly 'com.squareup.inject:assisted-inject-annotations-dagger2:0.5.0'
kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.0'
// Logging // Logging
implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.jakewharton.timber:timber:4.7.1'

View File

@ -378,7 +378,9 @@ class CommonTestHelper(context: Context) {
fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) } fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) }
fun signOutAndClose(session: Session) { fun signOutAndClose(session: Session) {
doSync<Unit>(60_000) { session.signOut(true, it) } runBlockingTest(timeout = 60_000) {
session.signOut(true)
}
// no need signout will close // no need signout will close
// session.close() // session.close()
} }

View File

@ -50,6 +50,8 @@ import org.junit.FixMethodOrder
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@ -296,4 +298,77 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.signOutAndClose(aliceSession1) mTestHelper.signOutAndClose(aliceSession1)
mTestHelper.signOutAndClose(aliceSession2) mTestHelper.signOutAndClose(aliceSession2)
} }
@Test
fun test_ImproperKeyShareBug() {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
mTestHelper.doSync<Unit> {
aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD
), it)
}
// Create an encrypted room and send a couple of messages
val roomId = mTestHelper.doSync<String> {
aliceSession.createRoom(
CreateRoomParams().apply {
visibility = RoomDirectoryVisibility.PRIVATE
enableEncryption()
},
it
)
}
val roomAlicePov = aliceSession.getRoom(roomId)
assertNotNull(roomAlicePov)
Thread.sleep(1_000)
assertTrue(roomAlicePov?.isEncrypted() == true)
val secondEventId = mTestHelper.sendTextMessage(roomAlicePov!!, "Message", 3)[1].eventId
// Create bob session
val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true))
mTestHelper.doSync<Unit> {
bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD
), it)
}
// Let alice invite bob
mTestHelper.doSync<Unit> {
roomAlicePov.invite(bobSession.myUserId, null, it)
}
mTestHelper.doSync<Unit> {
bobSession.joinRoom(roomAlicePov.roomId, null, emptyList(), it)
}
// we want to discard alice outbound session
aliceSession.cryptoService().discardOutboundSession(roomAlicePov.roomId)
// and now resend a new message to reset index to 0
mTestHelper.sendTextMessage(roomAlicePov, "After", 1)
val roomRoomBobPov = aliceSession.getRoom(roomId)
val beforeJoin = roomRoomBobPov!!.getTimeLineEvent(secondEventId)
var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") }
assert(dRes == null)
// Try to re-ask the keys
bobSession.cryptoService().reRequestRoomKeyForEvent(beforeJoin!!.root)
Thread.sleep(3_000)
// With the bug the first session would have improperly reshare that key :/
dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin.root, "") }
Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel<MessageContent>()?.body}")
assert(dRes?.clearEvent == null)
}
} }

View File

@ -26,6 +26,8 @@ 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.events.model.toContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
internal class UrlsExtractorTest : InstrumentedTest { internal class UrlsExtractorTest : InstrumentedTest {
@ -36,6 +38,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
fun wrongEventTypeTest() { fun wrongEventTypeTest() {
createEvent(body = "https://matrix.org") createEvent(body = "https://matrix.org")
.copy(type = EventType.STATE_ROOM_GUEST_ACCESS) .copy(type = EventType.STATE_ROOM_GUEST_ACCESS)
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) } .let { urlsExtractor.extract(it) }
.size shouldBeEqualTo 0 .size shouldBeEqualTo 0
} }
@ -43,6 +46,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test @Test
fun oneUrlTest() { fun oneUrlTest() {
createEvent(body = "https://matrix.org") createEvent(body = "https://matrix.org")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) } .let { urlsExtractor.extract(it) }
.let { result -> .let { result ->
result.size shouldBeEqualTo 1 result.size shouldBeEqualTo 1
@ -53,6 +57,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test @Test
fun withoutProtocolTest() { fun withoutProtocolTest() {
createEvent(body = "www.matrix.org") createEvent(body = "www.matrix.org")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) } .let { urlsExtractor.extract(it) }
.size shouldBeEqualTo 0 .size shouldBeEqualTo 0
} }
@ -60,6 +65,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test @Test
fun oneUrlWithParamTest() { fun oneUrlWithParamTest() {
createEvent(body = "https://matrix.org?foo=bar") createEvent(body = "https://matrix.org?foo=bar")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) } .let { urlsExtractor.extract(it) }
.let { result -> .let { result ->
result.size shouldBeEqualTo 1 result.size shouldBeEqualTo 1
@ -70,6 +76,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test @Test
fun oneUrlWithParamsTest() { fun oneUrlWithParamsTest() {
createEvent(body = "https://matrix.org?foo=bar&bar=foo") createEvent(body = "https://matrix.org?foo=bar&bar=foo")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) } .let { urlsExtractor.extract(it) }
.let { result -> .let { result ->
result.size shouldBeEqualTo 1 result.size shouldBeEqualTo 1
@ -80,16 +87,18 @@ internal class UrlsExtractorTest : InstrumentedTest {
@Test @Test
fun oneUrlInlinedTest() { fun oneUrlInlinedTest() {
createEvent(body = "Hello https://matrix.org, how are you?") createEvent(body = "Hello https://matrix.org, how are you?")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) } .let { urlsExtractor.extract(it) }
.let { result -> .let { result ->
result.size shouldBeEqualTo 1 result.size shouldBeEqualTo 1
result[0] shouldBeEqualTo "https://matrix.org" result[0] shouldBeEqualTo "https://matrix.org"
} }
} }
@Test @Test
fun twoUrlsTest() { fun twoUrlsTest() {
createEvent(body = "https://matrix.org https://example.org") createEvent(body = "https://matrix.org https://example.org")
.toFakeTimelineEvent()
.let { urlsExtractor.extract(it) } .let { urlsExtractor.extract(it) }
.let { result -> .let { result ->
result.size shouldBeEqualTo 2 result.size shouldBeEqualTo 2
@ -99,10 +108,26 @@ internal class UrlsExtractorTest : InstrumentedTest {
} }
private fun createEvent(body: String): Event = Event( private fun createEvent(body: String): Event = Event(
eventId = "!fake",
type = EventType.MESSAGE, type = EventType.MESSAGE,
content = MessageTextContent( content = MessageTextContent(
msgType = MessageType.MSGTYPE_TEXT, msgType = MessageType.MSGTYPE_TEXT,
body = body body = body
).toContent() ).toContent()
) )
private fun Event.toFakeTimelineEvent(): TimelineEvent {
return TimelineEvent(
root = this,
localId = 0L,
eventId = eventId!!,
displayIndex = 0,
senderInfo = SenderInfo(
userId = "",
displayName = null,
isUniqueDisplayName = true,
avatarUrl = null
)
)
}
} }

View File

@ -66,8 +66,8 @@ class TimelineForwardPaginationTest : InstrumentedTest {
numberOfMessagesToSend) numberOfMessagesToSend)
// Alice clear the cache // Alice clear the cache
commonTestHelper.doSync<Unit> { commonTestHelper.runBlockingTest {
aliceSession.clearCache(it) aliceSession.clearCache()
} }
// And restarts the sync // And restarts the sync

View File

@ -37,6 +37,6 @@ class SenderNotificationPermissionCondition(
fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean { fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean {
val powerLevelsHelper = PowerLevelsHelper(powerLevels) val powerLevelsHelper = PowerLevelsHelper(powerLevels)
return event.senderId != null && powerLevelsHelper.getUserPowerLevelValue(event.senderId) >= powerLevelsHelper.notificationLevel(key) return event.senderId != null && powerLevelsHelper.getUserPowerLevelValue(event.senderId) >= powerLevels.notificationLevel(key)
} }
} }

View File

@ -16,8 +16,6 @@
package org.matrix.android.sdk.api.session.cache package org.matrix.android.sdk.api.session.cache
import org.matrix.android.sdk.api.MatrixCallback
/** /**
* This interface defines a method to clear the cache. It's implemented at the session level. * This interface defines a method to clear the cache. It's implemented at the session level.
*/ */
@ -26,5 +24,5 @@ interface CacheService {
/** /**
* Clear the whole cached data, except credentials. Once done, the sync has to be restarted by the sdk user. * Clear the whole cached data, except credentials. Once done, the sync has to be restarted by the sdk user.
*/ */
fun clearCache(callback: MatrixCallback<Unit>) suspend fun clearCache()
} }

View File

@ -17,15 +17,16 @@
package org.matrix.android.sdk.api.session.media package org.matrix.android.sdk.api.session.media
import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.api.cache.CacheStrategy
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
interface MediaService { interface MediaService {
/** /**
* Extract URLs from an Event. * Extract URLs from a TimelineEvent.
* @return the list of URLs contains in the body of the Event. It does not mean that URLs in this list have UrlPreview data * @param event TimelineEvent to extract the URL from.
* @return the list of URLs contains in the body of the TimelineEvent. It does not mean that URLs in this list have UrlPreview data
*/ */
fun extractUrls(event: Event): List<String> fun extractUrls(event: TimelineEvent): List<String>
/** /**
* Get Raw Url Preview data from the homeserver. There is no cache management for this request * Get Raw Url Preview data from the homeserver. There is no cache management for this request

View File

@ -25,28 +25,85 @@ import org.matrix.android.sdk.api.session.room.powerlevels.Role
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class PowerLevelsContent( data class PowerLevelsContent(
/**
* The level required to ban a user. Defaults to 50 if unspecified.
*/
@Json(name = "ban") val ban: Int = Role.Moderator.value, @Json(name = "ban") val ban: Int = Role.Moderator.value,
/**
* The level required to kick a user. Defaults to 50 if unspecified.
*/
@Json(name = "kick") val kick: Int = Role.Moderator.value, @Json(name = "kick") val kick: Int = Role.Moderator.value,
/**
* The level required to invite a user. Defaults to 50 if unspecified.
*/
@Json(name = "invite") val invite: Int = Role.Moderator.value, @Json(name = "invite") val invite: Int = Role.Moderator.value,
/**
* The level required to redact an event. Defaults to 50 if unspecified.
*/
@Json(name = "redact") val redact: Int = Role.Moderator.value, @Json(name = "redact") val redact: Int = Role.Moderator.value,
/**
* The default level required to send message events. Can be overridden by the events key. Defaults to 0 if unspecified.
*/
@Json(name = "events_default") val eventsDefault: Int = Role.Default.value, @Json(name = "events_default") val eventsDefault: Int = Role.Default.value,
@Json(name = "events") val events: MutableMap<String, Int> = HashMap(), /**
* The level required to send specific event types. This is a mapping from event type to power level required.
*/
@Json(name = "events") val events: Map<String, Int> = emptyMap(),
/**
* The default power level for every user in the room, unless their user_id is mentioned in the users key. Defaults to 0 if unspecified.
*/
@Json(name = "users_default") val usersDefault: Int = Role.Default.value, @Json(name = "users_default") val usersDefault: Int = Role.Default.value,
@Json(name = "users") val users: MutableMap<String, Int> = HashMap(), /**
* The power levels for specific users. This is a mapping from user_id to power level for that user.
*/
@Json(name = "users") val users: Map<String, Int> = emptyMap(),
/**
* The default level required to send state events. Can be overridden by the events key. Defaults to 50 if unspecified.
*/
@Json(name = "state_default") val stateDefault: Int = Role.Moderator.value, @Json(name = "state_default") val stateDefault: Int = Role.Moderator.value,
@Json(name = "notifications") val notifications: Map<String, Any> = HashMap() /**
* The power level requirements for specific notification types. This is a mapping from key to power level for that notifications key.
*/
@Json(name = "notifications") val notifications: Map<String, Any> = emptyMap()
) { ) {
/** /**
* Alter this content with a new power level for the specified user * Return a copy of this content with a new power level for the specified user
* *
* @param userId the userId to alter the power level of * @param userId the userId to alter the power level of
* @param powerLevel the new power level, or null to set the default value. * @param powerLevel the new power level, or null to set the default value.
*/ */
fun setUserPowerLevel(userId: String, powerLevel: Int?) { fun setUserPowerLevel(userId: String, powerLevel: Int?): PowerLevelsContent {
if (powerLevel == null || powerLevel == usersDefault) { return copy(
users.remove(userId) users = users.toMutableMap().apply {
} else { if (powerLevel == null || powerLevel == usersDefault) {
users[userId] = powerLevel remove(userId)
} else {
put(userId, powerLevel)
}
}
)
}
/**
* Get the notification level for a dedicated key.
*
* @param key the notification key
* @return the level, default to Moderator if the key is not found
*/
fun notificationLevel(key: String): Int {
return when (val value = notifications[key]) {
// the first implementation was a string value
is String -> value.toInt()
is Double -> value.toInt()
is Int -> value
else -> Role.Moderator.value
} }
} }
companion object {
/**
* Key to use for content.notifications and get the level required to trigger an @room notification. Defaults to 50 if unspecified.
*/
const val NOTIFICATIONS_ROOM_KEY = "room"
}
} }

View File

@ -108,19 +108,4 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
val powerLevel = getUserPowerLevelValue(userId) val powerLevel = getUserPowerLevelValue(userId)
return powerLevel >= powerLevelsContent.redact return powerLevel >= powerLevelsContent.redact
} }
/**
* Get the notification level for a dedicated key.
*
* @param key the notification key
* @return the level
*/
fun notificationLevel(key: String): Int {
return when (val value = powerLevelsContent.notifications[key]) {
// the first implementation was a string value
is String -> value.toInt()
is Int -> value
else -> Role.Moderator.value
}
}
} }

View File

@ -91,6 +91,17 @@ data class TimelineEvent(
*/ */
fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null
/**
* Get the latest known eventId for an edited event, or the eventId for an Event which has not been edited
*/
fun TimelineEvent.getLatestEventId(): String {
return annotations
?.editSummary
?.sourceEvents
?.lastOrNull()
?: eventId
}
/** /**
* Get the relation content if any * Get the relation content if any
*/ */

View File

@ -16,9 +16,7 @@
package org.matrix.android.sdk.api.session.signout package org.matrix.android.sdk.api.session.signout
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.util.Cancelable
/** /**
* This interface defines a method to sign out, or to renew the token. It's implemented at the session level. * This interface defines a method to sign out, or to renew the token. It's implemented at the session level.
@ -29,19 +27,16 @@ interface SignOutService {
* Ask the homeserver for a new access token. * Ask the homeserver for a new access token.
* The same deviceId will be used * The same deviceId will be used
*/ */
fun signInAgain(password: String, suspend fun signInAgain(password: String)
callback: MatrixCallback<Unit>): Cancelable
/** /**
* Update the session with credentials received after SSO * Update the session with credentials received after SSO
*/ */
fun updateCredentials(credentials: Credentials, suspend fun updateCredentials(credentials: Credentials)
callback: MatrixCallback<Unit>): Cancelable
/** /**
* Sign out, and release the session, clear all the session data, including crypto data * Sign out, and release the session, clear all the session data, including crypto data
* @param signOutFromHomeserver true if the sign out request has to be done * @param signOutFromHomeserver true if the sign out request has to be done
*/ */
fun signOut(signOutFromHomeserver: Boolean, suspend fun signOut(signOutFromHomeserver: Boolean)
callback: MatrixCallback<Unit>): Cancelable
} }

View File

@ -32,6 +32,7 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.internal.crypto.model.rest.GossipingDefaultContent import org.matrix.android.sdk.internal.crypto.model.rest.GossipingDefaultContent
import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
@ -206,34 +207,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
Timber.v("## CRYPTO | GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}") Timber.v("## CRYPTO | GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
if (credentials.userId != userId) { if (credentials.userId != userId) {
Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request from other user") handleKeyRequestFromOtherUser(body, request, alg, roomId, userId, deviceId)
val senderKey = body.senderKey ?: return Unit
.also { Timber.w("missing senderKey") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
val sessionId = body.sessionId ?: return Unit
.also { Timber.w("missing sessionId") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
if (alg != MXCRYPTO_ALGORITHM_MEGOLM) {
return Unit
.also { Timber.w("Only megolm is accepted here") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
}
val roomEncryptor = roomEncryptorsStore.get(roomId) ?: return Unit
.also { Timber.w("no room Encryptor") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey)
if (isSuccess) {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
} else {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS)
}
}
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.RE_REQUESTED)
return return
} }
// TODO: should we queue up requests we don't yet have keys for, in case they turn up later? // TODO: should we queue up requests we don't yet have keys for, in case they turn up later?
@ -291,6 +265,42 @@ internal class IncomingGossipingRequestManager @Inject constructor(
onRoomKeyRequest(request) onRoomKeyRequest(request)
} }
private fun handleKeyRequestFromOtherUser(body: RoomKeyRequestBody,
request: IncomingRoomKeyRequest,
alg: String,
roomId: String,
userId: String,
deviceId: String) {
Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request from other user")
val senderKey = body.senderKey ?: return Unit
.also { Timber.w("missing senderKey") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
val sessionId = body.sessionId ?: return Unit
.also { Timber.w("missing sessionId") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
if (alg != MXCRYPTO_ALGORITHM_MEGOLM) {
return Unit
.also { Timber.w("Only megolm is accepted here") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
}
val roomEncryptor = roomEncryptorsStore.get(roomId) ?: return Unit
.also { Timber.w("no room Encryptor") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey)
if (isSuccess) {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
} else {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS)
}
}
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.RE_REQUESTED)
}
private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) { private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) {
val secretName = request.secretName ?: return Unit.also { val secretName = request.secretName ?: return Unit.also {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)

View File

@ -19,6 +19,8 @@ package org.matrix.android.sdk.internal.crypto
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXOutboundSessionInfo
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.SharedWithHelper
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
@ -46,7 +48,7 @@ internal class MXOlmDevice @Inject constructor(
*/ */
private val store: IMXCryptoStore, private val store: IMXCryptoStore,
private val inboundGroupSessionStore: InboundGroupSessionStore private val inboundGroupSessionStore: InboundGroupSessionStore
) { ) {
/** /**
* @return the Curve25519 key for the account. * @return the Curve25519 key for the account.
@ -63,11 +65,15 @@ internal class MXOlmDevice @Inject constructor(
// The OLM lib utility instance. // The OLM lib utility instance.
private var olmUtility: OlmUtility? = null private var olmUtility: OlmUtility? = null
private data class GroupSessionCacheItem(
val groupId: String,
val groupSession: OlmOutboundGroupSession
)
// The outbound group session. // The outbound group session.
// They are not stored in 'store' to avoid to remember to which devices we sent the session key. // Caches active outbound session to avoid to sync with DB before read
// Plus, in cryptography, it is good to refresh sessions from time to time. // The key is the session id, the value the <roomID,outbound group session>.
// The key is the session id, the value the outbound group session. private val outboundGroupSessionCache: MutableMap<String, GroupSessionCacheItem> = HashMap()
private val outboundGroupSessionStore: MutableMap<String, OlmOutboundGroupSession> = HashMap()
// Store a set of decrypted message indexes for each group session. // Store a set of decrypted message indexes for each group session.
// This partially mitigates a replay attack where a MITM resends a group // This partially mitigates a replay attack where a MITM resends a group
@ -135,6 +141,10 @@ internal class MXOlmDevice @Inject constructor(
*/ */
fun release() { fun release() {
olmUtility?.releaseUtility() olmUtility?.releaseUtility()
outboundGroupSessionCache.values.forEach {
it.groupSession.releaseSession()
}
outboundGroupSessionCache.clear()
} }
/** /**
@ -406,11 +416,12 @@ internal class MXOlmDevice @Inject constructor(
* *
* @return the session id for the outbound session. * @return the session id for the outbound session.
*/ */
fun createOutboundGroupSession(): String? { fun createOutboundGroupSessionForRoom(roomId: String): String? {
var session: OlmOutboundGroupSession? = null var session: OlmOutboundGroupSession? = null
try { try {
session = OlmOutboundGroupSession() session = OlmOutboundGroupSession()
outboundGroupSessionStore[session.sessionIdentifier()] = session outboundGroupSessionCache[session.sessionIdentifier()] = GroupSessionCacheItem(roomId, session)
store.storeCurrentOutboundGroupSessionForRoom(roomId, session)
return session.sessionIdentifier() return session.sessionIdentifier()
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "createOutboundGroupSession") Timber.e(e, "createOutboundGroupSession")
@ -421,6 +432,39 @@ internal class MXOlmDevice @Inject constructor(
return null return null
} }
fun storeOutboundGroupSessionForRoom(roomId: String, sessionId: String) {
outboundGroupSessionCache[sessionId]?.let {
store.storeCurrentOutboundGroupSessionForRoom(roomId, it.groupSession)
}
}
fun restoreOutboundGroupSessionForRoom(roomId: String): MXOutboundSessionInfo? {
val restoredOutboundGroupSession = store.getCurrentOutboundGroupSessionForRoom(roomId)
if (restoredOutboundGroupSession != null) {
val sessionId = restoredOutboundGroupSession.outboundGroupSession.sessionIdentifier()
// cache it
outboundGroupSessionCache[sessionId] = GroupSessionCacheItem(roomId, restoredOutboundGroupSession.outboundGroupSession)
return MXOutboundSessionInfo(
sessionId = sessionId,
sharedWithHelper = SharedWithHelper(roomId, sessionId, store),
restoredOutboundGroupSession.creationTime
)
}
return null
}
fun discardOutboundGroupSessionForRoom(roomId: String) {
val toDiscard = outboundGroupSessionCache.filter {
it.value.groupId == roomId
}
toDiscard.forEach { (sessionId, cacheItem) ->
cacheItem.groupSession.releaseSession()
outboundGroupSessionCache.remove(sessionId)
}
store.storeCurrentOutboundGroupSessionForRoom(roomId, null)
}
/** /**
* Get the current session key of an outbound group session. * Get the current session key of an outbound group session.
* *
@ -430,7 +474,7 @@ internal class MXOlmDevice @Inject constructor(
fun getSessionKey(sessionId: String): String? { fun getSessionKey(sessionId: String): String? {
if (sessionId.isNotEmpty()) { if (sessionId.isNotEmpty()) {
try { try {
return outboundGroupSessionStore[sessionId]!!.sessionKey() return outboundGroupSessionCache[sessionId]!!.groupSession.sessionKey()
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## getSessionKey() : failed") Timber.e(e, "## getSessionKey() : failed")
} }
@ -446,7 +490,7 @@ internal class MXOlmDevice @Inject constructor(
*/ */
fun getMessageIndex(sessionId: String): Int { fun getMessageIndex(sessionId: String): Int {
return if (sessionId.isNotEmpty()) { return if (sessionId.isNotEmpty()) {
outboundGroupSessionStore[sessionId]!!.messageIndex() outboundGroupSessionCache[sessionId]!!.groupSession.messageIndex()
} else 0 } else 0
} }
@ -460,7 +504,7 @@ internal class MXOlmDevice @Inject constructor(
fun encryptGroupMessage(sessionId: String, payloadString: String): String? { fun encryptGroupMessage(sessionId: String, payloadString: String): String? {
if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) { if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) {
try { try {
return outboundGroupSessionStore[sessionId]!!.encryptMessage(payloadString) return outboundGroupSessionCache[sessionId]!!.groupSession.encryptMessage(payloadString)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## encryptGroupMessage() : failed") Timber.e(e, "## encryptGroupMessage() : failed")
} }
@ -747,7 +791,7 @@ internal class MXOlmDevice @Inject constructor(
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON) throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
} }
val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey) val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
if (session != null) { if (session != null) {
// Check that the room id matches the original one for the session. This stops // Check that the room id matches the original one for the session. This stops

View File

@ -68,6 +68,10 @@ internal class MXMegolmEncryption(
// case outboundSession.shareOperation will be non-null.) // case outboundSession.shareOperation will be non-null.)
private var outboundSession: MXOutboundSessionInfo? = null private var outboundSession: MXOutboundSessionInfo? = null
init {
// restore existing outbound session if any
outboundSession = olmDevice.restoreOutboundGroupSessionForRoom(roomId)
}
// Default rotation periods // Default rotation periods
// TODO: Make it configurable via parameters // TODO: Make it configurable via parameters
// Session rotation periods // Session rotation periods
@ -86,6 +90,9 @@ internal class MXMegolmEncryption(
return encryptContent(outboundSession, eventType, eventContent) return encryptContent(outboundSession, eventType, eventContent)
.also { .also {
notifyWithheldForSession(devices.withHeldDevices, outboundSession) notifyWithheldForSession(devices.withHeldDevices, outboundSession)
// annoyingly we have to serialize again the saved outbound session to store message index :/
// if not we would see duplicate message index errors
olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId)
} }
} }
@ -107,6 +114,7 @@ internal class MXMegolmEncryption(
override fun discardSessionKey() { override fun discardSessionKey() {
outboundSession = null outboundSession = null
olmDevice.discardOutboundGroupSessionForRoom(roomId)
} }
/** /**
@ -116,7 +124,7 @@ internal class MXMegolmEncryption(
*/ */
private fun prepareNewSessionInRoom(): MXOutboundSessionInfo { private fun prepareNewSessionInRoom(): MXOutboundSessionInfo {
Timber.v("## CRYPTO | prepareNewSessionInRoom() ") Timber.v("## CRYPTO | prepareNewSessionInRoom() ")
val sessionId = olmDevice.createOutboundGroupSession() val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId)
val keysClaimedMap = HashMap<String, String>() val keysClaimedMap = HashMap<String, String>()
keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!! keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!!
@ -152,7 +160,7 @@ internal class MXMegolmEncryption(
val deviceIds = devicesInRoom.getUserDeviceIds(userId) val deviceIds = devicesInRoom.getUserDeviceIds(userId)
for (deviceId in deviceIds!!) { for (deviceId in deviceIds!!) {
val deviceInfo = devicesInRoom.getObject(userId, deviceId) val deviceInfo = devicesInRoom.getObject(userId, deviceId)
if (deviceInfo != null && !cryptoStore.wasSessionSharedWithUser(roomId, safeSession.sessionId, userId, deviceId).found) { if (deviceInfo != null && !cryptoStore.getSharedSessionInfo(roomId, safeSession.sessionId, userId, deviceId).found) {
val devices = shareMap.getOrPut(userId) { ArrayList() } val devices = shareMap.getOrPut(userId) { ArrayList() }
devices.add(deviceInfo) devices.add(deviceInfo)
} }
@ -401,11 +409,18 @@ internal class MXMegolmEncryption(
.also { Timber.w("## Crypto reshareKey: Device not found") } .also { Timber.w("## Crypto reshareKey: Device not found") }
// Get the chain index of the key we previously sent this device // Get the chain index of the key we previously sent this device
val chainIndex = outboundSession?.sharedWithHelper?.wasSharedWith(userId, deviceId) ?: return false val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, sessionId, userId, deviceId)
if (!wasSessionSharedWithUser.found) {
// This session was never shared with this user
// Send a room key with held
notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED)
Timber.w("## Crypto reshareKey: ERROR : Never shared megolm with this device")
return false
}
// if found chain index should not be null
val chainIndex = wasSessionSharedWithUser.chainIndex ?: return false
.also { .also {
// Send a room key with held Timber.w("## Crypto reshareKey: Null chain index")
notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED)
Timber.w("## Crypto reshareKey: ERROR : Never share megolm with this device")
} }
val devicesByUser = mapOf(userId to listOf(deviceInfo)) val devicesByUser = mapOf(userId to listOf(deviceInfo))

View File

@ -23,9 +23,9 @@ import timber.log.Timber
internal class MXOutboundSessionInfo( internal class MXOutboundSessionInfo(
// The id of the session // The id of the session
val sessionId: String, val sessionId: String,
val sharedWithHelper: SharedWithHelper) { val sharedWithHelper: SharedWithHelper,
// When the session was created // When the session was created
private val creationTime = System.currentTimeMillis() private val creationTime: Long = System.currentTimeMillis()) {
// Number of times this session has been used // Number of times this session has been used
var useCount: Int = 0 var useCount: Int = 0

View File

@ -28,10 +28,6 @@ internal class SharedWithHelper(
return cryptoStore.getSharedWithInfo(roomId, sessionId) return cryptoStore.getSharedWithInfo(roomId, sessionId)
} }
fun wasSharedWith(userId: String, deviceId: String): Int? {
return cryptoStore.wasSessionSharedWithUser(roomId, sessionId, userId, deviceId).chainIndex
}
fun markedSessionAsShared(userId: String, deviceId: String, chainIndex: Int) { fun markedSessionAsShared(userId: String, deviceId: String, chainIndex: Int) {
cryptoStore.markedSessionAsShared(roomId, sessionId, userId, deviceId, chainIndex) cryptoStore.markedSessionAsShared(roomId, sessionId, userId, deviceId, chainIndex)
} }

View File

@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,11 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.di package org.matrix.android.sdk.internal.crypto.model
import com.squareup.inject.assisted.dagger2.AssistedModule import org.matrix.olm.OlmOutboundGroupSession
import dagger.Module
@AssistedModule data class OutboundGroupSessionWrapper(
@Module(includes = [AssistedInject_SessionAssistedInjectModule::class]) val outboundGroupSession: OlmOutboundGroupSession,
interface SessionAssistedInjectModule val creationTime: Long
)

View File

@ -33,11 +33,13 @@ 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.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent 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.DeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmOutboundGroupSession
/** /**
* the crypto data store * the crypto data store
@ -293,6 +295,16 @@ internal interface IMXCryptoStore {
*/ */
fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2?
/**
* Get the current outbound group session for this encrypted room
*/
fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper?
/**
* Get the current outbound group session for this encrypted room
*/
fun storeCurrentOutboundGroupSessionForRoom(roomId: String, outboundGroupSession: OlmOutboundGroupSession?)
/** /**
* Remove an inbound group session * Remove an inbound group session
* *
@ -439,7 +451,15 @@ internal interface IMXCryptoStore {
fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent? fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent?
fun markedSessionAsShared(roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int) fun markedSessionAsShared(roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int)
fun wasSessionSharedWithUser(roomId: String?, sessionId: String, userId: String, deviceId: String): SharedSessionResult
/**
* Query for information on this session sharing history.
* @return SharedSessionResult
* if found is true then this session was initialy shared with that user|device,
* in this case chainIndex is not nullindicates the ratchet position.
* In found is false, chainIndex is null
*/
fun getSharedSessionInfo(roomId: String?, sessionId: String, userId: String, deviceId: String): SharedSessionResult
data class SharedSessionResult(val found: Boolean, val chainIndex: Int?) data class SharedSessionResult(val found: Boolean, val chainIndex: Int?)
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>

View File

@ -47,6 +47,7 @@ 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.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent 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.DeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
@ -73,6 +74,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSess
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity
@ -95,6 +97,7 @@ import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmException import org.matrix.olm.OlmException
import org.matrix.olm.OlmOutboundGroupSession
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.set import kotlin.collections.set
@ -756,6 +759,42 @@ internal class RealmCryptoStore @Inject constructor(
return inboundGroupSessionToRelease[key] return inboundGroupSessionToRelease[key]
} }
override fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper? {
return doWithRealm(realmConfiguration) { realm ->
realm.where<CryptoRoomEntity>()
.equalTo(CryptoRoomEntityFields.ROOM_ID, roomId)
.findFirst()?.outboundSessionInfo?.let { entity ->
entity.getOutboundGroupSession()?.let {
OutboundGroupSessionWrapper(
it,
entity.creationTime ?: 0
)
}
}
}
}
override fun storeCurrentOutboundGroupSessionForRoom(roomId: String, outboundGroupSession: OlmOutboundGroupSession?) {
// we can do this async, as it's just for restoring on next launch
// the olmdevice is caching the active instance
// this is called for each sent message (so not high frequency), thus we can use basic realm async without
// risk of reaching max async operation limit?
doRealmTransactionAsync(realmConfiguration) { realm ->
CryptoRoomEntity.getById(realm, roomId)?.let { entity ->
// we should delete existing outbound session info if any
entity.outboundSessionInfo?.deleteFromRealm()
if (outboundGroupSession != null) {
val info = realm.createObject(OutboundGroupSessionInfoEntity::class.java).apply {
creationTime = System.currentTimeMillis()
putOutboundGroupSession(outboundGroupSession)
}
entity.outboundSessionInfo = info
}
}
}
}
/** /**
* Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2, * Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2,
* so there is no need to use or update `inboundGroupSessionToRelease` for native memory management * so there is no need to use or update `inboundGroupSessionToRelease` for native memory management
@ -1645,7 +1684,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun wasSessionSharedWithUser(roomId: String?, sessionId: String, userId: String, deviceId: String): IMXCryptoStore.SharedSessionResult { override fun getSharedSessionInfo(roomId: String?, sessionId: String, userId: String, deviceId: String): IMXCryptoStore.SharedSessionResult {
return doWithRealm(realmConfiguration) { realm -> return doWithRealm(realmConfiguration) { realm ->
SharedSessionEntity.get(realm, roomId, sessionId, userId, deviceId)?.let { SharedSessionEntity.get(realm, roomId, sessionId, userId, deviceId)?.let {
IMXCryptoStore.SharedSessionResult(true, it.chainIndex) IMXCryptoStore.SharedSessionResult(true, it.chainIndex)

View File

@ -44,6 +44,7 @@ import org.matrix.android.sdk.internal.di.SerializeNulls
import io.realm.DynamicRealm import io.realm.DynamicRealm
import io.realm.RealmMigration import io.realm.RealmMigration
import io.realm.RealmObjectSchema import io.realm.RealmObjectSchema
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntityFields
import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2 import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -55,7 +56,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
// 0, 1, 2: legacy Riot-Android // 0, 1, 2: legacy Riot-Android
// 3: migrate to RiotX schema // 3: migrate to RiotX schema
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6) // 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
const val CRYPTO_STORE_SCHEMA_VERSION = 11L const val CRYPTO_STORE_SCHEMA_VERSION = 12L
} }
private fun RealmObjectSchema.addFieldIfNotExists(fieldName: String, fieldType: Class<*>): RealmObjectSchema { private fun RealmObjectSchema.addFieldIfNotExists(fieldName: String, fieldType: Class<*>): RealmObjectSchema {
@ -93,6 +94,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
if (oldVersion <= 8) migrateTo9(realm) if (oldVersion <= 8) migrateTo9(realm)
if (oldVersion <= 9) migrateTo10(realm) if (oldVersion <= 9) migrateTo10(realm)
if (oldVersion <= 10) migrateTo11(realm) if (oldVersion <= 10) migrateTo11(realm)
if (oldVersion <= 11) migrateTo12(realm)
} }
private fun migrateTo1Legacy(realm: DynamicRealm) { private fun migrateTo1Legacy(realm: DynamicRealm) {
@ -483,4 +485,16 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
realm.schema.get("CryptoMetadataEntity") realm.schema.get("CryptoMetadataEntity")
?.addField(CryptoMetadataEntityFields.DEVICE_KEYS_SENT_TO_SERVER, Boolean::class.java) ?.addField(CryptoMetadataEntityFields.DEVICE_KEYS_SENT_TO_SERVER, Boolean::class.java)
} }
// Version 12L added outbound group session persistence
private fun migrateTo12(realm: DynamicRealm) {
Timber.d("Step 11 -> 12")
val outboundEntitySchema = realm.schema.create("OutboundGroupSessionInfoEntity")
.addField(OutboundGroupSessionInfoEntityFields.SERIALIZED_OUTBOUND_SESSION_DATA, String::class.java)
.addField(OutboundGroupSessionInfoEntityFields.CREATION_TIME, Long::class.java)
.setNullable(OutboundGroupSessionInfoEntityFields.CREATION_TIME, true)
realm.schema.get("CryptoRoomEntity")
?.addRealmObjectField(CryptoRoomEntityFields.OUTBOUND_SESSION_INFO.`$`, outboundEntitySchema)
}
} }

View File

@ -33,6 +33,7 @@ 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.UserEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntity import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntity
import io.realm.annotations.RealmModule import io.realm.annotations.RealmModule
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity
/** /**
* Realm module for Crypto store classes * Realm module for Crypto store classes
@ -54,6 +55,7 @@ import io.realm.annotations.RealmModule
OutgoingGossipingRequestEntity::class, OutgoingGossipingRequestEntity::class,
MyDeviceLastSeenInfoEntity::class, MyDeviceLastSeenInfoEntity::class,
WithHeldSessionEntity::class, WithHeldSessionEntity::class,
SharedSessionEntity::class SharedSessionEntity::class,
OutboundGroupSessionInfoEntity::class
]) ])
internal class RealmCryptoStoreModule internal class RealmCryptoStoreModule

View File

@ -23,7 +23,12 @@ internal open class CryptoRoomEntity(
@PrimaryKey var roomId: String? = null, @PrimaryKey var roomId: String? = null,
var algorithm: String? = null, var algorithm: String? = null,
var shouldEncryptForInvitedMembers: Boolean? = null, var shouldEncryptForInvitedMembers: Boolean? = null,
var blacklistUnverifiedDevices: Boolean = false) var blacklistUnverifiedDevices: Boolean = false,
// Store the current outbound session for this room,
// to avoid re-create and re-share at each startup (if rotation not needed..)
// This is specific to megolm but not sure how to model it better
var outboundSessionInfo: OutboundGroupSessionInfoEntity? = null
)
: RealmObject() { : RealmObject() {
companion object companion object

View File

@ -0,0 +1,44 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.store.db.model
import io.realm.RealmObject
import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
import org.matrix.olm.OlmOutboundGroupSession
import timber.log.Timber
internal open class OutboundGroupSessionInfoEntity(
var serializedOutboundSessionData: String? = null,
var creationTime: Long? = null
) : RealmObject() {
fun getOutboundGroupSession(): OlmOutboundGroupSession? {
return try {
deserializeFromRealm(serializedOutboundSessionData)
} catch (failure: Throwable) {
Timber.e(failure, "## getOutboundGroupSession() Deserialization failure")
return null
}
}
fun putOutboundGroupSession(olmOutboundGroupSession: OlmOutboundGroupSession?) {
serializedOutboundSessionData = serializeForRealm(olmOutboundGroupSession)
}
companion object
}

View File

@ -20,7 +20,6 @@ import androidx.annotation.MainThread
import dagger.Lazy import dagger.Lazy
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.failure.GlobalError import org.matrix.android.sdk.api.failure.GlobalError
import org.matrix.android.sdk.api.pushrules.PushRuleService import org.matrix.android.sdk.api.pushrules.PushRuleService
@ -219,13 +218,13 @@ internal class DefaultSession @Inject constructor(
} }
} }
override fun clearCache(callback: MatrixCallback<Unit>) { override suspend fun clearCache() {
stopSync() stopSync()
stopAnyBackgroundSync() stopAnyBackgroundSync()
uiHandler.post { uiHandler.post {
lifecycleObservers.forEach { it.onClearCache() } lifecycleObservers.forEach { it.onClearCache() }
} }
cacheService.get().clearCache(callback) cacheService.get().clearCache()
workManagerProvider.cancelAllWorks() workManagerProvider.cancelAllWorks()
} }

View File

@ -27,7 +27,6 @@ import org.matrix.android.sdk.internal.crypto.SendGossipWorker
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker
import org.matrix.android.sdk.internal.di.MatrixComponent import org.matrix.android.sdk.internal.di.MatrixComponent
import org.matrix.android.sdk.internal.di.SessionAssistedInjectModule
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
import org.matrix.android.sdk.internal.session.account.AccountModule import org.matrix.android.sdk.internal.session.account.AccountModule
import org.matrix.android.sdk.internal.session.cache.CacheModule import org.matrix.android.sdk.internal.session.cache.CacheModule
@ -87,7 +86,6 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
TermsModule::class, TermsModule::class,
AccountDataModule::class, AccountDataModule::class,
ProfileModule::class, ProfileModule::class,
SessionAssistedInjectModule::class,
AccountModule::class, AccountModule::class,
CallModule::class, CallModule::class,
SearchModule::class, SearchModule::class,

View File

@ -16,23 +16,18 @@
package org.matrix.android.sdk.internal.session.cache package org.matrix.android.sdk.internal.session.cache
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.cache.CacheService import org.matrix.android.sdk.api.session.cache.CacheService
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import javax.inject.Inject import javax.inject.Inject
internal class DefaultCacheService @Inject constructor(@SessionDatabase internal class DefaultCacheService @Inject constructor(@SessionDatabase
private val clearCacheTask: ClearCacheTask, private val clearCacheTask: ClearCacheTask,
private val taskExecutor: TaskExecutor) : CacheService { private val taskExecutor: TaskExecutor
) : CacheService {
override fun clearCache(callback: MatrixCallback<Unit>) { override suspend fun clearCache() {
taskExecutor.cancelAll() taskExecutor.cancelAll()
clearCacheTask clearCacheTask.execute(Unit)
.configureWith {
this.callback = callback
}
.executeBy(taskExecutor)
} }
} }

View File

@ -47,22 +47,24 @@ internal object ThumbnailExtractor {
val mediaMetadataRetriever = MediaMetadataRetriever() val mediaMetadataRetriever = MediaMetadataRetriever()
try { try {
mediaMetadataRetriever.setDataSource(context, attachment.queryUri) mediaMetadataRetriever.setDataSource(context, attachment.queryUri)
val thumbnail = mediaMetadataRetriever.frameAtTime mediaMetadataRetriever.frameAtTime?.let { thumbnail ->
val outputStream = ByteArrayOutputStream()
val outputStream = ByteArrayOutputStream() thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) val thumbnailWidth = thumbnail.width
val thumbnailWidth = thumbnail.width val thumbnailHeight = thumbnail.height
val thumbnailHeight = thumbnail.height val thumbnailSize = outputStream.size()
val thumbnailSize = outputStream.size() thumbnailData = ThumbnailData(
thumbnailData = ThumbnailData( width = thumbnailWidth,
width = thumbnailWidth, height = thumbnailHeight,
height = thumbnailHeight, size = thumbnailSize.toLong(),
size = thumbnailSize.toLong(), bytes = outputStream.toByteArray(),
bytes = outputStream.toByteArray(), mimeType = MimeTypes.Jpeg
mimeType = MimeTypes.Jpeg )
) thumbnail.recycle()
thumbnail.recycle() outputStream.reset()
outputStream.reset() } ?: run {
Timber.e("Cannot extract video thumbnail at %s", attachment.queryUri.toString())
}
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Cannot extract video thumbnail") Timber.e(e, "Cannot extract video thumbnail")
} finally { } finally {

View File

@ -52,65 +52,60 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor(
val pepper = identityData.hashLookupPepper val pepper = identityData.hashLookupPepper
val hashDetailResponse = if (pepper == null) { val hashDetailResponse = if (pepper == null) {
// We need to fetch the hash details first // We need to fetch the hash details first
fetchAndStoreHashDetails(identityAPI) fetchHashDetails(identityAPI)
.also { identityStore.setHashDetails(it) }
} else { } else {
IdentityHashDetailResponse(pepper, identityData.hashLookupAlgorithm) IdentityHashDetailResponse(pepper, identityData.hashLookupAlgorithm)
} }
if (hashDetailResponse.algorithms.contains("sha256").not()) { if (hashDetailResponse.algorithms.contains(IdentityHashDetailResponse.ALGORITHM_SHA256).not()) {
// TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it // TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it
// Also, what we have in cache could be outdated, the identity server maybe now supports sha256 // Also, what we have in cache could be outdated, the identity server maybe now supports sha256
throw IdentityServiceError.BulkLookupSha256NotSupported throw IdentityServiceError.BulkLookupSha256NotSupported
} }
val hashedAddresses = withOlmUtility { olmUtility -> val lookUpData = lookUpInternal(identityAPI, params.threePids, hashDetailResponse, true)
params.threePids.map { threePid ->
base64ToBase64Url(
olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT)
+ " " + threePid.toMedium() + " " + hashDetailResponse.pepper)
)
}
}
val identityLookUpV2Response = lookUpInternal(identityAPI, hashedAddresses, hashDetailResponse, true)
// Convert back to List<FoundThreePid> // Convert back to List<FoundThreePid>
return handleSuccess(params.threePids, hashedAddresses, identityLookUpV2Response) return handleSuccess(params.threePids, lookUpData)
} }
data class LookUpData(
val hashedAddresses: List<String>,
val identityLookUpResponse: IdentityLookUpResponse
)
private suspend fun lookUpInternal(identityAPI: IdentityAPI, private suspend fun lookUpInternal(identityAPI: IdentityAPI,
hashedAddresses: List<String>, threePids: List<ThreePid>,
hashDetailResponse: IdentityHashDetailResponse, hashDetailResponse: IdentityHashDetailResponse,
canRetry: Boolean): IdentityLookUpResponse { canRetry: Boolean): LookUpData {
val hashedAddresses = getHashedAddresses(threePids, hashDetailResponse.pepper)
return try { return try {
executeRequest(null) { LookUpData(hashedAddresses,
apiCall = identityAPI.lookup(IdentityLookUpParams( executeRequest(null) {
hashedAddresses, apiCall = identityAPI.lookup(IdentityLookUpParams(
IdentityHashDetailResponse.ALGORITHM_SHA256, hashedAddresses,
hashDetailResponse.pepper IdentityHashDetailResponse.ALGORITHM_SHA256,
)) hashDetailResponse.pepper
} ))
})
} catch (failure: Throwable) { } catch (failure: Throwable) {
// Catch invalid hash pepper and retry // Catch invalid hash pepper and retry
if (canRetry && failure is Failure.ServerError && failure.error.code == MatrixError.M_INVALID_PEPPER) { if (canRetry && failure is Failure.ServerError && failure.error.code == MatrixError.M_INVALID_PEPPER) {
// This is not documented, but the error can contain the new pepper! // This is not documented, but the error can contain the new pepper!
if (!failure.error.newLookupPepper.isNullOrEmpty()) { val newHashDetailResponse = if (!failure.error.newLookupPepper.isNullOrEmpty()) {
// Store it and use it right now // Store it and use it right now
hashDetailResponse.copy(pepper = failure.error.newLookupPepper) hashDetailResponse.copy(pepper = failure.error.newLookupPepper)
.also { identityStore.setHashDetails(it) }
.let { lookUpInternal(identityAPI, hashedAddresses, it, false /* Avoid infinite loop */) }
} else { } else {
// Retrieve the new hash details // Retrieve the new hash details
val newHashDetailResponse = fetchAndStoreHashDetails(identityAPI) fetchHashDetails(identityAPI)
if (hashDetailResponse.algorithms.contains(IdentityHashDetailResponse.ALGORITHM_SHA256).not()) {
// TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it
// Also, what we have in cache is maybe outdated, the identity server maybe now support sha256
throw IdentityServiceError.BulkLookupSha256NotSupported
}
lookUpInternal(identityAPI, hashedAddresses, newHashDetailResponse, false /* Avoid infinite loop */)
} }
.also { identityStore.setHashDetails(it) }
if (newHashDetailResponse.algorithms.contains(IdentityHashDetailResponse.ALGORITHM_SHA256).not()) {
// TODO We should ask the user if he is ok to send their 3Pid in clear, but for the moment we do not do it
throw IdentityServiceError.BulkLookupSha256NotSupported
}
lookUpInternal(identityAPI, threePids, newHashDetailResponse, false /* Avoid infinite loop */)
} else { } else {
// Other error // Other error
throw failure throw failure
@ -118,16 +113,29 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor(
} }
} }
private suspend fun fetchAndStoreHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse { private fun getHashedAddresses(threePids: List<ThreePid>, pepper: String): List<String> {
return executeRequest<IdentityHashDetailResponse>(null) { return withOlmUtility { olmUtility ->
apiCall = identityAPI.hashDetails() threePids.map { threePid ->
base64ToBase64Url(
olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT)
+ " " + threePid.toMedium() + " " + pepper)
)
}
} }
.also { identityStore.setHashDetails(it) }
} }
private fun handleSuccess(threePids: List<ThreePid>, hashedAddresses: List<String>, identityLookUpResponse: IdentityLookUpResponse): List<FoundThreePid> { private suspend fun fetchHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse {
return identityLookUpResponse.mappings.keys.map { hashedAddress -> return executeRequest(null) {
FoundThreePid(threePids[hashedAddresses.indexOf(hashedAddress)], identityLookUpResponse.mappings[hashedAddress] ?: error("")) apiCall = identityAPI.hashDetails()
}
}
private fun handleSuccess(threePids: List<ThreePid>, lookupData: LookUpData): List<FoundThreePid> {
return lookupData.identityLookUpResponse.mappings.keys.map { hashedAddress ->
FoundThreePid(
threePids[lookupData.hashedAddresses.indexOf(hashedAddress)],
lookupData.identityLookUpResponse.mappings[hashedAddress] ?: error("")
)
} }
} }
} }

View File

@ -18,9 +18,10 @@ package org.matrix.android.sdk.internal.session.media
import androidx.collection.LruCache import androidx.collection.LruCache
import org.matrix.android.sdk.api.cache.CacheStrategy import org.matrix.android.sdk.api.cache.CacheStrategy
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.media.MediaService import org.matrix.android.sdk.api.session.media.MediaService
import org.matrix.android.sdk.api.session.media.PreviewUrlData import org.matrix.android.sdk.api.session.media.PreviewUrlData
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLatestEventId
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.util.getOrPut import org.matrix.android.sdk.internal.util.getOrPut
import javax.inject.Inject import javax.inject.Inject
@ -34,11 +35,12 @@ internal class DefaultMediaService @Inject constructor(
// Cache of extracted URLs // Cache of extracted URLs
private val extractedUrlsCache = LruCache<String, List<String>>(1_000) private val extractedUrlsCache = LruCache<String, List<String>>(1_000)
override fun extractUrls(event: Event): List<String> { override fun extractUrls(event: TimelineEvent): List<String> {
return extractedUrlsCache.getOrPut(event.cacheKey()) { urlsExtractor.extract(event) } return extractedUrlsCache.getOrPut(event.cacheKey()) { urlsExtractor.extract(event) }
} }
private fun Event.cacheKey() = "${eventId ?: ""}-${roomId ?: ""}" // Use the id of the latest Event edition
private fun TimelineEvent.cacheKey() = "${getLatestEventId()}-${root.roomId ?: ""}"
override suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict { override suspend fun getRawPreviewUrl(url: String, timestamp: Long?): JsonDict {
return getRawPreviewUrlTask.execute(GetRawPreviewUrlTask.Params(url, timestamp)) return getRawPreviewUrlTask.execute(GetRawPreviewUrlTask.Params(url, timestamp))

View File

@ -17,21 +17,19 @@
package org.matrix.android.sdk.internal.session.media package org.matrix.android.sdk.internal.session.media
import android.util.Patterns import android.util.Patterns
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.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import javax.inject.Inject import javax.inject.Inject
internal class UrlsExtractor @Inject constructor() { internal class UrlsExtractor @Inject constructor() {
// Sadly Patterns.WEB_URL_WITH_PROTOCOL is not public so filter the protocol later // Sadly Patterns.WEB_URL_WITH_PROTOCOL is not public so filter the protocol later
private val urlRegex = Patterns.WEB_URL.toRegex() private val urlRegex = Patterns.WEB_URL.toRegex()
fun extract(event: Event): List<String> { fun extract(event: TimelineEvent): List<String> {
return event.takeIf { it.getClearType() == EventType.MESSAGE } return event.takeIf { it.root.getClearType() == EventType.MESSAGE }
?.getClearContent() ?.getLastMessageContent()
?.toModel<MessageContent>()
?.takeIf { ?.takeIf {
it.msgType == MessageType.MSGTYPE_TEXT it.msgType == MessageType.MSGTYPE_TEXT
|| it.msgType == MessageType.MSGTYPE_NOTICE || it.msgType == MessageType.MSGTYPE_NOTICE

View File

@ -16,8 +16,9 @@
package org.matrix.android.sdk.internal.session.room.alias package org.matrix.android.sdk.internal.session.room.alias
import com.squareup.inject.assisted.Assisted import dagger.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import org.matrix.android.sdk.api.session.room.alias.AliasService import org.matrix.android.sdk.api.session.room.alias.AliasService
internal class DefaultAliasService @AssistedInject constructor( internal class DefaultAliasService @AssistedInject constructor(
@ -26,9 +27,9 @@ internal class DefaultAliasService @AssistedInject constructor(
private val addRoomAliasTask: AddRoomAliasTask private val addRoomAliasTask: AddRoomAliasTask
) : AliasService { ) : AliasService {
@AssistedInject.Factory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String): AliasService fun create(roomId: String): DefaultAliasService
} }
override suspend fun getRoomAliases(): List<String> { override suspend fun getRoomAliases(): List<String> {

View File

@ -16,8 +16,9 @@
package org.matrix.android.sdk.internal.session.room.call package org.matrix.android.sdk.internal.session.room.call
import com.squareup.inject.assisted.Assisted import dagger.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.room.call.RoomCallService import org.matrix.android.sdk.api.session.room.call.RoomCallService
import org.matrix.android.sdk.internal.session.room.RoomGetter import org.matrix.android.sdk.internal.session.room.RoomGetter
@ -27,9 +28,9 @@ internal class DefaultRoomCallService @AssistedInject constructor(
private val roomGetter: RoomGetter private val roomGetter: RoomGetter
) : RoomCallService { ) : RoomCallService {
@AssistedInject.Factory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String): RoomCallService fun create(roomId: String): DefaultRoomCallService
} }
override fun canStartCall(): Boolean { override fun canStartCall(): Boolean {

View File

@ -17,8 +17,9 @@
package org.matrix.android.sdk.internal.session.room.draft package org.matrix.android.sdk.internal.session.room.draft
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.squareup.inject.assisted.Assisted import dagger.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.session.room.send.DraftService import org.matrix.android.sdk.api.session.room.send.DraftService
import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.session.room.send.UserDraft
@ -30,9 +31,9 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private
private val coroutineDispatchers: MatrixCoroutineDispatchers private val coroutineDispatchers: MatrixCoroutineDispatchers
) : DraftService { ) : DraftService {
@AssistedInject.Factory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String): DraftService fun create(roomId: String): DefaultDraftService
} }
/** /**

View File

@ -17,8 +17,9 @@
package org.matrix.android.sdk.internal.session.room.membership package org.matrix.android.sdk.internal.session.room.membership
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.squareup.inject.assisted.Assisted import dagger.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
@ -58,9 +59,9 @@ internal class DefaultMembershipService @AssistedInject constructor(
private val userId: String private val userId: String
) : MembershipService { ) : MembershipService {
@AssistedInject.Factory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String): MembershipService fun create(roomId: String): DefaultMembershipService
} }
override fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback<Unit>): Cancelable { override fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback<Unit>): Cancelable {

View File

@ -55,7 +55,11 @@ internal class DefaultJoinRoomTask @Inject constructor(
roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.Joining) roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.Joining)
val joinRoomResponse = try { val joinRoomResponse = try {
executeRequest<JoinRoomResponse>(globalErrorReceiver) { executeRequest<JoinRoomResponse>(globalErrorReceiver) {
apiCall = roomAPI.join(params.roomIdOrAlias, params.viaServers, mapOf("reason" to params.reason)) apiCall = roomAPI.join(
roomIdOrAlias = params.roomIdOrAlias,
viaServers = params.viaServers.take(3),
params = mapOf("reason" to params.reason)
)
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.FailedJoining(failure)) roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.FailedJoining(failure))

View File

@ -18,8 +18,9 @@ package org.matrix.android.sdk.internal.session.room.notification
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted import dagger.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.pushrules.RuleScope import org.matrix.android.sdk.api.pushrules.RuleScope
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
@ -33,9 +34,9 @@ internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted
@SessionDatabase private val monarchy: Monarchy) @SessionDatabase private val monarchy: Monarchy)
: RoomPushRuleService { : RoomPushRuleService {
@AssistedInject.Factory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String): RoomPushRuleService fun create(roomId: String): DefaultRoomPushRuleService
} }
override fun getLiveRoomNotificationState(): LiveData<RoomNotificationState> { override fun getLiveRoomNotificationState(): LiveData<RoomNotificationState> {

View File

@ -18,8 +18,9 @@ package org.matrix.android.sdk.internal.session.room.read
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted import dagger.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.ReadReceipt
@ -46,9 +47,9 @@ internal class DefaultReadService @AssistedInject constructor(
@UserId private val userId: String @UserId private val userId: String
) : ReadService { ) : ReadService {
@AssistedInject.Factory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String): ReadService fun create(roomId: String): DefaultReadService
} }
override fun markAsRead(params: ReadService.MarkAsReadParams, callback: MatrixCallback<Unit>) { override fun markAsRead(params: ReadService.MarkAsReadParams, callback: MatrixCallback<Unit>) {

View File

@ -17,8 +17,9 @@ package org.matrix.android.sdk.internal.session.room.relation
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted import dagger.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
@ -56,9 +57,9 @@ internal class DefaultRelationService @AssistedInject constructor(
private val taskExecutor: TaskExecutor) private val taskExecutor: TaskExecutor)
: RelationService { : RelationService {
@AssistedInject.Factory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String): RelationService fun create(roomId: String): DefaultRelationService
} }
override fun sendReaction(targetEventId: String, reaction: String): Cancelable { override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
@ -140,7 +141,7 @@ internal class DefaultRelationService @AssistedInject constructor(
} }
override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) { override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
val params = FetchEditHistoryTask.Params(roomId, cryptoSessionInfoProvider.isRoomEncrypted(roomId), eventId) val params = FetchEditHistoryTask.Params(roomId, eventId)
fetchEditHistoryTask fetchEditHistoryTask
.configureWith(params) { .configureWith(params) {
this.callback = callback this.callback = callback

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.relation
import org.matrix.android.sdk.api.session.events.model.Event 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.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.RoomAPI
@ -25,25 +26,27 @@ import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject import javax.inject.Inject
internal interface FetchEditHistoryTask : Task<FetchEditHistoryTask.Params, List<Event>> { internal interface FetchEditHistoryTask : Task<FetchEditHistoryTask.Params, List<Event>> {
data class Params( data class Params(
val roomId: String, val roomId: String,
val isRoomEncrypted: Boolean,
val eventId: String val eventId: String
) )
} }
internal class DefaultFetchEditHistoryTask @Inject constructor( internal class DefaultFetchEditHistoryTask @Inject constructor(
private val roomAPI: RoomAPI, private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver private val globalErrorReceiver: GlobalErrorReceiver,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider
) : FetchEditHistoryTask { ) : FetchEditHistoryTask {
override suspend fun execute(params: FetchEditHistoryTask.Params): List<Event> { override suspend fun execute(params: FetchEditHistoryTask.Params): List<Event> {
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
val response = executeRequest<RelationsResponse>(globalErrorReceiver) { val response = executeRequest<RelationsResponse>(globalErrorReceiver) {
apiCall = roomAPI.getRelations(params.roomId, apiCall = roomAPI.getRelations(
params.eventId, roomId = params.roomId,
RelationType.REPLACE, eventId = params.eventId,
if (params.isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE) relationType = RelationType.REPLACE,
eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE
)
} }
val events = response.chunks.toMutableList() val events = response.chunks.toMutableList()

View File

@ -16,17 +16,18 @@
package org.matrix.android.sdk.internal.session.room.reporting package org.matrix.android.sdk.internal.session.room.reporting
import com.squareup.inject.assisted.Assisted import dagger.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import org.matrix.android.sdk.api.session.room.reporting.ReportingService import org.matrix.android.sdk.api.session.room.reporting.ReportingService
internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String, internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String,
private val reportContentTask: ReportContentTask private val reportContentTask: ReportContentTask
) : ReportingService { ) : ReportingService {
@AssistedInject.Factory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String): ReportingService fun create(roomId: String): DefaultReportingService
} }
override suspend fun reportContent(eventId: String, score: Int, reason: String) { override suspend fun reportContent(eventId: String, score: Int, reason: String) {

View File

@ -21,8 +21,9 @@ import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequest
import androidx.work.Operation import androidx.work.Operation
import com.squareup.inject.assisted.Assisted import dagger.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
@ -71,9 +72,9 @@ internal class DefaultSendService @AssistedInject constructor(
private val cancelSendTracker: CancelSendTracker private val cancelSendTracker: CancelSendTracker
) : SendService { ) : SendService {
@AssistedInject.Factory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String): SendService fun create(roomId: String): DefaultSendService
} }
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor() private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()

View File

@ -18,8 +18,9 @@ package org.matrix.android.sdk.internal.session.room.state
import android.net.Uri import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.squareup.inject.assisted.Assisted import dagger.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event 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.EventType
@ -35,18 +36,16 @@ import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.api.util.MimeTypes
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.content.FileUploader import org.matrix.android.sdk.internal.session.content.FileUploader
import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasTask
internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String, internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String,
private val stateEventDataSource: StateEventDataSource, private val stateEventDataSource: StateEventDataSource,
private val sendStateTask: SendStateTask, private val sendStateTask: SendStateTask,
private val fileUploader: FileUploader, private val fileUploader: FileUploader
private val addRoomAliasTask: AddRoomAliasTask
) : StateService { ) : StateService {
@AssistedInject.Factory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String): StateService fun create(roomId: String): DefaultStateService
} }
override fun getStateEvent(eventType: String, stateKey: QueryStringValue): Event? { override fun getStateEvent(eventType: String, stateKey: QueryStringValue): Event? {
@ -74,11 +73,19 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
roomId = roomId, roomId = roomId,
stateKey = stateKey, stateKey = stateKey,
eventType = eventType, eventType = eventType,
body = body body = body.toSafeJson(eventType)
) )
sendStateTask.execute(params) sendStateTask.execute(params)
} }
private fun JsonDict.toSafeJson(eventType: String): JsonDict {
// Safe treatment for PowerLevelContent
return when (eventType) {
EventType.STATE_ROOM_POWER_LEVELS -> toSafePowerLevelsContentDict()
else -> this
}
}
override suspend fun updateTopic(topic: String) { override suspend fun updateTopic(topic: String) {
sendStateEvent( sendStateEvent(
eventType = EventType.STATE_ROOM_TOPIC, eventType = EventType.STATE_ROOM_TOPIC,

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.state
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.api.util.JsonDict
@JsonClass(generateAdapter = true)
internal data class SerializablePowerLevelsContent(
@Json(name = "ban") val ban: Int = Role.Moderator.value,
@Json(name = "kick") val kick: Int = Role.Moderator.value,
@Json(name = "invite") val invite: Int = Role.Moderator.value,
@Json(name = "redact") val redact: Int = Role.Moderator.value,
@Json(name = "events_default") val eventsDefault: Int = Role.Default.value,
@Json(name = "events") val events: Map<String, Int> = emptyMap(),
@Json(name = "users_default") val usersDefault: Int = Role.Default.value,
@Json(name = "users") val users: Map<String, Int> = emptyMap(),
@Json(name = "state_default") val stateDefault: Int = Role.Moderator.value,
// `Int` is the diff here (instead of `Any`)
@Json(name = "notifications") val notifications: Map<String, Int> = emptyMap()
)
internal fun JsonDict.toSafePowerLevelsContentDict(): JsonDict {
return toModel<PowerLevelsContent>()
?.let { content ->
SerializablePowerLevelsContent(
ban = content.ban,
kick = content.kick,
invite = content.invite,
redact = content.redact,
eventsDefault = content.eventsDefault,
events = content.events,
usersDefault = content.usersDefault,
users = content.users,
stateDefault = content.stateDefault,
notifications = content.notifications.mapValues { content.notificationLevel(it.key) }
)
}
?.toContent()
?: emptyMap()
}

View File

@ -16,8 +16,9 @@
package org.matrix.android.sdk.internal.session.room.tags package org.matrix.android.sdk.internal.session.room.tags
import com.squareup.inject.assisted.Assisted import dagger.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import org.matrix.android.sdk.api.session.room.tags.TagsService import org.matrix.android.sdk.api.session.room.tags.TagsService
internal class DefaultTagsService @AssistedInject constructor( internal class DefaultTagsService @AssistedInject constructor(
@ -26,9 +27,9 @@ internal class DefaultTagsService @AssistedInject constructor(
private val deleteTagFromRoomTask: DeleteTagFromRoomTask private val deleteTagFromRoomTask: DeleteTagFromRoomTask
) : TagsService { ) : TagsService {
@AssistedInject.Factory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String): TagsService fun create(roomId: String): DefaultTagsService
} }
override suspend fun addTag(tag: String, order: Double?) { override suspend fun addTag(tag: String, order: Double?) {

View File

@ -18,8 +18,9 @@ package org.matrix.android.sdk.internal.session.room.timeline
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted import dagger.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Sort import io.realm.Sort
import io.realm.kotlin.where import io.realm.kotlin.where
@ -55,9 +56,9 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
private val loadRoomMembersTask: LoadRoomMembersTask private val loadRoomMembersTask: LoadRoomMembersTask
) : TimelineService { ) : TimelineService {
@AssistedInject.Factory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String): TimelineService fun create(roomId: String): DefaultTimelineService
} }
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline { override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {

View File

@ -21,16 +21,34 @@ import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class EventContextResponse( internal data class EventContextResponse(
/**
* Details of the requested event.
*/
@Json(name = "event") val event: Event, @Json(name = "event") val event: Event,
/**
* A token that can be used to paginate backwards with.
*/
@Json(name = "start") override val start: String? = null, @Json(name = "start") override val start: String? = null,
@Json(name = "events_before") val eventsBefore: List<Event> = emptyList(), /**
@Json(name = "events_after") val eventsAfter: List<Event> = emptyList(), * A list of room events that happened just before the requested event, in reverse-chronological order.
*/
@Json(name = "events_before") val eventsBefore: List<Event>? = null,
/**
* A list of room events that happened just after the requested event, in chronological order.
*/
@Json(name = "events_after") val eventsAfter: List<Event>? = null,
/**
* A token that can be used to paginate forwards with.
*/
@Json(name = "end") override val end: String? = null, @Json(name = "end") override val end: String? = null,
@Json(name = "state") override val stateEvents: List<Event> = emptyList() /**
* The state of the room at the last event returned.
*/
@Json(name = "state") override val stateEvents: List<Event>? = null
) : TokenChunkEvent { ) : TokenChunkEvent {
override val events: List<Event> by lazy { override val events: List<Event> by lazy {
eventsAfter.reversed() + listOf(event) + eventsBefore eventsAfter.orEmpty().reversed() + event + eventsBefore.orEmpty()
} }
} }

View File

@ -22,8 +22,28 @@ import org.matrix.android.sdk.api.session.events.model.Event
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class PaginationResponse( internal data class PaginationResponse(
/**
* The token the pagination starts from. If dir=b this will be the token supplied in from.
*/
@Json(name = "start") override val start: String? = null, @Json(name = "start") override val start: String? = null,
/**
* The token the pagination ends at. If dir=b this token should be used again to request even earlier events.
*/
@Json(name = "end") override val end: String? = null, @Json(name = "end") override val end: String? = null,
@Json(name = "chunk") override val events: List<Event> = emptyList(), /**
@Json(name = "state") override val stateEvents: List<Event> = emptyList() * A list of room events. The order depends on the dir parameter. For dir=b events will be in
) : TokenChunkEvent * reverse-chronological order, for dir=f in chronological order, so that events start at the from point.
*/
@Json(name = "chunk") val chunk: List<Event>? = null,
/**
* A list of state events relevant to showing the chunk. For example, if lazy_load_members is enabled
* in the filter then this may contain the membership events for the senders of events in the chunk.
*
* Unless include_redundant_members is true, the server may remove membership events which would have
* already been sent to the client in prior calls to this endpoint, assuming the membership of those members has not changed.
*/
@Json(name = "state") override val stateEvents: List<Event>? = null
) : TokenChunkEvent {
override val events: List<Event>
get() = chunk.orEmpty()
}

View File

@ -22,7 +22,7 @@ internal interface TokenChunkEvent {
val start: String? val start: String?
val end: String? val end: String?
val events: List<Event> val events: List<Event>
val stateEvents: List<Event> val stateEvents: List<Event>?
fun hasMore() = start != end fun hasMore() = start != end
} }

View File

@ -156,7 +156,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
} }
} }
return if (receivedChunk.events.isEmpty()) { return if (receivedChunk.events.isEmpty()) {
if (receivedChunk.start != receivedChunk.end) { if (receivedChunk.hasMore()) {
Result.SHOULD_FETCH_MORE Result.SHOULD_FETCH_MORE
} else { } else {
Result.REACHED_END Result.REACHED_END
@ -196,7 +196,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
for (stateEvent in stateEvents) { stateEvents?.forEach { stateEvent ->
val ageLocalTs = stateEvent.unsignedData?.age?.let { now - it } val ageLocalTs = stateEvent.unsignedData?.age?.let { now - it }
val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
currentChunk.addStateEvent(roomId, stateEventEntity, direction) currentChunk.addStateEvent(roomId, stateEventEntity, direction)
@ -205,9 +205,9 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
} }
} }
val eventIds = ArrayList<String>(eventList.size) val eventIds = ArrayList<String>(eventList.size)
for (event in eventList) { eventList.forEach { event ->
if (event.eventId == null || event.senderId == null) { if (event.eventId == null || event.senderId == null) {
continue return@forEach
} }
val ageLocalTs = event.unsignedData?.age?.let { now - it } val ageLocalTs = event.unsignedData?.age?.let { now - it }
eventIds.add(event.eventId) eventIds.add(event.eventId)

View File

@ -17,8 +17,9 @@
package org.matrix.android.sdk.internal.session.room.typing package org.matrix.android.sdk.internal.session.room.typing
import android.os.SystemClock import android.os.SystemClock
import com.squareup.inject.assisted.Assisted import dagger.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.typing.TypingService
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
@ -38,9 +39,9 @@ internal class DefaultTypingService @AssistedInject constructor(
private val sendTypingTask: SendTypingTask private val sendTypingTask: SendTypingTask
) : TypingService { ) : TypingService {
@AssistedInject.Factory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String): TypingService fun create(roomId: String): DefaultTypingService
} }
private var currentTask: Cancelable? = null private var currentTask: Cancelable? = null

View File

@ -16,8 +16,9 @@
package org.matrix.android.sdk.internal.session.room.uploads package org.matrix.android.sdk.internal.session.room.uploads
import com.squareup.inject.assisted.Assisted import dagger.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.room.uploads.GetUploadsResult import org.matrix.android.sdk.api.session.room.uploads.GetUploadsResult
import org.matrix.android.sdk.api.session.room.uploads.UploadsService import org.matrix.android.sdk.api.session.room.uploads.UploadsService
@ -28,9 +29,9 @@ internal class DefaultUploadsService @AssistedInject constructor(
private val cryptoService: CryptoService private val cryptoService: CryptoService
) : UploadsService { ) : UploadsService {
@AssistedInject.Factory @AssistedFactory
interface Factory { interface Factory {
fun create(roomId: String): UploadsService fun create(roomId: String): DefaultUploadsService
} }
override suspend fun getUploads(numberOfEvents: Int, since: String?): GetUploadsResult { override suspend fun getUploads(numberOfEvents: Int, since: String?): GetUploadsResult {

View File

@ -56,8 +56,8 @@ internal class DefaultGetUploadsTask @Inject constructor(
private val roomAPI: RoomAPI, private val roomAPI: RoomAPI,
private val tokenStore: SyncTokenStore, private val tokenStore: SyncTokenStore,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val globalErrorReceiver: GlobalErrorReceiver) private val globalErrorReceiver: GlobalErrorReceiver
: GetUploadsTask { ) : GetUploadsTask {
override suspend fun execute(params: GetUploadsTask.Params): GetUploadsResult { override suspend fun execute(params: GetUploadsTask.Params): GetUploadsResult {
val result: GetUploadsResult val result: GetUploadsResult

View File

@ -16,45 +16,25 @@
package org.matrix.android.sdk.internal.session.signout package org.matrix.android.sdk.internal.session.signout
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.session.signout.SignOutService import org.matrix.android.sdk.api.session.signout.SignOutService
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.auth.SessionParamsStore import org.matrix.android.sdk.internal.auth.SessionParamsStore
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.task.launchToCallback
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import javax.inject.Inject import javax.inject.Inject
internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask, internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
private val signInAgainTask: SignInAgainTask, private val signInAgainTask: SignInAgainTask,
private val sessionParamsStore: SessionParamsStore, private val sessionParamsStore: SessionParamsStore
private val coroutineDispatchers: MatrixCoroutineDispatchers, ) : SignOutService {
private val taskExecutor: TaskExecutor) : SignOutService {
override fun signInAgain(password: String, override suspend fun signInAgain(password: String) {
callback: MatrixCallback<Unit>): Cancelable { signInAgainTask.execute(SignInAgainTask.Params(password))
return signInAgainTask
.configureWith(SignInAgainTask.Params(password)) {
this.callback = callback
}
.executeBy(taskExecutor)
} }
override fun updateCredentials(credentials: Credentials, override suspend fun updateCredentials(credentials: Credentials) {
callback: MatrixCallback<Unit>): Cancelable { sessionParamsStore.updateCredentials(credentials)
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
sessionParamsStore.updateCredentials(credentials)
}
} }
override fun signOut(signOutFromHomeserver: Boolean, override suspend fun signOut(signOutFromHomeserver: Boolean) {
callback: MatrixCallback<Unit>): Cancelable { return signOutTask.execute(SignOutTask.Params(signOutFromHomeserver))
return signOutTask
.configureWith(SignOutTask.Params(signOutFromHomeserver)) {
this.callback = callback
}
.executeBy(taskExecutor)
} }
} }

View File

@ -50,8 +50,9 @@ abstract class SyncService : Service() {
private var sessionId: String? = null private var sessionId: String? = null
private var mIsSelfDestroyed: Boolean = false private var mIsSelfDestroyed: Boolean = false
private var syncTimeoutSeconds: Int = 6 private var syncTimeoutSeconds: Int = getDefaultSyncTimeoutSeconds()
private var syncDelaySeconds: Int = 60 private var syncDelaySeconds: Int = getDefaultSyncDelaySeconds()
private var periodic: Boolean = false private var periodic: Boolean = false
private var preventReschedule: Boolean = false private var preventReschedule: Boolean = false
@ -119,7 +120,11 @@ abstract class SyncService : Service() {
serviceScope.coroutineContext.cancelChildren() serviceScope.coroutineContext.cancelChildren()
if (!preventReschedule && periodic && sessionId != null && backgroundDetectionObserver.isInBackground) { if (!preventReschedule && periodic && sessionId != null && backgroundDetectionObserver.isInBackground) {
Timber.d("## Sync: Reschedule service in $syncDelaySeconds sec") Timber.d("## Sync: Reschedule service in $syncDelaySeconds sec")
onRescheduleAsked(sessionId ?: "", false, syncTimeoutSeconds, syncDelaySeconds) onRescheduleAsked(
sessionId = sessionId ?: "",
syncTimeoutSeconds = syncTimeoutSeconds,
syncDelaySeconds = syncDelaySeconds
)
} }
super.onDestroy() super.onDestroy()
} }
@ -166,15 +171,22 @@ abstract class SyncService : Service() {
} }
if (throwable is Failure.NetworkConnection) { if (throwable is Failure.NetworkConnection) {
// Timeout is not critical, so retry as soon as possible. // Timeout is not critical, so retry as soon as possible.
val retryDelay = if (isInitialSync || throwable.cause is SocketTimeoutException) { if (throwable.cause is SocketTimeoutException) {
0 // For big accounts, computing sync response can take time, but Synapse will cache the
} else { // result for the next request. So keep retrying in loop
syncDelaySeconds Timber.w("Timeout during sync, retry in loop")
doSync()
return
} }
// Network might be off, no need to reschedule endless alarms :/ // Network might be off, no need to reschedule endless alarms :/
preventReschedule = true preventReschedule = true
// Instead start a work to restart background sync when network is on // Instead start a work to restart background sync when network is on
onNetworkError(sessionId ?: "", isInitialSync, syncTimeoutSeconds, retryDelay) onNetworkError(
sessionId = sessionId ?: "",
syncTimeoutSeconds = syncTimeoutSeconds,
syncDelaySeconds = syncDelaySeconds,
isPeriodic = periodic
)
} }
// JobCancellation could be caught here when onDestroy cancels the coroutine context // JobCancellation could be caught here when onDestroy cancels the coroutine context
if (isRunning.get()) stopMe() if (isRunning.get()) stopMe()
@ -188,8 +200,8 @@ abstract class SyncService : Service() {
} }
val matrix = Matrix.getInstance(applicationContext) val matrix = Matrix.getInstance(applicationContext)
val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false
syncTimeoutSeconds = intent.getIntExtra(EXTRA_TIMEOUT_SECONDS, 6) syncTimeoutSeconds = intent.getIntExtra(EXTRA_TIMEOUT_SECONDS, getDefaultSyncTimeoutSeconds())
syncDelaySeconds = intent.getIntExtra(EXTRA_DELAY_SECONDS, 60) syncDelaySeconds = intent.getIntExtra(EXTRA_DELAY_SECONDS, getDefaultSyncDelaySeconds())
try { try {
val sessionComponent = matrix.sessionManager.getSessionComponent(safeSessionId) val sessionComponent = matrix.sessionManager.getSessionComponent(safeSessionId)
?: throw IllegalStateException("## Sync: You should have a session to make it work") ?: throw IllegalStateException("## Sync: You should have a session to make it work")
@ -208,11 +220,15 @@ abstract class SyncService : Service() {
} }
} }
abstract fun getDefaultSyncTimeoutSeconds(): Int
abstract fun getDefaultSyncDelaySeconds(): Int
abstract fun onStart(isInitialSync: Boolean) abstract fun onStart(isInitialSync: Boolean)
abstract fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, timeout: Int, delay: Int) abstract fun onRescheduleAsked(sessionId: String, syncTimeoutSeconds: Int, syncDelaySeconds: Int)
abstract fun onNetworkError(sessionId: String, isInitialSync: Boolean, timeout: Int, delay: Int) abstract fun onNetworkError(sessionId: String, syncTimeoutSeconds: Int, syncDelaySeconds: Int, isPeriodic: Boolean)
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
return null return null

View File

@ -20,6 +20,7 @@ import android.content.Context
import androidx.work.ListenableWorker import androidx.work.ListenableWorker
import androidx.work.WorkerFactory import androidx.work.WorkerFactory
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
@ -32,6 +33,8 @@ class MatrixWorkerFactory @Inject constructor(
workerClassName: String, workerClassName: String,
workerParameters: WorkerParameters workerParameters: WorkerParameters
): ListenableWorker? { ): ListenableWorker? {
Timber.d("MatrixWorkerFactory.createWorker for $workerClassName")
val foundEntry = val foundEntry =
workerFactories.entries.find { Class.forName(workerClassName).isAssignableFrom(it.key) } workerFactories.entries.find { Class.forName(workerClassName).isAssignableFrom(it.key) }
val factoryProvider = foundEntry?.value val factoryProvider = foundEntry?.value

View File

@ -242,4 +242,30 @@
<string name="notice_room_server_acl_set_banned">• Server shodující se s %s je zakázán.</string> <string name="notice_room_server_acl_set_banned">• Server shodující se s %s je zakázán.</string>
<string name="notice_room_server_acl_set_title_by_you">Nastavili jste ACL serveru pro tuto místnost.</string> <string name="notice_room_server_acl_set_title_by_you">Nastavili jste ACL serveru pro tuto místnost.</string>
<string name="notice_room_server_acl_set_title">%s nastavili ACL serveru pro tuto místnost.</string> <string name="notice_room_server_acl_set_title">%s nastavili ACL serveru pro tuto místnost.</string>
<string name="notice_room_canonical_alias_no_change_by_you">Změnili jste adresy pro tuto místnost.</string>
<string name="notice_room_canonical_alias_no_change">%1$s změnili adresy pro tuto místnost.</string>
<string name="notice_room_canonical_alias_main_and_alternative_changed_by_you">Změnili jste hlavní a alternativní adresu pro tuto místnost.</string>
<string name="notice_room_canonical_alias_main_and_alternative_changed">%1$s změnili hlavní a alternativní adresu pro tuto místnost.</string>
<string name="notice_room_canonical_alias_alternative_changed_by_you">Změnili jste alternativní adresu pro tuto místnost.</string>
<string name="notice_room_canonical_alias_alternative_changed">%1$s změnili alternativní adresu pro tuto místnost.</string>
<plurals name="notice_room_canonical_alias_alternative_removed_by_you">
<item quantity="one">Odstranili jste alternativní adresu %1$s pro tuto místnost.</item>
<item quantity="few">Odstranili jste alternativní adresy %1$s pro tuto místnost.</item>
<item quantity="other">Odstranili jste alternativní adresy %1$s pro tuto místnost.</item>
</plurals>
<plurals name="notice_room_canonical_alias_alternative_removed">
<item quantity="one">%1$s odstranili alternativní adresu %2$s pro tuto místnost.</item>
<item quantity="few">%1$s odstranili alternativní adresy %2$s pro tuto místnost.</item>
<item quantity="other">%1$s odstranili alternativní adresy %2$s pro tuto místnost.</item>
</plurals>
<plurals name="notice_room_canonical_alias_alternative_added_by_you">
<item quantity="one">Přidali jste alternativní adresu %1$s pro tuto místnost.</item>
<item quantity="few">Přidali jste alternativní adresy %1$s pro tuto místnost.</item>
<item quantity="other">Přidali jste alternativní adresy %1$s pro tuto místnost.</item>
</plurals>
<plurals name="notice_room_canonical_alias_alternative_added">
<item quantity="one">%1$s přidali alternativní adresu %2$s pro tuto místnost.</item>
<item quantity="few">%1$s přidali alternativní adresy %2$s pro tuto místnost.</item>
<item quantity="other">%1$s přidali alternativní adresy %2$s pro tuto místnost.</item>
</plurals>
</resources> </resources>

View File

@ -47,7 +47,7 @@
<string name="notice_crypto_error_unkwown_inbound_session_id">Sõnumi saatja seade ei ole selle sõnumi jaoks saatnud dekrüptimisvõtmeid.</string> <string name="notice_crypto_error_unkwown_inbound_session_id">Sõnumi saatja seade ei ole selle sõnumi jaoks saatnud dekrüptimisvõtmeid.</string>
<string name="could_not_redact">Ei saanud muuta sõnumit</string> <string name="could_not_redact">Ei saanud muuta sõnumit</string>
<string name="unable_to_send_message">Sõnumi saatmine ei õnnestunud</string> <string name="unable_to_send_message">Sõnumi saatmine ei õnnestunud</string>
<string name="message_failed_to_upload">Faili üles laadimine ei õnnestunud</string> <string name="message_failed_to_upload">Pildi üleslaadimine ei õnnestunud</string>
<string name="network_error">Võrguühenduse viga</string> <string name="network_error">Võrguühenduse viga</string>
<string name="matrix_error">Matrix\'i viga</string> <string name="matrix_error">Matrix\'i viga</string>
<string name="room_error_join_failed_empty_room">Hetkel ei ole võimalik uuesti liituda tühja jututoaga.</string> <string name="room_error_join_failed_empty_room">Hetkel ei ole võimalik uuesti liituda tühja jututoaga.</string>
@ -236,4 +236,26 @@
<string name="notice_room_server_acl_set_ip_literals_allowed">• Lubatud on serverid, mille ip-aadress vastab mustrile.</string> <string name="notice_room_server_acl_set_ip_literals_allowed">• Lubatud on serverid, mille ip-aadress vastab mustrile.</string>
<string name="notice_room_server_acl_set_allowed">• Lubatud on serverid, mille nimes leidub %s.</string> <string name="notice_room_server_acl_set_allowed">• Lubatud on serverid, mille nimes leidub %s.</string>
<string name="notice_room_server_acl_set_banned">• Keelatud on serverid, mille nimes leidub %s.</string> <string name="notice_room_server_acl_set_banned">• Keelatud on serverid, mille nimes leidub %s.</string>
<string name="notice_room_canonical_alias_no_change">%1$s muutis selle jututoa aadresse.</string>
<string name="notice_room_canonical_alias_main_and_alternative_changed_by_you">Sa muutsid selle jututoa põhiaadressi ja täiendavaid aadresse.</string>
<string name="notice_room_canonical_alias_alternative_changed">%1$s muutis selle jututoa täiendavaid aadresse.</string>
<string name="notice_room_canonical_alias_alternative_changed_by_you">Sa muutsid selle jututoa täiendavaid aadresse.</string>
<string name="notice_room_canonical_alias_main_and_alternative_changed">%1$s muutis selle jututoa põhiaadressi ja täiendavaid aadresse.</string>
<plurals name="notice_room_canonical_alias_alternative_removed_by_you">
<item quantity="one">Sa eemaldasid selle jututoa täiendava aadressi %1$s.</item>
<item quantity="other">Sa eemaldasid selle jututoa täiendavad aadressid %1$s.</item>
</plurals>
<plurals name="notice_room_canonical_alias_alternative_removed">
<item quantity="one">%1$s eemaldas selle jututoa täiendava aadressi %2$s.</item>
<item quantity="other">%1$s eemaldas selle jututoa täiendavad aadressid %2$s.</item>
</plurals>
<plurals name="notice_room_canonical_alias_alternative_added_by_you">
<item quantity="one">Sa lisasid sellele jututoale täiendava aadressi %1$s.</item>
<item quantity="other">Sa lisasid sellele jututoale täiendavad aadressid %1$s.</item>
</plurals>
<plurals name="notice_room_canonical_alias_alternative_added">
<item quantity="one">%1$s lisas sellele jututoale täiendava aadressi %2$s.</item>
<item quantity="other">%1$s lisas sellele jututoale täiendavad aadressid %2$s.</item>
</plurals>
<string name="notice_room_canonical_alias_no_change_by_you">Sa muutsid selle jututoa aadresse.</string>
</resources> </resources>

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