diff --git a/AUTHORS.md b/AUTHORS.md index a85beb2d6f..4fb5b8c994 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1,4 +1,4 @@ -A full developer contributors list can be found [here](https://github.com/vector-im/element-android/graphs/contributors). +A full developer contributors list can be found [here](https://github.com/vector-im/element-android/graphs/contributors). # Core team: @@ -33,3 +33,8 @@ First of all, we thank all contributors who use Element and report problems on t We do not forget all translators, for their work of translating Element into many languages. They are also the authors of Element. Feel free to add your name below, when you contribute to the project! + +Name | Matrix ID | GitHub +--------|---------------------|-------------------------------------- +gjpower | @gjpower:matrix.org | [gjpower](https://github.com/gjpower) + diff --git a/CHANGES.md b/CHANGES.md index b5e5c54669..fe88aed5ab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,9 +6,24 @@ Features ✨: Improvements 🙌: - Rework sending Event management (#154) + - New room creation screen: set topic and avatar in the room creation form (#2078) + - Toggle Low priority tag (#1490) + - Add option to send with enter (#1195) + - Use Hardware keyboard enter to send message (use shift-enter for new line) (#1881, #1440) + - Edit and remove icons are now visible on image attachment preview screen (#2294) + - Room profile: BigImageViewerActivity now only display the image. Use the room setting to change or delete the room Avatar + - Better visibility of text reactions in dark theme (#1118) + - Room member profile: Add action to create (or open) a DM (#2310) + - Prepare changelog for F-Droid (#2296) + - Add graphic resources for F-Droid (#812, #2220) + - Highlight text in the body of the displayed result (#2200) + - Considerably faster QR-code bitmap generation (#2331) Bugfix 🐛: - - + - Fixed ringtone handling (#2100 & #2246) + - Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252) + - Incoming call continues to ring if call is answered on another device (#1921) + - Search Result | scroll jumps after pagination (#2238) Translations 🗣: - diff --git a/build.gradle b/build.gradle index 05dcaa43ed..0c4b35b060 100644 --- a/build.gradle +++ b/build.gradle @@ -65,9 +65,8 @@ allprojects { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { // Warnings are potential errors, so stop ignoring them // You can override by passing `-PallWarningsAsErrors=false` in the command line - kotlinOptions.allWarningsAsErrors = project.properties['allWarningsAsErrors']?.toBoolean() ?: true + kotlinOptions.allWarningsAsErrors = project.getProperties().getOrDefault("allWarningsAsErrors", "true").toBoolean() } - } task clean(type: Delete) { diff --git a/fastlane/metadata/android/en-US/changelogs/40100100.txt b/fastlane/metadata/android/en-US/changelogs/40100100.txt new file mode 100644 index 0000000000..0ffdd02fcb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40100100.txt @@ -0,0 +1 @@ +// TODO \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png new file mode 100644 index 0000000000..97f45aafd3 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 0000000000..e449d60ca9 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png new file mode 100644 index 0000000000..f514bbece3 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png new file mode 100644 index 0000000000..59883465a0 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png new file mode 100644 index 0000000000..c103144063 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png new file mode 100644 index 0000000000..9903d47d37 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png new file mode 100644 index 0000000000..f5b842311c Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png new file mode 100644 index 0000000000..c45b1b8cbc Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png new file mode 100644 index 0000000000..2e6b92089e Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png differ diff --git a/fastlane/metadata/android/eo/full_description.txt b/fastlane/metadata/android/eo/full_description.txt new file mode 100644 index 0000000000..f564ab61d8 --- /dev/null +++ b/fastlane/metadata/android/eo/full_description.txt @@ -0,0 +1,30 @@ +Element estas nova speco de mesaĝilo kaj kunlaborilo, kiu: + +1. Lasas vin regi vian komunikadon por protekti vian privatecon +2. Lasas vin komuniki kun ĉiu ajn en la reto de Matrix, kaj eĉ pliaj, per interkompreno kun aplikaĵoj kiel ekzemple Slack +3. Protektas vin de reklamoj, kolektado de datumoj, kaj muritaj ĝardenoj +4. Sekurigas vian komunikadon per tutvoja ĉifrado, kun la eblo kontroli aliajn per delegaj subskriboj + +Element malsamas al aliaj mesaĝiloj kaj kunlaboriloj, ĉar ĝi estas sencentra kaj malfermitkoda. + +Element lasas vin gastigi vian propran servilon, aŭ elekti servilon, kiu plaĉas al vi, por ke vi ne perdu privatecon, kaj por ke vi daŭre regu kaj posedu viajn datumojn kaj interparolojn. Ĝi donas al vi aliron al malfermita reto; por ke via interparolado ne estu limigita nur al aliaj uzantoj de Element. Kaj ĝi estas tre sekura. + +Element povas fari ĉi ĉion, ĉar ĝi funkcias per Matrix – publika normo por malfermita, sencentra komunikado. + +Element lasas vi elekti, kiu gastigos viajn interparolojn. Per la aplikaĵo Element, vi povas elekti diversajn specojn de gastigado: + +1. Akiri senpagan konton ĉe la publika servilo matrix.org, gastigata de la programistoj de Matrix, aŭ elekti unu el miloj da publikaj serviloj, gastigataj de volontuloj +2. Memgastiĝi per via propra servilo ĉe via propra aparato +3. Registriĝi ĉe propra servilo per simpla pagaliĝo al la gastiga platformo «Element Matrix Services» + +Kial Element? + +POSEDU VIAJN DATUMOJN: Vi decidu, kie vi tenu viajn datumojn kaj mesaĝojn. Vi posedas kaj regas ilin, ne iu granda komerca firmao, kiu kolektas viajn datumojn aŭ donas aliron al aliuloj. + +MALFERMAJ MESAĜADO KAJ KUNLABORADO: Vi povas babili kun ĉiu alia en la reto de Matrix, ĉu ĝi uzas Elementon aŭ alian aplikaĵon de Matrix, kaj eĉ se ĝi uzas tute alian mesaĝilon, kiel ekzemple Slack, IRC, aŭ XMPP. + +TRE SEKURA: Vera tutvoja ĉifrado (nur la interparolantoj povas malĉifri siajn mesaĝojn), kaj delegaj subskriboj por kontroli la aparatojn de partoprenantoj. + +SENMANKA KOMUNIKADO: Mesaĝoj, voĉvokoj kaj vidvokoj, havigado de dosieroj, ekrano, kaj multaj diversaj kunigoj, robotoj kaj fenestraĵoj. Kreu ĉambrojn, komunumojn, komuniku kaj kunlaboru. + +ĈIE KUN VI: Tenu vin ĝisdata per historio de mesaĝoj plene spegulita trans ĉiuj viaj aparatoj, kaj sur la reto per https://app.element.io. diff --git a/fastlane/metadata/android/eo/short_description.txt b/fastlane/metadata/android/eo/short_description.txt new file mode 100644 index 0000000000..33013ce78f --- /dev/null +++ b/fastlane/metadata/android/eo/short_description.txt @@ -0,0 +1 @@ +Sekura kaj sencentrigita vokado kaj babilado. Tenu viajn datumojn sekuraj. diff --git a/fastlane/metadata/android/eo/title.txt b/fastlane/metadata/android/eo/title.txt new file mode 100644 index 0000000000..f56927e529 --- /dev/null +++ b/fastlane/metadata/android/eo/title.txt @@ -0,0 +1 @@ +Element (antaŭe Riot.im) diff --git a/fastlane/metadata/android/et/full_description.txt b/fastlane/metadata/android/et/full_description.txt new file mode 100644 index 0000000000..7c7f7195a8 --- /dev/null +++ b/fastlane/metadata/android/et/full_description.txt @@ -0,0 +1,30 @@ +Element on uut tüüpi suhtlus- ja koostöörakendus, mis: + +1. Võimaldab täielikku kontrolli privaatsuse üle +2. Võimaldab suhelda kõigiga Matrixi võrgus ja isegi väljaspool seda, olles integreeritud selliste rakendustega nagu Slack +3. Kaitseb sind reklaamide ja andmekogumise eest +4. Tagab turvalisuse läbiva krüptimise abil, kasutades risttunnustamist vestlejate tuvastamiseks + +Element erineb täielikult teistest sõnumside- ja koostöörakendustest, kuna see on detsentraliseeritud ja avatud lähtekoodiga. + +Element võimaldab ise hostida - või valida hosti -, et oleks tagatud privaatsus ja kontroll oma andmete ja vestluste üle. Element annab ka juurdepääsu avatud võrgule, nii et te ei pea vaid Elemendi kasutajatega rääkima. Ning kogu süsteem on väga turvaline. + +Element töötab Matrixil - avatud, detsentraliseeritud suhtlus-standardil. + +Võimaldades valida, kes vestlusi korraldab, annab Element annab kontrolli sinule. Rakendust Element saad kasutada mitmel viisil. + +1. Tasuta konto Matrixi arendajate hostitud avalikus serveris matrix.org või vali tuhandete avalike serverite hulgast, mida haldavad vabatahtlikud +2. Hosti oma kontot ise, paigaldades serveri oma riistvarale +3. Registreeruge serveris olevale kontole, tellides Element Matrix Services teenuseplatvormi + + Miks valida element? + + KONTROLL ANDMETE ÜLE: otsustad ise, kus oma andmeid ja sõnumeid hoida. Need kuuluvad sulle ja sinu käes on kontroll, mitte mõne MEGAFIRMA käes, mis andmeid oma kasuks kaevandab või kolmandatele isikutele juurdepääsu annab. + + AVATUD SUHTLUS JA KOOSTÖÖ : saad vestelda kõigi teistega Matrixi võrgus, olenemata sellest, kas nad kasutavad Elementi või mõnda muud Matrixi rakendust, ja isegi kui nad kasutavad teistsugust suhtlussüsteemi nagu Slack, IRC või XMPP. + + ÜLITURVALINE : tõeline läbiv krüptimine (ainult vestluses osalejad saavad sõnumeid lugeda) ja risttunnustamine vestluses osalejate tuvastamiseks. + + KÕIK SUHTLUSVÕIMALUSED: sõnumid, hääl- ja videokõned, failide jagamine, ekraani jagamine ja terve hulk lõiminguid, roboteid ja vidinaid. Loo tubasid, kogukondi, hoia ühendust ja saa asjad aetud. + + KÕIKJAL, KUS VIIBITE: saad suhelda kõigis oma seadmetes ja ka veebis aadressil https://app.element.io ning sealjuures täielikult sünkroonitud sõnumite ajalooga. diff --git a/fastlane/metadata/android/fa/full_description.txt b/fastlane/metadata/android/fa/full_description.txt new file mode 100644 index 0000000000..0a93676bbf --- /dev/null +++ b/fastlane/metadata/android/fa/full_description.txt @@ -0,0 +1,30 @@ +المنت گونه‌ای جدید از کاره‌های پیام‌رسانی و همکاری است که: + +۱. کنترل محرمانگیتان را در دست خودتان می‌گذارد +۲. می‌گذارد با هرکسی در شبکهٔ ماتریکس و حتا فراتر از آن، ارتباط برقرار کنید +۳. از شما در برابر تبلیغات، داده‌کاوری و دیوارهای پرداختی، محافظت می‌کند +۴. با رمزنگاری سرتاسری با ورود چندگانه، امنتان می‌کند + +المنت به خاطر نامتمرکز و نرم‌افزار آزاد بودن، کاملاً با دیگر کاره‌های پیام‌رسانی و همکاری، فرق دارد. + +المنت می‌گذارد خودمیزبانی کرده یا میزبانی برگزینید که امنیت، مالکیت و واپایش داده‌ها و گفت‌وگوهایتان را در اختیار داشته باشید. این کاره شما را به شبکه‌ای باز و شدیداً امن وصل کرده تا مجبور نباشید فقط با دیگر کاربران المنت صحبت کنید. + +المنت می‌تواند همهٔ این کارها را بکند، چرا که روی ماتریکس، استانداردی برای گفت‌وگوی باز و نامتمرکز عمل می‌کند. + +المنت با اجازه برای گزینش کسی که گفت‌وگوهایتان را میزبانی می‌کند، کنترل را به شما می‌دهد. با کارهٔ المنت، می‌توانید برگزینید که به روش‌های مختلفی میزبانی شوید: + +۱. گرفتن حسابی رایگان روی کارساز عمومی matrix.org که به دست توسعه‌دهندگان ماتریکس میزبانی می‌شود، یا گرینش از میان هزاران کارساز عمومی میزبانی‌شده به دست داوطلبان +۲. خودمیزبانی حسابتان با اجرای کراسازی روی سخت‌افزار خودتان +۳. ثبت‌نام برای حسابی روی یک کارساز سفارشی با اشتراک در بن‌یازهٔ میزبانی خدمات ماتریکس المنت + +چرا المنت را برگزینیم؟ + +مالک داده‌هایتان باشید: خوتان تصمیم می‌گیرید که داده‌ها و پیام‌هایتان را کجا نگه دارید. شما صاحبشان هستید و واپایششان می‌کنید، نه شرکت‌های بزرگی که داده‌هایتان را کاویده و به شرکت‌های دیگر دسترسی می‌دهند. + +پیام‌رسانی و همکاری باز: می‌توانید با هرکسی در شبکهٔ ماتریکس گپ بزنید، چه از المنت استفاده کنند و چه از هر کارهٔ ماتریکس دیگری؛ و حتا اگر از سامانهٔ پیام‌رسانی متفاوتی مثل اسلک، آی‌آرسی یا جبر استفاده کنند. + +فوق امن: رمزنگاری سرتاسری واقعی (فقط کسانی که در گفت‌وگو هستند،‌می‌توانند پیام‌ها را رمزگشایی کنند) و ورود چندگانه برای تأیید هویت افزاره‌های شرکت‌کنندگان در گفت‌وگو. + +ارتباط کامل: پیام‌رسانی، تماس‌های صوتی و تصویری،‌هم‌رسانی پرونده، هم‌رسانی صفحه و یه عالمه یکپارچگی، بات و ابزارک. اتاق و اجتماع ساخته، در دسترس بوده و کارها را انجام دهید. + +هرجا که هستید: هر کجا که هستید، با هم‌گام سازی کامل تاریخچهٔ پیام‌ها بین همهٔ افزاره‌هایتان و وب روی https://app.element.io در دسترس باشید. diff --git a/fastlane/metadata/android/fa/short_description.txt b/fastlane/metadata/android/fa/short_description.txt new file mode 100644 index 0000000000..4cfa767649 --- /dev/null +++ b/fastlane/metadata/android/fa/short_description.txt @@ -0,0 +1 @@ +گپ و تماس نامتمرکز امن. داده‌هایتان را از شرکت‌ها امن نگه دارید. diff --git a/fastlane/metadata/android/fa/title.txt b/fastlane/metadata/android/fa/title.txt new file mode 100644 index 0000000000..fb4ea4125e --- /dev/null +++ b/fastlane/metadata/android/fa/title.txt @@ -0,0 +1 @@ +المنت (ریوت سابق) diff --git a/fastlane/metadata/android/fr/short_description.txt b/fastlane/metadata/android/fr/short_description.txt new file mode 100644 index 0000000000..2fb9762e97 --- /dev/null +++ b/fastlane/metadata/android/fr/short_description.txt @@ -0,0 +1 @@ +Chat & VoIP sûr et décentralisé. Gardez vos données en sécurité. diff --git a/fastlane/metadata/android/sv/full_description.txt b/fastlane/metadata/android/sv/full_description.txt index afd0975586..d130e9214a 100644 --- a/fastlane/metadata/android/sv/full_description.txt +++ b/fastlane/metadata/android/sv/full_description.txt @@ -21,7 +21,7 @@ Element sätter dig i kontroll genom att låta dig välja att vara värd för di ÄG DIN DATA: Du väljer var du vill ha din data och dina meddelanden. Du äger den och kontrollerar den, inte nåt stort företag som samlar in din data och ger den till tredje parter. -ÖPPEN KOMMUNIKATION OCH ÖPPET SAMARBETE: Du kan chatta med med vem som helst på Matrix-nätverket, oavsett om de använder Element eller en annan Matrix-app, och till och med om de använder ett annat meddelandesystem som Slack, IRC eller XMPP. +ÖPPEN KOMMUNIKATION OCH ÖPPET SAMARBETE: Du kan chatta med vem som helst på Matrix-nätverket, oavsett om de använder Element eller en annan Matrix-app, och till och med om de använder ett annat meddelandesystem som Slack, IRC eller XMPP. SUPERSÄKER: Riktig totalsträckskryptering (bara de in konversationen kan avkryptera meddelandena), och korssingering för att verifiera konversationsmedlemmars enheter. diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt index 228e83faff..86f2d26808 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt @@ -142,6 +142,10 @@ class RxRoom(private val room: Room) { fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder { room.updateAvatar(avatarUri, fileName, it) } + + fun deleteAvatar(): Completable = completableBuilder { + room.deleteAvatar(it) + } } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 1c912b365f..cbe4cca8a3 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -88,7 +88,10 @@ class CommonTestHelper(context: Context) { fun syncSession(session: Session) { val lock = CountDownLatch(1) - GlobalScope.launch(Dispatchers.Main) { session.open() } + val job = GlobalScope.launch(Dispatchers.Main) { + session.open() + } + runBlocking { job.join() } session.startSync(true) @@ -341,7 +344,7 @@ class CommonTestHelper(context: Context) { } // Transform a method with a MatrixCallback to a synchronous method - inline fun doSync(block: (MatrixCallback) -> Unit): T { + inline fun doSync(timeout: Long? = TestConstants.timeOutMillis, block: (MatrixCallback) -> Unit): T { val lock = CountDownLatch(1) var result: T? = null @@ -354,7 +357,7 @@ class CommonTestHelper(context: Context) { block.invoke(callback) - await(lock) + await(lock, timeout) assertNotNull(result) return result!! @@ -366,8 +369,9 @@ class CommonTestHelper(context: Context) { fun Iterable.signOutAndClose() = forEach { signOutAndClose(it) } fun signOutAndClose(session: Session) { - doSync { session.signOut(true, it) } - session.close() + doSync(60_000) { session.signOut(true, it) } + // no need signout will close + // session.close() } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index 370b416f54..b67172a908 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -32,9 +32,6 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams -import org.matrix.android.sdk.api.session.room.timeline.Timeline -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData @@ -197,47 +194,16 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! - val lock = CountDownLatch(1) - - val bobEventsListener = object : Timeline.Listener { - override fun onTimelineFailure(throwable: Throwable) { - // noop - } - - override fun onNewTimelineEvents(eventIds: List) { - // noop - } - - override fun onTimelineUpdated(snapshot: List) { - val messages = snapshot.filter { it.root.getClearType() == EventType.MESSAGE } - .groupBy { it.root.senderId!! } - - // Alice has sent 2 messages and Bob has sent 3 messages - if (messages[aliceSession.myUserId]?.size == 2 && messages[bobSession.myUserId]?.size == 3) { - lock.countDown() - } - } - } - - val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20)) - bobTimeline.start() - bobTimeline.addListener(bobEventsListener) - // Alice sends a message - roomFromAlicePOV.sendTextMessage(messagesFromAlice[0]) + mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[0], 1) +// roomFromAlicePOV.sendTextMessage(messagesFromAlice[0]) // Bob send 3 messages - roomFromBobPOV.sendTextMessage(messagesFromBob[0]) - roomFromBobPOV.sendTextMessage(messagesFromBob[1]) - roomFromBobPOV.sendTextMessage(messagesFromBob[2]) - + mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1) + mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[1], 1) + mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[2], 1) // Alice sends a message - roomFromAlicePOV.sendTextMessage(messagesFromAlice[1]) - - mTestHelper.await(lock) - - bobTimeline.removeListener(bobEventsListener) - bobTimeline.dispose() + mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[1], 1) return cryptoTestData } @@ -285,14 +251,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { fun createDM(alice: Session, bob: Session): String { val roomId = mTestHelper.doSync { - alice.createRoom( - CreateRoomParams().apply { - invitedUserIds.add(bob.myUserId) - setDirectMessage() - enableEncryptionIfInvitedUsersSupportIt = true - }, - it - ) + alice.createDirectRoom(bob.myUserId, it) } mTestHelper.waitWithLatch { latch -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index 121d9fb401..34be1b8d05 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.crypto import android.content.Context import androidx.lifecycle.LiveData +import androidx.paging.PagedList import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService @@ -40,6 +41,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody +import kotlin.jvm.Throws interface CryptoService { @@ -142,10 +144,13 @@ interface CryptoService { fun removeSessionListener(listener: NewSessionListener) fun getOutgoingRoomKeyRequests(): List + fun getOutgoingRoomKeyRequestsPaged(): LiveData> fun getIncomingRoomKeyRequests(): List + fun getIncomingRoomKeyRequestsPaged(): LiveData> - fun getGossipingEventsTrail(): List + fun getGossipingEventsTrail(): LiveData> + fun getGossipingEvents(): List // For testing shared session fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index 965e7e23bb..b772225f51 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -35,6 +35,22 @@ interface RoomService { fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable + /** + * Create a direct room asynchronously. This is a facility method to create a direct room with the necessary parameters + */ + fun createDirectRoom(otherUserId: String, + callback: MatrixCallback): Cancelable { + return createRoom( + CreateRoomParams() + .apply { + invitedUserIds.add(otherUserId) + setDirectMessage() + enableEncryptionIfInvitedUsersSupportIt = true + }, + callback + ) + } + /** * Join a room by id * @param roomIdOrAlias the roomId or the room alias of the room to join @@ -113,5 +129,16 @@ interface RoomService { */ fun getChangeMembershipsLive(): LiveData> - fun getExistingDirectRoomWithUser(otherUserId: String): Room? + /** + * Return the roomId of an existing DM with the other user, or null if such room does not exist + * A room is a DM if: + * - it is listed in the `m.direct` account data + * - the current user has joined the room + * - the other user is invited or has joined the room + * - it has exactly 2 members + * Note: + * - the returning room can be encrypted or not + * - the power level of the users are not taken into account. Normally in a DM, the 2 members are admins of the room + */ + fun getExistingDirectRoomWithUser(otherUserId: String): String? } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt index f170c098bc..9455a83aff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt @@ -63,8 +63,13 @@ data class RoomSummary constructor( val hasNewMessages: Boolean get() = notificationCount != 0 + val isLowPriority: Boolean + get() = hasTag(RoomTag.ROOM_TAG_LOW_PRIORITY) + val isFavorite: Boolean - get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE } + get() = hasTag(RoomTag.ROOM_TAG_FAVOURITE) + + fun hasTag(tag: String) = tags.any { it.name == tag } val canStartCall: Boolean get() = joinedMembersCount == 2 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt index 0860b25d69..892a865751 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.api.session.room.model.create +import android.net.Uri import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility @@ -51,6 +52,11 @@ class CreateRoomParams { */ var topic: String? = null + /** + * If this is not null, the image uri will be sent to the media server and will be set as a room avatar. + */ + var avatarUri: Uri? = null + /** * A list of user IDs to invite to the room. * This will tell the server to invite everyone in the list to the newly created room. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt index 8c08743972..e4baa58c30 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt @@ -58,6 +58,11 @@ interface StateService { */ fun updateAvatar(avatarUri: Uri, fileName: String, callback: MatrixCallback): Cancelable + /** + * Delete the avatar of the room + */ + fun deleteAvatar(callback: MatrixCallback): Cancelable + fun sendStateEvent(eventType: String, stateKey: String?, body: JsonDict, callback: MatrixCallback): Cancelable fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt new file mode 100644 index 0000000000..e8a70615e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.query.whereType +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.util.fetchCopied +import javax.inject.Inject + +/** + * The crypto module needs some information regarding rooms that are stored + * in the session DB, this class encapsulate this functionality + */ +internal class CryptoSessionInfoProvider @Inject constructor( + @SessionDatabase private val monarchy: Monarchy +) { + + fun isRoomEncrypted(roomId: String): Boolean { + val encryptionEvent = monarchy.fetchCopied { realm -> + EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) + .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") + .isNotNull(EventEntityFields.STATE_KEY) // should be an empty key + .findFirst() + } + return encryptionEvent != null + } + + /** + * @param allActive if true return joined as well as invited, if false, only joined + */ + fun getRoomUserIds(roomId: String, allActive: Boolean): List { + var userIds: List = emptyList() + monarchy.doWithRealm { realm -> + userIds = if (allActive) { + RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() + } else { + RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds() + } + } + return userIds + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index b78afe6d41..326eac8f91 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -17,12 +17,10 @@ package org.matrix.android.sdk.internal.crypto import android.content.Context -import android.os.Handler -import android.os.Looper import androidx.annotation.VisibleForTesting import androidx.lifecycle.LiveData +import androidx.paging.PagedList import com.squareup.moshi.Types -import com.zhuinden.monarchy.Monarchy import dagger.Lazy import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -51,9 +49,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent -import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter -import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension @@ -68,7 +64,6 @@ import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent -import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent @@ -82,21 +77,15 @@ import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask -import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService -import org.matrix.android.sdk.internal.database.model.EventEntity -import org.matrix.android.sdk.internal.database.model.EventEntityFields -import org.matrix.android.sdk.internal.database.query.whereType import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.MoshiProvider -import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.extensions.foldToCallback import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask -import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskThread @@ -104,11 +93,11 @@ import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.launchToCallback import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import org.matrix.android.sdk.internal.util.fetchCopied import org.matrix.olm.OlmManager import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject +import kotlin.jvm.Throws import kotlin.math.max /** @@ -171,28 +160,16 @@ internal class DefaultCryptoService @Inject constructor( private val setDeviceNameTask: SetDeviceNameTask, private val uploadKeysTask: UploadKeysTask, private val loadRoomMembersTask: LoadRoomMembersTask, - @SessionDatabase private val monarchy: Monarchy, + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val taskExecutor: TaskExecutor, private val cryptoCoroutineScope: CoroutineScope, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val sendToDeviceTask: SendToDeviceTask, - private val messageEncrypter: MessageEncrypter + private val eventDecryptor: EventDecryptor ) : CryptoService { - init { - verificationService.cryptoService = this - } - - private val uiHandler = Handler(Looper.getMainLooper()) - private val isStarting = AtomicBoolean(false) private val isStarted = AtomicBoolean(false) - // The date of the last time we forced establishment - // of a new session for each user:device. - private val lastNewSessionForcedDates = MXUsersDevicesMap() - fun onStateEvent(roomId: String, event: Event) { when (event.getClearType()) { EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) @@ -209,6 +186,8 @@ internal class DefaultCryptoService @Inject constructor( } } + val gossipingBuffer = mutableListOf() + override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) { setDeviceNameTask .configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) { @@ -410,7 +389,7 @@ internal class DefaultCryptoService @Inject constructor( */ fun close() = runBlocking(coroutineDispatchers.crypto) { cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module")) - + incomingGossipingRequestManager.close() olmDevice.release() cryptoStore.close() } @@ -452,6 +431,13 @@ internal class DefaultCryptoService @Inject constructor( incomingGossipingRequestManager.processReceivedGossipingRequests() } } + + tryOrNull { + gossipingBuffer.toList().let { + cryptoStore.saveGossipingEvents(it) + } + gossipingBuffer.clear() + } } } @@ -612,13 +598,7 @@ internal class DefaultCryptoService @Inject constructor( * @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM */ override fun isRoomEncrypted(roomId: String): Boolean { - val encryptionEvent = monarchy.fetchCopied { realm -> - EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) - .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") - .isNotNull(EventEntityFields.STATE_KEY) - .findFirst() - } - return encryptionEvent != null + return cryptoSessionInfoProvider.isRoomEncrypted(roomId) } /** @@ -660,11 +640,8 @@ internal class DefaultCryptoService @Inject constructor( eventType: String, roomId: String, callback: MatrixCallback) { + // moved to crypto scope to have uptodate values cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { -// if (!isStarted()) { -// Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init") -// internalStart(false) -// } val userIds = getRoomUserIds(roomId) var alg = roomEncryptorsStore.get(roomId) if (alg == null) { @@ -720,14 +697,7 @@ internal class DefaultCryptoService @Inject constructor( * @param callback the callback to return data or null */ override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) { - cryptoCoroutineScope.launch { - val result = runCatching { - withContext(coroutineDispatchers.crypto) { - internalDecryptEvent(event, timeline) - } - } - result.foldToCallback(callback) - } + eventDecryptor.decryptEventAsync(event, timeline, callback) } /** @@ -739,42 +709,7 @@ internal class DefaultCryptoService @Inject constructor( */ @Throws(MXCryptoError::class) private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { - val eventContent = event.content - if (eventContent == null) { - Timber.e("## CRYPTO | decryptEvent : empty event content") - throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) - } else { - val algorithm = eventContent["algorithm"]?.toString() - val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm) - if (alg == null) { - val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm) - Timber.e("## CRYPTO | decryptEvent() : $reason") - throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) - } else { - try { - return alg.decryptEvent(event, timeline) - } catch (mxCryptoError: MXCryptoError) { - Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError") - if (algorithm == MXCRYPTO_ALGORITHM_OLM) { - if (mxCryptoError is MXCryptoError.Base - && mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) { - // need to find sending device - val olmContent = event.content.toModel() - cryptoStore.getUserDevices(event.senderId ?: "") - ?.values - ?.firstOrNull { it.identityKey() == olmContent?.senderKey } - ?.let { - markOlmSessionForUnwedging(event.senderId ?: "", it) - } - ?: run { - Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device") - } - } - } - throw mxCryptoError - } - } - } + return eventDecryptor.decryptEvent(event, timeline) } /** @@ -796,19 +731,19 @@ internal class DefaultCryptoService @Inject constructor( cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { when (event.getClearType()) { EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> { - cryptoStore.saveGossipingEvent(event) + gossipingBuffer.add(event) // Keys are imported directly, not waiting for end of sync onRoomKeyEvent(event) } EventType.REQUEST_SECRET, EventType.ROOM_KEY_REQUEST -> { // save audit trail - cryptoStore.saveGossipingEvent(event) + gossipingBuffer.add(event) // Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete) incomingGossipingRequestManager.onGossipingRequestEvent(event) } EventType.SEND_SECRET -> { - cryptoStore.saveGossipingEvent(event) + gossipingBuffer.add(event) onSecretSendReceived(event) } EventType.ROOM_KEY_WITHHELD -> { @@ -828,7 +763,7 @@ internal class DefaultCryptoService @Inject constructor( */ private fun onRoomKeyEvent(event: Event) { val roomKeyContent = event.getClearContent().toModel() ?: return - Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>") + Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>") if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : missing fields") return @@ -935,19 +870,9 @@ internal class DefaultCryptoService @Inject constructor( } private fun getRoomUserIds(roomId: String): List { - var userIds: List = emptyList() - monarchy.doWithRealm { realm -> - // Check whether the event content must be encrypted for the invited members. - val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser() - && shouldEncryptForInvitedMembers(roomId) - - userIds = if (encryptForInvitedMembers) { - RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() - } else { - RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds() - } - } - return userIds + val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser() + && shouldEncryptForInvitedMembers(roomId) + return cryptoSessionInfoProvider.getRoomUserIds(roomId, encryptForInvitedMembers) } /** @@ -1257,38 +1182,38 @@ internal class DefaultCryptoService @Inject constructor( incomingGossipingRequestManager.removeRoomKeysRequestListener(listener) } - private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) { - val deviceKey = deviceInfo.identityKey() - - val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 - val now = System.currentTimeMillis() - if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { - Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") - return - } - - Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") - lastNewSessionForcedDates.setObject(senderId, deviceKey, now) - - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) - - // Now send a blank message on that session so the other side knows about it. - // (The keyshare request is sent in the clear so that won't do) - // We send this first such that, as long as the toDevice messages arrive in the - // same order we sent them, the other end will get this first, set up the new session, - // then get the keyshare request and send the key over this new session (because it - // is the session it has most recently received a message on). - val payloadJson = mapOf("type" to EventType.DUMMY) - - val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) - val sendToDeviceMap = MXUsersDevicesMap() - sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) - Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") - val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) - sendToDeviceTask.execute(sendToDeviceParams) - } - } +// private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) { +// val deviceKey = deviceInfo.identityKey() +// +// val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 +// val now = System.currentTimeMillis() +// if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { +// Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") +// return +// } +// +// Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") +// lastNewSessionForcedDates.setObject(senderId, deviceKey, now) +// +// cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { +// ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) +// +// // Now send a blank message on that session so the other side knows about it. +// // (The keyshare request is sent in the clear so that won't do) +// // We send this first such that, as long as the toDevice messages arrive in the +// // same order we sent them, the other end will get this first, set up the new session, +// // then get the keyshare request and send the key over this new session (because it +// // is the session it has most recently received a message on). +// val payloadJson = mapOf("type" to EventType.DUMMY) +// +// val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) +// val sendToDeviceMap = MXUsersDevicesMap() +// sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) +// Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") +// val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) +// sendToDeviceTask.execute(sendToDeviceParams) +// } +// } /** * Provides the list of unknown devices @@ -1339,14 +1264,26 @@ internal class DefaultCryptoService @Inject constructor( return cryptoStore.getOutgoingRoomKeyRequests() } + override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { + return cryptoStore.getOutgoingRoomKeyRequestsPaged() + } + + override fun getIncomingRoomKeyRequestsPaged(): LiveData> { + return cryptoStore.getIncomingRoomKeyRequestsPaged() + } + override fun getIncomingRoomKeyRequests(): List { return cryptoStore.getIncomingRoomKeyRequests() } - override fun getGossipingEventsTrail(): List { + override fun getGossipingEventsTrail(): LiveData> { return cryptoStore.getGossipingEventsTrail() } + override fun getGossipingEvents(): List { + return cryptoStore.getGossipingEvents() + } + override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap { return cryptoStore.getSharedWithInfo(roomId, sessionId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt index ab30d3052d..42df6b354b 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -377,7 +377,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM } // Update devices trust for these users - dispatchDeviceChange(downloadUsers) + // dispatchDeviceChange(downloadUsers) return onKeysDownloadSucceed(filteredUsers, response.failures) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt new file mode 100644 index 0000000000..38488f1ca7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction +import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask +import org.matrix.android.sdk.internal.extensions.foldToCallback +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import timber.log.Timber +import javax.inject.Inject +import kotlin.jvm.Throws + +@SessionScope +internal class EventDecryptor @Inject constructor( + private val cryptoCoroutineScope: CoroutineScope, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val roomDecryptorProvider: RoomDecryptorProvider, + private val messageEncrypter: MessageEncrypter, + private val sendToDeviceTask: SendToDeviceTask, + private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, + private val cryptoStore: IMXCryptoStore +) { + + // The date of the last time we forced establishment + // of a new session for each user:device. + private val lastNewSessionForcedDates = MXUsersDevicesMap() + + /** + * Decrypt an event + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @return the MXEventDecryptionResult data, or throw in case of error + */ + @Throws(MXCryptoError::class) + fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + return internalDecryptEvent(event, timeline) + } + + /** + * Decrypt an event asynchronously + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @param callback the callback to return data or null + */ + fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback) { + // is it needed to do that on the crypto scope?? + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + runCatching { + internalDecryptEvent(event, timeline) + }.foldToCallback(callback) + } + } + + /** + * Decrypt an event + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @return the MXEventDecryptionResult data, or null in case of error + */ + @Throws(MXCryptoError::class) + private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { + val eventContent = event.content + if (eventContent == null) { + Timber.e("## CRYPTO | decryptEvent : empty event content") + throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) + } else { + val algorithm = eventContent["algorithm"]?.toString() + val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm) + if (alg == null) { + val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm) + Timber.e("## CRYPTO | decryptEvent() : $reason") + throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) + } else { + try { + return alg.decryptEvent(event, timeline) + } catch (mxCryptoError: MXCryptoError) { + Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError") + if (algorithm == MXCRYPTO_ALGORITHM_OLM) { + if (mxCryptoError is MXCryptoError.Base + && mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) { + // need to find sending device + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + val olmContent = event.content.toModel() + cryptoStore.getUserDevices(event.senderId ?: "") + ?.values + ?.firstOrNull { it.identityKey() == olmContent?.senderKey } + ?.let { + markOlmSessionForUnwedging(event.senderId ?: "", it) + } + ?: run { + Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device") + } + } + } + } + throw mxCryptoError + } + } + } + } + + // coroutineDispatchers.crypto scope + private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) { + val deviceKey = deviceInfo.identityKey() + + val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0 + val now = System.currentTimeMillis() + if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) { + Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another") + return + } + + Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}") + lastNewSessionForcedDates.setObject(senderId, deviceKey, now) + + // offload this from crypto thread (?) + cryptoCoroutineScope.launch(coroutineDispatchers.computation) { + ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) + + // Now send a blank message on that session so the other side knows about it. + // (The keyshare request is sent in the clear so that won't do) + // We send this first such that, as long as the toDevice messages arrive in the + // same order we sent them, the other end will get this first, set up the new session, + // then get the keyshare request and send the key over this new session (because it + // is the session it has most recently received a message on). + val payloadJson = mapOf("type" to EventType.DUMMY) + + val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload) + Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}") + withContext(coroutineDispatchers.io) { + val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) + sendToDeviceTask.execute(sendToDeviceParams) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt new file mode 100644 index 0000000000..06c667ee4a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto + +import android.util.LruCache +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2 +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers +import timber.log.Timber +import java.util.Timer +import java.util.TimerTask +import javax.inject.Inject + +/** + * Allows to cache and batch store operations on inbound group session store. + * Because it is used in the decrypt flow, that can be called quite rapidly + */ +internal class InboundGroupSessionStore @Inject constructor( + private val store: IMXCryptoStore, + private val cryptoCoroutineScope: CoroutineScope, + private val coroutineDispatchers: MatrixCoroutineDispatchers) { + + private data class CacheKey( + val sessionId: String, + val senderKey: String + ) + + private val sessionCache = object : LruCache(30) { + override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: OlmInboundGroupSessionWrapper2?, newValue: OlmInboundGroupSessionWrapper2?) { + if (evicted && oldValue != null) { + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + Timber.v("## Inbound: entryRemoved ${oldValue.roomId}-${oldValue.senderKey}") + store.storeInboundGroupSessions(listOf(oldValue)) + } + } + } + } + + private val timer = Timer() + private var timerTask: TimerTask? = null + + private val dirtySession = mutableListOf() + + @Synchronized + fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? { + synchronized(sessionCache) { + val known = sessionCache[CacheKey(sessionId, senderKey)] + Timber.v("## Inbound: getInboundGroupSession in cache ${known != null}") + return known ?: store.getInboundGroupSession(sessionId, senderKey)?.also { + Timber.v("## Inbound: getInboundGroupSession cache populate ${it.roomId}") + sessionCache.put(CacheKey(sessionId, senderKey), it) + } + } + } + + @Synchronized + fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2) { + Timber.v("## Inbound: getInboundGroupSession mark as dirty ${wrapper.roomId}-${wrapper.senderKey}") + // We want to batch this a bit for performances + dirtySession.add(wrapper) + + timerTask?.cancel() + timerTask = object : TimerTask() { + override fun run() { + batchSave() + } + } + timer.schedule(timerTask!!, 2_000) + } + + @Synchronized + private fun batchSave() { + val toSave = mutableListOf().apply { addAll(dirtySession) } + dirtySession.clear() + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + Timber.v("## Inbound: getInboundGroupSession batching save of ${dirtySession.size}") + tryOrNull { + store.storeInboundGroupSessions(toSave) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt index 8869e73432..97ae0b9d83 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import timber.log.Timber +import java.util.concurrent.Executors import javax.inject.Inject @SessionScope @@ -52,6 +53,7 @@ internal class IncomingGossipingRequestManager @Inject constructor( private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope) { + private val executor = Executors.newSingleThreadExecutor() // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations // we received in the current sync. private val receivedGossipingRequests = ArrayList() @@ -64,6 +66,10 @@ internal class IncomingGossipingRequestManager @Inject constructor( receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests()) } + fun close() { + executor.shutdownNow() + } + // Recently verified devices (map of deviceId and timestamp) private val recentlyVerifiedDevices = HashMap() @@ -99,7 +105,7 @@ internal class IncomingGossipingRequestManager @Inject constructor( fun onGossipingRequestEvent(event: Event) { Timber.v("## CRYPTO | GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}") val roomKeyShare = event.getClearContent().toModel() - val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } + // val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } when (roomKeyShare?.action) { GossipingToDeviceObject.ACTION_SHARE_REQUEST -> { if (event.getClearType() == EventType.REQUEST_SECRET) { @@ -108,8 +114,8 @@ internal class IncomingGossipingRequestManager @Inject constructor( // ignore, it was sent by me as * Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") } else { - // save in DB - cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) +// // save in DB +// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) receivedGossipingRequests.add(it) } } @@ -119,7 +125,7 @@ internal class IncomingGossipingRequestManager @Inject constructor( // ignore, it was sent by me as * Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo") } else { - cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) +// cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs) receivedGossipingRequests.add(it) } } @@ -144,13 +150,8 @@ internal class IncomingGossipingRequestManager @Inject constructor( fun processReceivedGossipingRequests() { val roomKeyRequestsToProcess = receivedGossipingRequests.toList() receivedGossipingRequests.clear() - for (request in roomKeyRequestsToProcess) { - if (request is IncomingRoomKeyRequest) { - processIncomingRoomKeyRequest(request) - } else if (request is IncomingSecretShareRequest) { - processIncomingSecretShareRequest(request) - } - } + + Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : ${roomKeyRequestsToProcess.size} request to process") var receivedRequestCancellations: List? = null @@ -161,24 +162,35 @@ internal class IncomingGossipingRequestManager @Inject constructor( } } - receivedRequestCancellations?.forEach { request -> - Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request") - // we should probably only notify the app of cancellations we told it - // about, but we don't currently have a record of that, so we just pass - // everything through. - if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) { - // ignore remote echo - return@forEach + executor.execute { + cryptoStore.storeIncomingGossipingRequests(roomKeyRequestsToProcess) + for (request in roomKeyRequestsToProcess) { + if (request is IncomingRoomKeyRequest) { + processIncomingRoomKeyRequest(request) + } else if (request is IncomingSecretShareRequest) { + processIncomingSecretShareRequest(request) + } } - val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "") - if (matchingIncoming == null) { - // ignore that? - return@forEach - } else { - // If it was accepted from this device, keep the information, do not mark as cancelled - if (matchingIncoming.state != GossipingRequestState.ACCEPTED) { - onRoomKeyRequestCancellation(request) - cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER) + + receivedRequestCancellations?.forEach { request -> + Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request") + // we should probably only notify the app of cancellations we told it + // about, but we don't currently have a record of that, so we just pass + // everything through. + if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) { + // ignore remote echo + return@forEach + } + val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "") + if (matchingIncoming == null) { + // ignore that? + return@forEach + } else { + // If it was accepted from this device, keep the information, do not mark as cancelled + if (matchingIncoming.state != GossipingRequestState.ACCEPTED) { + onRoomKeyRequestCancellation(request) + cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER) + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt index 7a546993b8..1a4d1136c8 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt @@ -44,7 +44,9 @@ internal class MXOlmDevice @Inject constructor( /** * The store where crypto data is saved. */ - private val store: IMXCryptoStore) { + private val store: IMXCryptoStore, + private val inboundGroupSessionStore: InboundGroupSessionStore + ) { /** * @return the Curve25519 key for the account. @@ -657,7 +659,7 @@ internal class MXOlmDevice @Inject constructor( timelineSet.add(messageIndexKey) } - store.storeInboundGroupSessions(listOf(session)) + inboundGroupSessionStore.storeInBoundGroupSession(session) val payload = try { val adapter = MoshiProvider.providesMoshi().adapter(JSON_DICT_PARAMETERIZED_TYPE) val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage) @@ -745,7 +747,7 @@ internal class MXOlmDevice @Inject constructor( throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON) } - val session = store.getInboundGroupSession(sessionId, senderKey) + val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey) if (session != null) { // Check that the room id matches the original one for the session. This stops diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt index efda663230..c86f2be0a3 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt @@ -88,7 +88,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor( * @param requestBody requestBody */ fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch(coroutineDispatchers.computation) { cancelRoomKeyRequest(requestBody, false) } } @@ -99,7 +99,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor( * @param requestBody requestBody */ fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch(coroutineDispatchers.computation) { cancelRoomKeyRequest(requestBody, true) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 466722788d..e55cf37118 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -16,10 +16,13 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Content +import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM @@ -39,6 +42,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.JsonCanonicalizer +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.convertToUTF8 import timber.log.Timber @@ -54,7 +58,9 @@ internal class MXMegolmEncryption( private val sendToDeviceTask: SendToDeviceTask, private val messageEncrypter: MessageEncrypter, private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, - private val taskExecutor: TaskExecutor + private val taskExecutor: TaskExecutor, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope ) : IMXEncrypting { // OutboundSessionInfo. Null if we haven't yet started setting one up. Note @@ -84,15 +90,18 @@ internal class MXMegolmEncryption( } private fun notifyWithheldForSession(devices: MXUsersDevicesMap, outboundSession: MXOutboundSessionInfo) { - mutableListOf>().apply { - devices.forEach { userId, deviceId, withheldCode -> - this.add(UserDevice(userId, deviceId) to withheldCode) + // offload to computation thread + cryptoCoroutineScope.launch(coroutineDispatchers.computation) { + mutableListOf>().apply { + devices.forEach { userId, deviceId, withheldCode -> + this.add(UserDevice(userId, deviceId) to withheldCode) + } + }.groupBy( + { it.second }, + { it.first } + ).forEach { (code, targets) -> + notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code) } - }.groupBy( - { it.second }, - { it.first } - ).forEach { (code, targets) -> - notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code) } } @@ -247,6 +256,15 @@ internal class MXMegolmEncryption( for ((userId, devicesToShareWith) in devicesByUser) { for ((deviceId) in devicesToShareWith) { session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex) + cryptoStore.saveGossipingEvent(Event( + type = EventType.ROOM_KEY, + senderId = credentials.userId, + content = submap.apply { + this["session_key"] = "" + // we add a fake key for trail + this["_dest"] = "$userId|$deviceId" + } + )) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt index 8f651692fc..f0cc15fb63 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm +import kotlinx.coroutines.CoroutineScope import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXOlmDevice @@ -26,6 +27,7 @@ import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepo import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import javax.inject.Inject internal class MXMegolmEncryptionFactory @Inject constructor( @@ -38,7 +40,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor( private val sendToDeviceTask: SendToDeviceTask, private val messageEncrypter: MessageEncrypter, private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, - private val taskExecutor: TaskExecutor) { + private val taskExecutor: TaskExecutor, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope) { fun create(roomId: String): MXMegolmEncryption { return MXMegolmEncryption( @@ -52,7 +56,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor( sendToDeviceTask, messageEncrypter, warnOnUnknownDevicesRepository, - taskExecutor + taskExecutor, + coroutineDispatchers, + cryptoCoroutineScope ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt index b5056a0efd..1871dba0e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -17,6 +17,8 @@ package org.matrix.android.sdk.internal.crypto.crosssigning import androidx.lifecycle.LiveData +import androidx.work.BackoffPolicy +import androidx.work.ExistingWorkPolicy import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService @@ -39,15 +41,20 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.withoutPrefix import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo +import org.matrix.android.sdk.internal.di.SessionId +import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.olm.OlmPkSigning import org.matrix.olm.OlmUtility import timber.log.Timber +import java.util.concurrent.TimeUnit import javax.inject.Inject @SessionScope internal class DefaultCrossSigningService @Inject constructor( @UserId private val userId: String, + @SessionId private val sessionId: String, private val cryptoStore: IMXCryptoStore, private val deviceListManager: DeviceListManager, private val initializeCrossSigningTask: InitializeCrossSigningTask, @@ -55,7 +62,7 @@ internal class DefaultCrossSigningService @Inject constructor( private val taskExecutor: TaskExecutor, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope, - private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { + private val workManagerProvider: WorkManagerProvider) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { private var olmUtility: OlmUtility? = null @@ -360,6 +367,12 @@ internal class DefaultCrossSigningService @Inject constructor( // First let's get my user key val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId)) + + return UserTrustResult.Success + } + + fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult { val myUserKey = myCrossSigningInfo?.userKey() ?: return UserTrustResult.CrossSigningNotConfigured(userId) @@ -368,15 +381,15 @@ internal class DefaultCrossSigningService @Inject constructor( } // Let's get the other user master key - val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey() - ?: return UserTrustResult.UnknownCrossSignatureInfo(otherUserId) + val otherMasterKey = otherInfo?.masterKey() + ?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "") val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures ?.get(userId) // Signatures made by me ?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}") if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) { - Timber.d("## CrossSigning checkUserTrust false for $otherUserId, not signed by my UserSigningKey") + Timber.d("## CrossSigning checkUserTrust false for ${otherInfo.userId}, not signed by my UserSigningKey") return UserTrustResult.KeyNotSigned(otherMasterKey) } @@ -396,6 +409,15 @@ internal class DefaultCrossSigningService @Inject constructor( // and that MSK is trusted (i know the private key, or is signed by a trusted device) val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(userId)) + } + + fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List?): UserTrustResult { + // Special case when it's me, + // I have to check that MSK -> USK -> SSK + // and that MSK is trusted (i know the private key, or is signed by a trusted device) +// val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId) + val myMasterKey = myCrossSigningInfo?.masterKey() ?: return UserTrustResult.CrossSigningNotConfigured(userId) @@ -423,7 +445,7 @@ internal class DefaultCrossSigningService @Inject constructor( // Maybe it's signed by a locally trusted device? myMasterKey.signatures?.get(userId)?.forEach { (key, value) -> val potentialDeviceId = key.withoutPrefix("ed25519:") - val potentialDevice = cryptoStore.getUserDevice(userId, potentialDeviceId) + val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId) if (potentialDevice != null && potentialDevice.isVerified) { // Check signature validity? try { @@ -561,6 +583,8 @@ internal class DefaultCrossSigningService @Inject constructor( cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoStore.markMyMasterKeyAsLocallyTrusted(true) checkSelfTrust() + // re-verify all trusts + onUsersDeviceUpdate(listOf(userId)) } } @@ -666,6 +690,55 @@ internal class DefaultCrossSigningService @Inject constructor( return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted)) } + fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo) : DeviceTrustResult { + val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified() + myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId)) + + if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys)) + + otherKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherDevice.userId)) + + // TODO should we force verification ? + if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys)) + + // Check if the trust chain is valid + /* + * ┏━━━━━━━━┓ ┏━━━━━━━━┓ + * ┃ ALICE ┃ ┃ BOB ┃ + * ┗━━━━━━━━┛ ┗━━━━━━━━┛ + * MSK ┌────────────▶MSK + * │ + * │ │ │ + * │ SSK │ └──▶ SSK ──────────────────┐ + * │ │ │ + * │ │ USK │ + * └──▶ USK ────────────┘ (not visible by │ + * Alice) │ + * ▼ + * ┌──────────────┐ + * │ BOB's Device │ + * └──────────────┘ + */ + + val otherSSKSignature = otherDevice.signatures?.get(otherKeys.userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}") + ?: return legacyFallbackTrust( + locallyTrusted, + DeviceTrustResult.MissingDeviceSignature(otherDevice.deviceId, otherKeys.selfSigningKey() + ?.unpaddedBase64PublicKey + ?: "" + ) + ) + + // Check bob's device is signed by bob's SSK + try { + olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable()) + } catch (e: Throwable) { + return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e)) + } + + return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted)) + } + private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult { return if (locallyTrusted == true) { DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true)) @@ -675,36 +748,18 @@ internal class DefaultCrossSigningService @Inject constructor( } override fun onUsersDeviceUpdate(userIds: List) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users") - userIds.forEach { otherUserId -> - checkUserTrust(otherUserId).let { - Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}") - setUserKeysAsTrusted(otherUserId, it.isVerified()) - } - } - } + Timber.d("## CrossSigning - onUsersDeviceUpdate for $userIds") + val workerParams = UpdateTrustWorker.Params(sessionId = sessionId, updatedUserIds = userIds) + val workerData = WorkerParamsFactory.toData(workerParams) - // now check device trust - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - userIds.forEach { otherUserId -> - // TODO if my keys have changes, i should recheck all devices of all users? - val devices = cryptoStore.getUserDeviceList(otherUserId) - devices?.forEach { device -> - val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false) - Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust") - cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified()) - } + val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setInputData(workerData) + .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) + .build() - if (otherUserId == userId) { - // It's me, i should check if a newly trusted device is signing my master key - // In this case it will change my MSK trust, and should then re-trigger a check of all other user trust - setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified()) - } - } - - eventBus.post(CryptoToSessionUserTrustChange(userIds)) - } + workManagerProvider.workManager + .beginUniqueWork("TRUST_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) + .enqueue() } private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt deleted file mode 100644 index 05ceba5965..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.matrix.android.sdk.internal.crypto.crosssigning - -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity -import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields -import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity -import org.matrix.android.sdk.internal.database.query.where -import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.session.SessionLifecycleObserver -import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater -import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.util.createBackgroundHandler -import io.realm.Realm -import io.realm.RealmConfiguration -import kotlinx.coroutines.android.asCoroutineDispatcher -import kotlinx.coroutines.launch -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import timber.log.Timber -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference -import javax.inject.Inject - -internal class ShieldTrustUpdater @Inject constructor( - private val eventBus: EventBus, - private val computeTrustTask: ComputeTrustTask, - private val taskExecutor: TaskExecutor, - @SessionDatabase private val sessionRealmConfiguration: RealmConfiguration, - private val roomSummaryUpdater: RoomSummaryUpdater -) : SessionLifecycleObserver { - - companion object { - private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD") - private val BACKGROUND_HANDLER_DISPATCHER = BACKGROUND_HANDLER.asCoroutineDispatcher() - } - - private val backgroundSessionRealm = AtomicReference() - - private val isStarted = AtomicBoolean() - - override fun onStart() { - if (isStarted.compareAndSet(false, true)) { - eventBus.register(this) - BACKGROUND_HANDLER.post { - backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration)) - } - } - } - - override fun onStop() { - if (isStarted.compareAndSet(true, false)) { - eventBus.unregister(this) - BACKGROUND_HANDLER.post { - backgroundSessionRealm.getAndSet(null).also { - it?.close() - } - } - } - } - - @Subscribe - fun onRoomMemberChange(update: SessionToCryptoRoomMembersUpdate) { - if (!isStarted.get()) { - return - } - taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) { - val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds, update.isDirect)) - // We need to send that back to session base - backgroundSessionRealm.get()?.executeTransaction { realm -> - roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust) - } - } - } - - @Subscribe - fun onTrustUpdate(update: CryptoToSessionUserTrustChange) { - if (!isStarted.get()) { - return - } - onCryptoDevicesChange(update.userIds) - } - - private fun onCryptoDevicesChange(users: List) { - taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) { - val realm = backgroundSessionRealm.get() ?: return@launch - val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java) - .`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray()) - .distinct(RoomMemberSummaryEntityFields.ROOM_ID) - .findAll() - .map { it.roomId } - - distinctRoomIds.forEach { roomId -> - val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - if (roomSummary?.isEncrypted.orFalse()) { - val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() - try { - val updatedTrust = computeTrustTask.execute( - ComputeTrustTask.Params(allActiveRoomMembers, roomSummary?.isDirect == true) - ) - realm.executeTransaction { - roomSummaryUpdater.updateShieldTrust(it, roomId, updatedTrust) - } - } catch (failure: Throwable) { - Timber.e(failure) - } - } - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt new file mode 100644 index 0000000000..f28fe7d642 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt @@ -0,0 +1,322 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.crosssigning + +import android.content.Context +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.kotlin.where +import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo +import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields +import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper +import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity +import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.CryptoDatabase +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.SessionComponent +import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker +import org.matrix.android.sdk.internal.worker.SessionWorkerParams +import timber.log.Timber +import javax.inject.Inject + +internal class UpdateTrustWorker(context: Context, + params: WorkerParameters) + : SessionSafeCoroutineWorker(context, params, Params::class.java) { + + @JsonClass(generateAdapter = true) + internal data class Params( + override val sessionId: String, + override val lastFailureMessage: String? = null, + val updatedUserIds: List + ) : SessionWorkerParams + + @Inject lateinit var crossSigningService: DefaultCrossSigningService + + // It breaks the crypto store contract, but we need to batch things :/ + @CryptoDatabase @Inject lateinit var realmConfiguration: RealmConfiguration + @UserId @Inject lateinit var myUserId: String + @Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper + @SessionDatabase @Inject lateinit var sessionRealmConfiguration: RealmConfiguration + + // @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater + @Inject lateinit var cryptoStore: IMXCryptoStore + + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } + + override suspend fun doSafeWork(params: Params): Result { + var userList = params.updatedUserIds + // Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user, + // or a new device?) So we check all again :/ + + Timber.d("## CrossSigning - Updating trust for $userList") + + // First we check that the users MSK are trusted by mine + // After that we check the trust chain for each devices of each users + Realm.getInstance(realmConfiguration).use { realm -> + realm.executeTransaction { + // By mapping here to model, this object is not live + // I should update it if needed + var myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId) + .findFirst()?.let { mapCrossSigningInfoEntity(it) } + + var myTrustResult: UserTrustResult? = null + + if (userList.contains(myUserId)) { + Timber.d("## CrossSigning - Clear all trust as a change on my user was detected") + // i am in the list.. but i don't know exactly the delta of change :/ + // If it's my cross signing keys we should refresh all trust + // do it anyway ? + userList = realm.where(CrossSigningInfoEntity::class.java) + .findAll().mapNotNull { it.userId } + Timber.d("## CrossSigning - Updating trust for all $userList") + + // check right now my keys and mark it as trusted as other trust depends on it + val myDevices = realm.where() + .equalTo(UserEntityFields.USER_ID, myUserId) + .findFirst() + ?.devices + ?.map { deviceInfo -> + CryptoMapper.mapToModel(deviceInfo) + } + myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices).also { + updateCrossSigningKeysTrust(realm, myUserId, it.isVerified()) + // update model reference + myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId) + .findFirst()?.let { mapCrossSigningInfoEntity(it) } + } + } + + val otherInfos = userList.map { + it to realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, it) + .findFirst()?.let { mapCrossSigningInfoEntity(it) } + } + .toMap() + + val trusts = otherInfos.map { infoEntry -> + infoEntry.key to when (infoEntry.key) { + myUserId -> myTrustResult + else -> { + crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, infoEntry.value).also { + Timber.d("## CrossSigning - user:${infoEntry.key} result:$it") + } + } + } + }.toMap() + + // TODO! if it's me and my keys has changed... I have to reset trust for everyone! + // i have all the new trusts, update DB + trusts.forEach { + val verified = it.value?.isVerified() == true + updateCrossSigningKeysTrust(realm, it.key, verified) + } + + // Ok so now we have to check device trust for all these users.. + Timber.v("## CrossSigning - Updating devices cross trust users ${trusts.keys}") + trusts.keys.forEach { + val devicesEntities = realm.where() + .equalTo(UserEntityFields.USER_ID, it) + .findFirst() + ?.devices + + val trustMap = devicesEntities?.map { device -> + // get up to date from DB has could have been updated + val otherInfo = realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, it) + .findFirst()?.let { mapCrossSigningInfoEntity(it) } + device to crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfo, CryptoMapper.mapToModel(device)) + }?.toMap() + + // Update trust if needed + devicesEntities?.forEach { device -> + val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified() + Timber.d("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}") + if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) { + Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified") + // need to save + val trustEntity = device.trustLevelEntity + if (trustEntity == null) { + realm.createObject(TrustLevelEntity::class.java).let { + it.locallyVerified = false + it.crossSignedVerified = crossSignedVerified + device.trustLevelEntity = it + } + } else { + trustEntity.crossSignedVerified = crossSignedVerified + } + } + } + } + } + } + + // So Cross Signing keys trust is updated, device trust is updated + // We can now update room shields? in the session DB? + + Timber.d("## CrossSigning - Updating shields for impacted rooms...") + Realm.getInstance(sessionRealmConfiguration).use { it -> + it.executeTransaction { realm -> + val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java) + .`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray()) + .distinct(RoomMemberSummaryEntityFields.ROOM_ID) + .findAll() + .map { it.roomId } + Timber.d("## CrossSigning - ... impacted rooms $distinctRoomIds") + distinctRoomIds.forEach { roomId -> + val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() + if (roomSummary?.isEncrypted == true) { + Timber.d("## CrossSigning - Check shield state for room $roomId") + val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() + try { + val updatedTrust = computeRoomShield(allActiveRoomMembers, roomSummary) + if (roomSummary.roomEncryptionTrustLevel != updatedTrust) { + Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust") + roomSummary.roomEncryptionTrustLevel = updatedTrust + } + } catch (failure: Throwable) { + Timber.e(failure) + } + } + } + } + } + + return Result.success() + } + + private fun updateCrossSigningKeysTrust(realm: Realm, userId: String, verified: Boolean) { + val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) + .findFirst() + xInfoEntity?.crossSigningKeys?.forEach { info -> + // optimization to avoid trigger updates when there is no change.. + if (info.trustLevelEntity?.isVerified() != verified) { + Timber.d("## CrossSigning - Trust change for $userId : $verified") + val level = info.trustLevelEntity + if (level == null) { + val newLevel = realm.createObject(TrustLevelEntity::class.java) + newLevel.locallyVerified = verified + newLevel.crossSignedVerified = verified + info.trustLevelEntity = newLevel + } else { + level.locallyVerified = verified + level.crossSignedVerified = verified + } + } + } + } + + private fun computeRoomShield(activeMemberUserIds: List, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel { + Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> $activeMemberUserIds") + // The set of “all users” depends on the type of room: + // For regular / topic rooms, all users including yourself, are considered when decorating a room + // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room + val listToCheck = if (roomSummaryEntity.isDirect) { + activeMemberUserIds.filter { it != myUserId } + } else { + activeMemberUserIds + } + + val allTrustedUserIds = listToCheck + .filter { userId -> + Realm.getInstance(realmConfiguration).use { + it.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) + .findFirst()?.let { mapCrossSigningInfoEntity(it) }?.isTrusted() == true + } + } + val myCrossKeys = Realm.getInstance(realmConfiguration).use { + it.where(CrossSigningInfoEntity::class.java) + .equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId) + .findFirst()?.let { mapCrossSigningInfoEntity(it) } + } + + return if (allTrustedUserIds.isEmpty()) { + RoomEncryptionTrustLevel.Default + } else { + // If one of the verified user as an untrusted device -> warning + // If all devices of all verified users are trusted -> green + // else -> black + allTrustedUserIds + .mapNotNull { uid -> + Realm.getInstance(realmConfiguration).use { + it.where() + .equalTo(UserEntityFields.USER_ID, uid) + .findFirst() + ?.devices + ?.map { + CryptoMapper.mapToModel(it) + } + } + } + .flatten() + .let { allDevices -> + Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }}") + if (myCrossKeys != null) { + allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() } + } else { + // Legacy method + allDevices.any { !it.isVerified } + } + } + .let { hasWarning -> + if (hasWarning) { + RoomEncryptionTrustLevel.Warning + } else { + if (listToCheck.size == allTrustedUserIds.size) { + // all users are trusted and all devices are verified + RoomEncryptionTrustLevel.Trusted + } else { + RoomEncryptionTrustLevel.Default + } + } + } + } + } + + private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo { + val userId = xsignInfo.userId ?: "" + return MXCrossSigningInfo( + userId = userId, + crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull { + crossSigningKeysMapper.map(userId, it) + } + ) + } + + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt index 64579c1b67..3abbf9b16e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -234,7 +234,10 @@ internal class DefaultKeysBackupService @Inject constructor( this.callback = object : MatrixCallback { override fun onSuccess(data: KeysVersion) { // Reset backup markers. - cryptoStore.resetBackupMarkers() + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + // move tx out of UI thread + cryptoStore.resetBackupMarkers() + } val keyBackupVersion = KeysVersionResult( algorithm = createKeysBackupVersionBody.algorithm, @@ -596,7 +599,9 @@ internal class DefaultKeysBackupService @Inject constructor( val importResult = awaitCallback { restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it) } - cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) + withContext(coroutineDispatchers.crypto) { + cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version) + } Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}") } else { Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}") @@ -685,7 +690,7 @@ internal class DefaultKeysBackupService @Inject constructor( // Get backed up keys from the homeserver val data = getKeys(sessionId, roomId, keysVersionResult.version!!) - withContext(coroutineDispatchers.crypto) { + withContext(coroutineDispatchers.computation) { val sessionsData = ArrayList() // Restore that data var sessionsFromHsCount = 0 @@ -1123,7 +1128,9 @@ internal class DefaultKeysBackupService @Inject constructor( if (retrievedMegolmBackupAuthData != null) { keysBackupVersion = keysVersionResult - cryptoStore.setKeyBackupVersion(keysVersionResult.version) + cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoStore.setKeyBackupVersion(keysVersionResult.version) + } onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index 0ae1e69124..72d541d4df 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.store import androidx.lifecycle.LiveData +import androidx.paging.PagedList import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.util.Optional @@ -126,7 +127,10 @@ internal interface IMXCryptoStore { fun getPendingIncomingRoomKeyRequests(): List fun getPendingIncomingGossipingRequests(): List + fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?) + + fun storeIncomingGossipingRequests(requests: List) // fun getPendingIncomingSecretShareRequests(): List /** @@ -364,6 +368,7 @@ internal interface IMXCryptoStore { fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map>): OutgoingSecretRequest? fun saveGossipingEvent(event: Event) + fun saveGossipingEvents(events: List) fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) { updateGossipingRequestState( @@ -441,10 +446,13 @@ internal interface IMXCryptoStore { // Dev tools fun getOutgoingRoomKeyRequests(): List + fun getOutgoingRoomKeyRequestsPaged(): LiveData> fun getOutgoingSecretKeyRequests(): List fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest? fun getIncomingRoomKeyRequests(): List - fun getGossipingEventsTrail(): List + fun getIncomingRoomKeyRequestsPaged(): LiveData> + fun getGossipingEventsTrail(): LiveData> + fun getGossipingEvents(): List fun setDeviceKeysUploaded(uploaded: Boolean) fun getDeviceKeysUploaded(): Boolean diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index b25349cba9..5d19e6d607 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto.store.db import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy import io.realm.Realm import io.realm.RealmConfiguration @@ -62,6 +64,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFie import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity +import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity @@ -998,7 +1001,50 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun getGossipingEventsTrail(): List { + override fun getIncomingRoomKeyRequestsPaged(): LiveData> { + val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> + realm.where() + .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + .sort(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Sort.DESCENDING) + } + val dataSourceFactory = realmDataSourceFactory.map { + it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest + ?: IncomingRoomKeyRequest( + requestBody = null, + deviceId = "", + userId = "", + requestId = "", + state = GossipingRequestState.NONE, + localCreationTimestamp = 0 + ) + } + return monarchy.findAllPagedWithChanges(realmDataSourceFactory, + LivePagedListBuilder(dataSourceFactory, + PagedList.Config.Builder() + .setPageSize(20) + .setEnablePlaceholders(false) + .setPrefetchDistance(1) + .build()) + ) + } + + override fun getGossipingEventsTrail(): LiveData> { + val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> + realm.where().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING) + } + val dataSourceFactory = realmDataSourceFactory.map { it.toModel() } + val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory, + LivePagedListBuilder(dataSourceFactory, + PagedList.Config.Builder() + .setPageSize(20) + .setEnablePlaceholders(false) + .setPrefetchDistance(1) + .build()) + ) + return trail + } + + override fun getGossipingEvents(): List { return monarchy.fetchAllCopiedSync { realm -> realm.where() }.map { @@ -1066,24 +1112,43 @@ internal class RealmCryptoStore @Inject constructor( return request } - override fun saveGossipingEvent(event: Event) { + override fun saveGossipingEvents(events: List) { val now = System.currentTimeMillis() - val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now - val entity = GossipingEventEntity( - type = event.type, - sender = event.senderId, - ageLocalTs = ageLocalTs, - content = ContentMapper.map(event.content) - ).apply { - sendState = SendState.SYNCED - decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult) - decryptionErrorCode = event.mCryptoError?.name - } - doRealmTransaction(realmConfiguration) { realm -> - realm.insertOrUpdate(entity) + monarchy.writeAsync { realm -> + events.forEach { event -> + val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now + val entity = GossipingEventEntity( + type = event.type, + sender = event.senderId, + ageLocalTs = ageLocalTs, + content = ContentMapper.map(event.content) + ).apply { + sendState = SendState.SYNCED + decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult) + decryptionErrorCode = event.mCryptoError?.name + } + realm.insertOrUpdate(entity) + } } } + override fun saveGossipingEvent(event: Event) { + monarchy.writeAsync { realm -> + val now = System.currentTimeMillis() + val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now + val entity = GossipingEventEntity( + type = event.type, + sender = event.senderId, + ageLocalTs = ageLocalTs, + content = ContentMapper.map(event.content) + ).apply { + sendState = SendState.SYNCED + decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult) + decryptionErrorCode = event.mCryptoError?.name + } + realm.insertOrUpdate(entity) + } + } // override fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? { // val statesIndex = states.map { it.ordinal }.toTypedArray() // return doRealmQueryAndCopy(realmConfiguration) { realm -> @@ -1284,6 +1349,28 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun storeIncomingGossipingRequests(requests: List) { + doRealmTransactionAsync(realmConfiguration) { realm -> + requests.forEach { request -> + // After a clear cache, we might have a + realm.createObject(IncomingGossipingRequestEntity::class.java).let { + it.otherDeviceId = request.deviceId + it.otherUserId = request.userId + it.requestId = request.requestId ?: "" + it.requestState = GossipingRequestState.PENDING + it.localCreationTimestamp = request.localCreationTimestamp ?: System.currentTimeMillis() + if (request is IncomingSecretShareRequest) { + it.type = GossipRequestType.SECRET + it.requestedInfoStr = request.secretName + } else if (request is IncomingRoomKeyRequest) { + it.type = GossipRequestType.KEY + it.requestedInfoStr = request.requestBody?.toJson() + } + } + } + } + } + // override fun getPendingIncomingSecretShareRequests(): List { // return doRealmQueryAndCopyList(realmConfiguration) { // it.where() @@ -1417,6 +1504,27 @@ internal class RealmCryptoStore @Inject constructor( .filterNotNull() } + override fun getOutgoingRoomKeyRequestsPaged(): LiveData> { + val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> + realm + .where(OutgoingGossipingRequestEntity::class.java) + .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name) + } + val dataSourceFactory = realmDataSourceFactory.map { + it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest + ?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED) + } + val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory, + LivePagedListBuilder(dataSourceFactory, + PagedList.Config.Builder() + .setPageSize(20) + .setEnablePlaceholders(false) + .setPrefetchDistance(1) + .build()) + ) + return trail + } + override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? { return doWithRealm(realmConfiguration) { realm -> val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt index c106c82538..08806b0627 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEnti import org.matrix.android.sdk.internal.di.SerializeNulls import io.realm.DynamicRealm import io.realm.RealmMigration +import io.realm.RealmObjectSchema import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2 import timber.log.Timber import javax.inject.Inject @@ -57,6 +58,27 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi const val CRYPTO_STORE_SCHEMA_VERSION = 11L } + private fun RealmObjectSchema.addFieldIfNotExists(fieldName: String, fieldType: Class<*>): RealmObjectSchema { + if (!hasField(fieldName)) { + addField(fieldName, fieldType) + } + return this + } + + private fun RealmObjectSchema.removeFieldIfExists(fieldName: String): RealmObjectSchema { + if (hasField(fieldName)) { + removeField(fieldName) + } + return this + } + + private fun RealmObjectSchema.setRequiredIfNotAlready(fieldName: String, isRequired: Boolean): RealmObjectSchema { + if (isRequired != isRequired(fieldName)) { + setRequired(fieldName, isRequired) + } + return this + } + override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.v("Migrating Realm Crypto from $oldVersion to $newVersion") @@ -89,13 +111,13 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields") realm.schema.get("IncomingRoomKeyRequestEntity") - ?.addField("requestBodyAlgorithm", String::class.java) - ?.addField("requestBodyRoomId", String::class.java) - ?.addField("requestBodySenderKey", String::class.java) - ?.addField("requestBodySessionId", String::class.java) + ?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java) + ?.addFieldIfNotExists("requestBodyRoomId", String::class.java) + ?.addFieldIfNotExists("requestBodySenderKey", String::class.java) + ?.addFieldIfNotExists("requestBodySessionId", String::class.java) ?.transform { dynamicObject -> - val requestBodyString = dynamicObject.getString("requestBodyString") try { + val requestBodyString = dynamicObject.getString("requestBodyString") // It was a map before val map: Map? = deserializeFromRealm(requestBodyString) @@ -109,18 +131,18 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi Timber.e(e, "Error") } } - ?.removeField("requestBodyString") + ?.removeFieldIfExists("requestBodyString") Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields") realm.schema.get("OutgoingRoomKeyRequestEntity") - ?.addField("requestBodyAlgorithm", String::class.java) - ?.addField("requestBodyRoomId", String::class.java) - ?.addField("requestBodySenderKey", String::class.java) - ?.addField("requestBodySessionId", String::class.java) + ?.addFieldIfNotExists("requestBodyAlgorithm", String::class.java) + ?.addFieldIfNotExists("requestBodyRoomId", String::class.java) + ?.addFieldIfNotExists("requestBodySenderKey", String::class.java) + ?.addFieldIfNotExists("requestBodySessionId", String::class.java) ?.transform { dynamicObject -> - val requestBodyString = dynamicObject.getString("requestBodyString") try { + val requestBodyString = dynamicObject.getString("requestBodyString") // It was a map before val map: Map? = deserializeFromRealm(requestBodyString) @@ -134,16 +156,18 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi Timber.e(e, "Error") } } - ?.removeField("requestBodyString") + ?.removeFieldIfExists("requestBodyString") Timber.d("Create KeysBackupDataEntity") - realm.schema.create("KeysBackupDataEntity") - .addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java) - .addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY) - .setRequired(KeysBackupDataEntityFields.PRIMARY_KEY, true) - .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java) - .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java) + if (!realm.schema.contains("KeysBackupDataEntity")) { + realm.schema.create("KeysBackupDataEntity") + .addField(KeysBackupDataEntityFields.PRIMARY_KEY, Integer::class.java) + .addPrimaryKey(KeysBackupDataEntityFields.PRIMARY_KEY) + .setRequired(KeysBackupDataEntityFields.PRIMARY_KEY, true) + .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_HASH, String::class.java) + .addField(KeysBackupDataEntityFields.BACKUP_LAST_SERVER_NUMBER_OF_KEYS, Integer::class.java) + } } private fun migrateTo3RiotX(realm: DynamicRealm) { @@ -151,8 +175,8 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi Timber.d("Migrate to RiotX model") realm.schema.get("CryptoRoomEntity") - ?.addField(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java) - ?.setRequired(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false) + ?.addFieldIfNotExists(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, Boolean::class.java) + ?.setRequiredIfNotAlready(CryptoRoomEntityFields.SHOULD_ENCRYPT_FOR_INVITED_MEMBERS, false) // Convert format of MXDeviceInfo, package has to be the same. realm.schema.get("DeviceInfoEntity") @@ -204,8 +228,13 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi // Version 4L added Cross Signing info persistence private fun migrateTo4(realm: DynamicRealm) { Timber.d("Step 3 -> 4") - Timber.d("Create KeyInfoEntity") + if (realm.schema.contains("TrustLevelEntity")) { + Timber.d("Skipping Step 3 -> 4 because entities already exist") + return + } + + Timber.d("Create KeyInfoEntity") val trustLevelEntityEntitySchema = realm.schema.create("TrustLevelEntity") .addField(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, Boolean::class.java) .setNullable(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED, true) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt index 1b88fbe9cc..56b267decd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt @@ -33,14 +33,13 @@ internal interface EncryptEventTask : Task { data class Params(val roomId: String, val event: Event, /**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/ - val keepKeys: List? = null, - val crypto: CryptoService + val keepKeys: List? = null ) } internal class DefaultEncryptEventTask @Inject constructor( -// private val crypto: CryptoService - private val localEchoRepository: LocalEchoRepository + private val localEchoRepository: LocalEchoRepository, + private val cryptoService: CryptoService ) : EncryptEventTask { override suspend fun execute(params: EncryptEventTask.Params): Event { // don't want to wait for any query @@ -60,7 +59,7 @@ internal class DefaultEncryptEventTask @Inject constructor( // try { // let it throws awaitCallback { - params.crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) + cryptoService.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it) }.let { result -> val modifiedContent = HashMap(result.eventContent) params.keepKeys?.forEach { toKeep -> @@ -81,7 +80,7 @@ internal class DefaultEncryptEventTask @Inject constructor( ).toContent(), forwardingCurve25519KeyChain = emptyList(), senderCurve25519Key = result.eventContent["sender_key"] as? String, - claimedEd25519Key = params.crypto.getMyDevice().fingerprint() + claimedEd25519Key = cryptoService.getMyDevice().fingerprint() ) } else { null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index 1a712036c8..8b739c4b64 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.crypto.tasks import org.greenrobot.eventbus.EventBus -import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.network.executeRequest @@ -29,8 +28,7 @@ import javax.inject.Inject internal interface SendEventTask : Task { data class Params( val event: Event, - val encrypt: Boolean, - val cryptoService: CryptoService? + val encrypt: Boolean ) } @@ -68,8 +66,7 @@ internal class DefaultSendEventTask @Inject constructor( return encryptEventTask.execute(EncryptEventTask.Params( params.event.roomId ?: "", params.event, - listOf("m.relates_to"), - params.cryptoService!! + listOf("m.relates_to") )) } return params.event diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt index 782300c7b0..cedb7a6618 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -15,21 +15,20 @@ */ package org.matrix.android.sdk.internal.crypto.tasks -import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.task.Task -import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface SendVerificationMessageTask : Task { data class Params( - val event: Event, - val cryptoService: CryptoService? + val event: Event ) } @@ -37,6 +36,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( private val localEchoRepository: LocalEchoRepository, private val encryptEventTask: DefaultEncryptEventTask, private val roomAPI: RoomAPI, + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val eventBus: EventBus) : SendVerificationMessageTask { override suspend fun execute(params: SendVerificationMessageTask.Params): String { @@ -62,13 +62,12 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( } private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event { - if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) { + if (cryptoSessionInfoProvider.isRoomEncrypted(params.event.roomId ?: "")) { try { return encryptEventTask.execute(EncryptEventTask.Params( params.event.roomId ?: "", params.event, - listOf("m.relates_to"), - params.cryptoService + listOf("m.relates_to") )) } catch (throwable: Throwable) { // We said it's ok to send verification request in clear diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt index c0f4671046..7f02750359 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt @@ -20,7 +20,6 @@ import android.os.Handler import android.os.Looper import dagger.Lazy import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME @@ -111,9 +110,6 @@ internal class DefaultVerificationService @Inject constructor( private val uiHandler = Handler(Looper.getMainLooper()) - // Cannot be injected in constructor as it creates a dependency cycle - lateinit var cryptoService: CryptoService - // map [sender : [transaction]] private val txMap = HashMap>() @@ -129,7 +125,8 @@ internal class DefaultVerificationService @Inject constructor( // Event received from the sync fun onToDeviceEvent(event: Event) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + Timber.d("## SAS onToDeviceEvent ${event.getClearType()}") + cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) { when (event.getClearType()) { EventType.KEY_VERIFICATION_START -> { onStartRequestReceived(event) @@ -163,7 +160,7 @@ internal class DefaultVerificationService @Inject constructor( } fun onRoomEvent(event: Event) { - cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { + cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) { when (event.getClearType()) { EventType.KEY_VERIFICATION_START -> { onRoomStartRequestReceived(event) @@ -240,6 +237,7 @@ internal class DefaultVerificationService @Inject constructor( } private fun dispatchRequestAdded(tx: PendingVerificationRequest) { + Timber.v("## SAS dispatchRequestAdded txId:${tx.transactionId}") uiHandler.post { listeners.forEach { try { @@ -303,11 +301,14 @@ internal class DefaultVerificationService @Inject constructor( // We don't want to block here val otherDeviceId = validRequestInfo.fromDevice + Timber.v("## SAS onRequestReceived from $senderId and device $otherDeviceId, txId:${validRequestInfo.transactionId}") + cryptoCoroutineScope.launch { if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) { Timber.e("## Verification device $otherDeviceId is not known") } } + Timber.v("## SAS onRequestReceived .. checkKeysAreDownloaded launched") // Remember this request val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() } @@ -1203,7 +1204,9 @@ internal class DefaultVerificationService @Inject constructor( // TODO refactor this with the DM one Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices") - val targetDevices = otherDevices ?: cryptoService.getUserDevices(otherUserId).map { it.deviceId } + val targetDevices = otherDevices ?: cryptoStore.getUserDevices(otherUserId) + ?.values?.map { it.deviceId } ?: emptyList() + val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() } val transport = verificationTransportToDeviceFactory.createTransport(null) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt index fa7cd2e6f9..538d7b56e9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt @@ -20,7 +20,6 @@ import androidx.work.Data import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.failure.shouldBeRetried -import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker @@ -47,7 +46,6 @@ internal class SendVerificationMessageWorker(context: Context, @Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask @Inject lateinit var localEchoRepository: LocalEchoRepository - @Inject lateinit var cryptoService: CryptoService @Inject lateinit var cancelSendTracker: CancelSendTracker override fun injectWith(injector: SessionComponent) { @@ -70,8 +68,7 @@ internal class SendVerificationMessageWorker(context: Context, return try { val resultEventId = sendVerificationMessageTask.execute( SendVerificationMessageTask.Params( - event = localEvent, - cryptoService = cryptoService + event = localEvent ) ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt index 4994325625..74827eeb2a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt @@ -15,7 +15,6 @@ */ package org.matrix.android.sdk.internal.crypto.verification -import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.events.model.Event @@ -34,12 +33,13 @@ import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import io.realm.Realm +import org.matrix.android.sdk.internal.crypto.EventDecryptor import timber.log.Timber import java.util.ArrayList import javax.inject.Inject internal class VerificationMessageProcessor @Inject constructor( - private val cryptoService: CryptoService, + private val eventDecryptor: EventDecryptor, private val verificationService: DefaultVerificationService, @UserId private val userId: String, @DeviceId private val deviceId: String? @@ -82,7 +82,7 @@ internal class VerificationMessageProcessor @Inject constructor( // TODO use a global event decryptor? attache to session and that listen to new sessionId? // for now decrypt sync try { - val result = cryptoService.decryptEvent(event, "") + val result = eventDecryptor.decryptEvent(event, "") event.mxDecryptionResult = OlmDecryptionResult( payload = result.clearEvent, senderKey = result.senderCurve25519Key, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt index a530ee5105..0fbab88524 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt @@ -17,10 +17,6 @@ package org.matrix.android.sdk.internal.database import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.session.crypto.CryptoService -import org.matrix.android.sdk.api.session.crypto.MXCryptoError -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertEntity @@ -31,12 +27,13 @@ import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import io.realm.RealmConfiguration import io.realm.RealmResults import kotlinx.coroutines.launch +import org.matrix.android.sdk.internal.crypto.EventDecryptor import timber.log.Timber import javax.inject.Inject internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>, - private val cryptoService: CryptoService) + private val eventDecryptor: EventDecryptor) : RealmLiveEntityObserver(realmConfiguration) { override val query = Monarchy.Query { @@ -74,7 +71,7 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real return@forEach } val domainEvent = event.asDomain() - decryptIfNeeded(domainEvent) +// decryptIfNeeded(domainEvent) processors.filter { it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType) }.forEach { @@ -89,22 +86,22 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real } } - private fun decryptIfNeeded(event: Event) { - if (event.isEncrypted() && event.mxDecryptionResult == null) { - try { - val result = cryptoService.decryptEvent(event, event.roomId ?: "") - event.mxDecryptionResult = OlmDecryptionResult( - payload = result.clearEvent, - senderKey = result.senderCurve25519Key, - keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, - forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain - ) - } catch (e: MXCryptoError) { - Timber.v("Failed to decrypt event") - // TODO -> we should keep track of this and retry, or some processing will never be handled - } - } - } +// private fun decryptIfNeeded(event: Event) { +// if (event.isEncrypted() && event.mxDecryptionResult == null) { +// try { +// val result = eventDecryptor.decryptEvent(event, event.roomId ?: "") +// event.mxDecryptionResult = OlmDecryptionResult( +// payload = result.clearEvent, +// senderKey = result.senderCurve25519Key, +// keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, +// forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain +// ) +// } catch (e: MXCryptoError) { +// Timber.v("Failed to decrypt event") +// // TODO -> we should keep track of this and retry, or some processing will never be handled +// } +// } +// } private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean { return processors.any { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 359dd265d8..7e182525a9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -65,7 +65,6 @@ import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor -import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.session.sync.job.SyncThread import org.matrix.android.sdk.internal.session.sync.job.SyncWorker @@ -115,7 +114,6 @@ internal class DefaultSession @Inject constructor( private val accountDataService: Lazy, private val _sharedSecretStorageService: Lazy, private val accountService: Lazy, - private val timelineEventDecryptor: TimelineEventDecryptor, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val defaultIdentityService: DefaultIdentityService, private val integrationManagerService: IntegrationManagerService, @@ -162,7 +160,6 @@ internal class DefaultSession @Inject constructor( lifecycleObservers.forEach { it.onStart() } } eventBus.register(this) - timelineEventDecryptor.start() eventSenderProcessor.start() } @@ -200,7 +197,7 @@ internal class DefaultSession @Inject constructor( override fun close() { assert(isOpen) stopSync() - timelineEventDecryptor.destroy() + // timelineEventDecryptor.destroy() uiHandler.post { lifecycleObservers.forEach { it.onStop() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index ed586d35f8..e6fd5a7a0c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker import org.matrix.android.sdk.internal.crypto.CryptoModule import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker import org.matrix.android.sdk.internal.crypto.SendGossipWorker +import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker import org.matrix.android.sdk.internal.di.MatrixComponent import org.matrix.android.sdk.internal.di.SessionAssistedInjectModule @@ -128,6 +129,8 @@ internal interface SessionComponent { fun inject(worker: SendGossipWorker) + fun inject(worker: UpdateTrustWorker) + @Component.Factory interface Factory { fun create( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index 102a34d9de..32949d60c4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.permalinks.PermalinkService import org.matrix.android.sdk.api.session.securestorage.SecureStorageService import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService import org.matrix.android.sdk.api.session.typing.TypingUsersTracker -import org.matrix.android.sdk.internal.crypto.crosssigning.ShieldTrustUpdater import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask @@ -333,10 +332,6 @@ internal abstract class SessionModule { @IntoSet abstract fun bindWidgetUrlFormatter(formatter: DefaultWidgetURLFormatter): SessionLifecycleObserver - @Binds - @IntoSet - abstract fun bindShieldTrustUpdated(updater: ShieldTrustUpdater): SessionLifecycleObserver - @Binds @IntoSet abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 171e90703c..d49c2f120c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -61,7 +61,7 @@ internal class DefaultRoomService @Inject constructor( return roomGetter.getRoom(roomId) } - override fun getExistingDirectRoomWithUser(otherUserId: String): Room? { + override fun getExistingDirectRoomWithUser(otherUserId: String): String? { return roomGetter.getDirectRoomWith(otherUserId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 9ff0deec89..d090ba5296 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -15,7 +15,7 @@ */ package org.matrix.android.sdk.internal.session.room -import org.matrix.android.sdk.api.session.crypto.CryptoService +import io.realm.Realm import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType @@ -47,7 +47,6 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor -import io.realm.Realm import timber.log.Timber import javax.inject.Inject @@ -78,9 +77,8 @@ private fun VerificationState?.toState(newState: VerificationState): Verificatio return newState } -internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String, - private val cryptoService: CryptoService -) : EventInsertLiveProcessor { +internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String) + : EventInsertLiveProcessor { private val allowedTypes = listOf( EventType.MESSAGE, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt index 2947518605..eb9cd9fcba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt @@ -25,13 +25,12 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import javax.inject.Inject internal interface RoomGetter { fun getRoom(roomId: String): Room? - fun getDirectRoomWith(otherUserId: String): Room? + fun getDirectRoomWith(otherUserId: String): String? } @SessionScope @@ -46,16 +45,14 @@ internal class DefaultRoomGetter @Inject constructor( } } - override fun getDirectRoomWith(otherUserId: String): Room? { + override fun getDirectRoomWith(otherUserId: String): String? { return realmSessionProvider.withRealm { realm -> RoomSummaryEntity.where(realm) .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) .findAll() - .filter { dm -> dm.otherMemberIds.contains(otherUserId) } - .map { it.roomId } - .firstOrNull { roomId -> otherUserId in RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() } - ?.let { roomId -> createRoom(realm, roomId) } + .firstOrNull { dm -> dm.otherMemberIds.size == 1 && dm.otherMemberIds.first() == otherUserId } + ?.roomId } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index 7e28200ccd..632fcab70b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -16,10 +16,10 @@ package org.matrix.android.sdk.internal.session.room.create +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.toMedium import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams @@ -27,11 +27,13 @@ import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.di.AuthenticatedIdentity import org.matrix.android.sdk.internal.network.token.AccessTokenProvider +import org.matrix.android.sdk.internal.session.content.FileUploader import org.matrix.android.sdk.internal.session.identity.EnsureIdentityTokenTask import org.matrix.android.sdk.internal.session.identity.data.IdentityStore import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody import java.security.InvalidParameterException +import java.util.UUID import javax.inject.Inject internal class CreateRoomBodyBuilder @Inject constructor( @@ -39,6 +41,7 @@ internal class CreateRoomBodyBuilder @Inject constructor( private val crossSigningService: CrossSigningService, private val deviceListManager: DeviceListManager, private val identityStore: IdentityStore, + private val fileUploader: FileUploader, @AuthenticatedIdentity private val accessTokenProvider: AccessTokenProvider ) { @@ -66,7 +69,8 @@ internal class CreateRoomBodyBuilder @Inject constructor( val initialStates = listOfNotNull( buildEncryptionWithAlgorithmEvent(params), - buildHistoryVisibilityEvent(params) + buildHistoryVisibilityEvent(params), + buildAvatarEvent(params) ) .takeIf { it.isNotEmpty() } @@ -85,15 +89,33 @@ internal class CreateRoomBodyBuilder @Inject constructor( ) } + private suspend fun buildAvatarEvent(params: CreateRoomParams): Event? { + return params.avatarUri?.let { avatarUri -> + // First upload the image, ignoring any error + tryOrNull { + fileUploader.uploadFromUri( + uri = avatarUri, + filename = UUID.randomUUID().toString(), + mimeType = "image/jpeg") + } + ?.let { response -> + Event( + type = EventType.STATE_ROOM_AVATAR, + stateKey = "", + content = mapOf("url" to response.contentUri) + ) + } + } + } + private fun buildHistoryVisibilityEvent(params: CreateRoomParams): Event? { return params.historyVisibility ?.let { - val contentMap = mapOf("history_visibility" to it) - Event( type = EventType.STATE_ROOM_HISTORY_VISIBILITY, stateKey = "", - content = contentMap.toContent()) + content = mapOf("history_visibility" to it) + ) } } @@ -111,12 +133,10 @@ internal class CreateRoomBodyBuilder @Inject constructor( if (it != MXCRYPTO_ALGORITHM_MEGOLM) { throw InvalidParameterException("Unsupported algorithm: $it") } - val contentMap = mapOf("algorithm" to it) - Event( type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "", - content = contentMap.toContent() + content = mapOf("algorithm" to it) ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index 38c542e07e..a7f3f83980 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -21,7 +21,6 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.message.MessageType @@ -31,13 +30,13 @@ import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.task.TaskExecutor @@ -47,11 +46,9 @@ import timber.log.Timber internal class DefaultRelationService @AssistedInject constructor( @Assisted private val roomId: String, - @SessionId private val sessionId: String, -// private val timeLineSendEventWorkCommon: TimelineSendEventWorkCommon, private val eventSenderProcessor: EventSenderProcessor, private val eventFactory: LocalEchoEventFactory, - private val cryptoService: CryptoService, + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val fetchEditHistoryTask: FetchEditHistoryTask, private val timelineEventMapper: TimelineEventMapper, @@ -122,7 +119,7 @@ internal class DefaultRelationService @AssistedInject constructor( val event = eventFactory .createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText) .also { saveLocalEcho(it) } - return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(roomId)) + return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) } override fun editReply(replyToEdit: TimelineEvent, @@ -139,11 +136,11 @@ internal class DefaultRelationService @AssistedInject constructor( compatibilityBodyText ) .also { saveLocalEcho(it) } - return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(roomId)) + return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) } override fun fetchEditHistory(eventId: String, callback: MatrixCallback>) { - val params = FetchEditHistoryTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), eventId) + val params = FetchEditHistoryTask.Params(roomId, cryptoSessionInfoProvider.isRoomEncrypted(roomId), eventId) fetchEditHistoryTask .configureWith(params) { this.callback = callback @@ -156,7 +153,7 @@ internal class DefaultRelationService @AssistedInject constructor( ?.also { saveLocalEcho(it) } ?: return null - return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(roomId)) + return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) } override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 5c395c1907..b13ce15da6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -25,7 +25,6 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.content.ContentAttachmentData -import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage @@ -45,6 +44,7 @@ import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.CancelableBag import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.content.UploadContentWorker @@ -64,7 +64,7 @@ internal class DefaultSendService @AssistedInject constructor( private val workManagerProvider: WorkManagerProvider, @SessionId private val sessionId: String, private val localEchoEventFactory: LocalEchoEventFactory, - private val cryptoService: CryptoService, + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val taskExecutor: TaskExecutor, private val localEchoRepository: LocalEchoRepository, private val eventSenderProcessor: EventSenderProcessor, @@ -251,7 +251,7 @@ internal class DefaultSendService @AssistedInject constructor( private fun internalSendMedia(allLocalEchoes: List, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable { val cancelableBag = CancelableBag() - allLocalEchoes.groupBy { cryptoService.isRoomEncrypted(it.roomId!!) } + allLocalEchoes.groupBy { cryptoSessionInfoProvider.isRoomEncrypted(it.roomId!!) } .apply { keys.forEach { isRoomEncrypted -> // Should never be empty @@ -282,7 +282,7 @@ internal class DefaultSendService @AssistedInject constructor( } private fun sendEvent(event: Event): Cancelable { - return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(event.roomId!!)) + return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(event.roomId!!)) } private fun createLocalEcho(event: Event) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt index 2c835ff56c..37a429d242 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt @@ -87,7 +87,7 @@ internal class SendEventWorker(context: Context, Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}") return try { - sendEventTask.execute(SendEventTask.Params(event, params.isEncrypted ?: cryptoService.isRoomEncrypted(event.roomId), cryptoService)) + sendEventTask.execute(SendEventTask.Params(event, params.isEncrypted ?: cryptoService.isRoomEncrypted(event.roomId))) Result.success() } catch (exception: Throwable) { if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt index 09da0908f9..21a4145a9d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt @@ -38,7 +38,7 @@ internal class SendEventQueuedTask( override fun toString() = "[SendEventRunnableTask ${event.eventId}]" override suspend fun execute() { - sendEventTask.execute(SendEventTask.Params(event, encrypt, cryptoService)) + sendEventTask.execute(SendEventTask.Params(event, encrypt)) } override fun onTaskFailed() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt index d21805f4f3..65d375e176 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt @@ -140,4 +140,15 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private ) } } + + override fun deleteAvatar(callback: MatrixCallback): Cancelable { + return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) { + sendStateEvent( + eventType = EventType.STATE_ROOM_AVATAR, + body = emptyMap(), + callback = callback, + stateKey = null + ) + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index f9a27c367c..8c71604183 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -16,10 +16,8 @@ package org.matrix.android.sdk.internal.session.room.summary -import dagger.Lazy import io.realm.Realm -import org.greenrobot.eventbus.EventBus -import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.Membership @@ -28,9 +26,11 @@ import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent import org.matrix.android.sdk.api.session.room.model.RoomNameContent import org.matrix.android.sdk.api.session.room.model.RoomTopicContent import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.EventDecryptor import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.android.sdk.internal.crypto.crosssigning.SessionToCryptoRoomMembersUpdate +import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntityFields @@ -46,7 +46,6 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper -import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications import timber.log.Timber @@ -56,8 +55,8 @@ internal class RoomSummaryUpdater @Inject constructor( @UserId private val userId: String, private val roomDisplayNameResolver: RoomDisplayNameResolver, private val roomAvatarResolver: RoomAvatarResolver, - private val timelineEventDecryptor: Lazy, - private val eventBus: EventBus) { + private val eventDecryptor: EventDecryptor, + private val crossSigningService: DefaultCrossSigningService) { fun update(realm: Realm, roomId: String, @@ -126,9 +125,14 @@ internal class RoomSummaryUpdater @Inject constructor( } roomSummaryEntity.updateHasFailedSending() - if (latestPreviewableEvent?.root?.type == EventType.ENCRYPTED && latestPreviewableEvent.root?.decryptionResultJson == null) { + val root = latestPreviewableEvent?.root + if (root?.type == EventType.ENCRYPTED && root.decryptionResultJson == null) { Timber.v("Should decrypt ${latestPreviewableEvent.eventId}") - timelineEventDecryptor.get().requestDecryption(TimelineEventDecryptor.DecryptionRequest(latestPreviewableEvent.eventId, "")) + // mmm i want to decrypt now or is it ok to do it async? + tryOrNull { + eventDecryptor.decryptEvent(root.asDomain(), "") + // eventDecryptor.decryptEventAsync(root.asDomain(), "", NoOpMatrixCallback()) + } } if (updateMembers) { @@ -142,7 +146,8 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) if (roomSummaryEntity.isEncrypted) { - eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, roomSummaryEntity.isDirect, roomSummaryEntity.otherMemberIds.toList() + userId)) + // mmm maybe we could only refresh shield instead of checking trust also? + crossSigningService.onUsersDeviceUpdate(roomSummaryEntity.otherMemberIds.toList()) } } } @@ -156,13 +161,4 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.updateHasFailedSending() roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) } - - fun updateShieldTrust(realm: Realm, - roomId: String, - trust: RoomEncryptionTrustLevel?) { - val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) - if (roomSummaryEntity.isEncrypted) { - roomSummaryEntity.roomEncryptionTrustLevel = trust - } - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 0ca902fd44..d75952e63d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -157,11 +157,14 @@ internal class DefaultTimeline( val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: throw IllegalStateException("Can't open a timeline without a room") - sendingEvents = roomEntity.sendingTimelineEvents.where().filterEventsWithSettings().findAll() + // We don't want to filter here because some sending events that are not displayed + // are still used for ui echo (relation like reaction) + sendingEvents = roomEntity.sendingTimelineEvents.where()/*.filterEventsWithSettings()*/.findAll() sendingEvents.addChangeListener { events -> uiEchoManager.sentEventsUpdated(events) postSnapshot() } + nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() filteredEvents = nonFilteredEvents.where() .filterEventsWithSettings() @@ -411,13 +414,16 @@ internal class DefaultTimeline( val builtSendingEvents = ArrayList() if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { builtSendingEvents.addAll(uiEchoManager.getInMemorySendingEvents().filterEventsWithSettings()) - sendingEvents.forEach { timelineEventEntity -> - if (builtSendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) { - val element = timelineEventMapper.map(timelineEventEntity) - uiEchoManager.updateSentStateWithUiEcho(element) - builtSendingEvents.add(element) - } - } + sendingEvents + .map { timelineEventMapper.map(it) } + // Filter out sending event that are not displayable! + .filterEventsWithSettings() + .forEach { timelineEvent -> + if (builtSendingEvents.find { it.eventId == timelineEvent.eventId } == null) { + uiEchoManager.updateSentStateWithUiEcho(timelineEvent) + builtSendingEvents.add(timelineEvent) + } + } } return builtSendingEvents } @@ -634,7 +640,7 @@ internal class DefaultTimeline( if (timelineEvent.isEncrypted() && timelineEvent.root.mxDecryptionResult == null) { - timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(it, timelineID)) } + timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineID)) } } val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size @@ -786,8 +792,8 @@ internal class DefaultTimeline( val filterType = !settings.filters.filterTypes || settings.filters.allowedTypes.contains(it.root.type) if (!filterType) return@filter false - val filterEdits = if (settings.filters.filterEdits && it.root.type == EventType.MESSAGE) { - val messageContent = it.root.content.toModel() + val filterEdits = if (settings.filters.filterEdits && it.root.getClearType() == EventType.MESSAGE) { + val messageContent = it.root.getClearContent().toModel() messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE } else { true @@ -817,7 +823,7 @@ internal class DefaultTimeline( private val inMemorySendingEvents = Collections.synchronizedList(ArrayList()) fun getInMemorySendingEvents(): List { - return inMemorySendingEvents + return inMemorySendingEvents.toList() } /** @@ -835,8 +841,13 @@ internal class DefaultTimeline( inMemorySendingStates.keys.removeAll { key -> events.find { it.eventId == key } == null } - inMemoryReactions.keys.removeAll { key -> - events.find { it.eventId == key } == null + + inMemoryReactions.forEach { (_, uiEchoData) -> + uiEchoData.removeAll { data -> + // I remove the uiEcho, when the related event is not anymore in the sending list + // (means that it is synced)! + events.find { it.eventId == data.localEchoId } == null + } } } @@ -900,6 +911,7 @@ internal class DefaultTimeline( relatedEventID ) val updateReactions = existingAnnotationSummary.reactionsSummary.toMutableList() + contents.forEach { uiEchoReaction -> val existing = updateReactions.firstOrNull { it.key == uiEchoReaction.reaction } if (existing == null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt index e91487eab0..3517f26c5d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -15,24 +15,22 @@ */ package org.matrix.android.sdk.internal.session.room.timeline +import io.realm.Realm +import io.realm.RealmConfiguration import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.internal.crypto.NewSessionListener import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent -import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.session.SessionScope -import io.realm.Realm -import io.realm.RealmConfiguration import timber.log.Timber import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import javax.inject.Inject -@SessionScope internal class TimelineEventDecryptor @Inject constructor( @SessionDatabase private val realmConfiguration: RealmConfiguration, @@ -83,14 +81,14 @@ internal class TimelineEventDecryptor @Inject constructor( synchronized(unknownSessionsFailure) { for (requests in unknownSessionsFailure.values) { if (request in requests) { - Timber.d("Skip Decryption request for event ${request.eventId}, unknown session") + Timber.d("Skip Decryption request for event ${request.event.eventId}, unknown session") return } } } synchronized(existingRequests) { if (!existingRequests.add(request)) { - Timber.d("Skip Decryption request for event ${request.eventId}, already requested") + Timber.d("Skip Decryption request for event ${request.event.eventId}, already requested") return } } @@ -101,25 +99,29 @@ internal class TimelineEventDecryptor @Inject constructor( } } - private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) = realm.executeTransaction { - val eventId = request.eventId + private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) { + val event = request.event val timelineId = request.timelineId - Timber.v("Decryption request for event $eventId") - val eventEntity = EventEntity.where(realm, eventId = eventId).findFirst() - ?: return@executeTransaction Unit.also { - Timber.d("Decryption request for unknown message") - } - val event = eventEntity.asDomain() try { - val result = cryptoService.decryptEvent(event, timelineId) - Timber.v("Successfully decrypted event $eventId") - eventEntity.setDecryptionResult(result) + val result = cryptoService.decryptEvent(request.event, timelineId) + Timber.v("Successfully decrypted event ${event.eventId}") + realm.executeTransaction { + EventEntity.where(it, eventId = event.eventId ?: "") + .findFirst() + ?.setDecryptionResult(result) + } } catch (e: MXCryptoError) { - Timber.v(e, "Failed to decrypt event $eventId") + Timber.v("Failed to decrypt event ${event.eventId} : ${e.localizedMessage}") if (e is MXCryptoError.Base /*&& e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID*/) { // Keep track of unknown sessions to automatically try to decrypt on new session - eventEntity.decryptionErrorCode = e.errorType.name - eventEntity.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription + realm.executeTransaction { + EventEntity.where(it, eventId = event.eventId ?: "") + .findFirst() + ?.let { + it.decryptionErrorCode = e.errorType.name + it.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription + } + } event.content?.toModel()?.let { content -> content.sessionId?.let { sessionId -> synchronized(unknownSessionsFailure) { @@ -130,7 +132,7 @@ internal class TimelineEventDecryptor @Inject constructor( } } } catch (t: Throwable) { - Timber.e("Failed to decrypt event $eventId, ${t.localizedMessage}") + Timber.e("Failed to decrypt event ${event.eventId}, ${t.localizedMessage}") } finally { synchronized(existingRequests) { existingRequests.remove(request) @@ -139,7 +141,7 @@ internal class TimelineEventDecryptor @Inject constructor( } data class DecryptionRequest( - val eventId: String, + val event: Event, val timelineId: String ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt index 8589889b30..b1b2f65dc2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt @@ -16,6 +16,9 @@ package org.matrix.android.sdk.internal.session.sync +import io.realm.Realm +import io.realm.kotlin.createObject +import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.R import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event @@ -54,16 +57,12 @@ import org.matrix.android.sdk.internal.session.room.read.FullyReadContent import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection -import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync import org.matrix.android.sdk.internal.session.sync.model.RoomSync import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse -import io.realm.Realm -import io.realm.kotlin.createObject -import org.greenrobot.eventbus.EventBus import timber.log.Timber import javax.inject.Inject @@ -76,8 +75,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private val roomTypingUsersHandler: RoomTypingUsersHandler, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, @UserId private val userId: String, - private val eventBus: EventBus, - private val timelineEventDecryptor: TimelineEventDecryptor) { + private val eventBus: EventBus) { sealed class HandlingStrategy { data class JOINED(val data: Map) : HandlingStrategy() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt index cfd7865269..74cba5e796 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -54,7 +54,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private val networkConnectivityChecker: NetworkConnectivityChecker, private val backgroundDetectionObserver: BackgroundDetectionObserver, private val activeCallHandler: ActiveCallHandler -) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { +) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { private var state: SyncState = SyncState.Idle private var liveState = MutableLiveData(state) diff --git a/matrix-sdk-android/src/main/res/values-bg/strings.xml b/matrix-sdk-android/src/main/res/values-bg/strings.xml index 9654fd00b5..c3a5f3be82 100644 --- a/matrix-sdk-android/src/main/res/values-bg/strings.xml +++ b/matrix-sdk-android/src/main/res/values-bg/strings.xml @@ -1,9 +1,7 @@ - + - %1$s: %2$s %1$s изпрати снимка. - Поканата на %s %1$s покани %2$s %1$s Ви покани @@ -22,7 +20,7 @@ %1$s смени името на стаята на: %2$s %s започна видео разговор. %s започна гласов разговор. - %s отговори на повикването. + %s отговори на обаждането. %s прекрати разговора. %1$s направи бъдещата история на стаята видима за %2$s всички членове, от момента на поканването им в нея. @@ -31,54 +29,39 @@ всеки. непозната (%s). %1$s включи шифроване от край до край (%2$s) - %1$s заяви VoIP групов разговор Започна VoIP групов разговор Груповият разговор приключи - (профилната снимка също беше сменена) %1$s премахна името на стаята %1$s премахна темата на стаята %1$s обнови своя профил %2$s %1$s изпрати покана на %2$s да се присъедини към стаята %1$s прие поканата за %2$s - ** Неуспешно разшифроване: %s ** Неуспешно премахване Неуспешно изпращане на съобщението - Неуспешно качване на снимката - Грешка в мрежата Matrix грешка - В момента не е възможно да се присъедините отново към празна стая. - Шифровано съобщение - Имейл адрес Телефонен номер - Устройството на подателя не изпрати ключовете за това съобщение. - %1$s изпрати стикер. - Покана от %s Покана за стая %1$s и %2$s - %1$s и 1 друг %1$s и %2$d други - Празна стая - Премахнато съобщение Съобщение премахнато от %1$s Премахнато съобщение [причина: %1$s] Съобщение премахнато от %1$s [причина: %2$s] - Начална синхронизация: \nИмпортиране на профил… Начална синхронизация: @@ -95,12 +78,9 @@ \nИмпортиране на общности Начална синхронизация: \nИмпортиране на данни за профила - %s обнови тази стая. - Изпращане на съобщение… Изчисти опашката за изпращане - %1$s оттегли поканата за присъединяване на %2$s към стаята поканата на %1$s. Причина: %2$s %1$s покани %2$s. Причина: %3$s @@ -115,34 +95,25 @@ %1$s премахна поканата за присъединяване на %2$s в стаята. Причина: %3$s %1$s прие поканата за %2$s. Причина: %3$s %1$s оттегли поканата на %2$s. Причина: %3$s - %1$s добави %2$s като адрес за тази стая. %1$s добави %2$s като адреси за тази стая. - %1$s премахна %2$s като адрес за тази стая. %1$s премахна %2$s като адреси за тази стая. - %1$s добави %2$s и премахна %3$s като адреси за тази стая. - %1$s настрой %2$s като основен адрес за тази стая. %1$s премахна основния адрес за тази стая. - %1$s разреши на гости да се присъединяват в стаята. %1$s предотврати присъединяването на гости в стаята. - %1$s включи шифроване от-край-до-край. %1$s включи шифроване от-край-до-край (неразпознат алгоритъм %2$s). - %s изпрати запитване за потвърждение на ключа ви, но клиентът ви не поддържа верифициране посредством чат. Ще трябва да използвате стария метод за верифициране на ключове. - %1$s създаде стаята Изпратихте снимка. Изпратихте стикер. - Ваша покана Създадохте стаята Поканихте %1$s @@ -152,4 +123,94 @@ Изгонихте %1$s Отблокирахте %1$s Блокирахте %1$s - + Включихте шифроване от-край-до-край (непознат алгоритъм: %1$s). + Включихте шифроване от-край-до-край. + Спряхте възможността гости да се присъединяват в стаята. + %1$s спря възможността гости да се присъединяват в стаята. + Спряхте възможността гости да се присъединяват в стаята. + Позволихте на гости да се присъединяват тук. + %1$s позволи на гости да се присъединяват тук. + Позволихте на гости да се присъединяват към стаята. + Премахнахте основния адрес на стаята. + Зададохте %1$s като основен адрес на стаята. + Добавихте %1$s и премахнахте %2$s от адресите за стаята. + + Премахнахте %1$s от адресите на стаята. + Премахнахте %1$s от адресите на стаята. + + + Добавихте %1$s като адрес за тази стая. + Добавихте %1$s като адреси за тази стая. + + Оттеглихте поканата на %1$s. Причина: %2$s + Приехте поканата за %1$s. Причина: %2$s + Оттеглихте поканата за присъединяване в стаята от %1$s. Причина: %2$s + Изпратихте покана към %1$s за присъединяване в стаята. Причина: %2$s + Блокирахте %1$s. Причина: %2$s + Отблокирахте %1$s. Причина: %2$s + Изгонихте %1$s. Причина: %2$s + Отхвърлихте поканата. Причина: %1$s + Напуснахте. Причина: %1$s + %1$s напусна. Причина: %2$s + Напуснахте стаята. Причина: %1$s + Присъединихте се. Причина: %1$s + %1$s се присъедини. Причина: %2$s + Присъединихте се в стаята. Причина: %1$s + Поканихте %1$s. Причина: %2$s + Ваша покана. Причина: %1$s + %1$s от %2$s на %3$s + %1$s промени нивото на достъп на %2$s. + Променихте нивото на достъп на %1$s. + Собствено ниво + Собствено ниво (%1$d) + По подразбиране + Модератор + Администратор + Променихте %1$s приспособлението + %1$s промени %2$s приспособлението + Премахнахте %1$s приспособлението + %1$s премахна %2$s приспособлението + Добавихте %1$s приспособление + %1$s добави %2$s приспособление + Приехте поканата за %1$s + Оттеглихте поканата от %1$s + %1$s оттегли поканата от %2$s + Оттеглихте поканата за присъединяване в стаята от %1$s + Поканихте %1$s + %1$s покани %2$s + Изпратихте покана към %1$s за присъединяване в стаята + Обновихте профила си %1$s + Премахнахте снимката на стаята + %1$s премахна снимката на стаята + Премахнахте темата на стаята + Премахнахте името на стаята + Заявихте VoIP конференция + Обновихте чата. + %s обнови чата. + Обновихте стаята. + Включихте шифроване от-край-до-край (%1$s) + Направихте бъдещите съобщения видими за %1$s + %1$s направи бъдещите съобщения видими за %2$s + Направихте бъдещата история на стаята видима за %1$s + Прекратихте разговора. + Започнахте видео разговор. + Отговорихте на обаждането. + Изпратихте данни за настройка на разговора. + %s изпрати данни за настройка на разговора. + Започнахте гласов разговор. + Променихте името на стаята на: %1$s + Променихте снимката на стаята + %1$s промени снимката на стаята + Променихте темата на: %1$s + Премахнахте името си (%1$s) + Променихте името си от %1$s на %2$s + Променихте името си на %1$s + Променихте снимката си + Оттеглихте поканата от %1$s + Напуснахте стаята + %1$s напусна стаята + Присъединихте се + %1$s се присъедини + Създадохте дискусията + %1$s създаде дискусията + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml b/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml index 8d31488283..35f8feaf0f 100644 --- a/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml +++ b/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml @@ -174,7 +174,7 @@ আপনি এই ঘরের ঠিকানা হিসাবে %1$s সরিয়েছেন। - আপনি এই ঘরের ঠিকানা হিসাবে %2$s গুলি সরিয়েছেন। + আপনি এই ঘরের ঠিকানা হিসাবে %1$s গুলি সরিয়েছেন। %1$s %2$s যোগ করেছে এবং %3$s গুলি এই ঘরের ঠিকানা হিসাবে সরানো হয়েছে। আপনি %1$s যোগ করেছেন এবং %2$s কে এই ঘরের ঠিকানা হিসাবে সরিয়ে দিয়েছেন। diff --git a/matrix-sdk-android/src/main/res/values-cs/strings.xml b/matrix-sdk-android/src/main/res/values-cs/strings.xml index 2ea2112b45..371689a2f2 100644 --- a/matrix-sdk-android/src/main/res/values-cs/strings.xml +++ b/matrix-sdk-android/src/main/res/values-cs/strings.xml @@ -1,9 +1,8 @@ - + %1$s: %2$s Uživatel %1$s poslal obrázek. Uživatel %1$s poslal nálepku. - Pozvání od uživatele %s Uživatel %1$s pozval uživatele %2$s Uživatel %1$s vás pozval @@ -31,51 +30,36 @@ kohokoliv. neznámým (%s). %1$s zapnuli end-to-end šifrování (%2$s) - %1$s požádali o VoIP konferenci Začala VoIP konference VoIP konference skončila - (profilový obrázek byl také změněn) %1$s odstranili název místnosti %1$s odstranili téma místnosti %1$s aktualizovali svůj profil %2$s %1$s do této místnosti pozvali %2$s %1$s přijali pozvání pro %2$s - ** Nelze dešifrovat: %s ** Odesílatelovo zařízení nám neposlalo klíče pro tuto zprávu. - Nelze vymazat Zprávu nelze odeslat - Obrázek nelze nahrát - Chyba sítě Chyba v Matrixu - V současnosti není možné znovu vstoupit do prázdné místnosti. - Šifrovaná zpráva - E-mailová adresa Telefonní číslo - Pozvání od %s Pozvání do místnosti - %1$s a %2$s - %1$s a jeden další %1$s a %2$d další %1$s a %2$d dalších - Prázdná místnost - %s povýšili tuto místnost. - Zpráva byla smazána [důvod: %1$s] Zpráva smazána uživatelem %1$s [důvod: %2$s] %1$s zrušili pozvánku do místnosti pro %2$s @@ -93,13 +77,10 @@ \nImportuji komunity Úvodní synchronizace: \nImportuji data účtu - Odesílám zprávu… - Úvodní synchronizace: \nImportuji pozvání Vymazat frontu neodeslaných zpráv - %1$s pozvali %2$s. Důvod: %3$s %1$s vás pozvali. Důvod: %2$s %1$s opustil místnost. Důvod: %2$s @@ -107,7 +88,6 @@ Zprávu odstranil/a %1$s Poslali jste obrázek. Poslali jste nálepku. - Vaše pozvání %1$s založil místnost Vy jste založili místnost @@ -136,7 +116,6 @@ Učinili jste budoucí historii místnosti viditelnou pro %1$s Zapnuli jste end-to-end šifrování (%1$s) Povýšili jste tuto místnost. - Požádali jste o VoIP konferenci Odstranili jste jméno místnosti Odstranili jste téma místnosti @@ -146,24 +125,20 @@ Poslali jste %1$s pozvání ke vstupu do místnosti Zrušili jste pozvánku ke vstupu do místnosti pro %1$s Přijali jste pozvání pro %1$s - %1$s přidali widget %2$s Přidali jste widget %1$s %1$s odstranili widget %2$s Odstranili jste widget %1$s %1$s změnil widget %2$s Změnili jste widget %1$s - Správce Moderátor Výchozí Vlastní (%1$d) Vlastní - Změnili jste %1$s stupeň oprávnění. %1$s změnili %2$s stupeň oprávnění. %1$s z %2$s na %3$s - Pozvání od %1$s. Důvod: %2$s Vaše pozvání. Důvod: %1$s Pozvali jste %1$s. Důvod: %2$s @@ -186,49 +161,39 @@ Přijali jste pozvání pro %1$s. Důvod: %2$s %1$s zrušili pozvání pro %2$s. Důvod: %3$s Zrušili jste pozvání od %1$s. Důvod: %2$s - %1$s přidali %2$s jako adresu pro tuto místnost. %1$s přidali %2$s jako adresy pro tuto místnost. %1$s přidali %2$s jako adresy pro tuto místnost. - Přidali jste %1$s jako adresu pro tuto místnost. Přidali jste %1$s jako adresy pro tuto místnost. Přidali jste %1$s jako adresy pro tuto místnost. - %1$s odstranili %2$s jako adresu pro tuto místnost. %1$s odstranili %2$s jako adresy pro tuto místnost. %1$s odstranili %2$s jako adresy pro tuto místnost. - - Odstranili jste %2$s jako adresu pro tuto místnost. - Odstranili jste %2$s jako adresuy pro tuto místnost. - Odstranili jste %2$s jako adresy pro tuto místnost. + Odstranili jste %1$s jako adresu pro tuto místnost. + Odstranili jste %1$s jako adresuy pro tuto místnost. + Odstranili jste %1$s jako adresy pro tuto místnost. - - %1$s přidali %2$ a odstranili %3$s jako adresy pro tuto místnost. + %1$s přidali %2$s a odstranili %3$s jako adresy pro tuto místnost. Přidali jste %1$s a odstranili %2$s jako adresy pro tuto místnost. - %1$s nastavili hlavní adresu této místnosti na %2$s. Nastavili jste %1$s na hlavní adresu této místnosti. %1$s odstranili hlavní adresu této místnosti. Odstranili jste hlavní adresu této místnosti. - "%1$s povolili hostům vstoupit do místnosti." Povolili jste hostům vstoupit do místnosti. %1$s zamezili hostům vstoupit do místnosti. Zamezili jste hostům vstoupit do místnosti. - %1$s zapnuli end-to-end šifrování. Zapnuli jste end-to-end šifrování. %1$s zapnuli end-to-end šifrování (neznámý algoritmus %2$s). - Zapnuli jste end-to-end šifrování (neznámý algoritmus %2$s). - + Zapnuli jste end-to-end šifrování (neznámý algoritmus %1$s). %s žádá ověření Vašeho klíče, ale Váš klient nepodporuje ověření klíče v chatu. Budete muset k ověření klíčů použít zastaralý způsob ověření. - - + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-de/strings.xml b/matrix-sdk-android/src/main/res/values-de/strings.xml index 9b10aae10d..4c574d578a 100644 --- a/matrix-sdk-android/src/main/res/values-de/strings.xml +++ b/matrix-sdk-android/src/main/res/values-de/strings.xml @@ -73,14 +73,22 @@ %s hat diesen Raum aufgewertet. Sende eine Nachricht… Sendewarteschlange leeren - Erste Synchronisation: Importiere Benutzerkonto… - Erste Synchronisation: Importiere Cryptoschlüssel - Erste Synchronisation: Importiere Räume - Erste Synchronisation: Importiere betretene Räume - Erste Synchronisation: Importiere eingeladene Räume - Erste Synchronisation: Importiere verlassene Räume - Erste Synchronisation: Importiere Gemeinschaften - Erste Synchronisation: Importiere Benutzerdaten + Erste Synchronisation: +\nImportiere Benutzerkonto… + Erste Synchronisation: +\nImportiere Cryptoschlüssel + Erste Synchronisation: +\nImportiere Räume + Erste Synchronisation: +\nImportiere betretene Räume + Erste Synchronisation: +\nImportiere eingeladene Räume + Erste Synchronisation: +\nImportiere verlassene Räume + Erste Synchronisation: +\nImportiere Communities + Erste Synchronisation: +\nImportiere Benutzerdaten %1$s hat die Einladung an %2$s, den Raum zu betreten, zurückgezogen %1$s\'s Einladung. Grund: %2$s %1$s hat %2$s eingeladen. Grund: %3$s @@ -107,7 +115,7 @@ %1$s legt die Hauptadresse fest für diesen Raum als %2$s fest. %1$s entfernt die Hauptadresse für diesen Raum. %1$s hat Gästen erlaubt den Raum zu betreten. - %1$s hat Gäste unterbunden den Raum zu betreten. + %1$s hat Gästen untersagt den Raum zu betreten. %1$s aktivierte Ende-zu-Ende-Verschlüsselung. %1$s aktivierte Ende-zu-Ende-Verschlüsselung (unbekannter Algorithmus %2$s). %s fordert zur Überprüfung deines Schlüssels auf, jedoch unterstützt dein Client nicht die Schlüsselüberprüfung im Chat. Du musst die herkömmliche Schlüsselüberprüfung verwenden, um die Schlüssel zu überprüfen. @@ -199,7 +207,7 @@ %1$s hat die Einladung für %2$s zurückgezogen Du hast %1$s eingeladen %1$s hat %2$s eingeladen - Du hast zukünftige Nachrichten für %2$s sichtbar gemacht + Du hast zukünftige Nachrichten für %1$s sichtbar gemacht %1$s hat zukünftige Nachrichten für %2$s sichtbar gemacht Du hast den Raum verlassen %1$s hat den Raum verlassen @@ -207,4 +215,10 @@ %1$s ist beigetreten Du hast eine Diskussion erstellt %1$s hat eine Diskussion erstellt + %s hat hier ein Upgrade durchgeführt. + Du hast hier ein Upgrade durchgeführt. + Du hast Gästen untersagt den Raum zu betreten. + %1$s hat Gästen untersagt den Raum zu betreten. + Du bist gegangen. Grund: %1$s + %1$s ist gegangen. Grund: %2$s \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-eo/strings.xml b/matrix-sdk-android/src/main/res/values-eo/strings.xml index 69b009ca7e..10be2103cf 100644 --- a/matrix-sdk-android/src/main/res/values-eo/strings.xml +++ b/matrix-sdk-android/src/main/res/values-eo/strings.xml @@ -1,26 +1,24 @@ - + %1$s sendis bildon. %1$s sendis glumarkon. - Invito de %s %1$s invitis uzanton %2$s %1$s invitis vin - %1$s alvenis - %1$s foriris - %1$s malakceptis la inviton + %1$s envenis + %1$s foriris de la ĉambro + %1$s rifuzis la inviton %1$s forpelis uzanton %2$s %1$s malforbaris uzanton %2$s %1$s forbaris uzanton %2$s %1$s nuligis inviton por %2$s %1$s ŝanĝis sian profilbildon ** Ne eblas malĉifri: %s ** - La aparato de la sendanto ne sendis al ni la ŝlosilojn por tiu mesaĝo. - + La aparato de la sendinto ne sendis al ni la ŝlosilojn por tiu mesaĝo. %1$s: %2$s - %1$s ŝanĝis sian vidigan nomon al %2$s - %1$s ŝanĝis sian vidigan nomon de %2$s al %3$s - %1$s forigis sian vidigan nomon (%2$s) + %1$s ŝanĝis sian prezentan nomon al %2$s + %1$s ŝanĝis sian prezentan nomon de %2$s al %3$s + %1$s forigis sian prezentan nomon (%2$s) %1$s ŝanĝis la temon al: %2$s %1$s ŝanĝis nomon de la ĉambro al: %2$s %s vidvokis. @@ -28,50 +26,38 @@ %s respondis la vokon. %s finis la vokon. %1$s videbligis estontan historion de ĉambro al %2$s - ĉiuj ĉambranoj, ekde iliaj invitoj. - ĉiuj ĉambranoj, ekde iliaj aliĝoj. + ĉiuj ĉambranoj, ekde siaj invitoj. + ĉiuj ĉambranoj, ekde siaj aliĝoj. ĉiuj ĉambranoj. ĉiu ajn. nekonata (%s). %1$s ŝaltis tutvojan ĉifradon (%2$s) %s gradaltigis la ĉambron. - Mesaĝo foriĝis - Mesaĝo foriĝis de %1$s + Mesaĝon forigis %1$s Mesaĝo foriĝis [kialo: %1$s] - Mesaĝo foriĝis de %1$s [kialo: %2$s] + Mesaĝon forigis %1$s [kialo: %2$s] %1$s ĝisdatigis sian profilon %2$s %1$s sendis aliĝan inviton al %2$s %1$s nuligis la aliĝan inviton por %2$s %1$s akceptis la inviton por %2$s - Ne povis redakti Ne povas sendi mesaĝon - Malsukcesis alŝuti bildon - Reta eraro Matrix-eraro - - Nun ne eblas re-aliĝi al malplena ĉambro - + Nun ne eblas re-aliĝi al malplena ĉambro. Ĉifrita mesaĝo - Retpoŝtadreso Telefonnumero - Invito de %s - Ĉambra invito - + Invito al ĉambro %1$s kaj %2$s - %1$s kaj 1 alia %1$s kaj %2$d aliaj - Malplena ĉambro - Komenca spegulado: \nEnportante konton… Komenca spegulado: @@ -88,52 +74,143 @@ \nEnportante komunumojn Komenca spegulado: \nEnportante datumojn de konto - Sendante mesaĝon… Vakigi sendan atendovicon - %1$s petis grupan vokon Grupa voko komenciĝis Grupa voko finiĝis - (ankaŭ profilbildo ŝanĝiĝis) %1$s forigis nomon de la ĉambro %1$s forigis temon de la ĉambro Invito de %1$s. Kialo: %2$s %1$s invitis uzanton %2$s. Kialo: %3$s %1$s invitis vin. Kialo: %2$s - %1$s aliĝis al la ĉambro. Kialo: %2$s + %1$s envenis. Kialo: %2$s %1$s foriris de la ĉambro. Kialo: %2$s %1$s rifuzis la inviton. Kialo: %2$s %1$s forpelis uzanton %2$s. Kialo: %3$s %1$s malforbaris uzanton %2$s. Kialo: %3$s %1$s forbaris uzanton %2$s. Kialo: %3$s - %1$s sendis inviton al la ĉambro al %2$s. Kialo: %3$s - %1$s nuligis la inviton al la ĉambro al %2$s. Kialo: %3$s + %1$s sendis al %2$s inviton al la ĉambro. Kialo: %3$s + %1$s nuligis la inviton al la ĉambro por %2$s. Kialo: %3$s %1$s akceptis la inviton por %2$s. Kialo: %3$s - %1$s nuligis la inviton al %2$s. Kialo: %3$s - + %1$s nuligis la inviton por %2$s. Kialo: %3$s %1$s aldonis %2$s kiel adreson por ĉi tiu ĉambro. %1$s aldonis %2$s kiel adresojn por ĉi tiu ĉambro. - %1$s forigis %2$s kiel adreson por ĉi tiu ĉambro. %1$s forigis %2$s kiel adresojn por ĉi tiu ĉambro. - %1$s aldonis %2$s kaj forigis %3$s kiel adresojn por ĉi tiu ĉambro. - - %1$s agordis la ĉefadreson por ĉi tiu ĉambro al %2$s. + %1$s agordis la ĉefadreson de ĉi tiu ĉambro al %2$s. %1$s forigis la ĉefadreson de ĉi tiu ĉambro. - - %1$s permesis al gastoj aliĝi al la ĉambro. - %1$s malpermesis al gastoj aliĝi al la ĉambro. - + %1$s permesis al gastoj enveni. + %1$s malpermesis al gastoj enveni. %1$s ŝaltis tutvojan ĉifradon. %1$s ŝaltis tutvojan ĉifradon (kun nerekonita algoritmo %2$s). - - %s petas kontrolon de via ŝlosilo, sed via kliento ne subtenas kontrolon de ŝlosiloj en la babilujo. Vi devos uzi malnovecan kontrolon de ŝlosiloj. - - + %s petas kontrolon de via ŝlosilo, sed via kliento ne subtenas kontrolon de ŝlosiloj en la babilujo. Vi devos uzi malnovan kontrolmanieron de ŝlosiloj. + Vi ŝanĝis la povnivelon de %1$s. + %1$s sanĝis la povnivelon de %2$s. + Vi ŝaltis tutvojan ĉifradon (kun nerekonita algoritmo %1$s). + Vi ŝaltis tutvojan ĉifradon. + Vi malpermesis al gastoj aliĝi. + %1$s malpermesis al gastoj aliĝi. + Vi malpermesis al gastoj enveni. + Vi permesis al gastoj aliĝi. + %1$s permesis al gastoj aliĝi. + Vi permesis al gastoj enveni. + Vi forigis la ĉefadreson de ĉi tiu ĉambro. + Vi agordis al ĉefadreson de ĉi tiu ĉambro al %1$s. + Vi aldonis %1$s kaj forigis %2$s kiel adresojn por ĉi tiu ĉambro. + + Vi forigis %1$s kiel adreson por ĉi tiu ĉambro. + Vi forigis %1$s kiel adresojn por ĉi tiu ĉambro. + + + Vi aldonis %1$s kiel adreson por ĉi tiu ĉambro. + Vi aldonis %1$s kiel adresojn por ĉi tiu ĉambro. + + Vi nuligis la inviton por %1$s. Kialo: %2$s + Vi akceptis la inviton por %1$s. Kialo: %2$s + Vi nuligis inviton al la ĉambro por %1$s. Kialo: %2$s + Vi sendis al %1$s inviton al la ĉambro. Kialo: %2$s + Vi forbaris uzanton %1$s. Kialo: %2$s + Vi malforbaris uzanton %1$s. Kialo: %2$s + Vi forpelis uzanton %1$s. Kialo: %2$s + Vi rifuzis la inviton. Kialo: %1$s + Vi foriris. Kialo: %1$s + %1$s foriris. Kialo: %2$s + Vi foriris de la ĉambro. Kialo: %1$s + Vi envenis. Kialo: %1$s + Vi aliĝis. Kialo: %1$s + %1$s aliĝis. Kialo: %2$s + Vi invitis uzanton %1$s. Kialo: %2$s + Via invito. Kialo: %1$s + %1$s de %2$s al %3$s + Propra + Ordinara + Propra (%1$d) + Reguligisto + Administranto + Vi ŝanĝis la fenestraĵon %1$s + %1$s ŝanĝis la fenestraĵon %2$s + Vi forigis la fenestraĵon %1$s + %1$s forigis la fenestraĵon %2$s + Vi aldonis la fenestraĵon %1$s + %1$s aldonis la fenestraĵon %2$s + Vi akceptis la inviton por %1$s + Vi nuligis la inviton por %1$s + %1$s nuligis la inviton por %2$s + Vi nuligis la aliĝan inviton por %1$s + Vi invitis uzanton %1$s + %1$s invitis uzanton %2$s + Vi sendis aliĝan inviton al %1$s + Vi ĝisdatigis vian profilon %1$s + Vi forigis bildon de la ĉambro + %1$s forigis bildon de la ĉambro + Vi forigis temon de la ĉambro + Vi forigis nomon de la ĉambro + Vi petis grupan vokon + Vi gradaltigis la interparolon. + %s gradaltigis la interparolon. + Vi gradaltigis la ĉambron. + Vi ŝaltis tutvojan ĉifradon (%1$s) + %1$s videbligis al %2$s estontajn mesaĝojn + Vi videbligis al %1$s estontajn mesaĝojn + Vi videbligis estontan historion de ĉambro al %1$s + Vi finis la vokon. + Vi respondis la vokon. + Vi sendis datumojn por prepari la vokon. + %s sendis datumojn por prepari la vokon. + Vi voĉvokis. + Vi vidvokis. + Vi ŝanĝis la nomon de la ĉambro al: %1$s + Vi ŝanĝis la bildon de la ĉambro + %1$s ŝanĝis la bildon de la ĉambro + Vi ŝanĝis la temon al: %1$s + Vi forigis vian prezentan nomon (%1$s) + Vi ŝanĝis vian prezentan nomon de %1$s al %2$s + Vi ŝanĝis vian prezentan nomon al %1$s + Vi ŝanĝis vian profilbildon + Vi nuligis inviton por %1$s + Vi forbaris uzanton %1$s + Vi malforbaris uzanton %1$s + Vi forpelis uzanton %1$s + Vi rifuzis la inviton + Vi foriris de la ĉambro + %1$s foriris de la ĉambro + Vi foriris de la ĉambro + Vi envenis + %1$s envenis + Vi envenis + Vi invitis uzanton %1$s + Vi kreis la diskuton + %1$s kreis la diskuton + Vi kreis la ĉambron + %1$s kreis la ĉambron + Via invito + Vi sendis glumarkon. + Vi sendis bildon. + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-es/strings.xml b/matrix-sdk-android/src/main/res/values-es/strings.xml index 1b1935602c..e2d09c7857 100644 --- a/matrix-sdk-android/src/main/res/values-es/strings.xml +++ b/matrix-sdk-android/src/main/res/values-es/strings.xml @@ -226,7 +226,7 @@ Quitaste %1$s como dirección para esta sala. - Quitaste %2$s como direcciones para esta sala. + Quitaste %1$s como direcciones para esta sala. "%1$s agregó %2$s y eliminó %3$s como direcciones para esta sala." diff --git a/matrix-sdk-android/src/main/res/values-fa/strings.xml b/matrix-sdk-android/src/main/res/values-fa/strings.xml index 25d92b4abe..042fda7ddd 100644 --- a/matrix-sdk-android/src/main/res/values-fa/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fa/strings.xml @@ -1,9 +1,8 @@ - + %1$s: %2$s %1$s تصویری فرستاد. %1$s برچسبی فرستاد. - دعوت %s ‫%1$s، %2$s را دعوت کرد %1$s دعوتتان کرد @@ -32,11 +31,9 @@ ناشناخته (%s). %1$s رمزنگاری سرتاسری را روشن کرد (%2$s) %s این اتاق را ارتقا داد. - %1$s درخواست یک گردهمایی صوتی داد گردهمایی صوتی آغاز شد گردهمایی صوتی پایان یافت - (تصویر هم عوض شد) %1$s نام اتاق را پاک کرد %1$s موضوع اتاق را پاک کرد @@ -47,36 +44,24 @@ %1$s دعوتی برای پیوستن %2$s به اتاق فرستاد %1$s دعوت پیوستن به اتاق %2$s را باطل کرد %1$s دعوت برای %2$s را پذیرفت - ** ناتوان در رمزگشایی: %s ** دستگاه فرستنده، کلیدهای این پیام را برایمان نفرستاده است. - ناتوان در فرستادن پیام - شکست در بارگذاری تصویر - خطای شبکه خطای ماتریکس - در حال حاضر امکان بازپیوست به اتاقی خالی وجود ندارد‌‌. - پیام رمزنگاری شده - نشانی رایانامه شماره تلفن - دعوت از %s دعوت اتاق - %1$s و %2$s - %1$s و ۱ نفر دیگر %1$s و %2$d نفر دیگر - اتاق خالی - همگام‌سازی نخستین: \nدر حال درون‌ریزی حساب… همگام‌سازی نخستین: @@ -93,10 +78,8 @@ \nدر حال درون‌ریزی انجمن‌ها همگام‌سازی نخستین: \nدر حال درون‌ریزی داده‌های حساب - در حال فرستادن پیام… پاک‌سازی صفِ در حال ارسال - دعوت %1$s. دلیل: %2$s %1$s، %2$s را دعوت کرد. دلیل: %3$s %1$s دعوتتان کرد. دلیل: %2$s @@ -110,36 +93,27 @@ %1$s دعوت %2$s برای پیوستن به اتاق را باطل کرد. دلیل: %3$s %1$s دعوت برای %2$s را پذیرفت. دلیل: %3$s %1$s دعوت %2$s را نپذیرفت. دلیل: %3$s - %1$s، %2$s را به عنوان نشانی‌ای برای این اتاق افزود. %1$s، %2$s را به عنوان نشانی‌هایی برای این اتاق افزود. - %1$s، %2$s را به عنوان نشانی‌ای برای این اتاق پاک کرد. %1$s، %3$s را به عنوان نشانی‌هایی برای این اتاق پاک کرد. - %1$s برای نشانی این اتاق، %2$s را افزود و %3$s را پاک کرد. - %1$s نشانی اصلی این اتاق را به %2$s تنظیم کرد. %1$s نشانی اصلی را برای این اتاق پاک کرد. - %1$s اجازه داد میمهانان به گروه بپیوندند. %1$s جلوی پیوستن میمهانان به گروه را گرفت. - %1$s رمزنگاری سرتاسری را روشن کرد. %1$s رمزنگاری سرتاسری را روشن کرد (الگوریتم تشخیص‌داده‌نشده %2$s ). - %s درخواست تأیید کلیدتان را دارد، ولی کارخواهتان تأیید کلید درون گپ را پشتیبانی نمی‌کند. برای تأیید کلیدها لازم است از تأییدیهٔ کلید قدیمی استفاده کنید. - %1$s اتاق را ایجاد کرد %1$s نمایه‌اش را به‌روز کرد %2$s نمی‌توان ویرایش کرد تصویری فرستادید. برچسبی فرستادید. - دعوتتان اتاق را ایجاد کردید از %1$s دعوت کردید @@ -167,7 +141,6 @@ تاریخچهٔ آتی اتاق را برای %1$s نمایان کردید رمزنگاری سرتاسری را روشن کردید (%1$s) این اتاق را ارتقا دادید. - دارخواست کنفرانس ویپ دادید نام اتاق را برداشتید موضوع اتاق را برداشتید @@ -177,24 +150,20 @@ برای %1$s دعوت پیوستن به اتاق فرستادید دعوت پیوستن %1$s به اتاق را پس گرفتید دعوت برای %1$s را پذیرفتید - %1$s ابزارک %2$s را افزود ابزارک %1$s را افزودید %1$s ابزارک %2$s را برداشت ابزارک %1$s را برداشتید %1$s ابزارک %2$s را دستکاری کرد ابزارک %1$s را دستکاری کردید - مدیر ناظم پیش‌گزیده سفارشی (%1$d) سفارشی - سطح قدرت %1$s را تغییر دادید. %1$s سطح قدرت %2$s را تغییر داد. %1$s از %2$s به %3$s - دعوتتان. دلیل: %1$s %1$s را دعوت کردید. دلیل: %2$s به اتاق پیوستید. دلیل: %1$s @@ -207,26 +176,41 @@ دعوت %1$s برای پیوستن به اتاق را پس گرفتید. دلیل: %2$s دعوت برای %1$s را پذیرفتید. دلیل: %2$s دعوت %1$s را رد کردید. دلیل: %2$s - نشانی %1$s را به این اتاق افزودید. نشانی‌های %1$s را به این اتاق افزودید. - نشانی %1$s ار از این اتاق برداشتید. نشانی‌های %1$s ار از این اتاق برداشتید. - نشانی %1$s ار افزوده و %2$s را از این اتاق برداشتید. - نشانی اصلی این اتاق را به %1$s تنظیم کردید. نشانی اصلی این اتاق را برداشتید. - به میهمانان اجازهٔ پیوستن به گروه دادید. میمهانان را از پیوستن به گروه بازداشتید. - رمزنگاری سرتاسری را روشن کردید. رمزنگاری سرتاسری را روشن کردید (الگوریتم ناشناخته %1$s). - - + مهمان‌ها را از پیوستن به اتاق بازداشتید. + %1$s مهمان‌ها را از پیوستن به اتاق بازداشت. + به مهمان‌ها اجازه دادید به این‌جا بپیوندند. + %1$s به مهمان‌ها اجازه داد به این‌جا بپیوندند. + رفتید. دلیل: %1$s + %1$s رفت. دلیل: %2$s + پیوستید. دلیل: %1$s + %1$sپیوست. دلیل: %2$s + دعوت %1$s را پس گرفتید + %1$s دعوت %2$s را پس گرفت + %1$s را دعوت کردید + %1$s، %2$s را دعوت کرد + این‌جا را ارتقا دادید. + %s این‌جا را ارتقا داد. + پیام‌های آینده را برای %1$s نمایان کردید + %1$s پیام‌های آینده را برای %2$s نمایان کرد + اتاق را ترک کردید + %1$s اتاق را ترک کرد + پیوستید + %1$s پیوست + گفت‌وگو را ایجاد کردید + %1$s گفت‌وگو را ایجاد کرد + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-fr/strings.xml b/matrix-sdk-android/src/main/res/values-fr/strings.xml index 71b956a7e7..9c5fa7d3b1 100644 --- a/matrix-sdk-android/src/main/res/values-fr/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fr/strings.xml @@ -1,9 +1,7 @@ - + - %1$s : %2$s %1$s a envoyé une image. - invitation de %s %1$s a invité %2$s %1$s vous a invité @@ -11,13 +9,13 @@ %1$s est parti du salon %1$s a rejeté l’invitation %1$s a expulsé %2$s - %1$s a révoqué le bannissement de %2$s - %1$s a banni %2$s + %1$s a révoqué l\'exclusion de %2$s + %1$s a exclus %2$s %1$s a annulé l’invitation de %2$s %1$s a changé d’avatar %1$s a modifié son nom affiché en %2$s - %1$s a modifié son nom affiché %2$s en %3$s - %1$s a supprimé son nom affiché (%2$s) + %1$s a modifié son nom affiché de %2$s en %3$s + %1$s a supprimé son nom affiché (précédemment %2$s) %1$s a changé le sujet en : %2$s %1$s a changé le nom du salon en : %2$s %s a passé un appel vidéo. @@ -31,54 +29,39 @@ n’importe qui. inconnu (%s). %1$s a activé le chiffrement de bout en bout (%2$s) - %1$s a demandé une téléconférence VoIP Téléconférence VoIP démarrée Téléconférence VoIP terminée - (l’avatar a aussi changé) %1$s a supprimé le nom du salon %1$s a supprimé le sujet du salon %1$s a mis à jour son profil %2$s %1$s a envoyé une invitation à %2$s pour rejoindre le salon %1$s a accepté l’invitation pour %2$s - ** Déchiffrement impossible : %s ** L’appareil de l’expéditeur ne nous a pas envoyé les clés pour ce message. - Effacement impossible Envoi du message impossible - L’envoi de l’image a échoué - Erreur de réseau Erreur de Matrix - Il est impossible pour le moment de revenir dans un salon vide. - Message chiffré - Adresse e-mail Numéro de téléphone - %1$s a envoyé un sticker. - Invitation de %s Invitation au salon Salon vide %1$s et %2$s - %1$s et 1 autre %1$s et %2$d autres - - Message supprimé Message supprimé par %1$s Message supprimé [motif : %1$s] Message supprimé par %1$s [motif : %2$s] - Synchronisation initiale : \nImportation du compte… Synchronisation initiale : @@ -95,12 +78,9 @@ \nImportation des communautés Synchronisation initiale : \nImportation des données du compte - %s a mis à niveau ce salon. - Envoi du message… Vider la file d’envoi - %1$s a révoqué l’invitation pour %2$s à rejoindre le salon Invitation de %1$s. Raison : %2$s %1$s a invité %2$s. Raison : %3$s @@ -109,35 +89,128 @@ %1$s est parti du salon. Raison : %2$s %1$s a refusé l’invitation. Raison : %2$s %1$s a expulsé %2$s. Raison : %3$s - %1$s a révoqué le bannissement de %2$s. Raison : %3$s - %1$s a banni %2$s. Raison : %3$s + %1$s a révoqué l\'exclusion de %2$s. Raison : %3$s + %1$s a exclus %2$s. Raison : %3$s %1$s a envoyé une invitation à %2$s pour rejoindre le salon. Raison : %3$s %1$s a révoqué l’invitation de %2$s à rejoindre le salon. Raison : %3$s %1$s a accepté l’invitation pour %2$s. Raison : %3$s %1$s a annulé l’invitation de %2$s. Raison : %3$s - %1$s a ajouté %2$s comme adresse pour ce salon. %1$s a ajouté %2$s comme adresses pour ce salon. - %1$s a supprimé %2$s comme adresse pour ce salon. %1$s a supprimé %3$s comme adresses pour ce salon. - %1$s a ajouté %2$s et supprimé %3$s comme adresses pour ce salon. - %1$s a défini %2$s comme adresse principale pour ce salon. %1$s a supprimé l’adresse principale de ce salon. - %1$s a autorisé les visiteurs à rejoindre le salon. %1$s a empêché les visiteurs de rejoindre le salon. - %1$s a activé le chiffrement de bout en bout. %1$s a activé le chiffrement de bout en bout (algorithme %2$s inconnu). - %s demande à vérifier votre clé, mais votre client ne supporte pas la vérification de clés dans les discussions. Vous devrez utiliser l’ancienne vérification de clés pour vérifier les clés. - %1$s a créé le salon - + Vous avez mis cet endroit à niveau. + %s a mis cet endroit à niveau. + Vous avez mis à niveau ce salon. + Vous avez expulsé %1$s + Vous avez rejeté l\'invitation + Vous avez quitté le salon + %1$s a quitté le salon + Vous avez quitté le salon + Vous avez rejoint le salon + %1$s a rejoint le salon + Vous avez rejoint le salon + Vous avez invité %1$s + Vous avez créé la discussion + %1$s a créé la discussion + Vous avez créé le salon + Votre invitation + Vous avez envoyé un autocollant. + Vous avez envoyé une image. + Vous avez activé le chiffrement de bout en bout (algorithme %1$s inconnu). + Vous avez activé le chiffrement de bout en bout. + Vous avez empêché les visiteurs de rejoindre le salon. + %1$s a empêché les visiteurs de rejoindre le salon. + Vous avez empêché les visiteurs de rejoindre le salon. + Vous avez autorisé les visiteurs à venir ici. + %1$s a autorisé les visiteurs à venir ici. + Vous avez autorisé les visiteurs à rejoindre le salon. + Vous avez supprimé l’adresse principale de ce salon. + Vous avez défini %1$s comme adresse principale pour ce salon. + Vous avez ajouté %1$s et supprimé %2$s comme adresses pour ce salon. + + Vous avez supprimé %1$s comme adresse pour ce salon. + Vous avez supprimé %1$s comme adresses pour ce salon. + + + Vous avez ajouté %1$s comme adresse pour ce salon. + Vous avez ajouté %1$s comme adresses pour ce salon. + + Vous avez annulé l’invitation de %1$s. Raison : %2$s + Vous avez accepté l’invitation pour %1$s. Raison : %2$s + Vous avez révoqué l’invitation de %1$s à rejoindre le salon. Raison : %2$s + Vous avez envoyé une invitation à %1$s pour rejoindre le salon. Raison : %2$s + Vous avez refusé l’invitation. Raison : %1$s + Vous êtes parti. Raison : %1$s + %1$s est parti. Raison : %2$s + Vous êtes parti du salon. Raison : %1$s + %1$s rejoint. Raison : %2$s + Vous avez rejoint. Raison : %1$s + Vous avez rejoint le salon. Raison : %1$s + Vous avez invité %1$s. Raison : %2$s + Votre invitation. Raison %1$s + %1$s de %2$s à %3$s + %1$s a modifié le niveau de pouvoir de %2$s. + Vous avez modifié le niveau de pouvoir de %1$s. + Personnalisé + Personnalisé (%1$d) + Défaut + Modérateur + Admin + Vous avez modifié le widget %1$s + %1$s a modifié le widget %2$s + Vous avez supprimé le widget %1$s + %1$s a supprimé le widget %2$s + Vous avez ajouté le widget %1$s + %1$s a ajouté le widget %2$s + Vous avez accepté l’invitation pour %1$s + Vous avez révoqué l\'invitation pour %1$s + %1$s a révoqué l\'invitation pour %2$s + Vous avez rendu les futurs messages visible pour %1$s + %1$s a rendu les futurs messages visible pour %2$s + Vous avez supprimé l\'avatar du salon + %1$s a supprimé l\'avatar du salon + Vous avez supprimé le nom du salon + Vous avez demandé une téléconférence VoIP + Vous avez activé le chiffrement de bout en bout (%1$s) + Vous avez rendu l’historique futur du salon visible pour %1$s + Vous avez raccroché. + Vous avez répondu à l’appel. + Vous avez envoyé les données pour configurer l\'appel. + %s a envoyé les données pour configurer l\'appel. + Vous avez passé un appel vocal. + Vous avez passé un appel vidéo. + Vous avez changé le nom du salon en : %1$s + Vous avez modifié l\'avatar du salon + %1$s a modifié l\'avatar du salon + Vous avez modifié votre nom affiché de %1$s en %2$s + Vous avez modifié votre nom affiché en %1$s + Vous avez changé votre avatar + Vous avez annulé l’invitation de %1$s + Vous avez changé le sujet en : %1$s + Vous avez supprimé votre nom affiché (précédemment %1$s) + Vous avez révoqué l’invitation pour %1$s à rejoindre le salon + Vous avez invité %1$s + %1$s a invité %2$s + Vous avez envoyé une invitation à %1$s pour rejoindre le salon + Vous avez mis à jour votre profile %1$s + Vous avez supprimé le sujet du salon + Vous avez exclus %1$s. Raison : %2$s + Vous avez révoqué l\'exclusion de %1$s. Raison : %2$s + Vous avez exclus %1$s + Vous avez révoqué l\'exclusion de %1$s + Vous avez expulsé %1$s. Raison : %2$s + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-it/strings.xml b/matrix-sdk-android/src/main/res/values-it/strings.xml index d4479be674..5eab8c57df 100644 --- a/matrix-sdk-android/src/main/res/values-it/strings.xml +++ b/matrix-sdk-android/src/main/res/values-it/strings.xml @@ -183,7 +183,7 @@ Hai rimosso %1$s come indirizzo per questa stanza. - Hai rimosso %2$s come indirizzi per questa stanza. + Hai rimosso %1$s come indirizzi per questa stanza. Hai aggiunto %1$s e rimosso %2$s come indirizzi per questa stanza. Hai impostato l\'indirizzo principale per questa stanza a %1$s. diff --git a/matrix-sdk-android/src/main/res/values-kab/strings.xml b/matrix-sdk-android/src/main/res/values-kab/strings.xml index e557a7c824..8b94fad9eb 100644 --- a/matrix-sdk-android/src/main/res/values-kab/strings.xml +++ b/matrix-sdk-android/src/main/res/values-kab/strings.xml @@ -172,7 +172,7 @@ Tekkseḍ %1$s am tansa i texxamt-a. - Tekkseḍ %2$s am tansiwin i texxamt-a. + Tekkseḍ %1$s am tansiwin i texxamt-a. %1$s yerna %2$s terniḍ tekkseḍ %3$s am tansiwin i texxamt-a. Terniḍ %1$s terniḍ tekkseḍ %2$s am tansiwin i texxamt-a. diff --git a/matrix-sdk-android/src/main/res/values-lt/strings.xml b/matrix-sdk-android/src/main/res/values-lt/strings.xml index b867219408..4b07e1ec88 100644 --- a/matrix-sdk-android/src/main/res/values-lt/strings.xml +++ b/matrix-sdk-android/src/main/res/values-lt/strings.xml @@ -1,8 +1,19 @@ - + %1$s: %2$s - %1$s išsiuntė atvaizdą. + %1$s išsiuntė vaizdą. %1$s išsiuntė lipduką. - %s pakvietimas - + Jūs prisijungėte prie kambario + %1$s prisijungė prie kambario + %1$s pakvietė jus + Jūs pakvietėte %1$s + %1$s pakvietė %2$s + Jūs sukūrėte diskusiją + %1$s sukūrė diskusiją + Jūs sukūrėte kambarį + %1$s sukūrė kambarį + Jūsų pakvietimas + Jūs išsiuntėte lipduką. + Jūs išsiuntėte vaizdą. + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml index 4e62a21c0e..ed9f91cdb3 100644 --- a/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml +++ b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml @@ -182,7 +182,7 @@ Você removeu %1$s como um endereço desta sala. - Você removeu %2$s como endereços desta sala. + Você removeu %1$s como endereços desta sala. %1$s adicionou %2$s e removeu %3$s como endereços desta sala. Você adicionou %1$s e removeu %2$s como endereços desta sala. diff --git a/matrix-sdk-android/src/main/res/values-ru/strings.xml b/matrix-sdk-android/src/main/res/values-ru/strings.xml index 97643a34fe..ef9cda1dc2 100644 --- a/matrix-sdk-android/src/main/res/values-ru/strings.xml +++ b/matrix-sdk-android/src/main/res/values-ru/strings.xml @@ -225,4 +225,6 @@ %1$s вошел(ла) Вы создали обсуждение %1$s создал(а) обсуждение + Вы обновили. + %s обновлена. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-sk/strings.xml b/matrix-sdk-android/src/main/res/values-sk/strings.xml index c75c8b4832..15924d02e1 100644 --- a/matrix-sdk-android/src/main/res/values-sk/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sk/strings.xml @@ -183,8 +183,8 @@ Odstránili ste adresu %1$s pre túto miestnosť. - Odstránili ste adresy %2$s pre túto miestnosť. - Odstránili ste adresy %2$s pre túto miestnosť. + Odstránili ste adresy %1$s pre túto miestnosť. + Odstránili ste adresy %1$s pre túto miestnosť. Pridali ste %1$s a odstránili adresy %2$s pre túto miestnosť. Nastavili ste hlavnú adresu tejto miestnosti %1$s. diff --git a/matrix-sdk-android/src/main/res/values-sq/strings.xml b/matrix-sdk-android/src/main/res/values-sq/strings.xml index 9756a11762..a1b23e7da2 100644 --- a/matrix-sdk-android/src/main/res/values-sq/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml @@ -1,4 +1,4 @@ - + %1$s: %2$s %1$s dërgoi një figurë. @@ -25,39 +25,26 @@ %1$s kërkoi një konferencë VoIP Konferenca VoIP filloi Konferenca VoIP përfundoi - (u ndryshua edhe avatari) %1$s hoqi emrin e dhomës %1$s përditësoi profilin e tij %2$s %1$s pranoi ftesën tuaj për %2$s - ** S’arrihet të shfshehtëzohet: %s ** Pajisja e dërguesit nuk na ka dërguar kyçet për këtë mesazh. - S’u redaktua dot S’arrihet të dërgohet mesazh - Ngarkimi i figurës dështoi - Gabim rrjeti Gabim Matrix - Hëpërhë s’është e mundur të rihyhet në një dhomë të zbrazët. - - U fshehtëzua mesazhi - + Mesazh i fshehtëzuar Adresë email Numër telefoni - Ftesë nga %s Ftesë Dhome - %1$s dhe %2$s - Dhomë e zbrazët - %1$s dërgoi një ngjitës. - Ftesë e %s %1$s hoqi dëbimin për %2$s %1$s tërhoqi mbrapsht ftesën për %2$s @@ -65,20 +52,17 @@ %1$s ndryshoi emrin e tyre në ekran nga %2$s në %3$s %1$s hoqi emrin e tij në ekran (%2$s) %1$s aktivizoi fshehtëzim skaj-më-skaj (%2$s) - %1$s hoqi temën e dhomës %1$s dërgoi një ftesë për %2$s që të marrë pjesë në dhomë %1$s dhe 1 tjetër %1$s dhe %2$d të tjerë - Mesazhi u hoq Mesazhi u hoq nga %1$s Mesazh i hequr [arsye: %1$s] Mesazh i hequr nga %1$s [arsye: %2$s] %s e përmirësoi këtë dhomë. - Njëkohësimi Fillestar: \nPo importohet llogaria… Njëkohësimi Fillestar: @@ -95,10 +79,8 @@ \nPo importohen Bashkësi Njëkohësimi Fillestar: \nPo importohet të Dhëna Llogarie - Po dërgohet mesazh… Spastro radhë pritjeje - %1$s shfuqizoi ftesën për %2$s për pjesëmarrje te dhoma Ftesë e %1$s. Arsye: %2$s %1$s ftoi %2$s. Arsye: %3$s @@ -113,34 +95,25 @@ %1$s shfuqizoi ftesën për %2$s për të ardhur në dhomë. Arsye: %3$s %1$s pranoi ftesën për %2$s. Arsye: %3$s %1$s tërhoqi mbrapsht ftesën për %2$s. Arsye: %3$s - %1$s shtoi %2$s si një adresë për këtë dhomë. %1$s shtoi %2$s si adresa për këtë dhomë. - %1$s hoqi %2$s si adresë për këtë dhomë. %1$s hoqi %3$s si adresa për këtë dhomë. - %1$s shtoi %2$s dhe hoqi %3$s si adresa për këtë dhomë. - %1$s caktoi %2$s si adresë kryesore për këtë dhomë. %1$s hoqi adresën kryesore për këtë dhomë. - %1$s ka lejuar vizitorë të marrin pjesë në dhomë. %1$s ka penguar vizitorë të marrin pjesë në dhomë. - %1$s aktivizoi fshehtëzim skaj-më-skaj. %1$s aktivizoi fshehtëzim skaj-më-skaj (algoritëm i papranuar %2$s). - %s po kërkon të verifikojë kyçin tuaj, por klienti juaj nuk mbulon verifikim kyçesh brenda fjalosjeje. Që të verifikoni kyça, do t’ju duhet të përdorni verifikim të dikurshëm kyçesh. - %1$s krijo dhomën Dërguat një figurë. Dërguat një ngjitës. - Ftesa juaj Krijuat dhomën Ftuat %1$s @@ -168,7 +141,6 @@ E bëtë historikun e ardhshëm të dhomë të dukshëm për %1$s Aktivizuat fshehtëzim skaj-më-skaj (%1$s) Përmirësuat këtë dhomë. - Kërkuat një konferencë VoIP Hoqët emrin e dhomës Hoqët temën e dhomës @@ -178,24 +150,20 @@ Dërguat një ftesë te %1$s për të ardhur te dhoma Shfuqizuat ftesën për ardhjen në dhomë të %1$s Pranuat ftesën për %1$s - %1$s shtoi widget-in %2$s Shtuat widget-in %1$s %1$s hoqi widget-in %2$s Hoqët widget-in %1$s %1$s ndryshoi widget-in %2$s Ndryshuat widget-in %1$s - Përgjegjës Moderator Parazgjedhje Vetjake (%1$d) Vetjake - Ndryshuat shkallën e pushtetit për %1$s. %1$s ndryshoi shkallën e pushtetit për %2$s. %1$s nga %2$s në %3$s - Ftesa juaj. Arsye: %1$s Ftuat %1$s. Arsye: %2$s Erdhët në dhomë, Arsye: %1$s @@ -208,26 +176,19 @@ Shfuqizuat ftesën për ardhjen në dhomë të %1$s. Arsye: %2$s Pranuat ftesën për %1$s. Arsye: %2$s Tërhoqët mbrapsht ftesën për %1$s. Arsye: %2$s - Shtuat %1$s si një adresë për këtë dhomë. Shtuat %1$s si adresa për këtë dhomë. - Hoqët %1$s si një adresë për këtë dhomë. Hoqët %1$s si adresa për këtë dhomë. - Shtuat %1$s dhe hoqët %2$s si adresa për këtë dhomë. - Caktuat si adresë kryesore për këtë dhomë %1$s. Hoqët adresën kryesore për këtë dhomë. - Keni lejuar të vijnë mysafirë në dhomë. U keni penguar mysafirëve të vijnë në dhomë. - Aktivizuat fshehtëzimin skaj-më-skaj. Aktivizuat fshehtëzimin skaj-më-skaj (algoritëm %1$s i panjohur). - - + \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-sv/strings.xml b/matrix-sdk-android/src/main/res/values-sv/strings.xml index 25e51b69e5..d42c6ba2ca 100644 --- a/matrix-sdk-android/src/main/res/values-sv/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sv/strings.xml @@ -174,7 +174,7 @@ Du tog bort %1$s som en adress för det här rummet. - Du tog bort %2$s som adresser för det här rummet. + Du tog bort %1$s som adresser för det här rummet. %1$s lade till %2$s och tog bort %3$s som adresser för det här rummet. Du lade till %1$s och tog bort %2$s som adresser för det här rummet. diff --git a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml index 496bbe6bf8..5c5da36c26 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml @@ -177,7 +177,7 @@ 您新增了 %1$s 为此聊天室的地址。 - 您移除了此聊天室的 %2$s 地址。 + 您移除了此聊天室的 %1$s 地址。 您为此聊天室新增了 %1$s 并移除了 %2$s 地址。 您将此聊天室的主地址设为了 %1$s。 @@ -196,7 +196,7 @@ %1$s 邀请了 %2$s 您在此处升级。 %s 在此处升级。 - 您使未来的消息对 %2$s 可见 + 您使未来的消息对 %1$s 可见 %1$s 使未来的消息对 %2$s 可见 您离开了聊天室 %1$s 离开了聊天室 @@ -204,4 +204,8 @@ %1$s 已加入 您创建了讨论 %1$s 创建了讨论 + 你已阻止客人加入房间。 + %1$s已阻止客人加入房间。 + 你已允许客人加入这里。 + %1$s 已允许客人加入这里。 \ No newline at end of file diff --git a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml index 4a3293b195..b3de5910a5 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml @@ -177,7 +177,7 @@ 您為此聊天室新增了 %1$s 作為地址。 - 您為此聊天室移除了 %2$s 作為地址。 + 您為此聊天室移除了 %1$s 作為地址。 您為此聊天室新增了 %1$s 並移除了 %2$s 作為地址。 您將此聊天室的主要地址設定為 %1$s。 diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 3f75b715f6..27f083269f 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -225,7 +225,7 @@ You removed %1$s as an address for this room. - You removed %2$s as addresses for this room. + You removed %1$s as addresses for this room. %1$s added %2$s and removed %3$s as addresses for this room. diff --git a/tools/check/check_code_quality.sh b/tools/check/check_code_quality.sh index e855440e81..0b4272cbfe 100755 --- a/tools/check/check_code_quality.sh +++ b/tools/check/check_code_quality.sh @@ -91,7 +91,6 @@ ${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_resources.txt ./vector/src/main/res/color \ ./vector/src/main/res/layout \ ./vector/src/main/res/values \ - ./vector/src/main/res/values-v21 \ ./vector/src/main/res/xml resultForbiddenStringInResource=$? diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 6e879df7ab..63a3fad109 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===82 +enum class===83 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/tools/release/download_buildkite_artifacts.py b/tools/release/download_buildkite_artifacts.py index 4439c2fb8c..067a1a4dfe 100755 --- a/tools/release/download_buildkite_artifacts.py +++ b/tools/release/download_buildkite_artifacts.py @@ -45,6 +45,10 @@ parser.add_argument('-e', '--expecting', type=int, help='the expected number of artifacts. If omitted, no check will be done.') +parser.add_argument('-i', + '--ignoreErrors', + help='Ignore errors that can be ignored. Build state and number of artifacts.', + action="store_true") parser.add_argument('-d', '--directory', default="", @@ -91,9 +95,14 @@ print(" git commit : \"%s\"" % data0.get('commit')) print(" git commit message : \"%s\"" % data0.get('message')) print(" build state : %s" % data0.get('state')) +error = False + if data0.get('state') != 'passed': print("❌ Error, the build is in state '%s', and not 'passed'" % data0.get('state')) - exit(1) + if args.ignoreErrors: + error = True + else: + exit(1) ### Fetch artifacts list @@ -110,8 +119,11 @@ data = json.loads(r.content.decode()) print(" %d artifact(s) found." % len(data)) if args.expecting is not None and args.expecting != len(data): - print("Error, expecting %d artifacts and found %d." % (args.expecting, len(data))) - exit(1) + print("❌ Error, expecting %d artifacts and found %d." % (args.expecting, len(data))) + if args.ignoreErrors: + error = True + else: + exit(1) if args.verbose: print("Json data:") @@ -128,8 +140,6 @@ else: if not args.simulate: os.mkdir(targetDir) -error = False - for elt in data: if args.verbose: print() @@ -157,7 +167,7 @@ for elt in data: print("❌ Checksum mismatch: expecting %s and get %s" % (elt.get("sha1sum"), hash)) if error: - print("❌ Error(s) occurred, check the log") + print("❌ Error(s) occurred, please check the log") exit(1) else: print("Done!") diff --git a/vector/build.gradle b/vector/build.gradle index 546f568555..96b4994a7a 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -39,9 +39,9 @@ def generateVersionCodeFromVersionName() { def getVersionCode() { if (gitBranchName() == "develop") { - return generateVersionCodeFromTimestamp() * 10 + return generateVersionCodeFromTimestamp() } else { - return generateVersionCodeFromVersionName() * 10 + return generateVersionCodeFromVersionName() } } @@ -166,13 +166,14 @@ android { } applicationVariants.all { variant -> + // assign different version code for each output + def baseVariantVersion = variant.versionCode * 10 variant.outputs.each { output -> def baseAbiVersionCode = project.ext.abiVersionCodes.get(output.getFilter(OutputFile.ABI)) // Known limitation: it does not modify the value in the BuildConfig.java generated file - print "ABI " + output.getFilter(OutputFile.ABI) + " \tvariant.versionCode " + variant.versionCode // See https://issuetracker.google.com/issues/171133218 - output.versionCodeOverride = variant.versionCode + baseAbiVersionCode - print " \t-> VersionCode = " + output.versionCodeOverride + "\n" + output.versionCodeOverride = baseVariantVersion + baseAbiVersionCode + print "ABI " + output.getFilter(OutputFile.ABI) + " \t-> VersionCode = " + output.versionCodeOverride + "\n" } } diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index 4f89763cda..5be313d719 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -42,6 +42,7 @@ import im.vector.app.core.di.HasVectorInjector import im.vector.app.core.di.VectorComponent import im.vector.app.core.extensions.configureAndStart import im.vector.app.core.rx.RxConfig +import im.vector.app.features.call.WebRtcPeerConnectionManager import im.vector.app.features.configuration.VectorConfiguration import im.vector.app.features.disclaimer.doNotShowDisclaimerDialog import im.vector.app.features.lifecycle.VectorActivityLifecycleCallbacks @@ -89,6 +90,7 @@ class VectorApplication : @Inject lateinit var rxConfig: RxConfig @Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var pinLocker: PinLocker + @Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager lateinit var vectorComponent: VectorComponent @@ -173,6 +175,7 @@ class VectorApplication : }) ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler) ProcessLifecycleOwner.get().lifecycle.addObserver(pinLocker) + ProcessLifecycleOwner.get().lifecycle.addObserver(webRtcPeerConnectionManager) // This should be done as early as possible // initKnownEmojiHashSet(appContext) diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 014a244bf8..acdad5407c 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -87,6 +87,7 @@ import im.vector.app.features.roomprofile.uploads.RoomUploadsFragment import im.vector.app.features.roomprofile.uploads.files.RoomUploadsFilesFragment import im.vector.app.features.roomprofile.uploads.media.RoomUploadsMediaFragment import im.vector.app.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment +import im.vector.app.features.settings.VectorSettingsGeneralFragment import im.vector.app.features.settings.VectorSettingsHelpAboutFragment import im.vector.app.features.settings.VectorSettingsLabsFragment import im.vector.app.features.settings.VectorSettingsNotificationPreferenceFragment @@ -292,6 +293,11 @@ interface FragmentModule { @FragmentKey(VectorSettingsPinFragment::class) fun bindVectorSettingsPinFragment(fragment: VectorSettingsPinFragment): Fragment + @Binds + @IntoMap + @FragmentKey(VectorSettingsGeneralFragment::class) + fun bindVectorSettingsGeneralFragment(fragment: VectorSettingsGeneralFragment): Fragment + @Binds @IntoMap @FragmentKey(PushRulesFragment::class) diff --git a/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt new file mode 100644 index 0000000000..7198cdb4a2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/dialogs/GalleryOrCameraDialogHelper.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.dialogs + +import android.app.Activity +import android.net.Uri +import androidx.appcompat.app.AlertDialog +import androidx.core.net.toUri +import androidx.fragment.app.Fragment +import com.yalantis.ucrop.UCrop +import im.vector.app.R +import im.vector.app.core.extensions.registerStartForActivityResult +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO +import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.registerForPermissionsResult +import im.vector.app.features.media.createUCropWithDefaultSettings +import im.vector.lib.multipicker.MultiPicker +import im.vector.lib.multipicker.entity.MultiPickerImageType +import java.io.File + +/** + * Use to let the user choose between Camera (with permission handling) and Gallery (with single image selection), + * then edit the image + * [Listener.onImageReady] will be called with an uri of a square image store in the cache of the application. + * It's up to the caller to delete the file. + */ +class GalleryOrCameraDialogHelper( + // must implement GalleryOrCameraDialogHelper.Listener + private val fragment: Fragment, + private val colorProvider: ColorProvider +) { + interface Listener { + fun onImageReady(uri: Uri?) + } + + private val activity + get() = fragment.requireActivity() + + private val listener = fragment as? Listener ?: error("Fragment must implement GalleryOrCameraDialogHelper.Listener") + + private val takePhotoPermissionActivityResultLauncher = fragment.registerForPermissionsResult { allGranted -> + if (allGranted) { + doOpenCamera() + } + } + + private val takePhotoActivityResultLauncher = fragment.registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + avatarCameraUri?.let { uri -> + MultiPicker.get(MultiPicker.CAMERA) + .getTakenPhoto(activity, uri) + ?.let { startUCrop(it) } + } + } + } + + private val pickImageActivityResultLauncher = fragment.registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + MultiPicker + .get(MultiPicker.IMAGE) + .getSelectedFiles(activity, activityResult.data) + .firstOrNull() + ?.let { startUCrop(it) } + } + } + + private val uCropActivityResultLauncher = fragment.registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK) { + activityResult.data?.let { listener.onImageReady(UCrop.getOutput(it)) } + } + } + + private fun startUCrop(image: MultiPickerImageType) { + val destinationFile = File(activity.cacheDir, "${image.displayName}_e_${System.currentTimeMillis()}") + val uri = image.contentUri + createUCropWithDefaultSettings(colorProvider, uri, destinationFile.toUri(), fragment.getString(R.string.rotate_and_crop_screen_title)) + .withAspectRatio(1f, 1f) + .getIntent(activity) + .let { uCropActivityResultLauncher.launch(it) } + } + + private enum class Type { + Camera, + Gallery + } + + fun show() { + AlertDialog.Builder(activity) + .setTitle(R.string.attachment_type_dialog_title) + .setItems(arrayOf( + fragment.getString(R.string.attachment_type_camera), + fragment.getString(R.string.attachment_type_gallery) + )) { _, which -> + onAvatarTypeSelected(if (which == 0) Type.Camera else Type.Gallery) + } + .setPositiveButton(R.string.cancel, null) + .show() + } + + private fun onAvatarTypeSelected(type: Type) { + when (type) { + Type.Camera -> + if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, activity, takePhotoPermissionActivityResultLauncher)) { + avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(activity, takePhotoActivityResultLauncher) + } + Type.Gallery -> + MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher) + } + } + + private var avatarCameraUri: Uri? = null + private fun doOpenCamera() { + avatarCameraUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(activity, takePhotoActivityResultLauncher) + } +} diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt index a495f14b6e..1c80e6a85c 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetRoomPreviewItem.kt @@ -44,8 +44,10 @@ abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel(R.id.bottomSheetRoomPreviewAvatar) val roomName by bind(R.id.bottomSheetRoomPreviewName) + val roomLowPriority by bind(R.id.bottomSheetRoomPreviewLowPriority) val roomFavorite by bind(R.id.bottomSheetRoomPreviewFavorite) val roomSettings by bind(R.id.bottomSheetRoomPreviewSettings) } diff --git a/vector/src/main/java/im/vector/app/core/qrcode/QrCode.kt b/vector/src/main/java/im/vector/app/core/qrcode/QrCode.kt index f79ae7afd9..170baa04fe 100644 --- a/vector/src/main/java/im/vector/app/core/qrcode/QrCode.kt +++ b/vector/src/main/java/im/vector/app/core/qrcode/QrCode.kt @@ -34,12 +34,15 @@ fun String.toBitMatrix(size: Int): BitMatrix { fun BitMatrix.toBitmap(@ColorInt backgroundColor: Int = Color.WHITE, @ColorInt foregroundColor: Int = Color.BLACK): Bitmap { - val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - for (x in 0 until width) { - for (y in 0 until height) { - bmp.setPixel(x, y, if (get(x, y)) foregroundColor else backgroundColor) + val colorBuffer = IntArray(width * height) + var rowOffset = 0 + for (y in 0 until height) { + for (x in 0 until width) { + val arrayIndex = x + rowOffset + colorBuffer[arrayIndex] = if (get(x, y)) foregroundColor else backgroundColor } + rowOffset += width } - return bmp + return Bitmap.createBitmap(colorBuffer, width, height, Bitmap.Config.ARGB_8888) } diff --git a/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt b/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt index b14a097eb6..d5d8bb14dd 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt @@ -17,6 +17,8 @@ package im.vector.app.core.services import android.content.Context +import android.media.Ringtone +import android.media.RingtoneManager import android.media.AudioAttributes import android.media.AudioManager import android.media.MediaPlayer @@ -25,7 +27,26 @@ import androidx.core.content.getSystemService import im.vector.app.R import timber.log.Timber -class CallRingPlayer( +class CallRingPlayerIncoming( + context: Context +) { + + private val applicationContext = context.applicationContext + private var r: Ringtone? = null + + fun start() { + val notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + r = RingtoneManager.getRingtone(applicationContext, notification) + Timber.v("## VOIP Starting ringing incomming") + r?.play() + } + + fun stop() { + r?.stop() + } +} + +class CallRingPlayerOutgoing( context: Context ) { @@ -44,12 +65,12 @@ class CallRingPlayer( try { if (player?.isPlaying == false) { player?.start() - Timber.v("## VOIP Starting ringing") + Timber.v("## VOIP Starting ringing outgoing") } else { Timber.v("## VOIP already playing") } } catch (failure: Throwable) { - Timber.e(failure, "## VOIP Failed to start ringing") + Timber.e(failure, "## VOIP Failed to start ringing outgoing") player = null } } else { @@ -74,7 +95,7 @@ class CallRingPlayer( } else { mediaPlayer.setAudioAttributes(AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING) + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) .build()) } return mediaPlayer diff --git a/vector/src/main/java/im/vector/app/core/services/CallService.kt b/vector/src/main/java/im/vector/app/core/services/CallService.kt index 1362c20be1..075b237be2 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallService.kt @@ -40,7 +40,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe private lateinit var notificationUtils: NotificationUtils private lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager - private var callRingPlayer: CallRingPlayer? = null + private var callRingPlayerIncoming: CallRingPlayerIncoming? = null + private var callRingPlayerOutgoing: CallRingPlayerOutgoing? = null private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null private var bluetoothHeadsetStateReceiver: BluetoothHeadsetReceiver? = null @@ -63,14 +64,16 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe super.onCreate() notificationUtils = vectorComponent().notificationUtils() webRtcPeerConnectionManager = vectorComponent().webRtcPeerConnectionManager() - callRingPlayer = CallRingPlayer(applicationContext) + callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext) + callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext) wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this) bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(this, this) } override fun onDestroy() { super.onDestroy() - callRingPlayer?.stop() + callRingPlayerIncoming?.stop() + callRingPlayerOutgoing?.stop() wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(this, it) } wiredHeadsetStateReceiver = null bluetoothHeadsetStateReceiver?.let { BluetoothHeadsetReceiver.unRegister(this, it) } @@ -100,16 +103,17 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe when (intent.action) { ACTION_INCOMING_RINGING_CALL -> { mediaSession?.isActive = true - callRingPlayer?.start() + callRingPlayerIncoming?.start() displayIncomingCallNotification(intent) } ACTION_OUTGOING_RINGING_CALL -> { mediaSession?.isActive = true - callRingPlayer?.start() + callRingPlayerOutgoing?.start() displayOutgoingRingingCallNotification(intent) } ACTION_ONGOING_CALL -> { - callRingPlayer?.stop() + callRingPlayerIncoming?.stop() + callRingPlayerOutgoing?.stop() displayCallInProgressNotification(intent) } ACTION_NO_ACTIVE_CALL -> hideCallNotifications() @@ -117,7 +121,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe // lower notification priority displayCallInProgressNotification(intent) // stop ringing - callRingPlayer?.stop() + callRingPlayerIncoming?.stop() + callRingPlayerOutgoing?.stop() } ACTION_ONGOING_CALL_BG -> { // there is an ongoing call but call activity is in background @@ -125,7 +130,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe } else -> { // Should not happen - callRingPlayer?.stop() + callRingPlayerIncoming?.stop() + callRingPlayerOutgoing?.stop() myStopSelf() } } diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index b5552e4d62..e553b5e0d3 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -21,6 +21,7 @@ import android.content.Intent import android.os.Bundle import android.os.Parcelable import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.Lifecycle import com.bumptech.glide.Glide import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder @@ -205,13 +206,15 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity { } private fun displayError(failure: Throwable) { - AlertDialog.Builder(this) - .setTitle(R.string.dialog_title_error) - .setMessage(errorFormatter.toHumanReadable(failure)) - .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() } - .setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() } - .setCancelable(false) - .show() + if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { + AlertDialog.Builder(this) + .setTitle(R.string.dialog_title_error) + .setMessage(errorFormatter.toHumanReadable(failure)) + .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() } + .setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() } + .setCancelable(false) + .show() + } } private fun startNextActivityAndFinish() { diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt index b040101c84..9f3ba39bbe 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -19,7 +19,6 @@ package im.vector.app.features.attachments.preview import android.app.Activity.RESULT_CANCELED import android.app.Activity.RESULT_OK -import android.content.Intent import android.os.Bundle import android.os.Parcelable import android.view.Menu @@ -39,6 +38,7 @@ import com.airbnb.mvrx.withState import com.yalantis.ucrop.UCrop import im.vector.app.R import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.OnSnapPositionChangeListener @@ -49,7 +49,6 @@ import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_attachments_preview.* import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.content.ContentAttachmentData -import timber.log.Timber import java.io.File import javax.inject.Inject @@ -80,20 +79,15 @@ class AttachmentsPreviewFragment @Inject constructor( } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - // TODO handle this one (Ucrop lib) - @Suppress("DEPRECATION") - super.onActivityResult(requestCode, resultCode, data) - - if (resultCode == RESULT_OK) { - if (requestCode == UCrop.REQUEST_CROP && data != null) { - Timber.v("Crop success") - handleCropResult(data) + private val uCropActivityResultLauncher = registerStartForActivityResult { activityResult -> + if (activityResult.resultCode == RESULT_OK) { + val resultUri = activityResult.data?.let { UCrop.getOutput(it) } + if (resultUri != null) { + viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultUri)) + } else { + Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show() } } - if (resultCode == UCrop.RESULT_ERROR) { - Timber.v("Crop error") - } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -170,15 +164,6 @@ class AttachmentsPreviewFragment @Inject constructor( } } - private fun handleCropResult(result: Intent) { - val resultUri = UCrop.getOutput(result) - if (resultUri != null) { - viewModel.handle(AttachmentsPreviewAction.UpdatePathOfCurrentAttachment(resultUri)) - } else { - Toast.makeText(requireContext(), "Cannot retrieve cropped value", Toast.LENGTH_SHORT).show() - } - } - private fun handleRemoveAction() { viewModel.handle(AttachmentsPreviewAction.RemoveCurrentAttachment) } @@ -187,8 +172,9 @@ class AttachmentsPreviewFragment @Inject constructor( val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState val destinationFile = File(requireContext().cacheDir, "${currentAttachment.name}_edited_image_${System.currentTimeMillis()}") val uri = currentAttachment.queryUri - createUCropWithDefaultSettings(requireContext(), uri, destinationFile.toUri(), currentAttachment.name) - .start(requireContext(), this) + createUCropWithDefaultSettings(colorProvider, uri, destinationFile.toUri(), currentAttachment.name) + .getIntent(requireContext()) + .let { intent -> uCropActivityResultLauncher.launch(intent) } } private fun setupRecyclerViews() { diff --git a/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt b/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt index 9b3c42ab5d..3a24cf6d48 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallAudioManager.kt @@ -93,6 +93,10 @@ class CallAudioManager( fun startForCall(mxCall: MxCall) { Timber.v("## VOIP: AudioManager startForCall ${mxCall.callId}") + } + + private fun setupAudioManager(mxCall: MxCall) { + Timber.v("## VOIP: AudioManager setupAudioManager ${mxCall.callId}") val audioManager = audioManager ?: return savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn savedIsMicrophoneMute = audioManager.isMicrophoneMute @@ -150,7 +154,7 @@ class CallAudioManager( fun onCallConnected(mxCall: MxCall) { Timber.v("##VOIP: AudioManager call answered, adjusting current sound device") - adjustCurrentSoundDevice(mxCall) + setupAudioManager(mxCall) } fun getAvailableSoundDevices(): List { diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index edb75441c8..445f40e5b1 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -88,6 +88,11 @@ class VectorCallViewModel @AssistedInject constructor( private val currentCallListener = object : WebRtcPeerConnectionManager.CurrentCallListener { override fun onCurrentCallChange(call: MxCall?) { + // we need to check the state + if (call == null) { + // we should dismiss, e.g handled by other session? + _viewEvents.post(VectorCallViewEvents.DismissNoCall) + } } override fun onCaptureStateChanged() { diff --git a/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt b/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt index c70b52b09b..86b38c1158 100644 --- a/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt @@ -19,10 +19,14 @@ package im.vector.app.features.call import android.content.Context import android.hardware.camera2.CameraManager import androidx.core.content.getSystemService +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent import im.vector.app.ActiveSessionDataSource import im.vector.app.core.services.BluetoothHeadsetReceiver import im.vector.app.core.services.CallService import im.vector.app.core.services.WiredHeadsetStateReceiver +import im.vector.app.push.fcm.FcmHelper import io.reactivex.disposables.Disposable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.ReplaySubject @@ -72,7 +76,7 @@ import javax.inject.Singleton class WebRtcPeerConnectionManager @Inject constructor( private val context: Context, private val activeSessionDataSource: ActiveSessionDataSource -) : CallsListener { +) : CallsListener, LifecycleObserver { private val currentSession: Session? get() = activeSessionDataSource.currentValue?.orNull() @@ -170,6 +174,8 @@ class WebRtcPeerConnectionManager @Inject constructor( private var currentCaptureMode: CaptureFormat = CaptureFormat.HD + private var isInBackground: Boolean = true + var capturerIsInError = false set(value) { field = value @@ -201,6 +207,16 @@ class WebRtcPeerConnectionManager @Inject constructor( } } + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun entersForeground() { + isInBackground = false + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + fun entersBackground() { + isInBackground = true + } + var currentCall: CallContext? = null set(value) { field = value @@ -702,6 +718,18 @@ class WebRtcPeerConnectionManager @Inject constructor( ) callContext.offerSdp = callInviteContent.offer + + // If this is received while in background, the app will not sync, + // and thus won't be able to received events. For example if the call is + // accepted on an other session this device will continue ringing + if (isInBackground) { + if (FcmHelper.isPushSupported()) { + // only for push version as fdroid version is already doing it? + currentSession?.startAutomaticBackgroundSync(30, 0) + } else { + // Maybe increase sync freq? but how to set back to default values? + } + } } private fun createAnswer() { @@ -849,6 +877,16 @@ class WebRtcPeerConnectionManager @Inject constructor( Timber.v("## VOIP onCallManagedByOtherSession: $callId") currentCall = null CallService.onNoActiveCall(context) + + // did we start background sync? so we should stop it + if (isInBackground) { + if (FcmHelper.isPushSupported()) { + currentSession?.stopAnyBackgroundSync() + } else { + // for fdroid we should not stop, it should continue syncing + // maybe we should restore default timeout/delay though? + } + } } private inner class StreamObserver(val callContext: CallContext) : PeerConnection.Observer { diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt index 0a1b631344..e42eb6de6f 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.util.toMatrixItem +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -116,6 +117,7 @@ class IncomingVerificationRequestHandler @Inject constructor( } override fun verificationRequestCreated(pr: PendingVerificationRequest) { + Timber.v("## SAS verificationRequestCreated ${pr.transactionId}") // For incoming request we should prompt (if not in activity where this request apply) if (pr.isIncoming) { val name = session?.getUser(pr.otherUserId)?.displayName diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt index 2720c20fb0..2d09974687 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -49,7 +49,6 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.LocalEcho -import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 @@ -233,7 +232,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( override fun handle(action: VerificationAction) = withState { state -> val otherUserId = state.otherUserMxItem?.id ?: return@withState val roomId = state.roomId - ?: session.getExistingDirectRoomWithUser(otherUserId)?.roomId + ?: session.getExistingDirectRoomWithUser(otherUserId) when (action) { is VerificationAction.RequestVerificationByDM -> { @@ -245,14 +244,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( pendingRequest = Loading() ) } - val roomParams = CreateRoomParams() - .apply { - invitedUserIds.add(otherUserId) - setDirectMessage() - enableEncryptionIfInvitedUsersSupportIt = true - } - - session.createRoom(roomParams, object : MatrixCallback { + session.createDirectRoom(otherUserId, object : MatrixCallback { override fun onSuccess(data: String) { setState { copy( diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt new file mode 100644 index 0000000000..c5a45d8f1b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/form/FormEditableAvatarItem.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.app.features.form + +import android.net.Uri +import android.view.View +import android.widget.ImageView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import com.bumptech.glide.request.RequestOptions +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.onClick +import im.vector.app.core.glide.GlideApp +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.util.MatrixItem + +@EpoxyModelClass(layout = R.layout.item_editable_avatar) +abstract class FormEditableAvatarItem : EpoxyModelWithHolder() { + + @EpoxyAttribute + var avatarRenderer: AvatarRenderer? = null + + @EpoxyAttribute + var matrixItem: MatrixItem? = null + + @EpoxyAttribute + var enabled: Boolean = true + + @EpoxyAttribute + var imageUri: Uri? = null + + @EpoxyAttribute + var clickListener: ClickListener? = null + + @EpoxyAttribute + var deleteListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.imageContainer.onClick(clickListener?.takeIf { enabled }) + if (matrixItem != null) { + avatarRenderer?.render(matrixItem!!, holder.image) + } else { + GlideApp.with(holder.image) + .load(imageUri) + .apply(RequestOptions.circleCropTransform()) + .into(holder.image) + } + holder.delete.isVisible = enabled && (imageUri != null || matrixItem?.avatarUrl?.isNotEmpty() == true) + holder.delete.onClick(deleteListener?.takeIf { enabled }) + } + + class Holder : VectorEpoxyHolder() { + val imageContainer by bind(R.id.itemEditableAvatarImageContainer) + val image by bind(R.id.itemEditableAvatarImage) + val delete by bind(R.id.itemEditableAvatarDelete) + } +} diff --git a/vector/src/main/java/im/vector/app/features/form/FormSubmitButtonItem.kt b/vector/src/main/java/im/vector/app/features/form/FormSubmitButtonItem.kt new file mode 100644 index 0000000000..2d2a5e7aec --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/form/FormSubmitButtonItem.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.app.features.form + +import android.widget.Button +import androidx.annotation.StringRes +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.setTextOrHide + +@EpoxyModelClass(layout = R.layout.item_form_submit_button) +abstract class FormSubmitButtonItem : EpoxyModelWithHolder() { + + @EpoxyAttribute + var enabled: Boolean = true + + @EpoxyAttribute + var buttonTitle: String? = null + + @EpoxyAttribute + @StringRes + var buttonTitleId: Int? = null + + @EpoxyAttribute + var buttonClickListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + if (buttonTitleId != null) { + holder.button.setText(buttonTitleId!!) + } else { + holder.button.setTextOrHide(buttonTitle) + } + + holder.button.isEnabled = enabled + holder.button.onClick(buttonClickListener) + } + + class Holder : VectorEpoxyHolder() { + val button by bind