Merge branch 'develop' into feature/fga/voip_v1_start
This commit is contained in:
commit
f4fd8af3b4
50
CHANGES.md
50
CHANGES.md
|
@ -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 ✨:
|
||||
- Enable url previews for notices (#2562)
|
||||
- Edit room permissions (#2471)
|
||||
|
||||
Improvements 🙌:
|
||||
- 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 🐛:
|
||||
- Unspecced msgType field in m.sticker (#2580)
|
||||
|
@ -15,19 +49,17 @@ Bugfix 🐛:
|
|||
- Room Topic not displayed correctly after visiting a link (#2551)
|
||||
- Hiding membership events works the exact opposite (#2603)
|
||||
- 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)
|
||||
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 🧱:
|
||||
- Remove dependency to org.greenrobot.eventbus library
|
||||
|
||||
Test:
|
||||
-
|
||||
|
||||
Other changes:
|
||||
- Migrate to ViewBindings (#1072)
|
||||
|
||||
|
|
|
@ -32,11 +32,11 @@ buildscript {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
compileSdkVersion 30
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
|
|
@ -18,15 +18,19 @@
|
|||
package im.vector.lib.attachmentviewer
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
|
@ -94,14 +98,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// This is important for the dispatchTouchEvent, if not we must correct
|
||||
// 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)
|
||||
setDecorViewFullScreen()
|
||||
|
||||
views = ActivityAttachmentViewerBinding.inflate(layoutInflater)
|
||||
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) {
|
||||
attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition)?.let {
|
||||
(it as? BaseViewHolder)?.onSelected(false)
|
||||
|
@ -313,28 +333,48 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
|
|||
?.handleCommand(commands)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun hideSystemUI() {
|
||||
systemUiVisibility = false
|
||||
// Enables regular immersive mode.
|
||||
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
|
||||
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
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)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
window.setDecorFitsSystemWindows(false)
|
||||
// new API instead of SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
window.decorView.windowInsetsController?.hide(WindowInsets.Type.navigationBars())
|
||||
// New API instead of SYSTEM_UI_FLAG_IMMERSIVE
|
||||
window.decorView.windowInsetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
|
||||
// New API instead of FLAG_TRANSLUCENT_STATUS
|
||||
window.statusBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
||||
// New API instead of FLAG_TRANSLUCENT_NAVIGATION
|
||||
window.navigationBarColor = ContextCompat.getColor(this, R.color.half_transparent_status_bar)
|
||||
} else {
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||
// Set the content to appear under the system bars so that the
|
||||
// content doesn't resize when the system bars hide and show.
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
// Hide the nav bar and status bar
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
||||
}
|
||||
}
|
||||
|
||||
// Shows the system bars by removing all the flags
|
||||
// except for the ones that make the content appear under the system bars.
|
||||
@Suppress("DEPRECATION")
|
||||
private fun showSystemUI() {
|
||||
systemUiVisibility = true
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// New API instead of SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN and SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
window.setDecorFitsSystemWindows(false)
|
||||
} else {
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<resources>
|
||||
|
||||
<color name="half_transparent_status_bar">#80000000</color>
|
||||
|
||||
</resources>
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -1,2 +1,2 @@
|
|||
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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1 +1,2 @@
|
|||
// برای انجام
|
||||
این نگارش جدید به طور عمده شامل رفع اشکالها و بهبودها است. ارسال پیام اکنون بسیار سریعتر است.
|
||||
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.0.10
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
این نگارش جدید به طور عمده شامل رابط کاربری و بهبود تجربه کاربر است. اکنون میتوانید با پویش کدهای QR دوستانتان را دعوت کرده و بسیار سریع پیام مستقیم ایجاد کنید.
|
||||
گزارش دگرگونی کامل: https://github.com/vector-im/element-android/releases/tag/v1.0.11
|
|
@ -1 +1 @@
|
|||
گپ و تماس نامتمرکز امن. دادههایتان را از شرکتها امن نگه دارید.
|
||||
گپ و تماس نامتمرکز امن. دادههایتان را از اشخاص سوم امن نگه دارید.
|
||||
|
|
|
@ -1 +1 @@
|
|||
المنت (ریوت سابق)
|
||||
Element (پیشتر Riot.im)
|
||||
|
|
|
@ -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
|
|
@ -1,30 +1,30 @@
|
|||
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
|
||||
3. Suojaa sinua mainonnalta, tietojen keräämiseltä ja suljetuilta alustoilta
|
||||
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 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 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.
|
||||
2. Isännöi tiliäsi itse suorittamalla palvelinta omalla laitteellasi
|
||||
3. Luo tili mukautetulla palvelimella yksinkertaisesti tilaamalla Element Matrix Services -palvelu
|
||||
2. Isännöi tiliäsi itse ylläpitämällä palvelinta omalla laitteellasi
|
||||
3. Luo tili sinua varten tehdyllä palvelimella tilaamalla Element Matrix Services -palvelu
|
||||
|
||||
<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>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.
|
||||
|
|
|
@ -1 +1 @@
|
|||
Turvallista, hajautettua, keskusteluja ja VoIP-puheluita. Pidä tietosi turvassa.
|
||||
Turvallista, hajautettua keskustelua ja VoIP-puheluita. Pidä tietosi turvassa.
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
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.
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
Sikker desentralisert chat & VoIP. Beskytt dataene dine fra tredjeparter.
|
|
@ -0,0 +1 @@
|
|||
Element (tidligere Riot.im)
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
Эта новая версия в основном содержит исправления ошибок и улучшения. Отправка сообщения стала намного быстрее.
|
||||
Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.0.10
|
|
@ -0,0 +1,2 @@
|
|||
Эта новая версия в основном содержит улучшения пользовательского интерфейса и взаимодействия с пользователем. Теперь вы можете приглашать друзей и очень быстро создавать чаты, сканируя QR-коды.
|
||||
Полный список изменений: https://github.com/vector-im/element-android/releases/tag/v1.0.11
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
Ця нова версія містить переважно поліпшення інтерфейсу та зручності користування. Тепер ви можете запросити друзів і створити прямі повідомлення дуже швидко, скануючи QR-коди.
|
||||
Повний перелік змін: https://github.com/vector-im/element-android/releases/tag/v1.0.11
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
Element (trước là Riot.im)
|
|
@ -0,0 +1,2 @@
|
|||
這個新版本主要包含使用者介面與使用者體驗改善。現在您可以邀請朋友,並透過掃描 QR code 來快速建立直接訊息了。
|
||||
完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.11
|
|
@ -0,0 +1,2 @@
|
|||
此版本中的主要變更:URL 預覽、新的表情符號鍵盤、新的聊天室設定功能以及聖誕節降雪!
|
||||
完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.12
|
|
@ -0,0 +1,2 @@
|
|||
此版本中的主要變更:URL 預覽、新的表情符號鍵盤、新的聊天室設定功能以及聖誕節降雪!
|
||||
完整變更紀錄:https://github.com/vector-im/element-android/releases/tag/v1.0.12
|
|
@ -1,6 +1,6 @@
|
|||
#Mon Dec 07 18:05:35 CET 2020
|
||||
#Fri Jan 29 18:05:42 CET 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
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
|
||||
|
|
|
@ -3,11 +3,11 @@ apply plugin: 'kotlin-android'
|
|||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
compileSdkVersion 30
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
|
|
|
@ -14,12 +14,12 @@ buildscript {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
compileSdkVersion 30
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
versionName "0.0.1"
|
||||
// Multidex is useful for tests
|
||||
|
@ -112,7 +112,7 @@ dependencies {
|
|||
def lifecycle_version = '2.2.0'
|
||||
def arch_version = '2.1.0'
|
||||
def markwon_version = '3.1.0'
|
||||
def daggerVersion = '2.29.1'
|
||||
def daggerVersion = '2.31'
|
||||
def work_version = '2.4.0'
|
||||
def retrofit_version = '2.6.2'
|
||||
|
||||
|
@ -160,8 +160,6 @@ dependencies {
|
|||
// DI
|
||||
implementation "com.google.dagger:dagger:$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
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
|
|
|
@ -378,7 +378,9 @@ class CommonTestHelper(context: Context) {
|
|||
fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) }
|
||||
|
||||
fun signOutAndClose(session: Session) {
|
||||
doSync<Unit>(60_000) { session.signOut(true, it) }
|
||||
runBlockingTest(timeout = 60_000) {
|
||||
session.signOut(true)
|
||||
}
|
||||
// no need signout will close
|
||||
// session.close()
|
||||
}
|
||||
|
|
|
@ -50,6 +50,8 @@ import org.junit.FixMethodOrder
|
|||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
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
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
@ -296,4 +298,77 @@ class KeyShareTests : InstrumentedTest {
|
|||
mTestHelper.signOutAndClose(aliceSession1)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.room.model.message.MessageTextContent
|
||||
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)
|
||||
internal class UrlsExtractorTest : InstrumentedTest {
|
||||
|
@ -36,6 +38,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
|
|||
fun wrongEventTypeTest() {
|
||||
createEvent(body = "https://matrix.org")
|
||||
.copy(type = EventType.STATE_ROOM_GUEST_ACCESS)
|
||||
.toFakeTimelineEvent()
|
||||
.let { urlsExtractor.extract(it) }
|
||||
.size shouldBeEqualTo 0
|
||||
}
|
||||
|
@ -43,6 +46,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
|
|||
@Test
|
||||
fun oneUrlTest() {
|
||||
createEvent(body = "https://matrix.org")
|
||||
.toFakeTimelineEvent()
|
||||
.let { urlsExtractor.extract(it) }
|
||||
.let { result ->
|
||||
result.size shouldBeEqualTo 1
|
||||
|
@ -53,6 +57,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
|
|||
@Test
|
||||
fun withoutProtocolTest() {
|
||||
createEvent(body = "www.matrix.org")
|
||||
.toFakeTimelineEvent()
|
||||
.let { urlsExtractor.extract(it) }
|
||||
.size shouldBeEqualTo 0
|
||||
}
|
||||
|
@ -60,6 +65,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
|
|||
@Test
|
||||
fun oneUrlWithParamTest() {
|
||||
createEvent(body = "https://matrix.org?foo=bar")
|
||||
.toFakeTimelineEvent()
|
||||
.let { urlsExtractor.extract(it) }
|
||||
.let { result ->
|
||||
result.size shouldBeEqualTo 1
|
||||
|
@ -70,6 +76,7 @@ internal class UrlsExtractorTest : InstrumentedTest {
|
|||
@Test
|
||||
fun oneUrlWithParamsTest() {
|
||||
createEvent(body = "https://matrix.org?foo=bar&bar=foo")
|
||||
.toFakeTimelineEvent()
|
||||
.let { urlsExtractor.extract(it) }
|
||||
.let { result ->
|
||||
result.size shouldBeEqualTo 1
|
||||
|
@ -80,16 +87,18 @@ internal class UrlsExtractorTest : InstrumentedTest {
|
|||
@Test
|
||||
fun oneUrlInlinedTest() {
|
||||
createEvent(body = "Hello https://matrix.org, how are you?")
|
||||
.toFakeTimelineEvent()
|
||||
.let { urlsExtractor.extract(it) }
|
||||
.let { result ->
|
||||
result.size shouldBeEqualTo 1
|
||||
result[0] shouldBeEqualTo "https://matrix.org"
|
||||
result[0] shouldBeEqualTo "https://matrix.org"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun twoUrlsTest() {
|
||||
createEvent(body = "https://matrix.org https://example.org")
|
||||
.toFakeTimelineEvent()
|
||||
.let { urlsExtractor.extract(it) }
|
||||
.let { result ->
|
||||
result.size shouldBeEqualTo 2
|
||||
|
@ -99,10 +108,26 @@ internal class UrlsExtractorTest : InstrumentedTest {
|
|||
}
|
||||
|
||||
private fun createEvent(body: String): Event = Event(
|
||||
eventId = "!fake",
|
||||
type = EventType.MESSAGE,
|
||||
content = MessageTextContent(
|
||||
msgType = MessageType.MSGTYPE_TEXT,
|
||||
body = body
|
||||
).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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,8 +66,8 @@ class TimelineForwardPaginationTest : InstrumentedTest {
|
|||
numberOfMessagesToSend)
|
||||
|
||||
// Alice clear the cache
|
||||
commonTestHelper.doSync<Unit> {
|
||||
aliceSession.clearCache(it)
|
||||
commonTestHelper.runBlockingTest {
|
||||
aliceSession.clearCache()
|
||||
}
|
||||
|
||||
// And restarts the sync
|
||||
|
|
|
@ -37,6 +37,6 @@ class SenderNotificationPermissionCondition(
|
|||
|
||||
fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
|
||||
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.
|
||||
*/
|
||||
|
@ -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.
|
||||
*/
|
||||
fun clearCache(callback: MatrixCallback<Unit>)
|
||||
suspend fun clearCache()
|
||||
}
|
||||
|
|
|
@ -17,15 +17,16 @@
|
|||
package org.matrix.android.sdk.api.session.media
|
||||
|
||||
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
|
||||
|
||||
interface MediaService {
|
||||
/**
|
||||
* Extract URLs from an Event.
|
||||
* @return the list of URLs contains in the body of the Event. It does not mean that URLs in this list have UrlPreview data
|
||||
* Extract URLs from a TimelineEvent.
|
||||
* @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
|
||||
|
|
|
@ -25,28 +25,85 @@ import org.matrix.android.sdk.api.session.room.powerlevels.Role
|
|||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PowerLevelsContent(
|
||||
/**
|
||||
* The level required to ban a user. Defaults to 50 if unspecified.
|
||||
*/
|
||||
@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,
|
||||
/**
|
||||
* The level required to invite a user. Defaults to 50 if unspecified.
|
||||
*/
|
||||
@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,
|
||||
/**
|
||||
* 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") 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") 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 = "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 powerLevel the new power level, or null to set the default value.
|
||||
*/
|
||||
fun setUserPowerLevel(userId: String, powerLevel: Int?) {
|
||||
if (powerLevel == null || powerLevel == usersDefault) {
|
||||
users.remove(userId)
|
||||
} else {
|
||||
users[userId] = powerLevel
|
||||
fun setUserPowerLevel(userId: String, powerLevel: Int?): PowerLevelsContent {
|
||||
return copy(
|
||||
users = users.toMutableMap().apply {
|
||||
if (powerLevel == null || powerLevel == usersDefault) {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,19 +108,4 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
|
|||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,6 +91,17 @@ data class TimelineEvent(
|
|||
*/
|
||||
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
|
||||
*/
|
||||
|
|
|
@ -16,9 +16,7 @@
|
|||
|
||||
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.util.Cancelable
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* The same deviceId will be used
|
||||
*/
|
||||
fun signInAgain(password: String,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun signInAgain(password: String)
|
||||
|
||||
/**
|
||||
* Update the session with credentials received after SSO
|
||||
*/
|
||||
fun updateCredentials(credentials: Credentials,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun updateCredentials(credentials: Credentials)
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
fun signOut(signOutFromHomeserver: Boolean,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
suspend fun signOut(signOutFromHomeserver: Boolean)
|
||||
}
|
||||
|
|
|
@ -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.model.rest.GossipingDefaultContent
|
||||
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.di.SessionId
|
||||
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}")
|
||||
if (credentials.userId != userId) {
|
||||
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)
|
||||
handleKeyRequestFromOtherUser(body, request, alg, roomId, userId, deviceId)
|
||||
return
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
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) {
|
||||
val secretName = request.secretName ?: return Unit.also {
|
||||
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)
|
||||
|
|
|
@ -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.util.JSON_DICT_PARAMETERIZED_TYPE
|
||||
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.model.OlmInboundGroupSessionWrapper2
|
||||
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
|
||||
|
@ -46,7 +48,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||
*/
|
||||
private val store: IMXCryptoStore,
|
||||
private val inboundGroupSessionStore: InboundGroupSessionStore
|
||||
) {
|
||||
) {
|
||||
|
||||
/**
|
||||
* @return the Curve25519 key for the account.
|
||||
|
@ -63,11 +65,15 @@ internal class MXOlmDevice @Inject constructor(
|
|||
// The OLM lib utility instance.
|
||||
private var olmUtility: OlmUtility? = null
|
||||
|
||||
private data class GroupSessionCacheItem(
|
||||
val groupId: String,
|
||||
val groupSession: OlmOutboundGroupSession
|
||||
)
|
||||
|
||||
// The outbound group session.
|
||||
// They are not stored in 'store' to avoid to remember to which devices we sent the session key.
|
||||
// Plus, in cryptography, it is good to refresh sessions from time to time.
|
||||
// The key is the session id, the value the outbound group session.
|
||||
private val outboundGroupSessionStore: MutableMap<String, OlmOutboundGroupSession> = HashMap()
|
||||
// Caches active outbound session to avoid to sync with DB before read
|
||||
// The key is the session id, the value the <roomID,outbound group session>.
|
||||
private val outboundGroupSessionCache: MutableMap<String, GroupSessionCacheItem> = HashMap()
|
||||
|
||||
// Store a set of decrypted message indexes for each group session.
|
||||
// This partially mitigates a replay attack where a MITM resends a group
|
||||
|
@ -135,6 +141,10 @@ internal class MXOlmDevice @Inject constructor(
|
|||
*/
|
||||
fun release() {
|
||||
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.
|
||||
*/
|
||||
fun createOutboundGroupSession(): String? {
|
||||
fun createOutboundGroupSessionForRoom(roomId: String): String? {
|
||||
var session: OlmOutboundGroupSession? = null
|
||||
try {
|
||||
session = OlmOutboundGroupSession()
|
||||
outboundGroupSessionStore[session.sessionIdentifier()] = session
|
||||
outboundGroupSessionCache[session.sessionIdentifier()] = GroupSessionCacheItem(roomId, session)
|
||||
store.storeCurrentOutboundGroupSessionForRoom(roomId, session)
|
||||
return session.sessionIdentifier()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "createOutboundGroupSession")
|
||||
|
@ -421,6 +432,39 @@ internal class MXOlmDevice @Inject constructor(
|
|||
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.
|
||||
*
|
||||
|
@ -430,7 +474,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||
fun getSessionKey(sessionId: String): String? {
|
||||
if (sessionId.isNotEmpty()) {
|
||||
try {
|
||||
return outboundGroupSessionStore[sessionId]!!.sessionKey()
|
||||
return outboundGroupSessionCache[sessionId]!!.groupSession.sessionKey()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## getSessionKey() : failed")
|
||||
}
|
||||
|
@ -446,7 +490,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||
*/
|
||||
fun getMessageIndex(sessionId: String): Int {
|
||||
return if (sessionId.isNotEmpty()) {
|
||||
outboundGroupSessionStore[sessionId]!!.messageIndex()
|
||||
outboundGroupSessionCache[sessionId]!!.groupSession.messageIndex()
|
||||
} else 0
|
||||
}
|
||||
|
||||
|
@ -460,7 +504,7 @@ internal class MXOlmDevice @Inject constructor(
|
|||
fun encryptGroupMessage(sessionId: String, payloadString: String): String? {
|
||||
if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) {
|
||||
try {
|
||||
return outboundGroupSessionStore[sessionId]!!.encryptMessage(payloadString)
|
||||
return outboundGroupSessionCache[sessionId]!!.groupSession.encryptMessage(payloadString)
|
||||
} catch (e: Exception) {
|
||||
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)
|
||||
}
|
||||
|
||||
val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
|
||||
val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
|
||||
|
||||
if (session != null) {
|
||||
// Check that the room id matches the original one for the session. This stops
|
||||
|
|
|
@ -68,6 +68,10 @@ internal class MXMegolmEncryption(
|
|||
// case outboundSession.shareOperation will be non-null.)
|
||||
private var outboundSession: MXOutboundSessionInfo? = null
|
||||
|
||||
init {
|
||||
// restore existing outbound session if any
|
||||
outboundSession = olmDevice.restoreOutboundGroupSessionForRoom(roomId)
|
||||
}
|
||||
// Default rotation periods
|
||||
// TODO: Make it configurable via parameters
|
||||
// Session rotation periods
|
||||
|
@ -86,6 +90,9 @@ internal class MXMegolmEncryption(
|
|||
return encryptContent(outboundSession, eventType, eventContent)
|
||||
.also {
|
||||
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() {
|
||||
outboundSession = null
|
||||
olmDevice.discardOutboundGroupSessionForRoom(roomId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -116,7 +124,7 @@ internal class MXMegolmEncryption(
|
|||
*/
|
||||
private fun prepareNewSessionInRoom(): MXOutboundSessionInfo {
|
||||
Timber.v("## CRYPTO | prepareNewSessionInRoom() ")
|
||||
val sessionId = olmDevice.createOutboundGroupSession()
|
||||
val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId)
|
||||
|
||||
val keysClaimedMap = HashMap<String, String>()
|
||||
keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!!
|
||||
|
@ -152,7 +160,7 @@ internal class MXMegolmEncryption(
|
|||
val deviceIds = devicesInRoom.getUserDeviceIds(userId)
|
||||
for (deviceId in deviceIds!!) {
|
||||
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() }
|
||||
devices.add(deviceInfo)
|
||||
}
|
||||
|
@ -401,11 +409,18 @@ internal class MXMegolmEncryption(
|
|||
.also { Timber.w("## Crypto reshareKey: Device not found") }
|
||||
|
||||
// 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 {
|
||||
// Send a room key with held
|
||||
notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED)
|
||||
Timber.w("## Crypto reshareKey: ERROR : Never share megolm with this device")
|
||||
Timber.w("## Crypto reshareKey: Null chain index")
|
||||
}
|
||||
|
||||
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
||||
|
|
|
@ -23,9 +23,9 @@ import timber.log.Timber
|
|||
internal class MXOutboundSessionInfo(
|
||||
// The id of the session
|
||||
val sessionId: String,
|
||||
val sharedWithHelper: SharedWithHelper) {
|
||||
// When the session was created
|
||||
private val creationTime = System.currentTimeMillis()
|
||||
val sharedWithHelper: SharedWithHelper,
|
||||
// When the session was created
|
||||
private val creationTime: Long = System.currentTimeMillis()) {
|
||||
|
||||
// Number of times this session has been used
|
||||
var useCount: Int = 0
|
||||
|
|
|
@ -28,10 +28,6 @@ internal class SharedWithHelper(
|
|||
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) {
|
||||
cryptoStore.markedSessionAsShared(roomId, sessionId, userId, deviceId, chainIndex)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
@ -14,11 +14,11 @@
|
|||
* 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 dagger.Module
|
||||
import org.matrix.olm.OlmOutboundGroupSession
|
||||
|
||||
@AssistedModule
|
||||
@Module(includes = [AssistedInject_SessionAssistedInjectModule::class])
|
||||
interface SessionAssistedInjectModule
|
||||
data class OutboundGroupSessionWrapper(
|
||||
val outboundGroupSession: OlmOutboundGroupSession,
|
||||
val creationTime: Long
|
||||
)
|
|
@ -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.OlmInboundGroupSessionWrapper2
|
||||
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.rest.DeviceInfo
|
||||
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||
import org.matrix.olm.OlmAccount
|
||||
import org.matrix.olm.OlmOutboundGroupSession
|
||||
|
||||
/**
|
||||
* the crypto data store
|
||||
|
@ -293,6 +295,16 @@ internal interface IMXCryptoStore {
|
|||
*/
|
||||
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
|
||||
*
|
||||
|
@ -439,7 +451,15 @@ internal interface IMXCryptoStore {
|
|||
fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent?
|
||||
|
||||
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?)
|
||||
|
||||
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
|
||||
|
|
|
@ -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.OlmInboundGroupSessionWrapper2
|
||||
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.rest.DeviceInfo
|
||||
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.OlmSessionEntity
|
||||
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.OutgoingGossipingRequestEntityFields
|
||||
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.olm.OlmAccount
|
||||
import org.matrix.olm.OlmException
|
||||
import org.matrix.olm.OlmOutboundGroupSession
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.set
|
||||
|
@ -756,6 +759,42 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
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,
|
||||
* 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 ->
|
||||
SharedSessionEntity.get(realm, roomId, sessionId, userId, deviceId)?.let {
|
||||
IMXCryptoStore.SharedSessionResult(true, it.chainIndex)
|
||||
|
|
|
@ -44,6 +44,7 @@ import org.matrix.android.sdk.internal.di.SerializeNulls
|
|||
import io.realm.DynamicRealm
|
||||
import io.realm.RealmMigration
|
||||
import io.realm.RealmObjectSchema
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntityFields
|
||||
import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
@ -55,7 +56,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
|||
// 0, 1, 2: legacy Riot-Android
|
||||
// 3: migrate to RiotX schema
|
||||
// 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 {
|
||||
|
@ -93,6 +94,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
|||
if (oldVersion <= 8) migrateTo9(realm)
|
||||
if (oldVersion <= 9) migrateTo10(realm)
|
||||
if (oldVersion <= 10) migrateTo11(realm)
|
||||
if (oldVersion <= 11) migrateTo12(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1Legacy(realm: DynamicRealm) {
|
||||
|
@ -483,4 +485,16 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
|||
realm.schema.get("CryptoMetadataEntity")
|
||||
?.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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.WithHeldSessionEntity
|
||||
import io.realm.annotations.RealmModule
|
||||
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity
|
||||
|
||||
/**
|
||||
* Realm module for Crypto store classes
|
||||
|
@ -54,6 +55,7 @@ import io.realm.annotations.RealmModule
|
|||
OutgoingGossipingRequestEntity::class,
|
||||
MyDeviceLastSeenInfoEntity::class,
|
||||
WithHeldSessionEntity::class,
|
||||
SharedSessionEntity::class
|
||||
SharedSessionEntity::class,
|
||||
OutboundGroupSessionInfoEntity::class
|
||||
])
|
||||
internal class RealmCryptoStoreModule
|
||||
|
|
|
@ -23,7 +23,12 @@ internal open class CryptoRoomEntity(
|
|||
@PrimaryKey var roomId: String? = null,
|
||||
var algorithm: String? = 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() {
|
||||
|
||||
companion object
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -20,7 +20,6 @@ import androidx.annotation.MainThread
|
|||
import dagger.Lazy
|
||||
import io.realm.RealmConfiguration
|
||||
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.failure.GlobalError
|
||||
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()
|
||||
stopAnyBackgroundSync()
|
||||
uiHandler.post {
|
||||
lifecycleObservers.forEach { it.onClearCache() }
|
||||
}
|
||||
cacheService.get().clearCache(callback)
|
||||
cacheService.get().clearCache()
|
||||
workManagerProvider.cancelAllWorks()
|
||||
}
|
||||
|
||||
|
|
|
@ -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.verification.SendVerificationMessageWorker
|
||||
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.session.account.AccountModule
|
||||
import org.matrix.android.sdk.internal.session.cache.CacheModule
|
||||
|
@ -87,7 +86,6 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
|||
TermsModule::class,
|
||||
AccountDataModule::class,
|
||||
ProfileModule::class,
|
||||
SessionAssistedInjectModule::class,
|
||||
AccountModule::class,
|
||||
CallModule::class,
|
||||
SearchModule::class,
|
||||
|
|
|
@ -16,23 +16,18 @@
|
|||
|
||||
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.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
||||
import org.matrix.android.sdk.internal.task.configureWith
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultCacheService @Inject constructor(@SessionDatabase
|
||||
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()
|
||||
clearCacheTask
|
||||
.configureWith {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
clearCacheTask.execute(Unit)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,22 +47,24 @@ internal object ThumbnailExtractor {
|
|||
val mediaMetadataRetriever = MediaMetadataRetriever()
|
||||
try {
|
||||
mediaMetadataRetriever.setDataSource(context, attachment.queryUri)
|
||||
val thumbnail = mediaMetadataRetriever.frameAtTime
|
||||
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||
val thumbnailWidth = thumbnail.width
|
||||
val thumbnailHeight = thumbnail.height
|
||||
val thumbnailSize = outputStream.size()
|
||||
thumbnailData = ThumbnailData(
|
||||
width = thumbnailWidth,
|
||||
height = thumbnailHeight,
|
||||
size = thumbnailSize.toLong(),
|
||||
bytes = outputStream.toByteArray(),
|
||||
mimeType = MimeTypes.Jpeg
|
||||
)
|
||||
thumbnail.recycle()
|
||||
outputStream.reset()
|
||||
mediaMetadataRetriever.frameAtTime?.let { thumbnail ->
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||
val thumbnailWidth = thumbnail.width
|
||||
val thumbnailHeight = thumbnail.height
|
||||
val thumbnailSize = outputStream.size()
|
||||
thumbnailData = ThumbnailData(
|
||||
width = thumbnailWidth,
|
||||
height = thumbnailHeight,
|
||||
size = thumbnailSize.toLong(),
|
||||
bytes = outputStream.toByteArray(),
|
||||
mimeType = MimeTypes.Jpeg
|
||||
)
|
||||
thumbnail.recycle()
|
||||
outputStream.reset()
|
||||
} ?: run {
|
||||
Timber.e("Cannot extract video thumbnail at %s", attachment.queryUri.toString())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Cannot extract video thumbnail")
|
||||
} finally {
|
||||
|
|
|
@ -52,65 +52,60 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor(
|
|||
val pepper = identityData.hashLookupPepper
|
||||
val hashDetailResponse = if (pepper == null) {
|
||||
// We need to fetch the hash details first
|
||||
fetchAndStoreHashDetails(identityAPI)
|
||||
fetchHashDetails(identityAPI)
|
||||
.also { identityStore.setHashDetails(it) }
|
||||
} else {
|
||||
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
|
||||
// Also, what we have in cache could be outdated, the identity server maybe now supports sha256
|
||||
throw IdentityServiceError.BulkLookupSha256NotSupported
|
||||
}
|
||||
|
||||
val hashedAddresses = withOlmUtility { olmUtility ->
|
||||
params.threePids.map { threePid ->
|
||||
base64ToBase64Url(
|
||||
olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT)
|
||||
+ " " + threePid.toMedium() + " " + hashDetailResponse.pepper)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val identityLookUpV2Response = lookUpInternal(identityAPI, hashedAddresses, hashDetailResponse, true)
|
||||
val lookUpData = lookUpInternal(identityAPI, params.threePids, hashDetailResponse, true)
|
||||
|
||||
// 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,
|
||||
hashedAddresses: List<String>,
|
||||
threePids: List<ThreePid>,
|
||||
hashDetailResponse: IdentityHashDetailResponse,
|
||||
canRetry: Boolean): IdentityLookUpResponse {
|
||||
canRetry: Boolean): LookUpData {
|
||||
val hashedAddresses = getHashedAddresses(threePids, hashDetailResponse.pepper)
|
||||
return try {
|
||||
executeRequest(null) {
|
||||
apiCall = identityAPI.lookup(IdentityLookUpParams(
|
||||
hashedAddresses,
|
||||
IdentityHashDetailResponse.ALGORITHM_SHA256,
|
||||
hashDetailResponse.pepper
|
||||
))
|
||||
}
|
||||
LookUpData(hashedAddresses,
|
||||
executeRequest(null) {
|
||||
apiCall = identityAPI.lookup(IdentityLookUpParams(
|
||||
hashedAddresses,
|
||||
IdentityHashDetailResponse.ALGORITHM_SHA256,
|
||||
hashDetailResponse.pepper
|
||||
))
|
||||
})
|
||||
} catch (failure: Throwable) {
|
||||
// Catch invalid hash pepper and retry
|
||||
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!
|
||||
if (!failure.error.newLookupPepper.isNullOrEmpty()) {
|
||||
val newHashDetailResponse = if (!failure.error.newLookupPepper.isNullOrEmpty()) {
|
||||
// Store it and use it right now
|
||||
hashDetailResponse.copy(pepper = failure.error.newLookupPepper)
|
||||
.also { identityStore.setHashDetails(it) }
|
||||
.let { lookUpInternal(identityAPI, hashedAddresses, it, false /* Avoid infinite loop */) }
|
||||
} else {
|
||||
// Retrieve the new hash details
|
||||
val newHashDetailResponse = fetchAndStoreHashDetails(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 */)
|
||||
fetchHashDetails(identityAPI)
|
||||
}
|
||||
.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 {
|
||||
// Other error
|
||||
throw failure
|
||||
|
@ -118,16 +113,29 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAndStoreHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse {
|
||||
return executeRequest<IdentityHashDetailResponse>(null) {
|
||||
apiCall = identityAPI.hashDetails()
|
||||
private fun getHashedAddresses(threePids: List<ThreePid>, pepper: String): List<String> {
|
||||
return withOlmUtility { olmUtility ->
|
||||
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> {
|
||||
return identityLookUpResponse.mappings.keys.map { hashedAddress ->
|
||||
FoundThreePid(threePids[hashedAddresses.indexOf(hashedAddress)], identityLookUpResponse.mappings[hashedAddress] ?: error(""))
|
||||
private suspend fun fetchHashDetails(identityAPI: IdentityAPI): IdentityHashDetailResponse {
|
||||
return executeRequest(null) {
|
||||
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("")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,9 +18,10 @@ package org.matrix.android.sdk.internal.session.media
|
|||
|
||||
import androidx.collection.LruCache
|
||||
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.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.internal.util.getOrPut
|
||||
import javax.inject.Inject
|
||||
|
@ -34,11 +35,12 @@ internal class DefaultMediaService @Inject constructor(
|
|||
// Cache of extracted URLs
|
||||
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) }
|
||||
}
|
||||
|
||||
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 {
|
||||
return getRawPreviewUrlTask.execute(GetRawPreviewUrlTask.Params(url, timestamp))
|
||||
|
|
|
@ -17,21 +17,19 @@
|
|||
package org.matrix.android.sdk.internal.session.media
|
||||
|
||||
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.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.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class UrlsExtractor @Inject constructor() {
|
||||
// Sadly Patterns.WEB_URL_WITH_PROTOCOL is not public so filter the protocol later
|
||||
private val urlRegex = Patterns.WEB_URL.toRegex()
|
||||
|
||||
fun extract(event: Event): List<String> {
|
||||
return event.takeIf { it.getClearType() == EventType.MESSAGE }
|
||||
?.getClearContent()
|
||||
?.toModel<MessageContent>()
|
||||
fun extract(event: TimelineEvent): List<String> {
|
||||
return event.takeIf { it.root.getClearType() == EventType.MESSAGE }
|
||||
?.getLastMessageContent()
|
||||
?.takeIf {
|
||||
it.msgType == MessageType.MSGTYPE_TEXT
|
||||
|| it.msgType == MessageType.MSGTYPE_NOTICE
|
||||
|
|
|
@ -16,8 +16,9 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room.alias
|
||||
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
import org.matrix.android.sdk.api.session.room.alias.AliasService
|
||||
|
||||
internal class DefaultAliasService @AssistedInject constructor(
|
||||
|
@ -26,9 +27,9 @@ internal class DefaultAliasService @AssistedInject constructor(
|
|||
private val addRoomAliasTask: AddRoomAliasTask
|
||||
) : AliasService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(roomId: String): AliasService
|
||||
fun create(roomId: String): DefaultAliasService
|
||||
}
|
||||
|
||||
override suspend fun getRoomAliases(): List<String> {
|
||||
|
|
|
@ -16,8 +16,9 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room.call
|
||||
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.room.call.RoomCallService
|
||||
import org.matrix.android.sdk.internal.session.room.RoomGetter
|
||||
|
@ -27,9 +28,9 @@ internal class DefaultRoomCallService @AssistedInject constructor(
|
|||
private val roomGetter: RoomGetter
|
||||
) : RoomCallService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(roomId: String): RoomCallService
|
||||
fun create(roomId: String): DefaultRoomCallService
|
||||
}
|
||||
|
||||
override fun canStartCall(): Boolean {
|
||||
|
|
|
@ -17,8 +17,9 @@
|
|||
package org.matrix.android.sdk.internal.session.room.draft
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.session.room.send.DraftService
|
||||
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
|
||||
) : DraftService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(roomId: String): DraftService
|
||||
fun create(roomId: String): DefaultDraftService
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,8 +17,9 @@
|
|||
package org.matrix.android.sdk.internal.session.room.membership
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
|
@ -58,9 +59,9 @@ internal class DefaultMembershipService @AssistedInject constructor(
|
|||
private val userId: String
|
||||
) : MembershipService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(roomId: String): MembershipService
|
||||
fun create(roomId: String): DefaultMembershipService
|
||||
}
|
||||
|
||||
override fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
|
|
|
@ -55,7 +55,11 @@ internal class DefaultJoinRoomTask @Inject constructor(
|
|||
roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.Joining)
|
||||
val joinRoomResponse = try {
|
||||
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) {
|
||||
roomChangeMembershipStateDataSource.updateState(params.roomIdOrAlias, ChangeMembershipState.FailedJoining(failure))
|
||||
|
|
|
@ -18,8 +18,9 @@ package org.matrix.android.sdk.internal.session.room.notification
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.pushrules.RuleScope
|
||||
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)
|
||||
: RoomPushRuleService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(roomId: String): RoomPushRuleService
|
||||
fun create(roomId: String): DefaultRoomPushRuleService
|
||||
}
|
||||
|
||||
override fun getLiveRoomNotificationState(): LiveData<RoomNotificationState> {
|
||||
|
|
|
@ -18,8 +18,9 @@ package org.matrix.android.sdk.internal.session.room.read
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
||||
|
@ -46,9 +47,9 @@ internal class DefaultReadService @AssistedInject constructor(
|
|||
@UserId private val userId: String
|
||||
) : ReadService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(roomId: String): ReadService
|
||||
fun create(roomId: String): DefaultReadService
|
||||
}
|
||||
|
||||
override fun markAsRead(params: ReadService.MarkAsReadParams, callback: MatrixCallback<Unit>) {
|
||||
|
|
|
@ -17,8 +17,9 @@ package org.matrix.android.sdk.internal.session.room.relation
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
|
@ -56,9 +57,9 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||
private val taskExecutor: TaskExecutor)
|
||||
: RelationService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(roomId: String): RelationService
|
||||
fun create(roomId: String): DefaultRelationService
|
||||
}
|
||||
|
||||
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>>) {
|
||||
val params = FetchEditHistoryTask.Params(roomId, cryptoSessionInfoProvider.isRoomEncrypted(roomId), eventId)
|
||||
val params = FetchEditHistoryTask.Params(roomId, eventId)
|
||||
fetchEditHistoryTask
|
||||
.configureWith(params) {
|
||||
this.callback = callback
|
||||
|
|
|
@ -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.EventType
|
||||
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.executeRequest
|
||||
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
|
||||
|
||||
internal interface FetchEditHistoryTask : Task<FetchEditHistoryTask.Params, List<Event>> {
|
||||
|
||||
data class Params(
|
||||
val roomId: String,
|
||||
val isRoomEncrypted: Boolean,
|
||||
val eventId: String
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultFetchEditHistoryTask @Inject constructor(
|
||||
private val roomAPI: RoomAPI,
|
||||
private val globalErrorReceiver: GlobalErrorReceiver
|
||||
private val globalErrorReceiver: GlobalErrorReceiver,
|
||||
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider
|
||||
) : FetchEditHistoryTask {
|
||||
|
||||
override suspend fun execute(params: FetchEditHistoryTask.Params): List<Event> {
|
||||
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
|
||||
val response = executeRequest<RelationsResponse>(globalErrorReceiver) {
|
||||
apiCall = roomAPI.getRelations(params.roomId,
|
||||
params.eventId,
|
||||
RelationType.REPLACE,
|
||||
if (params.isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE)
|
||||
apiCall = roomAPI.getRelations(
|
||||
roomId = params.roomId,
|
||||
eventId = params.eventId,
|
||||
relationType = RelationType.REPLACE,
|
||||
eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE
|
||||
)
|
||||
}
|
||||
|
||||
val events = response.chunks.toMutableList()
|
||||
|
|
|
@ -16,17 +16,18 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room.reporting
|
||||
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
import org.matrix.android.sdk.api.session.room.reporting.ReportingService
|
||||
|
||||
internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||
private val reportContentTask: ReportContentTask
|
||||
) : ReportingService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(roomId: String): ReportingService
|
||||
fun create(roomId: String): DefaultReportingService
|
||||
}
|
||||
|
||||
override suspend fun reportContent(eventId: String, score: Int, reason: String) {
|
||||
|
|
|
@ -21,8 +21,9 @@ import androidx.work.BackoffPolicy
|
|||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.Operation
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
|
@ -71,9 +72,9 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
private val cancelSendTracker: CancelSendTracker
|
||||
) : SendService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(roomId: String): SendService
|
||||
fun create(roomId: String): DefaultSendService
|
||||
}
|
||||
|
||||
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
|
||||
|
|
|
@ -18,8 +18,9 @@ package org.matrix.android.sdk.internal.session.room.state
|
|||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
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.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.Optional
|
||||
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,
|
||||
private val stateEventDataSource: StateEventDataSource,
|
||||
private val sendStateTask: SendStateTask,
|
||||
private val fileUploader: FileUploader,
|
||||
private val addRoomAliasTask: AddRoomAliasTask
|
||||
private val fileUploader: FileUploader
|
||||
) : StateService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(roomId: String): StateService
|
||||
fun create(roomId: String): DefaultStateService
|
||||
}
|
||||
|
||||
override fun getStateEvent(eventType: String, stateKey: QueryStringValue): Event? {
|
||||
|
@ -74,11 +73,19 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
|
|||
roomId = roomId,
|
||||
stateKey = stateKey,
|
||||
eventType = eventType,
|
||||
body = body
|
||||
body = body.toSafeJson(eventType)
|
||||
)
|
||||
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) {
|
||||
sendStateEvent(
|
||||
eventType = EventType.STATE_ROOM_TOPIC,
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -16,8 +16,9 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room.tags
|
||||
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
import org.matrix.android.sdk.api.session.room.tags.TagsService
|
||||
|
||||
internal class DefaultTagsService @AssistedInject constructor(
|
||||
|
@ -26,9 +27,9 @@ internal class DefaultTagsService @AssistedInject constructor(
|
|||
private val deleteTagFromRoomTask: DeleteTagFromRoomTask
|
||||
) : TagsService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(roomId: String): TagsService
|
||||
fun create(roomId: String): DefaultTagsService
|
||||
}
|
||||
|
||||
override suspend fun addTag(tag: String, order: Double?) {
|
||||
|
|
|
@ -18,8 +18,9 @@ package org.matrix.android.sdk.internal.session.room.timeline
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import io.realm.Sort
|
||||
import io.realm.kotlin.where
|
||||
|
@ -55,9 +56,9 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
|||
private val loadRoomMembersTask: LoadRoomMembersTask
|
||||
) : TimelineService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(roomId: String): TimelineService
|
||||
fun create(roomId: String): DefaultTimelineService
|
||||
}
|
||||
|
||||
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
|
||||
|
|
|
@ -21,16 +21,34 @@ import com.squareup.moshi.JsonClass
|
|||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class EventContextResponse(
|
||||
internal data class EventContextResponse(
|
||||
/**
|
||||
* Details of the requested 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 = "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 = "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 {
|
||||
|
||||
override val events: List<Event> by lazy {
|
||||
eventsAfter.reversed() + listOf(event) + eventsBefore
|
||||
eventsAfter.orEmpty().reversed() + event + eventsBefore.orEmpty()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,8 +22,28 @@ import org.matrix.android.sdk.api.session.events.model.Event
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
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,
|
||||
/**
|
||||
* 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 = "chunk") override val events: List<Event> = emptyList(),
|
||||
@Json(name = "state") override val stateEvents: List<Event> = emptyList()
|
||||
) : TokenChunkEvent
|
||||
/**
|
||||
* A list of room events. The order depends on the dir parameter. For dir=b events will be in
|
||||
* 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()
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ internal interface TokenChunkEvent {
|
|||
val start: String?
|
||||
val end: String?
|
||||
val events: List<Event>
|
||||
val stateEvents: List<Event>
|
||||
val stateEvents: List<Event>?
|
||||
|
||||
fun hasMore() = start != end
|
||||
}
|
||||
|
|
|
@ -156,7 +156,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
|
|||
}
|
||||
}
|
||||
return if (receivedChunk.events.isEmpty()) {
|
||||
if (receivedChunk.start != receivedChunk.end) {
|
||||
if (receivedChunk.hasMore()) {
|
||||
Result.SHOULD_FETCH_MORE
|
||||
} else {
|
||||
Result.REACHED_END
|
||||
|
@ -196,7 +196,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
|
|||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
for (stateEvent in stateEvents) {
|
||||
stateEvents?.forEach { stateEvent ->
|
||||
val ageLocalTs = stateEvent.unsignedData?.age?.let { now - it }
|
||||
val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
|
||||
currentChunk.addStateEvent(roomId, stateEventEntity, direction)
|
||||
|
@ -205,9 +205,9 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
|
|||
}
|
||||
}
|
||||
val eventIds = ArrayList<String>(eventList.size)
|
||||
for (event in eventList) {
|
||||
eventList.forEach { event ->
|
||||
if (event.eventId == null || event.senderId == null) {
|
||||
continue
|
||||
return@forEach
|
||||
}
|
||||
val ageLocalTs = event.unsignedData?.age?.let { now - it }
|
||||
eventIds.add(event.eventId)
|
||||
|
|
|
@ -17,8 +17,9 @@
|
|||
package org.matrix.android.sdk.internal.session.room.typing
|
||||
|
||||
import android.os.SystemClock
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
import org.matrix.android.sdk.api.session.room.typing.TypingService
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
|
@ -38,9 +39,9 @@ internal class DefaultTypingService @AssistedInject constructor(
|
|||
private val sendTypingTask: SendTypingTask
|
||||
) : TypingService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(roomId: String): TypingService
|
||||
fun create(roomId: String): DefaultTypingService
|
||||
}
|
||||
|
||||
private var currentTask: Cancelable? = null
|
||||
|
|
|
@ -16,8 +16,9 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room.uploads
|
||||
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.assisted.AssistedFactory
|
||||
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.UploadsService
|
||||
|
@ -28,9 +29,9 @@ internal class DefaultUploadsService @AssistedInject constructor(
|
|||
private val cryptoService: CryptoService
|
||||
) : UploadsService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(roomId: String): UploadsService
|
||||
fun create(roomId: String): DefaultUploadsService
|
||||
}
|
||||
|
||||
override suspend fun getUploads(numberOfEvents: Int, since: String?): GetUploadsResult {
|
||||
|
|
|
@ -56,8 +56,8 @@ internal class DefaultGetUploadsTask @Inject constructor(
|
|||
private val roomAPI: RoomAPI,
|
||||
private val tokenStore: SyncTokenStore,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val globalErrorReceiver: GlobalErrorReceiver)
|
||||
: GetUploadsTask {
|
||||
private val globalErrorReceiver: GlobalErrorReceiver
|
||||
) : GetUploadsTask {
|
||||
|
||||
override suspend fun execute(params: GetUploadsTask.Params): GetUploadsResult {
|
||||
val result: GetUploadsResult
|
||||
|
|
|
@ -16,45 +16,25 @@
|
|||
|
||||
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.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.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
|
||||
|
||||
internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
|
||||
private val signInAgainTask: SignInAgainTask,
|
||||
private val sessionParamsStore: SessionParamsStore,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val taskExecutor: TaskExecutor) : SignOutService {
|
||||
private val sessionParamsStore: SessionParamsStore
|
||||
) : SignOutService {
|
||||
|
||||
override fun signInAgain(password: String,
|
||||
callback: MatrixCallback<Unit>): Cancelable {
|
||||
return signInAgainTask
|
||||
.configureWith(SignInAgainTask.Params(password)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
override suspend fun signInAgain(password: String) {
|
||||
signInAgainTask.execute(SignInAgainTask.Params(password))
|
||||
}
|
||||
|
||||
override fun updateCredentials(credentials: Credentials,
|
||||
callback: MatrixCallback<Unit>): Cancelable {
|
||||
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||
sessionParamsStore.updateCredentials(credentials)
|
||||
}
|
||||
override suspend fun updateCredentials(credentials: Credentials) {
|
||||
sessionParamsStore.updateCredentials(credentials)
|
||||
}
|
||||
|
||||
override fun signOut(signOutFromHomeserver: Boolean,
|
||||
callback: MatrixCallback<Unit>): Cancelable {
|
||||
return signOutTask
|
||||
.configureWith(SignOutTask.Params(signOutFromHomeserver)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
override suspend fun signOut(signOutFromHomeserver: Boolean) {
|
||||
return signOutTask.execute(SignOutTask.Params(signOutFromHomeserver))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,8 +50,9 @@ abstract class SyncService : Service() {
|
|||
private var sessionId: String? = null
|
||||
private var mIsSelfDestroyed: Boolean = false
|
||||
|
||||
private var syncTimeoutSeconds: Int = 6
|
||||
private var syncDelaySeconds: Int = 60
|
||||
private var syncTimeoutSeconds: Int = getDefaultSyncTimeoutSeconds()
|
||||
private var syncDelaySeconds: Int = getDefaultSyncDelaySeconds()
|
||||
|
||||
private var periodic: Boolean = false
|
||||
private var preventReschedule: Boolean = false
|
||||
|
||||
|
@ -119,7 +120,11 @@ abstract class SyncService : Service() {
|
|||
serviceScope.coroutineContext.cancelChildren()
|
||||
if (!preventReschedule && periodic && sessionId != null && backgroundDetectionObserver.isInBackground) {
|
||||
Timber.d("## Sync: Reschedule service in $syncDelaySeconds sec")
|
||||
onRescheduleAsked(sessionId ?: "", false, syncTimeoutSeconds, syncDelaySeconds)
|
||||
onRescheduleAsked(
|
||||
sessionId = sessionId ?: "",
|
||||
syncTimeoutSeconds = syncTimeoutSeconds,
|
||||
syncDelaySeconds = syncDelaySeconds
|
||||
)
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
@ -166,15 +171,22 @@ abstract class SyncService : Service() {
|
|||
}
|
||||
if (throwable is Failure.NetworkConnection) {
|
||||
// Timeout is not critical, so retry as soon as possible.
|
||||
val retryDelay = if (isInitialSync || throwable.cause is SocketTimeoutException) {
|
||||
0
|
||||
} else {
|
||||
syncDelaySeconds
|
||||
if (throwable.cause is SocketTimeoutException) {
|
||||
// For big accounts, computing sync response can take time, but Synapse will cache the
|
||||
// result for the next request. So keep retrying in loop
|
||||
Timber.w("Timeout during sync, retry in loop")
|
||||
doSync()
|
||||
return
|
||||
}
|
||||
// Network might be off, no need to reschedule endless alarms :/
|
||||
preventReschedule = true
|
||||
// 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
|
||||
if (isRunning.get()) stopMe()
|
||||
|
@ -188,8 +200,8 @@ abstract class SyncService : Service() {
|
|||
}
|
||||
val matrix = Matrix.getInstance(applicationContext)
|
||||
val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false
|
||||
syncTimeoutSeconds = intent.getIntExtra(EXTRA_TIMEOUT_SECONDS, 6)
|
||||
syncDelaySeconds = intent.getIntExtra(EXTRA_DELAY_SECONDS, 60)
|
||||
syncTimeoutSeconds = intent.getIntExtra(EXTRA_TIMEOUT_SECONDS, getDefaultSyncTimeoutSeconds())
|
||||
syncDelaySeconds = intent.getIntExtra(EXTRA_DELAY_SECONDS, getDefaultSyncDelaySeconds())
|
||||
try {
|
||||
val sessionComponent = matrix.sessionManager.getSessionComponent(safeSessionId)
|
||||
?: 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 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? {
|
||||
return null
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
|||
import androidx.work.ListenableWorker
|
||||
import androidx.work.WorkerFactory
|
||||
import androidx.work.WorkerParameters
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
|
@ -32,6 +33,8 @@ class MatrixWorkerFactory @Inject constructor(
|
|||
workerClassName: String,
|
||||
workerParameters: WorkerParameters
|
||||
): ListenableWorker? {
|
||||
Timber.d("MatrixWorkerFactory.createWorker for $workerClassName")
|
||||
|
||||
val foundEntry =
|
||||
workerFactories.entries.find { Class.forName(workerClassName).isAssignableFrom(it.key) }
|
||||
val factoryProvider = foundEntry?.value
|
||||
|
|
|
@ -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_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_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>
|
|
@ -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="could_not_redact">Ei saanud muuta sõnumit</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="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>
|
||||
|
@ -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_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_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>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue