Merge branch 'develop' into feature/fga/voip_v1_start

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

View File

@ -1,12 +1,46 @@
Changes in Element 1.0.14 (2020-XX-XX)
Changes in Element 1.0.15 (2020-XX-XX)
===================================================
Features ✨:
-
Improvements 🙌:
-
Bugfix 🐛:
- Fix clear cache issue: sometimes, after a clear cache, there is still a token, so the init sync service is not started.
- Sidebar too large in horizontal orientation or tablets (#475)
- UrlPreview should be updated when the url is edited and changed (#2678)
- When receiving a new pepper from identity server, use it on the next hash lookup (#2708)
- Crashes reported by PlayStore (new in 1.0.14) (#2707)
Translations 🗣:
-
SDK API changes ⚠️:
- Increase targetSdkVersion to 30 (#2600)
Build 🧱:
- Compile with Android SDK 30 (Android 11)
Test:
-
Other changes:
- Update Dagger to 2.31 version so we can use the embedded AssistedInject feature
Changes in Element 1.0.14 (2020-01-15)
===================================================
Features ✨:
- 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)

View File

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

View File

@ -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)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,30 +1,30 @@
Element on uudenlainen viestinsovellus, joka:
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.

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ Az Element a te kezedbe adja az irányítást azáltal, hogy eldöntheted, ki t
2. A saját számítógépeden is futtathatsz szervert
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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
#Mon Dec 07 18:05:35 CET 2020
#Fri Jan 29 18:05:42 CET 2021
distributionBase=GRADLE_USER_HOME
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

View File

@ -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"

View File

@ -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'

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -26,6 +26,8 @@ import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.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
)
)
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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

View File

@ -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"
}
}

View File

@ -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
}
}
}

View File

@ -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
*/

View File

@ -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)
}

View File

@ -32,6 +32,7 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.internal.crypto.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)

View File

@ -19,6 +19,8 @@ package org.matrix.android.sdk.internal.crypto
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.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

View File

@ -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))

View File

@ -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

View File

@ -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)
}

View File

@ -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
)

View File

@ -33,11 +33,13 @@ import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.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>

View File

@ -47,6 +47,7 @@ import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.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)

View File

@ -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)
}
}

View File

@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.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

View File

@ -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

View File

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

View File

@ -20,7 +20,6 @@ import androidx.annotation.MainThread
import dagger.Lazy
import 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()
}

View File

@ -27,7 +27,6 @@ import org.matrix.android.sdk.internal.crypto.SendGossipWorker
import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
import org.matrix.android.sdk.internal.crypto.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,

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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("")
)
}
}
}

View File

@ -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))

View File

@ -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

View File

@ -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> {

View File

@ -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 {

View File

@ -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
}
/**

View File

@ -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 {

View File

@ -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))

View File

@ -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> {

View File

@ -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>) {

View File

@ -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

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.relation
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.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()

View File

@ -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) {

View File

@ -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()

View File

@ -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,

View File

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

View File

@ -16,8 +16,9 @@
package org.matrix.android.sdk.internal.session.room.tags
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?) {

View File

@ -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 {

View File

@ -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()
}
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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)

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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))
}
}

View File

@ -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

View File

@ -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

View File

@ -242,4 +242,30 @@
<string name="notice_room_server_acl_set_banned">• Server shodující se s %s je zakázán.</string>
<string name="notice_room_server_acl_set_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>

View File

@ -47,7 +47,7 @@
<string name="notice_crypto_error_unkwown_inbound_session_id">Sõnumi saatja seade ei ole selle sõnumi jaoks saatnud dekrüptimisvõtmeid.</string>
<string name="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